261 lines
11 KiB
C#
261 lines
11 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using GerbilManagerWebAPI.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using GerbilManagerWebAPI.Services;
|
|
|
|
namespace GerbilManagerWebAPI.Import
|
|
{
|
|
public sealed class IngestResolvedService
|
|
{
|
|
private readonly ApplicationContext _db;
|
|
private readonly string _resolvedJsonPath;
|
|
private readonly string _sourceDir;
|
|
private readonly string _photoRoot;
|
|
|
|
public IngestResolvedService(ApplicationContext db, IConfiguration config, IWebHostEnvironment? env)
|
|
{
|
|
_db = db;
|
|
var contentRoot = env?.ContentRootPath ?? Directory.GetCurrentDirectory();
|
|
_sourceDir = config["Import:SourcePath"]
|
|
?? Path.GetFullPath(Path.Combine(contentRoot, "..", "tools", "import", "output"));
|
|
_resolvedJsonPath = Path.Combine(_sourceDir, "resolved_import.json");
|
|
_photoRoot = config["Photos:RootPath"]
|
|
?? Path.Combine(contentRoot, "photo-storage");
|
|
}
|
|
|
|
public async Task<string> RunAsync()
|
|
{
|
|
if (!File.Exists(_resolvedJsonPath))
|
|
{
|
|
return $"Error: Resolved import file not found at {_resolvedJsonPath}. Run resolve_import.py first.";
|
|
}
|
|
|
|
string jsonContent = await File.ReadAllTextAsync(_resolvedJsonPath);
|
|
var jsonOptions = new JsonSerializerOptions
|
|
{
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
jsonOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
|
|
var data = JsonSerializer.Deserialize<ResolvedImportData>(jsonContent, jsonOptions);
|
|
|
|
if (data == null)
|
|
{
|
|
return "Error: Failed to deserialize resolved_import.json.";
|
|
}
|
|
|
|
// 1. Provider-agnostic clean wipe of dependent tables to prevent duplicate keys
|
|
if (_db.Database.ProviderName == "Microsoft.EntityFrameworkCore.InMemory")
|
|
{
|
|
// Break circular references first
|
|
foreach (var l in _db.Litters)
|
|
{
|
|
l.FatherId = null;
|
|
l.MotherId = null;
|
|
}
|
|
foreach (var g in _db.Gerbils)
|
|
{
|
|
g.LitterId = null;
|
|
}
|
|
await _db.SaveChangesAsync();
|
|
|
|
_db.GerbilPhotos.RemoveRange(_db.GerbilPhotos);
|
|
_db.WeightRecords.RemoveRange(_db.WeightRecords);
|
|
_db.HealthRecords.RemoveRange(_db.HealthRecords);
|
|
_db.SaleContracts.RemoveRange(_db.SaleContracts);
|
|
_db.Gerbils.RemoveRange(_db.Gerbils);
|
|
_db.Litters.RemoveRange(_db.Litters);
|
|
_db.Contacts.RemoveRange(_db.Contacts);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
else
|
|
{
|
|
// Break circular references first using ExecuteUpdateAsync
|
|
await _db.Litters.ExecuteUpdateAsync(s => s
|
|
.SetProperty(l => l.FatherId, (Guid?)null)
|
|
.SetProperty(l => l.MotherId, (Guid?)null));
|
|
|
|
await _db.Gerbils.ExecuteUpdateAsync(s => s
|
|
.SetProperty(g => g.LitterId, (Guid?)null));
|
|
|
|
await _db.GerbilPhotos.ExecuteDeleteAsync();
|
|
await _db.WeightRecords.ExecuteDeleteAsync();
|
|
await _db.HealthRecords.ExecuteDeleteAsync();
|
|
await _db.SaleContracts.ExecuteDeleteAsync();
|
|
await _db.Gerbils.ExecuteDeleteAsync();
|
|
await _db.Litters.ExecuteDeleteAsync();
|
|
await _db.Contacts.ExecuteDeleteAsync();
|
|
}
|
|
|
|
// 2. Import Contacts (Add new ones, and update roles/details of existing ones)
|
|
var existingContacts = await _db.Contacts.ToDictionaryAsync(c => c.Id);
|
|
var addedContactIds = new HashSet<Guid>();
|
|
int contactsAdded = 0;
|
|
int contactsUpdated = 0;
|
|
foreach (var c in data.Contacts)
|
|
{
|
|
if (existingContacts.TryGetValue(c.Id, out var existingContact))
|
|
{
|
|
existingContact.Name = c.Name;
|
|
existingContact.Email = c.Email;
|
|
existingContact.Phone = c.Phone;
|
|
existingContact.Address = c.Address;
|
|
existingContact.Notes = c.Notes;
|
|
existingContact.IsBreeder = c.IsBreeder;
|
|
existingContact.IsReceiver = c.IsReceiver;
|
|
contactsUpdated++;
|
|
}
|
|
else if (addedContactIds.Add(c.Id))
|
|
{
|
|
_db.Contacts.Add(c);
|
|
contactsAdded++;
|
|
}
|
|
}
|
|
await _db.SaveChangesAsync();
|
|
|
|
// 3. Pass 1: Insert all Litters with parent references set to null to avoid FK errors
|
|
var littersToInsert = new List<Litter>();
|
|
foreach (var l in data.Litters)
|
|
{
|
|
littersToInsert.Add(new Litter
|
|
{
|
|
Id = l.Id,
|
|
Name = l.Name,
|
|
Date = l.Date,
|
|
TotalBorn = l.TotalBorn,
|
|
DeathsWithin8Weeks = l.DeathsWithin8Weeks,
|
|
FatherId = null, // Set in Pass 2
|
|
MotherId = null, // Set in Pass 2
|
|
ExpectedGoHomeDate = l.ExpectedGoHomeDate,
|
|
Notes = l.Notes,
|
|
PairingCode = l.PairingCode,
|
|
ExternalRef = l.ExternalRef,
|
|
LitterLetter = l.LitterLetter
|
|
});
|
|
}
|
|
_db.Litters.AddRange(littersToInsert);
|
|
await _db.SaveChangesAsync();
|
|
|
|
// 4. Pass 1: Insert all Gerbils with LitterId set to null to avoid FK errors
|
|
var gerbilsToInsert = new List<Gerbil>();
|
|
foreach (var g in data.Gerbils)
|
|
{
|
|
var importedGerbil = new Gerbil
|
|
{
|
|
Id = g.Id,
|
|
Name = g.Name,
|
|
Gender = g.Gender,
|
|
Status = g.Status,
|
|
LitterId = null, // Set in Pass 2
|
|
OriginContactId = g.OriginContactId,
|
|
ReceiverContactId = g.ReceiverContactId,
|
|
EnclosureId = g.EnclosureId,
|
|
ColorVarietyId = g.ColorVarietyId,
|
|
DateOfBirth = g.DateOfBirth,
|
|
DateOfDeath = g.DateOfDeath,
|
|
CauseOfDeath = g.CauseOfDeath,
|
|
GoHomeDate = g.GoHomeDate,
|
|
Genotype = g.Genotype,
|
|
Notes = g.Notes,
|
|
ImportSource = g.ImportSource,
|
|
ExternalRef = g.ExternalRef,
|
|
RawImportData = g.RawImportData,
|
|
OriginBreeder = g.OriginBreeder,
|
|
NameSearch = g.NameSearch,
|
|
CharacterTraits = g.CharacterTraits,
|
|
CharacterNote = g.CharacterNote,
|
|
IsDeaf = g.IsDeaf,
|
|
IsResident = g.IsResident
|
|
};
|
|
GerbilStatusService.Apply(importedGerbil, DateOnly.FromDateTime(DateTime.UtcNow));
|
|
gerbilsToInsert.Add(importedGerbil);
|
|
}
|
|
_db.Gerbils.AddRange(gerbilsToInsert);
|
|
await _db.SaveChangesAsync();
|
|
|
|
// 5. Pass 2: Set the actual foreign key relationships
|
|
// Update Litters with their parent animal IDs
|
|
var littersInDb = await _db.Litters.ToDictionaryAsync(l => l.Id);
|
|
foreach (var l in data.Litters)
|
|
{
|
|
if (littersInDb.TryGetValue(l.Id, out var dbLitter))
|
|
{
|
|
dbLitter.FatherId = l.FatherId;
|
|
dbLitter.MotherId = l.MotherId;
|
|
}
|
|
}
|
|
|
|
// Update Gerbils with their actual LitterId
|
|
var gerbilsInDb = await _db.Gerbils.ToDictionaryAsync(g => g.Id);
|
|
foreach (var g in data.Gerbils)
|
|
{
|
|
if (gerbilsInDb.TryGetValue(g.Id, out var dbGerbil))
|
|
{
|
|
dbGerbil.LitterId = g.LitterId;
|
|
}
|
|
}
|
|
await _db.SaveChangesAsync();
|
|
|
|
// 6. Insert all Photos & copy physical files
|
|
Directory.CreateDirectory(_photoRoot);
|
|
foreach (var p in data.GerbilPhotos)
|
|
{
|
|
if (!string.IsNullOrEmpty(p.SourcePath))
|
|
{
|
|
var relPath = p.SourcePath.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
|
|
var src = Path.Combine(_sourceDir, relPath);
|
|
if (File.Exists(src))
|
|
{
|
|
var dest = Path.Combine(_photoRoot, p.FileName);
|
|
try
|
|
{
|
|
File.Copy(src, dest, overwrite: true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Warning: Failed to copy photo {src} to {dest}: {ex.Message}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"Warning: Source photo file not found at {src}");
|
|
}
|
|
}
|
|
|
|
_db.GerbilPhotos.Add(new GerbilPhoto
|
|
{
|
|
Id = p.Id,
|
|
GerbilId = p.GerbilId,
|
|
FileName = p.FileName,
|
|
Caption = p.Caption,
|
|
SortOrder = p.SortOrder,
|
|
CreatedAt = DateTimeOffset.UtcNow
|
|
});
|
|
}
|
|
await _db.SaveChangesAsync();
|
|
|
|
return $"Ingestion successful! Imported {contactsAdded} contacts ({contactsUpdated} updated), {data.Litters.Count} litters, {data.Gerbils.Count} gerbils, and {data.GerbilPhotos.Count} photos.";
|
|
}
|
|
}
|
|
|
|
public class ResolvedImportData
|
|
{
|
|
public List<Contact> Contacts { get; set; } = new();
|
|
public List<Litter> Litters { get; set; } = new();
|
|
public List<Gerbil> Gerbils { get; set; } = new();
|
|
public List<ResolvedPhoto> GerbilPhotos { get; set; } = new();
|
|
}
|
|
|
|
public class ResolvedPhoto
|
|
{
|
|
public Guid Id { get; set; }
|
|
public Guid GerbilId { get; set; }
|
|
public required string FileName { get; set; }
|
|
public string? Caption { get; set; }
|
|
public int SortOrder { get; set; }
|
|
|
|
[JsonPropertyName("_source_path")]
|
|
public string? SourcePath { get; set; }
|
|
}
|
|
}
|