using Lagerbestand.Data; using Lagerbestand.Models; using Microsoft.EntityFrameworkCore; namespace Lagerbestand.Services; /// /// Result of merging a local customer into a Lexware customer. /// public class CustomerMergeResult { public Guid TargetCustomerId { get; set; } public int VehiclesMoved { get; set; } public int TireStacksMoved { get; set; } public int TasksMoved { get; set; } } public interface ICustomerMergeService { /// /// Local customers are created by this app and use the "K-" number prefix. /// Lexware customers carry the numeric Lexware customer number. /// bool IsLocalCustomer(Customer customer); /// /// Moves all vehicles, tire stacks and task links from a locally created customer /// to an existing Lexware customer and permanently deletes the local customer. /// Task MergeAsync(Guid sourceId, Guid targetId); } public class CustomerMergeService : ICustomerMergeService { private readonly AppDbContext _context; private readonly ILogger _logger; public CustomerMergeService(AppDbContext context, ILogger logger) { _context = context; _logger = logger; } public static bool IsLocalCustomerNumber(string? customerNumber) => string.IsNullOrWhiteSpace(customerNumber) || customerNumber.StartsWith("K-", StringComparison.OrdinalIgnoreCase); public bool IsLocalCustomer(Customer customer) => IsLocalCustomerNumber(customer.CustomerNumber); public async Task MergeAsync(Guid sourceId, Guid targetId) { if (sourceId == targetId) throw new InvalidOperationException("Quell- und Zielkunde dürfen nicht identisch sein."); var strategy = _context.Database.CreateExecutionStrategy(); return await strategy.ExecuteAsync(async () => { await using var transaction = await _context.Database.BeginTransactionAsync(); var source = await _context.Customers.FirstOrDefaultAsync(c => c.Id == sourceId) ?? throw new InvalidOperationException("Lokaler Kunde wurde nicht gefunden."); var target = await _context.Customers.FirstOrDefaultAsync(c => c.Id == targetId) ?? throw new InvalidOperationException("Lexware-Kunde wurde nicht gefunden."); if (!IsLocalCustomerNumber(source.CustomerNumber)) throw new InvalidOperationException("Nur lokal angelegte Kunden (K-…) können zusammengeführt werden."); if (IsLocalCustomerNumber(target.CustomerNumber)) throw new InvalidOperationException("Der Zielkunde muss ein Lexware-Kunde sein."); var result = new CustomerMergeResult { TargetCustomerId = targetId }; var now = DateTime.UtcNow; // Vehicles var targetHasPrimary = await _context.Vehicles .AnyAsync(v => v.CustomerId == targetId && v.IsActive && v.IsPrimary); var vehicles = await _context.Vehicles .Where(v => v.CustomerId == sourceId) .ToListAsync(); foreach (var v in vehicles) { v.CustomerId = targetId; if (targetHasPrimary && v.IsPrimary) v.IsPrimary = false; v.UpdatedAt = now; } result.VehiclesMoved = vehicles.Count; // Tire stacks (including history / picked-up ones) var stacks = await _context.TireStacks .Where(ts => ts.CustomerId == sourceId) .ToListAsync(); foreach (var ts in stacks) { ts.CustomerId = targetId; ts.UpdatedAt = now; } result.TireStacksMoved = stacks.Count; // Kanban task links (composite key TaskId + CustomerId -> recreate) var taskLinks = await _context.KanbanTaskCustomers .Where(tc => tc.CustomerId == sourceId) .ToListAsync(); if (taskLinks.Count > 0) { var taskIds = taskLinks.Select(tc => tc.TaskId).ToList(); var existingTargetTaskIds = (await _context.KanbanTaskCustomers .Where(tc => tc.CustomerId == targetId && taskIds.Contains(tc.TaskId)) .Select(tc => tc.TaskId) .ToListAsync()).ToHashSet(); foreach (var link in taskLinks) { if (!existingTargetTaskIds.Contains(link.TaskId)) { _context.KanbanTaskCustomers.Add(new KanbanTaskCustomer { TaskId = link.TaskId, CustomerId = targetId, VehicleId = link.VehicleId }); result.TasksMoved++; } _context.KanbanTaskCustomers.Remove(link); } } // Fill empty contact data on target from source var targetChanged = false; if (string.IsNullOrWhiteSpace(target.Phone) && !string.IsNullOrWhiteSpace(source.Phone)) { target.Phone = source.Phone; targetChanged = true; } if (string.IsNullOrWhiteSpace(target.Email) && !string.IsNullOrWhiteSpace(source.Email)) { target.Email = source.Email; targetChanged = true; } if (!string.IsNullOrWhiteSpace(source.Notes)) { var mergedNotes = string.IsNullOrWhiteSpace(target.Notes) ? source.Notes : $"{target.Notes}\n[Zusammengeführt aus {source.CustomerNumber}] {source.Notes}"; if (mergedNotes.Length > 500) mergedNotes = mergedNotes[..500]; target.Notes = mergedNotes; targetChanged = true; } if (!target.IsActive) { target.IsActive = true; targetChanged = true; } if (targetChanged) target.UpdatedAt = now; // Persist FK changes first, then hard-delete the source customer await _context.SaveChangesAsync(); _context.Customers.Remove(source); await _context.SaveChangesAsync(); await transaction.CommitAsync(); _logger.LogInformation( "Customer {SourceNumber} merged into {TargetNumber}: {Vehicles} vehicles, {Stacks} tire stacks, {Tasks} task links", source.CustomerNumber, target.CustomerNumber, result.VehiclesMoved, result.TireStacksMoved, result.TasksMoved); return result; }); } }