221 lines
9.6 KiB
C#
221 lines
9.6 KiB
C#
|
|
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<List<SupplierInvoiceDto>> 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<SupplierInvoiceResponse>(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<SupplierInvoiceDetailDto> GetInvoiceAsync(int id)
|
||
|
|
{
|
||
|
|
var data = await apiClient.GetResourceAsync<SupplierInvoiceDetailResponse>($"supplierinvoices/{id}");
|
||
|
|
var dto = SupplierInvoiceMapper.MapToDetailDto(data);
|
||
|
|
|
||
|
|
if (dto.SupplierId > 0)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var supplier = await apiClient.GetResourceAsync<ClientResponse>($"thirdparties/{dto.SupplierId}");
|
||
|
|
dto.SupplierName = supplier?.name;
|
||
|
|
}
|
||
|
|
catch { /* supplier lookup is best-effort */ }
|
||
|
|
}
|
||
|
|
|
||
|
|
return dto;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<int> 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<SupplierInvoiceDetailResponse>($"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<SupplierInvoiceDetailResponse>($"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<SupplierInvoiceDetailResponse>($"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<string, object>
|
||
|
|
{
|
||
|
|
["datepaye"] = unixNow,
|
||
|
|
["payment_mode_id"] = 6,
|
||
|
|
["closepaidinvoices"] = "yes",
|
||
|
|
["accountid"] = 1,
|
||
|
|
["amount"] = amount
|
||
|
|
};
|
||
|
|
await apiClient.PostAsync($"supplierinvoices/{id}/payments", payBody);
|
||
|
|
var paidInv = await apiClient.GetResourceAsync<SupplierInvoiceDetailResponse>($"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<SupplierInvoiceDetailResponse>($"supplierinvoices/{id}");
|
||
|
|
await notifications.NotifyInvoiceStatusChangedAsync(id, updatedInv.@ref ?? $"#{id}", normalized);
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<string> AddLineAsync(int invoiceId, CreateInvoiceLineDto dto)
|
||
|
|
{
|
||
|
|
var invoice = await apiClient.GetResourceAsync<SupplierInvoiceDetailResponse>($"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<SupplierInvoiceDetailResponse>($"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<string, object?>
|
||
|
|
{
|
||
|
|
["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<SupplierInvoiceDetailResponse>($"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<List<InvoicePaymentDto>> GetPaymentsAsync(int invoiceId)
|
||
|
|
{
|
||
|
|
var dataList = await apiClient.GetCollectionAsync<InvoicePaymentResponse>($"supplierinvoices/{invoiceId}/payments");
|
||
|
|
return dataList.Select(InvoiceMapper.MapToInvoicePaymentDto).ToList();
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<int> 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<Dictionary<int, string>> GetSupplierNamesAsync(List<int> 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<ClientResponse>($"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 ?? "");
|
||
|
|
}
|
||
|
|
}
|