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 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; }
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);
// Note: customers not present in Lexware are intentionally left untouched.
// The local IsActive flag is managed exclusively within this application.
// Update sync timestamp
settings.LastSyncAt = DateTime.UtcNow;
settings.LastSyncCount = result.TotalProcessed;
settings.LastSyncResult = $"{result.CustomersCreated} erstellt, {result.CustomersUpdated} aktualisiert";
await _siteSettingsService.SaveLexwareApiSettingsAsync(settings);
result.Success = true;
result.Message = $"Synchronisierung erfolgreich: {result.CustomersCreated} erstellt, {result.CustomersUpdated} aktualisiert ({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");
}
}