using System.ComponentModel.DataAnnotations; using System.Globalization; using DoliMiddlewareApi.Dtos; using DoliMiddlewareApi.Dtos.Dolibarr; using DoliMiddlewareApi.Dtos.command; using DoliMiddlewareApi.Dtos.query; using DoliMiddlewareApi.Exceptions; using DoliMiddlewareApi.Mappers; using DoliMiddlewareApi.Services.Clients; using DoliMiddlewareApi.Services.Notifications; namespace DoliMiddlewareApi.Services; public class SupplierInvoiceService(IDolibarrApiClient apiClient, INotificationService notifications) { public async Task> GetInvoicesAsync(int limit, int page, string? status) { var endpoint = $"supplierinvoices?limit={limit}&page={page - 1}"; if (!string.IsNullOrEmpty(status)) endpoint += $"&status={status}"; var list = await apiClient.GetCollectionAsync(endpoint); var dtos = list.Select(SupplierInvoiceMapper.MapToDto).ToList(); if (dtos.Count == 0) return dtos; var supplierIds = dtos.Select(d => d.SupplierId).Distinct().ToList(); var names = await GetSupplierNamesAsync(supplierIds); foreach (var dto in dtos) dto.SupplierName = names.GetValueOrDefault(dto.SupplierId); return dtos; } public async Task GetInvoiceAsync(int id) { var data = await apiClient.GetResourceAsync($"supplierinvoices/{id}"); var dto = SupplierInvoiceMapper.MapToDetailDto(data); if (dto.SupplierId > 0) { try { var supplier = await apiClient.GetResourceAsync($"thirdparties/{dto.SupplierId}"); dto.SupplierName = supplier?.name; } catch { /* supplier lookup is best-effort */ } } return dto; } public async Task CreateInvoiceAsync(CreateSupplierInvoiceDto dto) { var payload = new { socid = dto.SupplierId.ToString(), ref_supplier = dto.SupplierRef, date = ((DateTimeOffset)dto.Date).ToUnixTimeSeconds().ToString(), date_lim_reglement = dto.ExpireDate.HasValue ? ((DateTimeOffset)dto.ExpireDate.Value).ToUnixTimeSeconds().ToString() : null, note_public = dto.NotePublic, note_private = dto.NotePrivate, lines = dto.Lines.Select(l => new { desc = l.Description, qty = l.Quantity.ToString(CultureInfo.InvariantCulture), subprice = l.UnitPrice.ToString(CultureInfo.InvariantCulture), tva_tx = l.TaxRate.ToString(CultureInfo.InvariantCulture) }).ToArray() }; var result = await apiClient.PostAsync("supplierinvoices", payload); return int.TryParse(result.Trim('"'), out int id) ? id : 0; } public async Task UpdateInvoiceAsync(int id, UpdateSupplierInvoiceDto dto) { var current = await apiClient.GetResourceAsync($"supplierinvoices/{id}"); if (dto.SupplierRef != null) current.ref_supplier = dto.SupplierRef; if (dto.NotePublic != null) current.note_public = dto.NotePublic; if (dto.NotePrivate != null) current.note_private = dto.NotePrivate; if (dto.ExpireDate.HasValue) current.date_lim_reglement = ((DateTimeOffset)dto.ExpireDate.Value).ToUnixTimeSeconds(); await apiClient.PutAsync($"supplierinvoices/{id}", current); } public async Task DeleteInvoiceAsync(int id) { var invoice = await apiClient.GetResourceAsync($"supplierinvoices/{id}"); if (invoice.statut != "0") throw new ForbiddenException("Solo se pueden eliminar facturas de proveedor en borrador"); await apiClient.DeleteAsync($"supplierinvoices/{id}"); } public async Task ChangeStatusAsync(int id, string status) { var normalized = status.Trim().ToLowerInvariant(); if (normalized == "paid") { var inv = await apiClient.GetResourceAsync($"supplierinvoices/{id}"); var remain = double.TryParse(inv.remaintopay, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var r) ? r : 0; var amount = remain > 0 ? remain : double.TryParse(inv.total_ttc, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var t) ? t : 0; var unixNow = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds(); var payBody = new Dictionary { ["datepaye"] = unixNow, ["payment_mode_id"] = 6, ["closepaidinvoices"] = "yes", ["accountid"] = 1, ["amount"] = amount }; await apiClient.PostAsync($"supplierinvoices/{id}/payments", payBody); var paidInv = await apiClient.GetResourceAsync($"supplierinvoices/{id}"); await notifications.NotifyInvoiceStatusChangedAsync(id, paidInv.@ref ?? $"#{id}", "paid"); return; } var endpoint = normalized switch { "draft" => $"supplierinvoices/{id}/settodraft", "unpaid" => $"supplierinvoices/{id}/validate", _ => throw new ValidationException("Estado invalido. Usa: draft, unpaid, paid.") }; await apiClient.PostAsync(endpoint, new { }); var updatedInv = await apiClient.GetResourceAsync($"supplierinvoices/{id}"); await notifications.NotifyInvoiceStatusChangedAsync(id, updatedInv.@ref ?? $"#{id}", normalized); } public async Task AddLineAsync(int invoiceId, CreateInvoiceLineDto dto) { var invoice = await apiClient.GetResourceAsync($"supplierinvoices/{invoiceId}"); if (invoice.statut != "0") throw new ForbiddenException("Solo se pueden añadir líneas a facturas en borrador"); var requestBody = new { desc = dto.Description, qty = dto.Quantity.ToString(CultureInfo.InvariantCulture), subprice = dto.UnitPrice.ToString(CultureInfo.InvariantCulture), tva_tx = dto.TaxRate.ToString(CultureInfo.InvariantCulture) }; return await apiClient.PostAsync($"supplierinvoices/{invoiceId}/lines", requestBody); } public async Task UpdateLineAsync(int invoiceId, int lineId, UpdateInvoiceLineDto dto) { var invoice = await apiClient.GetResourceAsync($"supplierinvoices/{invoiceId}"); if (invoice.statut != "0") throw new ForbiddenException("Solo se pueden modificar líneas de facturas en borrador"); var existingLine = invoice.Lines?.FirstOrDefault(l => l.id == lineId.ToString()); if (existingLine == null) throw new NotFoundException($"Línea {lineId} no encontrada en factura {invoiceId}"); var requestBody = new Dictionary { ["desc"] = dto.Description ?? existingLine.description ?? existingLine.desc, ["qty"] = dto.Quantity?.ToString(CultureInfo.InvariantCulture) ?? existingLine.qty, ["subprice"] = dto.UnitPrice?.ToString(CultureInfo.InvariantCulture) ?? existingLine.subprice, ["tva_tx"] = dto.TaxRate?.ToString(CultureInfo.InvariantCulture) ?? existingLine.tva_tx }; await apiClient.PutAsync($"supplierinvoices/{invoiceId}/lines/{lineId}", requestBody); } public async Task DeleteLineAsync(int invoiceId, int lineId) { var invoice = await apiClient.GetResourceAsync($"supplierinvoices/{invoiceId}"); if (invoice.statut != "0") throw new ForbiddenException("Solo se pueden eliminar líneas de facturas en borrador"); await apiClient.DeleteAsync($"supplierinvoices/{invoiceId}/lines/{lineId}"); } public async Task> GetPaymentsAsync(int invoiceId) { var dataList = await apiClient.GetCollectionAsync($"supplierinvoices/{invoiceId}/payments"); return dataList.Select(InvoiceMapper.MapToInvoicePaymentDto).ToList(); } public async Task AddPaymentAsync(int invoiceId, CreateInvoicePaymentDto dto) { var paymentModeId = dto.PaymentMethodId ?? dto.PaymentModeId; var requestBody = new { datepaye = dto.PaymentDate.ToString("yyyy-MM-dd"), paymentid = paymentModeId, closepaidinvoices = dto.ClosePaidInvoices, accountid = dto.AccountId, num_payment = dto.PaymentNumber, amount = dto.Amount?.ToString(CultureInfo.InvariantCulture) }; var responseBody = await apiClient.PostAsync($"supplierinvoices/{invoiceId}/payments", requestBody); return int.TryParse(responseBody.Trim('"'), out int result) ? result : 0; } private async Task> GetSupplierNamesAsync(List ids) { var validIds = ids.Distinct().Where(id => id > 0).ToList(); if (validIds.Count == 0) return []; var tasks = validIds.Select(async id => { try { return await apiClient.GetResourceAsync($"thirdparties/{id}"); } catch { return null; } }); var results = await Task.WhenAll(tasks); return results .Where(c => c is { id: not null }) .ToDictionary(c => int.Parse(c!.id!), c => c!.name ?? ""); } }