Files

162 lines
6.6 KiB
C#

using Lagerbestand.Data;
using Lagerbestand.Models;
using Microsoft.EntityFrameworkCore;
namespace Lagerbestand.Services;
/// <summary>
/// Result of merging a local customer into a Lexware customer.
/// </summary>
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
{
/// <summary>
/// Local customers are created by this app and use the "K-" number prefix.
/// Lexware customers carry the numeric Lexware customer number.
/// </summary>
bool IsLocalCustomer(Customer customer);
/// <summary>
/// Moves all vehicles, tire stacks and task links from a locally created customer
/// to an existing Lexware customer and permanently deletes the local customer.
/// </summary>
Task<CustomerMergeResult> MergeAsync(Guid sourceId, Guid targetId);
}
public class CustomerMergeService : ICustomerMergeService
{
private readonly AppDbContext _context;
private readonly ILogger<CustomerMergeService> _logger;
public CustomerMergeService(AppDbContext context, ILogger<CustomerMergeService> 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<CustomerMergeResult> 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;
});
}
}