162 lines
6.6 KiB
C#
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;
|
|
});
|
|
}
|
|
}
|