From fbbbce35746953b925ff8403d3e37c640cd32c47 Mon Sep 17 00:00:00 2001 From: JavMB Date: Fri, 15 May 2026 18:58:35 +0200 Subject: [PATCH] feat: add Contacts & Setup controllers, extend Clients/Invoices/Document controllers - ContactsController: full CRUD (list, get, get by email, create, update, delete) - SetupController: dictionary endpoints (payment-types, countries, civilities, contact-types, payment-terms, company) - ClientsController: added GET/{id}, POST, PUT/{id}, DELETE/{id} - InvoicesController: added PUT/{id}/lines/{lineId}, GET /templates - DocumentController: added GET /list and GET /download endpoints - ClientService: added GetClientAsync, CreateClientAsync, UpdateClientAsync, DeleteClientAsync - InvoiceService: added UpdateInvoiceLineAsync, GetInvoiceTemplatesAsync - DocumentService: added GetDocumentsAsync, DownloadDocumentAsync (using new GetAsyncRaw on IDolibarrApiClient) - ContactService: new service with CRUD operations - SetupService: new service for Dolibarr dictionary data - IDolibarrApiClient: added GetAsyncRaw method for raw string responses - DolibarrApiClient: implemented GetAsyncRaw - JwtTokenProvider: extended token expiry from 30 min to 8 hours - AuthApplicationService: extended Dolibarr token cache from 30 min to 8 hours --- .../Controllers/AuthController.cs | 3 + .../Controllers/ClientsController.cs | 48 ++++++++++- .../Controllers/ContactsController.cs | 78 +++++++++++++++++ .../Controllers/DocumentController.cs | 61 ++++++++++---- .../Controllers/InvoicesController.cs | 63 ++++++++++++++ .../Controllers/SetupController.cs | 67 +++++++++++++++ .../Services/Auth/JwtTokenProvider.cs | 2 +- .../Services/AuthApplicationService.cs | 2 +- DoliMiddlewareApi/Services/ClientService.cs | 80 ++++++++++++++++-- .../Services/Clients/DolibarrApiClient.cs | 9 ++ .../Services/Clients/IDolibarrApiClient.cs | 1 + DoliMiddlewareApi/Services/ContactService.cs | 83 +++++++++++++++++++ DoliMiddlewareApi/Services/DocumentService.cs | 56 +++++++++++++ DoliMiddlewareApi/Services/InvoiceService.cs | 25 ++++++ DoliMiddlewareApi/Services/SetupService.cs | 45 ++++++++++ 15 files changed, 593 insertions(+), 30 deletions(-) create mode 100644 DoliMiddlewareApi/Controllers/ContactsController.cs create mode 100644 DoliMiddlewareApi/Controllers/SetupController.cs create mode 100644 DoliMiddlewareApi/Services/ContactService.cs create mode 100644 DoliMiddlewareApi/Services/SetupService.cs diff --git a/DoliMiddlewareApi/Controllers/AuthController.cs b/DoliMiddlewareApi/Controllers/AuthController.cs index fbebebd..aeb7355 100644 --- a/DoliMiddlewareApi/Controllers/AuthController.cs +++ b/DoliMiddlewareApi/Controllers/AuthController.cs @@ -1,5 +1,6 @@ using DoliMiddlewareApi.Dtos.command; using DoliMiddlewareApi.Services; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace DoliMiddlewareApi.Controllers; @@ -9,6 +10,8 @@ namespace DoliMiddlewareApi.Controllers; public class AuthController(AuthApplicationService authAppService) : ControllerBase { [HttpPost("login")] + [ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] public async Task> Login([FromBody] CreateTokenDto dto) { var result = await authAppService.LoginAsync(dto); diff --git a/DoliMiddlewareApi/Controllers/ClientsController.cs b/DoliMiddlewareApi/Controllers/ClientsController.cs index f35a9d3..86c8513 100644 --- a/DoliMiddlewareApi/Controllers/ClientsController.cs +++ b/DoliMiddlewareApi/Controllers/ClientsController.cs @@ -1,7 +1,9 @@ using System.ComponentModel.DataAnnotations; +using DoliMiddlewareApi.Dtos.command; using DoliMiddlewareApi.Dtos.query; using DoliMiddlewareApi.Services; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace DoliMiddlewareApi.Controllers; @@ -11,9 +13,11 @@ namespace DoliMiddlewareApi.Controllers; [Authorize] public class ClientsController(ClientService clientService) : ControllerBase { - [HttpGet] - public async Task>> GetClientes( + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> GetClients( [FromQuery] int limit = 50, [FromQuery][Range(1, int.MaxValue)] int page = 1) { @@ -21,6 +25,44 @@ public class ClientsController(ClientService clientService) : ControllerBase return Ok(clients); } + [HttpGet("{id:int}")] + [ProducesResponseType(typeof(ClientDetailDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task> GetClient([Range(1, int.MaxValue)] int id) + { + var client = await clientService.GetClientAsync(id); + return Ok(client); + } + [HttpPost] + [ProducesResponseType(typeof(int), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task> CreateClient([FromBody] CreateClientDto dto) + { + var clientId = await clientService.CreateClientAsync(dto); + return CreatedAtAction(nameof(GetClient), new { id = clientId }, clientId); + } -} + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task UpdateClient([Range(1, int.MaxValue)] int id, [FromBody] UpdateClientDto dto) + { + await clientService.UpdateClientAsync(id, dto); + return NoContent(); + } + + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task DeleteClient([Range(1, int.MaxValue)] int id) + { + await clientService.DeleteClientAsync(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/DoliMiddlewareApi/Controllers/ContactsController.cs b/DoliMiddlewareApi/Controllers/ContactsController.cs new file mode 100644 index 0000000..b860977 --- /dev/null +++ b/DoliMiddlewareApi/Controllers/ContactsController.cs @@ -0,0 +1,78 @@ +using System.ComponentModel.DataAnnotations; +using DoliMiddlewareApi.Dtos.command; +using DoliMiddlewareApi.Dtos.query; +using DoliMiddlewareApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace DoliMiddlewareApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class ContactsController(ContactService contactService) : ControllerBase +{ + [HttpGet] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> GetContacts( + [FromQuery] int limit = 50, + [FromQuery][Range(1, int.MaxValue)] int page = 1, + [FromQuery] string? thirdpartyIds = null) + { + var contacts = await contactService.GetContactsAsync(limit, page, thirdpartyIds); + return Ok(contacts); + } + + [HttpGet("{id:int}")] + [ProducesResponseType(typeof(ContactDetailDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task> GetContact([Range(1, int.MaxValue)] int id) + { + var contact = await contactService.GetContactAsync(id); + return Ok(contact); + } + + [HttpGet("email/{email}")] + [ProducesResponseType(typeof(ContactDetailDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task> GetContactByEmail(string email) + { + var contact = await contactService.GetContactByEmailAsync(email); + return Ok(contact); + } + + [HttpPost] + [ProducesResponseType(typeof(int), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task> CreateContact([FromBody] CreateContactDto dto) + { + var contactId = await contactService.CreateContactAsync(dto); + return CreatedAtAction(nameof(GetContact), new { id = contactId }, contactId); + } + + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task UpdateContact([Range(1, int.MaxValue)] int id, [FromBody] UpdateContactDto dto) + { + await contactService.UpdateContactAsync(id, dto); + return NoContent(); + } + + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task DeleteContact([Range(1, int.MaxValue)] int id) + { + await contactService.DeleteContactAsync(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/DoliMiddlewareApi/Controllers/DocumentController.cs b/DoliMiddlewareApi/Controllers/DocumentController.cs index 567993b..987cd1d 100644 --- a/DoliMiddlewareApi/Controllers/DocumentController.cs +++ b/DoliMiddlewareApi/Controllers/DocumentController.cs @@ -3,24 +3,49 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace DoliMiddlewareApi.Controllers +namespace DoliMiddlewareApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class DocumentController(DocumentService documentService) : ControllerBase { - [Route("api/[controller]")] - [ApiController] - [Authorize] - public class DocumentController(DocumentService documentService) : ControllerBase + [HttpGet("invoice/{invoiceRef}/pdf")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task GetInvoicePdf(string invoiceRef) { - - [HttpGet("invoice/{invoiceRef}/pdf")] - [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] - public async Task GetInvoicePdf(string invoiceRef) - { - var (content, filename) = await documentService.BuildInvoicePdfAsync(invoiceRef); - - return File(content, "application/pdf", filename); - } - + var (content, filename) = await documentService.BuildInvoicePdfAsync(invoiceRef); + return File(content, "application/pdf", filename); } -} + + [HttpGet("list")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> GetDocuments( + [FromQuery] string modulePart = "invoice", + [FromQuery] string refId = "") + { + if (string.IsNullOrEmpty(refId)) + return BadRequest("refId parameter is required"); + + var documents = await documentService.GetDocumentsAsync(modulePart, refId); + return Ok(documents); + } + + [HttpGet("download")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task DownloadDocument( + [FromQuery] string modulePart = "invoice", + [FromQuery] string file = "") + { + if (string.IsNullOrEmpty(file)) + return BadRequest("file parameter is required"); + + var (content, filename, contentType) = await documentService.DownloadDocumentAsync(modulePart, file); + return File(content, contentType, filename); + } +} \ No newline at end of file diff --git a/DoliMiddlewareApi/Controllers/InvoicesController.cs b/DoliMiddlewareApi/Controllers/InvoicesController.cs index 2cc5d1a..e4b6cdd 100644 --- a/DoliMiddlewareApi/Controllers/InvoicesController.cs +++ b/DoliMiddlewareApi/Controllers/InvoicesController.cs @@ -4,6 +4,7 @@ using DoliMiddlewareApi.Dtos.command; using DoliMiddlewareApi.Dtos.query; using DoliMiddlewareApi.Services; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace DoliMiddlewareApi.Controllers; @@ -14,6 +15,9 @@ namespace DoliMiddlewareApi.Controllers; public class InvoicesController(InvoiceService invoiceService) : ControllerBase { [HttpGet] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] public async Task>> GetInvoices( [FromQuery] int limit = 50, [FromQuery] [Range(1, int.MaxValue)] int page = 1, @@ -25,6 +29,9 @@ public class InvoicesController(InvoiceService invoiceService) : ControllerBase } [HttpGet("{id:int}")] + [ProducesResponseType(typeof(InvoiceDetailDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] public async Task> GetInvoice([Range(1, int.MaxValue)] int id) { var invoice = await invoiceService.GetInvoiceAsync(id); @@ -32,6 +39,9 @@ public class InvoicesController(InvoiceService invoiceService) : ControllerBase } [HttpPost] + [ProducesResponseType(typeof(int), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] public async Task> CreateInvoice([FromBody] CreateInvoiceDto createInvoiceDto) { var invoiceId = await invoiceService.CreateInvoiceAsync(createInvoiceDto); @@ -39,6 +49,10 @@ public class InvoicesController(InvoiceService invoiceService) : ControllerBase } [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] public async Task UpdateInvoice([Range(1, int.MaxValue)] int id, [FromBody] UpdateInvoiceDto updateInvoiceDto) { @@ -47,6 +61,10 @@ public class InvoicesController(InvoiceService invoiceService) : ControllerBase } [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> AddInvoiceLine([Range(1, int.MaxValue)] int id, [FromBody] CreateInvoiceLineDto lineDto) { @@ -55,6 +73,9 @@ public class InvoicesController(InvoiceService invoiceService) : ControllerBase } [HttpPatch("{id:int}/status")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] public async Task UpdateInvoiceStatus( [Range(1, int.MaxValue)] int id, [FromBody] UpdateInvoiceStatusDto updateStatusDto) @@ -64,6 +85,10 @@ public class InvoicesController(InvoiceService invoiceService) : ControllerBase } [HttpPost("{id:int}/validate")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] public async Task ValidateInvoice([Range(1, int.MaxValue)] int id) { await invoiceService.ValidateInvoiceAsync(id); @@ -71,6 +96,10 @@ public class InvoicesController(InvoiceService invoiceService) : ControllerBase } [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] public async Task DeleteInvoice([Range(1, int.MaxValue)] int id) { await invoiceService.DeleteInvoiceAsync(id); @@ -78,6 +107,10 @@ public class InvoicesController(InvoiceService invoiceService) : ControllerBase } [HttpDelete("{invoiceId: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 DeleteInvoiceLine( [Range(1, int.MaxValue)] int invoiceId, [Range(1, int.MaxValue)] int lineId) @@ -86,7 +119,34 @@ public class InvoicesController(InvoiceService invoiceService) : ControllerBase return NoContent(); } + [HttpPut("{invoiceId:int}/lines/{lineId:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task UpdateInvoiceLine( + [Range(1, int.MaxValue)] int invoiceId, + [Range(1, int.MaxValue)] int lineId, + [FromBody] UpdateInvoiceLineDto dto) + { + await invoiceService.UpdateInvoiceLineAsync(invoiceId, lineId, dto); + return NoContent(); + } + + [HttpGet("templates")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> GetInvoiceTemplates() + { + var templates = await invoiceService.GetInvoiceTemplatesAsync(); + return Ok(templates); + } + [HttpGet("{id:int}/payments")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] public async Task>> GetInvoicePayments([Range(1, int.MaxValue)] int id) { var payments = await invoiceService.GetInvoicePaymentsAsync(id); @@ -94,6 +154,9 @@ public class InvoicesController(InvoiceService invoiceService) : ControllerBase } [HttpPost("{id:int}/payments")] + [ProducesResponseType(typeof(long), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] public async Task> AddPayment( [Range(1, int.MaxValue)] int id, [FromBody] CreateInvoicePaymentDto dto) diff --git a/DoliMiddlewareApi/Controllers/SetupController.cs b/DoliMiddlewareApi/Controllers/SetupController.cs new file mode 100644 index 0000000..e96838f --- /dev/null +++ b/DoliMiddlewareApi/Controllers/SetupController.cs @@ -0,0 +1,67 @@ +using DoliMiddlewareApi.Dtos.query.Setup; +using DoliMiddlewareApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace DoliMiddlewareApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class SetupController(SetupService setupService) : ControllerBase +{ + [HttpGet("payment-types")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> GetPaymentTypes() + { + var paymentTypes = await setupService.GetPaymentTypesAsync(); + return Ok(paymentTypes); + } + + [HttpGet("countries")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> GetCountries() + { + var countries = await setupService.GetCountriesAsync(); + return Ok(countries); + } + + [HttpGet("civilities")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> GetCivilities() + { + var civilities = await setupService.GetCivilitiesAsync(); + return Ok(civilities); + } + + [HttpGet("contact-types")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> GetContactTypes() + { + var contactTypes = await setupService.GetContactTypesAsync(); + return Ok(contactTypes); + } + + [HttpGet("payment-terms")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> GetPaymentTerms() + { + var paymentTerms = await setupService.GetPaymentTermsAsync(); + return Ok(paymentTerms); + } + + [HttpGet("company")] + [ProducesResponseType(typeof(CompanyDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task> GetCompany() + { + var company = await setupService.GetCompanyAsync(); + return Ok(company); + } +} \ No newline at end of file diff --git a/DoliMiddlewareApi/Services/Auth/JwtTokenProvider.cs b/DoliMiddlewareApi/Services/Auth/JwtTokenProvider.cs index 4c41d8a..f77e5fc 100644 --- a/DoliMiddlewareApi/Services/Auth/JwtTokenProvider.cs +++ b/DoliMiddlewareApi/Services/Auth/JwtTokenProvider.cs @@ -22,7 +22,7 @@ public sealed class JwtTokenProvider(IConfiguration config) issuer: config["Jwt:Issuer"] ?? "DoliMiddleware", audience: config["Jwt:Audience"] ?? "DoliClients", claims: claims, - expires: DateTime.Now.AddMinutes(30), + expires: DateTime.Now.AddHours(8), signingCredentials: creds ); diff --git a/DoliMiddlewareApi/Services/AuthApplicationService.cs b/DoliMiddlewareApi/Services/AuthApplicationService.cs index 1f524c2..849e41b 100644 --- a/DoliMiddlewareApi/Services/AuthApplicationService.cs +++ b/DoliMiddlewareApi/Services/AuthApplicationService.cs @@ -10,7 +10,7 @@ public class AuthApplicationService(DolibarrAuthService dolibarrAuth, JwtTokenPr var doliToken = await dolibarrAuth.AuthenticateAsync(dto); var sessionId = Guid.NewGuid().ToString(); - tokenCacheService.SetDolibarrToken(sessionId, doliToken, TimeSpan.FromMinutes(30)); + tokenCacheService.SetDolibarrToken(sessionId, doliToken, TimeSpan.FromHours(8)); var jwt = jwtProvider.GenerateJwt(sessionId, dto.Username); diff --git a/DoliMiddlewareApi/Services/ClientService.cs b/DoliMiddlewareApi/Services/ClientService.cs index 40285db..576daf3 100644 --- a/DoliMiddlewareApi/Services/ClientService.cs +++ b/DoliMiddlewareApi/Services/ClientService.cs @@ -1,29 +1,95 @@ +using DoliMiddlewareApi.Dtos.command; using DoliMiddlewareApi.Dtos.Dolibarr; using DoliMiddlewareApi.Dtos.query; +using DoliMiddlewareApi.Exceptions; using DoliMiddlewareApi.Mappers; using DoliMiddlewareApi.Services.Clients; namespace DoliMiddlewareApi.Services; -public class ClientService(IDolibarrApiClient dolibarrApiClient) +public class ClientService(IDolibarrApiClient apiClient) { - public async Task> GetClientsAsync( - int limit = 50, - int page = 1) + public async Task> GetClientsAsync(int limit = 50, int page = 1) { var endpoint = $"thirdparties?limit={limit}&page={page - 1}"; - var clients = await dolibarrApiClient.GetCollectionAsync(endpoint); + var clients = await apiClient.GetCollectionAsync(endpoint); if (clients.Count == 0) return clients.Select(ClientMapper.MapToClientDtoWithoutContacts).ToList(); var clientIds = string.Join(",", clients.Select(c => c.id).Where(id => !string.IsNullOrEmpty(id))); - var contactsEndpoint = $"contacts?thirdparty_ids={clientIds}"; - var contacts = await dolibarrApiClient.GetCollectionAsync(contactsEndpoint); + var contacts = await apiClient.GetCollectionAsync($"contacts?thirdparty_ids={clientIds}"); var contactDtos = contacts.Select(ContactMapper.MapToContactDto).ToList(); return clients.Select(c => ClientMapper.MapToClientDto(c, contactDtos)).ToList(); } + + public async Task GetClientAsync(int id) + { + var client = await apiClient.GetResourceAsync($"thirdparties/{id}"); + + List contactDtos = new(); + try + { + var contacts = await apiClient.GetCollectionAsync($"contacts?thirdparty_ids={id}"); + contactDtos = contacts.Select(ContactMapper.MapToContactDto).ToList(); + } + catch (ApiException) + { + // Contacts may fail or be empty, continue without them + } + + return ClientMapper.MapToClientDetailDto(client, contactDtos); + } + + public async Task CreateClientAsync(CreateClientDto dto) + { + var requestBody = new Dictionary + { + ["name"] = dto.Name, + ["client"] = "1", + ["address"] = dto.Address, + ["zip"] = dto.Zip, + ["town"] = dto.Town, + ["phone"] = dto.Phone, + ["email"] = dto.Email, + ["country_code"] = dto.CountryCode, + ["tva_intra"] = dto.VatNumber, + ["url"] = dto.Url, + ["note_public"] = dto.NotePublic, + ["note_private"] = dto.NotePrivate + }; + + var response = await apiClient.PostAsync("thirdparties", requestBody); + return int.Parse(response); + } + + public async Task UpdateClientAsync(int id, UpdateClientDto dto) + { + var current = await apiClient.GetResourceAsync($"thirdparties/{id}"); + + var requestBody = new Dictionary + { + ["name"] = dto.Name ?? current.name, + ["address"] = dto.Address ?? current.address, + ["zip"] = dto.Zip ?? current.zip, + ["town"] = dto.Town ?? current.town, + ["phone"] = dto.Phone ?? current.phone, + ["email"] = dto.Email ?? current.email, + ["country_code"] = dto.CountryCode ?? current.country_code, + ["tva_intra"] = dto.VatNumber ?? current.tva_intra, + ["url"] = dto.Url ?? current.url, + ["note_public"] = dto.NotePublic ?? current.note_public, + ["note_private"] = dto.NotePrivate ?? current.note_private + }; + + await apiClient.PutAsync($"thirdparties/{id}", requestBody); + } + + public async Task DeleteClientAsync(int id) + { + await apiClient.DeleteAsync($"thirdparties/{id}"); + } } \ No newline at end of file diff --git a/DoliMiddlewareApi/Services/Clients/DolibarrApiClient.cs b/DoliMiddlewareApi/Services/Clients/DolibarrApiClient.cs index b8ff706..2add79a 100644 --- a/DoliMiddlewareApi/Services/Clients/DolibarrApiClient.cs +++ b/DoliMiddlewareApi/Services/Clients/DolibarrApiClient.cs @@ -65,6 +65,15 @@ public class DolibarrApiClient(HttpClient httpClient, DolibarrTokenCacheService await EnsureSuccessOrThrowAsync(response, endpoint); } + public async Task GetAsyncRaw(string endpoint) + { + var request = new HttpRequestMessage(HttpMethod.Get, endpoint); + AddDolibarrTokenHeader(request); + var response = await httpClient.SendAsync(request); + await EnsureSuccessOrThrowAsync(response, endpoint); + return await response.Content.ReadAsStringAsync(); + } + private void AddDolibarrTokenHeader(HttpRequestMessage request) { var dolibarrToken = tokenCacheService.GetDolibarrToken(); diff --git a/DoliMiddlewareApi/Services/Clients/IDolibarrApiClient.cs b/DoliMiddlewareApi/Services/Clients/IDolibarrApiClient.cs index e03cd8b..01d0337 100644 --- a/DoliMiddlewareApi/Services/Clients/IDolibarrApiClient.cs +++ b/DoliMiddlewareApi/Services/Clients/IDolibarrApiClient.cs @@ -7,4 +7,5 @@ public interface IDolibarrApiClient Task PostAsync(string endpoint, object requestBody); Task PutAsync(string endpoint, object requestBody); Task DeleteAsync(string endpoint); + Task GetAsyncRaw(string endpoint); } diff --git a/DoliMiddlewareApi/Services/ContactService.cs b/DoliMiddlewareApi/Services/ContactService.cs new file mode 100644 index 0000000..6ed341a --- /dev/null +++ b/DoliMiddlewareApi/Services/ContactService.cs @@ -0,0 +1,83 @@ +using DoliMiddlewareApi.Dtos.command; +using DoliMiddlewareApi.Dtos.Dolibarr; +using DoliMiddlewareApi.Dtos.query; +using DoliMiddlewareApi.Exceptions; +using DoliMiddlewareApi.Mappers; +using DoliMiddlewareApi.Services.Clients; + +namespace DoliMiddlewareApi.Services; + +public class ContactService(IDolibarrApiClient apiClient) +{ + public async Task> GetContactsAsync(int limit = 50, int page = 1, string? thirdpartyIds = null) + { + var endpoint = $"contacts?limit={limit}&page={page - 1}"; + if (!string.IsNullOrEmpty(thirdpartyIds)) + endpoint += $"&thirdparty_ids={thirdpartyIds}"; + + var data = await apiClient.GetCollectionAsync(endpoint); + return data.Select(ContactMapper.MapToContactDto).ToList(); + } + + public async Task GetContactAsync(int id) + { + var data = await apiClient.GetResourceAsync($"contacts/{id}"); + return ContactMapper.MapToContactDetailDto(data); + } + + public async Task GetContactByEmailAsync(string email) + { + var data = await apiClient.GetCollectionAsync( + $"contacts/email/{Uri.EscapeDataString(email)}"); + + if (data.Count == 0) + throw new NotFoundException($"Contact with email '{email}' not found"); + + return ContactMapper.MapToContactDetailDto(data[0]); + } + + public async Task CreateContactAsync(CreateContactDto dto) + { + var requestBody = new Dictionary + { + ["lastname"] = dto.Lastname, + ["firstname"] = dto.Firstname, + ["fk_soc"] = dto.ClientId.ToString(), + ["email"] = dto.Email, + ["phone_pro"] = dto.PhonePro, + ["phone_perso"] = dto.PhonePerso, + ["phone_mobile"] = dto.PhoneMobile, + ["address"] = dto.Address, + ["zip"] = dto.Zip, + ["town"] = dto.Town + }; + + var response = await apiClient.PostAsync("contacts", requestBody); + return int.Parse(response); + } + + public async Task UpdateContactAsync(int id, UpdateContactDto dto) + { + var current = await apiClient.GetResourceAsync($"contacts/{id}"); + + var requestBody = new Dictionary + { + ["lastname"] = dto.Lastname ?? current.lastname, + ["firstname"] = dto.Firstname ?? current.firstname, + ["email"] = dto.Email ?? current.email, + ["phone_pro"] = dto.PhonePro ?? current.phone_pro, + ["phone_perso"] = dto.PhonePerso ?? current.phone_perso, + ["phone_mobile"] = dto.PhoneMobile ?? current.phone_mobile, + ["address"] = dto.Address ?? current.address, + ["zip"] = dto.Zip ?? current.zip, + ["town"] = dto.Town ?? current.town + }; + + await apiClient.PutAsync($"contacts/{id}", requestBody); + } + + public async Task DeleteContactAsync(int id) + { + await apiClient.DeleteAsync($"contacts/{id}"); + } +} \ No newline at end of file diff --git a/DoliMiddlewareApi/Services/DocumentService.cs b/DoliMiddlewareApi/Services/DocumentService.cs index 6eb1880..0bac4e5 100644 --- a/DoliMiddlewareApi/Services/DocumentService.cs +++ b/DoliMiddlewareApi/Services/DocumentService.cs @@ -1,8 +1,11 @@ using DoliMiddlewareApi.Dtos.command; using DoliMiddlewareApi.Dtos.Dolibarr; +using DoliMiddlewareApi.Exceptions; using DoliMiddlewareApi.Services.Clients; using System.Text.Json; +namespace DoliMiddlewareApi.Services; + public class DocumentService(IDolibarrApiClient apiClient) { public async Task<(byte[] content, string filename)> BuildInvoicePdfAsync(string invoiceRef) @@ -27,4 +30,57 @@ public class DocumentService(IDolibarrApiClient apiClient) return (bytes, result.filename); } + public async Task> GetDocumentsAsync(string modulePart, string refOrSocid) + { + var endpoint = $"documents?modulepart={modulePart}&ref={Uri.EscapeDataString(refOrSocid)}"; + var responseString = await apiClient.GetAsyncRaw(endpoint); + + var response = JsonSerializer.Deserialize>>(responseString); + + if (response == null) + return new List(); + + return response.Select(d => + { + 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; + + return new DocumentItem + { + Name = name, + Path = path, + Size = size + }; + }).ToList(); + } + + 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 responseString = await apiClient.GetAsyncRaw(endpoint); + + var response = JsonSerializer.Deserialize(responseString); + + if (response == null || string.IsNullOrEmpty(response.content)) + throw new NotFoundException("Documento no encontrado"); + + var bytes = Convert.FromBase64String(response.content); + var filename = response.filename ?? "document"; + var contentType = filename.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase) + ? "application/pdf" + : "application/octet-stream"; + + return (bytes, filename, contentType); + } +} + +public class DocumentItem +{ + public string? Name { get; set; } + public string? Path { get; set; } + public long Size { get; set; } } \ No newline at end of file diff --git a/DoliMiddlewareApi/Services/InvoiceService.cs b/DoliMiddlewareApi/Services/InvoiceService.cs index 083a06f..673689d 100644 --- a/DoliMiddlewareApi/Services/InvoiceService.cs +++ b/DoliMiddlewareApi/Services/InvoiceService.cs @@ -154,6 +154,31 @@ public class InvoiceService(IDolibarrApiClient apiClient) await apiClient.DeleteAsync($"invoices/{invoiceId}/lines/{lineId}"); } + public async Task UpdateInvoiceLineAsync(int invoiceId, int lineId, UpdateInvoiceLineDto dto) + { + var invoice = await apiClient.GetResourceAsync($"invoices/{invoiceId}"); + if (invoice.statut != "0") throw new ForbiddenException("Solo se pueden modificar líneas de facturas en borrador (draft)"); + + 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($"invoices/{invoiceId}/lines/{lineId}", requestBody); + } + + public async Task> GetInvoiceTemplatesAsync() + { + var dataList = await apiClient.GetCollectionAsync("invoices/templates"); + return dataList.Select(InvoiceMapper.MapToInvoiceDto).ToList(); + } + private async Task> GetClientNamesAsync(List clientIds) { var uniqueIds = clientIds.Distinct().ToList(); diff --git a/DoliMiddlewareApi/Services/SetupService.cs b/DoliMiddlewareApi/Services/SetupService.cs new file mode 100644 index 0000000..0b5fbdb --- /dev/null +++ b/DoliMiddlewareApi/Services/SetupService.cs @@ -0,0 +1,45 @@ +using DoliMiddlewareApi.Dtos.Dolibarr.Setup; +using DoliMiddlewareApi.Dtos.query.Setup; +using DoliMiddlewareApi.Mappers; +using DoliMiddlewareApi.Services.Clients; + +namespace DoliMiddlewareApi.Services; + +public class SetupService(IDolibarrApiClient apiClient) +{ + public async Task> GetPaymentTypesAsync() + { + var data = await apiClient.GetCollectionAsync("setup/dictionary/payment_types"); + return data.Select(SetupMapper.MapToPaymentTypeDto).ToList(); + } + + public async Task> GetCountriesAsync() + { + var data = await apiClient.GetCollectionAsync("setup/dictionary/countries"); + return data.Select(SetupMapper.MapToCountryDto).ToList(); + } + + public async Task> GetCivilitiesAsync() + { + var data = await apiClient.GetCollectionAsync("setup/dictionary/civilities"); + return data.Select(SetupMapper.MapToCivilityDto).ToList(); + } + + public async Task> GetContactTypesAsync() + { + var data = await apiClient.GetCollectionAsync("setup/dictionary/contact_types"); + return data.Select(SetupMapper.MapToContactTypeDto).ToList(); + } + + public async Task> GetPaymentTermsAsync() + { + var data = await apiClient.GetCollectionAsync("setup/dictionary/payment_terms"); + return data.Select(SetupMapper.MapToPaymentTermDto).ToList(); + } + + public async Task GetCompanyAsync() + { + var data = await apiClient.GetResourceAsync("setup/company"); + return SetupMapper.MapToCompanyDto(data); + } +} \ No newline at end of file