using System.Net.Http.Headers; using System.Text.Json; using System.Text.Json.Serialization; using Lagerbestand.Data; using Lagerbestand.Models; using Microsoft.EntityFrameworkCore; namespace Lagerbestand.Services; /// /// Result of a Lexware customer sync operation /// public class LexwareSyncResult { public bool Success { get; set; } public string Message { get; set; } = string.Empty; public int CustomersCreated { get; set; } public int CustomersUpdated { get; set; } public int CustomersDeactivated { get; set; } public int TotalProcessed { get; set; } } /// /// Lexware API contact response models /// public class LexwareContactsResponse { [JsonPropertyName("content")] public List Content { get; set; } = new(); [JsonPropertyName("totalPages")] public int TotalPages { get; set; } [JsonPropertyName("totalElements")] public int TotalElements { get; set; } [JsonPropertyName("last")] public bool Last { get; set; } [JsonPropertyName("number")] public int Number { get; set; } } public class LexwareContact { [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; [JsonPropertyName("organizationId")] public string? OrganizationId { get; set; } [JsonPropertyName("version")] public int Version { get; set; } [JsonPropertyName("roles")] public LexwareRoles? Roles { get; set; } [JsonPropertyName("company")] public LexwareCompany? Company { get; set; } [JsonPropertyName("person")] public LexwarePerson? Person { get; set; } [JsonPropertyName("addresses")] public LexwareAddresses? Addresses { get; set; } [JsonPropertyName("emailAddresses")] public LexwareEmailAddresses? EmailAddresses { get; set; } [JsonPropertyName("phoneNumbers")] public LexwarePhoneNumbers? PhoneNumbers { get; set; } [JsonPropertyName("note")] public string? Note { get; set; } [JsonPropertyName("archived")] public bool Archived { get; set; } } public class LexwareRoles { [JsonPropertyName("customer")] public LexwareCustomerRole? Customer { get; set; } } public class LexwareCustomerRole { [JsonPropertyName("number")] public long? Number { get; set; } } public class LexwareCompany { [JsonPropertyName("name")] public string? Name { get; set; } } public class LexwarePerson { [JsonPropertyName("salutation")] public string? Salutation { get; set; } [JsonPropertyName("firstName")] public string? FirstName { get; set; } [JsonPropertyName("lastName")] public string? LastName { get; set; } } public class LexwareAddresses { [JsonPropertyName("billing")] public List? Billing { get; set; } } public class LexwareAddress { [JsonPropertyName("street")] public string? Street { get; set; } [JsonPropertyName("zip")] public string? Zip { get; set; } [JsonPropertyName("city")] public string? City { get; set; } [JsonPropertyName("countryCode")] public string? CountryCode { get; set; } } public class LexwareEmailAddresses { [JsonPropertyName("business")] public List? Business { get; set; } [JsonPropertyName("private")] public List? Private { get; set; } [JsonPropertyName("other")] public List? Other { get; set; } } public class LexwarePhoneNumbers { [JsonPropertyName("business")] public List? Business { get; set; } [JsonPropertyName("private")] public List? Private { get; set; } [JsonPropertyName("mobile")] public List? Mobile { get; set; } } public interface ILexwareSyncService { /// /// Tests the API connection with the given settings /// Task<(bool Success, string Message)> TestConnectionAsync(LexwareApiSettings settings); /// /// Syncs customers from Lexware API to the local database /// Task SyncCustomersAsync(CancellationToken cancellationToken = default); } public class LexwareSyncService : ILexwareSyncService { private readonly AppDbContext _context; private readonly ISiteSettingsService _siteSettingsService; private readonly ICustomerService _customerService; private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; public LexwareSyncService( AppDbContext context, ISiteSettingsService siteSettingsService, ICustomerService customerService, ILogger logger, IHttpClientFactory httpClientFactory) { _context = context; _siteSettingsService = siteSettingsService; _customerService = customerService; _logger = logger; _httpClientFactory = httpClientFactory; } public async Task<(bool Success, string Message)> TestConnectionAsync(LexwareApiSettings settings) { if (!settings.IsConfigured) return (false, "API URL und API-Key müssen konfiguriert sein."); try { using var client = CreateHttpClient(settings); var response = await client.GetAsync("contacts?page=0&size=1"); if (response.IsSuccessStatusCode) { var json = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize(json); return (true, $"Verbindung erfolgreich! {result?.TotalElements ?? 0} Kontakte gefunden."); } return response.StatusCode switch { System.Net.HttpStatusCode.Unauthorized => (false, "Authentifizierung fehlgeschlagen. Bitte API-Key prüfen."), System.Net.HttpStatusCode.Forbidden => (false, "Zugriff verweigert. API-Key hat keine ausreichenden Berechtigungen."), System.Net.HttpStatusCode.NotFound => (false, "API-Endpunkt nicht gefunden. Bitte URL prüfen."), _ => (false, $"API-Fehler: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}") }; } catch (HttpRequestException ex) { return (false, $"Verbindungsfehler: {ex.Message}"); } catch (TaskCanceledException) { return (false, "Zeitüberschreitung bei der Verbindung."); } catch (Exception ex) { return (false, $"Fehler: {ex.Message}"); } } public async Task SyncCustomersAsync(CancellationToken cancellationToken = default) { var settings = await _siteSettingsService.GetLexwareApiSettingsAsync(); if (!settings.IsEnabled || !settings.IsConfigured) { return new LexwareSyncResult { Success = false, Message = "Lexware API ist nicht konfiguriert oder deaktiviert." }; } var result = new LexwareSyncResult(); try { _logger.LogInformation("Starting Lexware customer sync..."); using var client = CreateHttpClient(settings); var allContacts = new List(); // Fetch all pages of customers var page = 0; bool lastPage = false; while (!lastPage) { cancellationToken.ThrowIfCancellationRequested(); var response = await client.GetAsync($"contacts?page={page}&size=250&customer=true", cancellationToken); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(cancellationToken); var pageResult = JsonSerializer.Deserialize(json); if (pageResult?.Content != null) { // Only include contacts with customer role that are not archived allContacts.AddRange(pageResult.Content.Where(c => !c.Archived)); } lastPage = pageResult?.Last ?? true; page++; } _logger.LogInformation("Fetched {Count} customer contacts from Lexware", allContacts.Count); // Get all existing customers indexed by CustomerNumber (include inactive for reactivation) var existingCustomers = await _context.Customers .Include(c => c.Vehicles.Where(v => v.IsActive)) .ToListAsync(cancellationToken); var customersByNumber = existingCustomers .Where(c => !string.IsNullOrEmpty(c.CustomerNumber)) .ToDictionary(c => c.CustomerNumber, c => c); foreach (var contact in allContacts) { result.TotalProcessed++; var customerNumber = contact.Roles?.Customer?.Number?.ToString() ?? ""; if (string.IsNullOrEmpty(customerNumber)) continue; var firstName = contact.Person?.FirstName?.Trim() ?? ""; var lastName = contact.Person?.LastName?.Trim() ?? ""; // Skip contacts without a name if (string.IsNullOrEmpty(firstName) && string.IsNullOrEmpty(lastName)) { // Use company name as last name fallback if (!string.IsNullOrEmpty(contact.Company?.Name)) { lastName = contact.Company.Name; } else { continue; } } var company = contact.Company?.Name?.Trim(); var phone = GetFirstPhone(contact.PhoneNumbers); var email = GetFirstEmail(contact.EmailAddresses); var notes = contact.Note?.Trim(); if (customersByNumber.TryGetValue(customerNumber, out var existing)) { // Update existing customer bool changed = false; if (existing.FirstName != firstName) { existing.FirstName = firstName; changed = true; } if (existing.LastName != lastName) { existing.LastName = lastName; changed = true; } if (existing.Company != company) { existing.Company = company; changed = true; } if (existing.Phone != phone) { existing.Phone = phone; changed = true; } if (existing.Email != email) { existing.Email = email; changed = true; } if (existing.Notes != notes) { existing.Notes = notes; changed = true; } // Reactivate if previously deactivated if (!existing.IsActive) { existing.IsActive = true; changed = true; } if (changed) { existing.UpdatedAt = DateTime.UtcNow; _context.Customers.Update(existing); result.CustomersUpdated++; } } else { // Create new customer var newCustomer = new Customer { CustomerNumber = customerNumber, FirstName = string.IsNullOrEmpty(firstName) ? "-" : firstName, LastName = string.IsNullOrEmpty(lastName) ? "-" : lastName, Company = company, Phone = phone, Email = email, Notes = notes, IsActive = true, CreatedAt = DateTime.UtcNow }; _context.Customers.Add(newCustomer); result.CustomersCreated++; } } await _context.SaveChangesAsync(cancellationToken); // Deactivate customers not found in Lexware (Lexware is single source of truth) var lexwareCustomerNumbers = allContacts .Select(c => c.Roles?.Customer?.Number?.ToString()) .Where(n => !string.IsNullOrEmpty(n)) .ToHashSet(); var customersToDeactivate = await _context.Customers .Where(c => c.IsActive && !string.IsNullOrEmpty(c.CustomerNumber) && !lexwareCustomerNumbers.Contains(c.CustomerNumber)) .ToListAsync(cancellationToken); foreach (var customer in customersToDeactivate) { customer.IsActive = false; customer.UpdatedAt = DateTime.UtcNow; result.CustomersDeactivated++; _logger.LogWarning("Customer {CustomerNumber} ({Name}) deactivated - not found in Lexware", customer.CustomerNumber, customer.FullName); } if (customersToDeactivate.Count > 0) await _context.SaveChangesAsync(cancellationToken); // Update sync timestamp settings.LastSyncAt = DateTime.UtcNow; settings.LastSyncCount = result.TotalProcessed; settings.LastSyncResult = $"{result.CustomersCreated} erstellt, {result.CustomersUpdated} aktualisiert, {result.CustomersDeactivated} deaktiviert"; await _siteSettingsService.SaveLexwareApiSettingsAsync(settings); result.Success = true; result.Message = $"Synchronisierung erfolgreich: {result.CustomersCreated} erstellt, {result.CustomersUpdated} aktualisiert, {result.CustomersDeactivated} deaktiviert ({result.TotalProcessed} verarbeitet)"; _logger.LogInformation("Lexware sync completed: {Message}", result.Message); } catch (HttpRequestException ex) { result.Success = false; result.Message = $"API-Verbindungsfehler: {ex.Message}"; _logger.LogError(ex, "Lexware sync failed: HTTP error"); } catch (Exception ex) { result.Success = false; result.Message = $"Synchronisierungsfehler: {ex.Message}"; _logger.LogError(ex, "Lexware sync failed"); } return result; } private HttpClient CreateHttpClient(LexwareApiSettings settings) { var client = _httpClientFactory.CreateClient("LexwareApi"); client.BaseAddress = new Uri(settings.BaseUrl.TrimEnd('/') + "/"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", settings.ApiKey); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.Timeout = TimeSpan.FromSeconds(30); return client; } private static string? GetFirstPhone(LexwarePhoneNumbers? phones) { if (phones == null) return null; return phones.Mobile?.FirstOrDefault() ?? phones.Business?.FirstOrDefault() ?? phones.Private?.FirstOrDefault(); } private static string? GetFirstEmail(LexwareEmailAddresses? emails) { if (emails == null) return null; return emails.Business?.FirstOrDefault() ?? emails.Private?.FirstOrDefault() ?? emails.Other?.FirstOrDefault(); } } /// /// Background service that runs daily Lexware customer sync /// public class LexwareSyncBackgroundService : BackgroundService { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; public LexwareSyncBackgroundService(IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Lexware sync background service started"); while (!stoppingToken.IsCancellationRequested) { try { using var scope = _serviceProvider.CreateScope(); var settingsService = scope.ServiceProvider.GetRequiredService(); var settings = await settingsService.GetLexwareApiSettingsAsync(); if (settings is { IsEnabled: true, IsConfigured: true, AutoSyncEnabled: true }) { // Parse target time if (TimeSpan.TryParse(settings.AutoSyncTime, out var targetTime)) { var now = DateTime.UtcNow; var nextRun = now.Date.Add(targetTime); // If we've already passed the target time today, schedule for tomorrow if (now > nextRun) nextRun = nextRun.AddDays(1); var delay = nextRun - now; _logger.LogDebug("Lexware auto-sync scheduled for {NextRun} (in {Delay})", nextRun, delay); await Task.Delay(delay, stoppingToken); if (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Running scheduled Lexware customer sync..."); using var syncScope = _serviceProvider.CreateScope(); var syncService = syncScope.ServiceProvider.GetRequiredService(); var result = await syncService.SyncCustomersAsync(stoppingToken); _logger.LogInformation("Scheduled Lexware sync result: {Message}", result.Message); } } else { // Invalid time format, wait and retry await Task.Delay(TimeSpan.FromHours(1), stoppingToken); } } else { // Not configured or disabled, check again in 15 minutes await Task.Delay(TimeSpan.FromMinutes(15), stoppingToken); } } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { _logger.LogError(ex, "Error in Lexware sync background service"); await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); } } _logger.LogInformation("Lexware sync background service stopped"); } }