añadir cosas, apis y ajustes
This commit is contained in:
parent
7c6ed689f6
commit
62bc070115
|
|
@ -19,9 +19,10 @@ public class ClientsController(ClientService clientService) : ControllerBase
|
|||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<ClientDto>>> GetClients(
|
||||
[FromQuery] int limit = 50,
|
||||
[FromQuery][Range(1, int.MaxValue)] int page = 1)
|
||||
[FromQuery][Range(1, int.MaxValue)] int page = 1,
|
||||
[FromQuery] bool supplier = false)
|
||||
{
|
||||
var clients = await clientService.GetClientsAsync(limit, page);
|
||||
var clients = await clientService.GetClientsAsync(limit, page, supplier);
|
||||
return Ok(clients);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ public class DocumentController(DocumentService documentService) : ControllerBas
|
|||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<DocumentItem>>> GetDocuments(
|
||||
[FromQuery] string modulePart = "invoice",
|
||||
[FromQuery] string refId = "")
|
||||
[FromQuery] int id = 0)
|
||||
{
|
||||
if (string.IsNullOrEmpty(refId))
|
||||
return BadRequest("refId parameter is required");
|
||||
if (id <= 0)
|
||||
return BadRequest("id parameter is required");
|
||||
|
||||
var documents = await documentService.GetDocumentsAsync(modulePart, refId);
|
||||
var documents = await documentService.GetDocumentsAsync(modulePart, id);
|
||||
return Ok(documents);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,16 @@ public class InvoicesController(InvoiceService invoiceService) : ControllerBase
|
|||
return Ok(templates);
|
||||
}
|
||||
|
||||
[HttpGet("templates/{id:int}")]
|
||||
[ProducesResponseType(typeof(InvoiceDetailDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<InvoiceDetailDto>> GetInvoiceTemplate([Range(1, int.MaxValue)] int id)
|
||||
{
|
||||
var template = await invoiceService.GetInvoiceTemplateAsync(id);
|
||||
return Ok(template);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/payments")]
|
||||
[ProducesResponseType(typeof(List<InvoicePaymentDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
using DoliMiddlewareApi.Services.Notifications;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DoliMiddlewareApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class SettingsController(WebhookSettings webhookSettings) : ControllerBase
|
||||
{
|
||||
[HttpGet("webhook")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
public IActionResult GetWebhook()
|
||||
{
|
||||
return Ok(new { url = webhookSettings.WebhookUrl });
|
||||
}
|
||||
|
||||
[HttpPut("webhook")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public IActionResult SetWebhook([FromBody] SetWebhookDto dto)
|
||||
{
|
||||
webhookSettings.WebhookUrl = string.IsNullOrWhiteSpace(dto.Url) ? null : dto.Url.Trim();
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
public class SetWebhookDto
|
||||
{
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using DoliMiddlewareApi.Dtos;
|
||||
using DoliMiddlewareApi.Dtos.command;
|
||||
using DoliMiddlewareApi.Dtos.query;
|
||||
using DoliMiddlewareApi.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DoliMiddlewareApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class SupplierInvoicesController(SupplierInvoiceService service) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<SupplierInvoiceDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<SupplierInvoiceDto>>> GetInvoices(
|
||||
[FromQuery] int limit = 50,
|
||||
[FromQuery] [Range(1, int.MaxValue)] int page = 1,
|
||||
[FromQuery] string? status = null)
|
||||
{
|
||||
var invoices = await service.GetInvoicesAsync(limit, page, status);
|
||||
return Ok(invoices);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(int), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<int>> CreateInvoice([FromBody] CreateSupplierInvoiceDto dto)
|
||||
{
|
||||
var id = await service.CreateInvoiceAsync(dto);
|
||||
return CreatedAtAction(nameof(GetInvoice), new { id }, id);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(SupplierInvoiceDetailDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<SupplierInvoiceDetailDto>> GetInvoice([Range(1, int.MaxValue)] int id)
|
||||
{
|
||||
var invoice = await service.GetInvoiceAsync(id);
|
||||
return Ok(invoice);
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> UpdateInvoice([Range(1, int.MaxValue)] int id, [FromBody] UpdateSupplierInvoiceDto dto)
|
||||
{
|
||||
await service.UpdateInvoiceAsync(id, dto);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> DeleteInvoice([Range(1, int.MaxValue)] int id)
|
||||
{
|
||||
await service.DeleteInvoiceAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/status")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> ChangeStatus([Range(1, int.MaxValue)] int id, [FromBody] UpdateInvoiceStatusDto dto)
|
||||
{
|
||||
await service.ChangeStatusAsync(id, dto.Status);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/lines")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<string>> AddLine([Range(1, int.MaxValue)] int id, [FromBody] CreateInvoiceLineDto dto)
|
||||
{
|
||||
var result = await service.AddLineAsync(id, dto);
|
||||
return StatusCode(StatusCodes.Status201Created, result);
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}/lines/{lineId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> UpdateLine([Range(1, int.MaxValue)] int id, [Range(1, int.MaxValue)] int lineId, [FromBody] UpdateInvoiceLineDto dto)
|
||||
{
|
||||
await service.UpdateLineAsync(id, lineId, dto);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}/lines/{lineId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> DeleteLine([Range(1, int.MaxValue)] int id, [Range(1, int.MaxValue)] int lineId)
|
||||
{
|
||||
await service.DeleteLineAsync(id, lineId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/payments")]
|
||||
[ProducesResponseType(typeof(List<InvoicePaymentDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<InvoicePaymentDto>>> GetPayments([Range(1, int.MaxValue)] int id)
|
||||
{
|
||||
var payments = await service.GetPaymentsAsync(id);
|
||||
return Ok(payments);
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/payments")]
|
||||
[ProducesResponseType(typeof(int), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<int>> AddPayment([Range(1, int.MaxValue)] int id, [FromBody] CreateInvoicePaymentDto dto)
|
||||
{
|
||||
var paymentId = await service.AddPaymentAsync(id, dto);
|
||||
return StatusCode(StatusCodes.Status201Created, paymentId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace DoliMiddlewareApi.Dtos.Dolibarr;
|
||||
|
||||
public class SupplierInvoiceDetailResponse : SupplierInvoiceResponse
|
||||
{
|
||||
public List<InvoiceLineResponse>? Lines { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
namespace DoliMiddlewareApi.Dtos.Dolibarr;
|
||||
|
||||
public class SupplierInvoiceResponse
|
||||
{
|
||||
public string? id { get; set; }
|
||||
public string? @ref { get; set; }
|
||||
public string? ref_supplier { get; set; }
|
||||
public long? date { get; set; }
|
||||
public long? date_lim_reglement { get; set; }
|
||||
public string? socid { get; set; }
|
||||
public string? total_ht { get; set; }
|
||||
public string? total_tva { get; set; }
|
||||
public string? total_ttc { get; set; }
|
||||
public string? remaintopay { get; set; }
|
||||
public string? statut { get; set; }
|
||||
public string? note_public { get; set; }
|
||||
public string? note_private { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
namespace DoliMiddlewareApi.Dtos.Dolibarr;
|
||||
|
||||
public class TemplateInvoiceLineResponse
|
||||
{
|
||||
public int id { get; set; }
|
||||
public string? description { get; set; }
|
||||
public decimal qty { get; set; }
|
||||
public decimal subprice { get; set; }
|
||||
public decimal tva_tx { get; set; }
|
||||
public decimal total_ht { get; set; }
|
||||
public decimal total_tva { get; set; }
|
||||
public decimal total_ttc { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
namespace DoliMiddlewareApi.Dtos.Dolibarr;
|
||||
|
||||
public class TemplateInvoiceResponse
|
||||
{
|
||||
public int id { get; set; }
|
||||
public string? @ref { get; set; }
|
||||
public int socid { get; set; }
|
||||
public decimal total_ht { get; set; }
|
||||
public decimal total_tva { get; set; }
|
||||
public decimal total_ttc { get; set; }
|
||||
public string? note_public { get; set; }
|
||||
public string? note_private { get; set; }
|
||||
public List<TemplateInvoiceLineResponse>? lines { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DoliMiddlewareApi.Dtos.command;
|
||||
|
||||
public class CreateSupplierInvoiceDto
|
||||
{
|
||||
[Required, Range(1, int.MaxValue)]
|
||||
public int SupplierId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string SupplierRef { get; set; } = "";
|
||||
|
||||
[Required]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
public DateTime? ExpireDate { get; set; }
|
||||
|
||||
public string? NotePublic { get; set; }
|
||||
public string? NotePrivate { get; set; }
|
||||
|
||||
[Required, MinLength(1)]
|
||||
public List<CreateInvoiceLineDto> Lines { get; set; } = [];
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DoliMiddlewareApi.Dtos.command;
|
||||
|
||||
public class UpdateSupplierInvoiceDto
|
||||
{
|
||||
public DateTime? ExpireDate { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string? SupplierRef { get; set; }
|
||||
|
||||
public string? NotePublic { get; set; }
|
||||
public string? NotePrivate { get; set; }
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ public class InvoiceDetailDto
|
|||
public DateTime? Date { get; set; }
|
||||
public DateTime? ExpireDate { get; set; }
|
||||
public int ClientId { get; set; }
|
||||
public decimal? TotalHt { get; set; }
|
||||
public decimal? TotalTax { get; set; }
|
||||
public decimal? Total { get; set; }
|
||||
public decimal? RemainToPay { get; set; }
|
||||
public string Status { get; set; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace DoliMiddlewareApi.Dtos;
|
||||
|
||||
public class SupplierInvoiceDetailDto : SupplierInvoiceDto
|
||||
{
|
||||
public List<InvoiceLineDto>? Lines { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
namespace DoliMiddlewareApi.Dtos;
|
||||
|
||||
public class SupplierInvoiceDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? Number { get; set; }
|
||||
public string? SupplierRef { get; set; }
|
||||
public DateTime? Date { get; set; }
|
||||
public DateTime? ExpireDate { get; set; }
|
||||
public int SupplierId { get; set; }
|
||||
public string? SupplierName { get; set; }
|
||||
public decimal? TotalHt { get; set; }
|
||||
public decimal? TotalTax { get; set; }
|
||||
public decimal? Total { get; set; }
|
||||
public decimal? RemainToPay { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public string? NotePublic { get; set; }
|
||||
public string? NotePrivate { get; set; }
|
||||
}
|
||||
|
|
@ -60,6 +60,8 @@ public static class InvoiceMapper
|
|||
Date = baseDto.Date,
|
||||
ExpireDate = baseDto.ExpireDate,
|
||||
ClientId = baseDto.ClientId,
|
||||
TotalHt = baseDto.TotalHt,
|
||||
TotalTax = baseDto.TotalTax,
|
||||
Total = baseDto.Total,
|
||||
RemainToPay = baseDto.RemainToPay,
|
||||
Status = baseDto.Status,
|
||||
|
|
@ -121,6 +123,41 @@ public static class InvoiceMapper
|
|||
}
|
||||
|
||||
|
||||
public static InvoiceDto MapTemplateToInvoiceDto(TemplateInvoiceResponse r) => new()
|
||||
{
|
||||
Id = r.id,
|
||||
Number = r.@ref ?? "SIN-REF",
|
||||
ClientId = r.socid,
|
||||
TotalHt = Math.Round(r.total_ht, 2),
|
||||
TotalTax = Math.Round(r.total_tva, 2),
|
||||
Total = Math.Round(r.total_ttc, 2),
|
||||
Status = "template",
|
||||
NotePublic = r.note_public,
|
||||
NotePrivate = r.note_private
|
||||
};
|
||||
|
||||
public static InvoiceDetailDto MapTemplateToDetailDto(TemplateInvoiceResponse r) => new()
|
||||
{
|
||||
Id = r.id,
|
||||
Number = r.@ref ?? "SIN-REF",
|
||||
ClientId = r.socid,
|
||||
TotalHt = Math.Round(r.total_ht, 2),
|
||||
TotalTax = Math.Round(r.total_tva, 2),
|
||||
Total = Math.Round(r.total_ttc, 2),
|
||||
Status = "template",
|
||||
NotePublic = r.note_public,
|
||||
NotePrivate = r.note_private,
|
||||
Lines = r.lines?.Select(l => new InvoiceLineDto
|
||||
{
|
||||
Id = l.id,
|
||||
Description = l.description ?? "",
|
||||
Quantity = l.qty,
|
||||
UnitPrice = Math.Round(l.subprice, 2),
|
||||
TaxRate = Math.Round(l.tva_tx, 2),
|
||||
Total = Math.Round(l.total_ttc, 2)
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
public static InvoicePaymentDto MapToInvoicePaymentDto(InvoicePaymentResponse response)
|
||||
{
|
||||
return new InvoicePaymentDto
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
using System.Globalization;
|
||||
using DoliMiddlewareApi.Dtos;
|
||||
using DoliMiddlewareApi.Dtos.Dolibarr;
|
||||
|
||||
namespace DoliMiddlewareApi.Mappers;
|
||||
|
||||
public static class SupplierInvoiceMapper
|
||||
{
|
||||
public static SupplierInvoiceDto MapToDto(SupplierInvoiceResponse r)
|
||||
{
|
||||
return new SupplierInvoiceDto
|
||||
{
|
||||
Id = int.TryParse(r.id, out int id) ? id : 0,
|
||||
Number = r.@ref ?? "SIN-REF",
|
||||
SupplierRef = r.ref_supplier,
|
||||
|
||||
Date = r.date.HasValue
|
||||
? DateTimeOffset.FromUnixTimeSeconds(r.date.Value).DateTime
|
||||
: null,
|
||||
ExpireDate = r.date_lim_reglement.HasValue
|
||||
? DateTimeOffset.FromUnixTimeSeconds(r.date_lim_reglement.Value).DateTime
|
||||
: null,
|
||||
|
||||
SupplierId = int.TryParse(r.socid, out int supplierId) ? supplierId : 0,
|
||||
|
||||
TotalHt = Parse(r.total_ht),
|
||||
TotalTax = Parse(r.total_tva),
|
||||
Total = Parse(r.total_ttc),
|
||||
RemainToPay = Parse(r.remaintopay),
|
||||
|
||||
Status = ConvertStatus(r.statut),
|
||||
NotePublic = r.note_public,
|
||||
NotePrivate = r.note_private
|
||||
};
|
||||
}
|
||||
|
||||
public static SupplierInvoiceDetailDto MapToDetailDto(SupplierInvoiceDetailResponse r)
|
||||
{
|
||||
var base_ = MapToDto(r);
|
||||
return new SupplierInvoiceDetailDto
|
||||
{
|
||||
Id = base_.Id,
|
||||
Number = base_.Number,
|
||||
SupplierRef = base_.SupplierRef,
|
||||
Date = base_.Date,
|
||||
ExpireDate = base_.ExpireDate,
|
||||
SupplierId = base_.SupplierId,
|
||||
TotalHt = base_.TotalHt,
|
||||
TotalTax = base_.TotalTax,
|
||||
Total = base_.Total,
|
||||
RemainToPay = base_.RemainToPay,
|
||||
Status = base_.Status,
|
||||
NotePublic = base_.NotePublic,
|
||||
NotePrivate = base_.NotePrivate,
|
||||
Lines = r.Lines?.Select(InvoiceMapper.MapToInvoiceLineDto).ToList() ?? []
|
||||
};
|
||||
}
|
||||
|
||||
private static decimal? Parse(string? value) =>
|
||||
decimal.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal d)
|
||||
? Math.Round(d, 2)
|
||||
: null;
|
||||
|
||||
private static string ConvertStatus(string? statut) => statut switch
|
||||
{
|
||||
"0" => "draft",
|
||||
"1" => "unpaid",
|
||||
"2" => "paid",
|
||||
"3" => "cancelled",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
|
|
@ -166,6 +166,9 @@ builder.Services.AddScoped<SetupService>();
|
|||
// Servicio de negocio (banco)
|
||||
builder.Services.AddScoped<BankService>();
|
||||
|
||||
// Servicio de facturas de proveedores
|
||||
builder.Services.AddScoped<SupplierInvoiceService>();
|
||||
|
||||
// Servicio de aplicación (orquesta login + cache)
|
||||
builder.Services.AddScoped<AuthApplicationService>();
|
||||
|
||||
|
|
@ -190,7 +193,9 @@ builder.Services.AddHttpClient("Webhook");
|
|||
// NOTIFICACIONES — Webhook (Teams / Slack)
|
||||
// Configura Notifications:WebhookUrl en appsettings o variable de entorno
|
||||
// Si no está configurado, las notificaciones se ignoran silenciosamente
|
||||
// WebhookSettings es singleton para permitir actualización en tiempo de ejecución
|
||||
// =========================================
|
||||
builder.Services.AddSingleton<WebhookSettings>();
|
||||
builder.Services.AddScoped<INotificationService, WebhookNotificationService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ namespace DoliMiddlewareApi.Services;
|
|||
|
||||
public class ClientService(IDolibarrApiClient apiClient)
|
||||
{
|
||||
public async Task<List<ClientDto>> GetClientsAsync(int limit = 50, int page = 1)
|
||||
public async Task<List<ClientDto>> GetClientsAsync(int limit = 50, int page = 1, bool supplier = false)
|
||||
{
|
||||
var endpoint = $"thirdparties?limit={limit}&page={page - 1}";
|
||||
if (supplier) endpoint += "&mode=4";
|
||||
|
||||
var clients = await apiClient.GetCollectionAsync<ClientResponse>(endpoint);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,37 +30,52 @@ public class DocumentService(IDolibarrApiClient apiClient)
|
|||
return (bytes, result.filename);
|
||||
}
|
||||
|
||||
public async Task<List<DocumentItem>> GetDocumentsAsync(string modulePart, string refOrSocid)
|
||||
public async Task<List<DocumentItem>> GetDocumentsAsync(string modulePart, int id)
|
||||
{
|
||||
var endpoint = $"documents?modulepart={modulePart}&ref={Uri.EscapeDataString(refOrSocid)}";
|
||||
var responseString = await apiClient.GetAsyncRaw(endpoint);
|
||||
|
||||
var response = JsonSerializer.Deserialize<List<Dictionary<string, JsonElement>>>(responseString);
|
||||
|
||||
if (response == null)
|
||||
return new List<DocumentItem>();
|
||||
|
||||
return response.Select(d =>
|
||||
try
|
||||
{
|
||||
var name = d.TryGetValue("name", out var nameProp) && nameProp.ValueKind == JsonValueKind.String
|
||||
? nameProp.GetString() : null;
|
||||
var path = d.TryGetValue("path", out var pathProp) && pathProp.ValueKind == JsonValueKind.String
|
||||
? pathProp.GetString() : null;
|
||||
var size = d.TryGetValue("size", out var sizeProp) && sizeProp.ValueKind == JsonValueKind.Number
|
||||
? sizeProp.GetInt64() : 0;
|
||||
var endpoint = $"documents?modulepart={modulePart}&id={id}";
|
||||
var responseString = await apiClient.GetAsyncRaw(endpoint);
|
||||
|
||||
return new DocumentItem
|
||||
var response = JsonSerializer.Deserialize<List<Dictionary<string, JsonElement>>>(responseString);
|
||||
if (response == null) return [];
|
||||
|
||||
return response.Select(d =>
|
||||
{
|
||||
Name = name,
|
||||
Path = path,
|
||||
Size = size
|
||||
};
|
||||
}).ToList();
|
||||
// Dolibarr's ECM array_merge overwrites 'name' with null (inherited CommonObject property).
|
||||
// Fall back to 'filename' which the ECM record keeps correctly.
|
||||
var name = d.TryGetValue("name", out var nameProp) && nameProp.ValueKind == JsonValueKind.String
|
||||
? nameProp.GetString() : null;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
name = d.TryGetValue("filename", out var fnProp) && fnProp.ValueKind == JsonValueKind.String
|
||||
? fnProp.GetString() : null;
|
||||
var path = d.TryGetValue("path", out var pathProp) && pathProp.ValueKind == JsonValueKind.String
|
||||
? pathProp.GetString() : null;
|
||||
var size = d.TryGetValue("size", out var sizeProp) && sizeProp.ValueKind == JsonValueKind.Number
|
||||
? sizeProp.GetInt64() : 0;
|
||||
|
||||
// level1name is the invoice ref directory (e.g. "IN2605-0001").
|
||||
// Dolibarr's download endpoint needs a relative path like "IN2605-0001/IN2605-0001.pdf",
|
||||
// not the absolute filesystem path stored in 'path'.
|
||||
var level1name = d.TryGetValue("level1name", out var l1Prop) && l1Prop.ValueKind == JsonValueKind.String
|
||||
? l1Prop.GetString() : null;
|
||||
var relativePath = (!string.IsNullOrEmpty(level1name) && !string.IsNullOrEmpty(name))
|
||||
? $"{level1name}/{name}"
|
||||
: name;
|
||||
|
||||
return new DocumentItem { Name = name, Path = path, Size = size, RelativePath = relativePath };
|
||||
}).ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Module may not be active in Dolibarr — return empty list gracefully
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(byte[] content, string filename, string contentType)> DownloadDocumentAsync(string modulePart, string fileRef)
|
||||
{
|
||||
var endpoint = $"documents/download?modulepart={modulePart}&file={Uri.EscapeDataString(fileRef)}";
|
||||
var endpoint = $"documents/download?modulepart={modulePart}&original_file={Uri.EscapeDataString(fileRef)}";
|
||||
var responseString = await apiClient.GetAsyncRaw(endpoint);
|
||||
|
||||
var response = JsonSerializer.Deserialize<DolibarrPdfResponse>(responseString);
|
||||
|
|
@ -82,5 +97,6 @@ public class DocumentItem
|
|||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Path { get; set; }
|
||||
public string? RelativePath { get; set; }
|
||||
public long Size { get; set; }
|
||||
}
|
||||
|
|
@ -182,14 +182,51 @@ public class InvoiceService(IDolibarrApiClient apiClient, INotificationService n
|
|||
|
||||
public async Task<List<InvoiceDto>> GetInvoiceTemplatesAsync()
|
||||
{
|
||||
var dataList = await apiClient.GetCollectionAsync<InvoiceResponse>("invoices/templates");
|
||||
return dataList.Select(InvoiceMapper.MapToInvoiceDto).ToList();
|
||||
// Dolibarr has no list endpoint for templates — only GET invoices/templates/{id}.
|
||||
// FactureRec serializes numeric fields as JSON numbers (not strings), so we use
|
||||
// TemplateInvoiceResponse with proper int/decimal types.
|
||||
// Non-existent IDs return HTTP 200 with id=0 (Dolibarr quirk), so we filter by id > 0.
|
||||
const int maxId = 50;
|
||||
var tasks = Enumerable.Range(1, maxId).Select(async id =>
|
||||
{
|
||||
try { return await apiClient.GetResourceAsync<TemplateInvoiceResponse>($"invoices/templates/{id}"); }
|
||||
catch { return null; }
|
||||
});
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
var dtos = results
|
||||
.Where(r => r is { id: > 0 })
|
||||
.Select(r => InvoiceMapper.MapTemplateToInvoiceDto(r!))
|
||||
.ToList();
|
||||
|
||||
if (dtos.Count == 0) return dtos;
|
||||
|
||||
var clientIds = dtos.Select(d => d.ClientId).Distinct().ToList();
|
||||
var names = await GetClientNamesAsync(clientIds);
|
||||
foreach (var dto in dtos)
|
||||
dto.ClientName = names.GetValueOrDefault(dto.ClientId);
|
||||
|
||||
return dtos;
|
||||
}
|
||||
|
||||
public async Task<InvoiceDetailDto> GetInvoiceTemplateAsync(int id)
|
||||
{
|
||||
var data = await apiClient.GetResourceAsync<TemplateInvoiceResponse>($"invoices/templates/{id}");
|
||||
if (data.id == 0) throw new NotFoundException($"Template invoice {id} not found");
|
||||
return InvoiceMapper.MapTemplateToDetailDto(data);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<int, string>> GetClientNamesAsync(List<int> clientIds)
|
||||
{
|
||||
var uniqueIds = clientIds.Distinct().ToList();
|
||||
var tasks = uniqueIds.Select(id => apiClient.GetResourceAsync<ClientResponse>($"thirdparties/{id}"));
|
||||
var validIds = clientIds.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 clients = await Task.WhenAll(tasks);
|
||||
return clients.Where(c => c is { id: not null })
|
||||
.ToDictionary(c => int.Parse(c!.id!), c => c!.name ?? "");
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace DoliMiddlewareApi.Services.Notifications;
|
|||
/// </summary>
|
||||
public class WebhookNotificationService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IConfiguration configuration,
|
||||
WebhookSettings webhookSettings,
|
||||
ILogger<WebhookNotificationService> logger) : INotificationService
|
||||
{
|
||||
private static readonly Dictionary<string, (string label, string color)> StatusMeta = new()
|
||||
|
|
@ -22,7 +22,7 @@ public class WebhookNotificationService(
|
|||
|
||||
public async Task NotifyInvoiceStatusChangedAsync(int invoiceId, string invoiceRef, string newStatus)
|
||||
{
|
||||
var webhookUrl = configuration["Notifications:WebhookUrl"];
|
||||
var webhookUrl = webhookSettings.WebhookUrl;
|
||||
if (string.IsNullOrWhiteSpace(webhookUrl))
|
||||
return; // Notificaciones desactivadas — no hay webhook configurado
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ public class WebhookNotificationService(
|
|||
/// </summary>
|
||||
private object BuildPayload(int invoiceId, string invoiceRef, string label, string color)
|
||||
{
|
||||
var webhookUrl = configuration["Notifications:WebhookUrl"] ?? "";
|
||||
var webhookUrl = webhookSettings.WebhookUrl ?? "";
|
||||
|
||||
if (webhookUrl.Contains("slack.com"))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
namespace DoliMiddlewareApi.Services.Notifications;
|
||||
|
||||
public class WebhookSettings
|
||||
{
|
||||
public string? WebhookUrl { get; set; }
|
||||
|
||||
public WebhookSettings(IConfiguration configuration)
|
||||
{
|
||||
WebhookUrl = configuration["Notifications:WebhookUrl"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
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;
|
||||
|
||||
namespace DoliMiddlewareApi.Services;
|
||||
|
||||
public class SupplierInvoiceService(IDolibarrApiClient apiClient)
|
||||
{
|
||||
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();
|
||||
var endpoint = normalized switch
|
||||
{
|
||||
"draft" => $"supplierinvoices/{id}/settodraft",
|
||||
"unpaid" => $"supplierinvoices/{id}/validate",
|
||||
"paid" => $"supplierinvoices/{id}/setpaid",
|
||||
_ => throw new ValidationException("Estado invalido. Usa: draft, unpaid, paid.")
|
||||
};
|
||||
|
||||
await apiClient.PostAsync(endpoint, new { });
|
||||
}
|
||||
|
||||
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 ?? "");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue