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
This commit is contained in:
parent
32028e21e1
commit
fbbbce3574
|
|
@ -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<ActionResult<LoginResponse>> Login([FromBody] CreateTokenDto dto)
|
||||
{
|
||||
var result = await authAppService.LoginAsync(dto);
|
||||
|
|
|
|||
|
|
@ -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<ActionResult<List<ClientDto>>> GetClientes(
|
||||
[ProducesResponseType(typeof(List<ClientDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<ClientDto>>> 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<ActionResult<ClientDetailDto>> 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<ActionResult<int>> 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<IActionResult> 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<IActionResult> DeleteClient([Range(1, int.MaxValue)] int id)
|
||||
{
|
||||
await clientService.DeleteClientAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ContactDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<ContactDto>>> 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<ActionResult<ContactDetailDto>> 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<ActionResult<ContactDetailDto>> 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<ActionResult<int>> 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<IActionResult> 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<IActionResult> DeleteContact([Range(1, int.MaxValue)] int id)
|
||||
{
|
||||
await contactService.DeleteContactAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IActionResult> 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<IActionResult> 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<ActionResult<List<DocumentItem>>> 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<IActionResult> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<InvoiceDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<InvoiceDto>>> 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<ActionResult<InvoiceDetailDto>> 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<ActionResult<int>> 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<IActionResult> 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<ActionResult<string>> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<InvoiceDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<InvoiceDto>>> GetInvoiceTemplates()
|
||||
{
|
||||
var templates = await invoiceService.GetInvoiceTemplatesAsync();
|
||||
return Ok(templates);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/payments")]
|
||||
[ProducesResponseType(typeof(List<InvoicePaymentDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<InvoicePaymentDto>>> 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<ActionResult<long>> AddPayment(
|
||||
[Range(1, int.MaxValue)] int id,
|
||||
[FromBody] CreateInvoicePaymentDto dto)
|
||||
|
|
|
|||
|
|
@ -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<PaymentTypeDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<PaymentTypeDto>>> GetPaymentTypes()
|
||||
{
|
||||
var paymentTypes = await setupService.GetPaymentTypesAsync();
|
||||
return Ok(paymentTypes);
|
||||
}
|
||||
|
||||
[HttpGet("countries")]
|
||||
[ProducesResponseType(typeof(List<CountryDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<CountryDto>>> GetCountries()
|
||||
{
|
||||
var countries = await setupService.GetCountriesAsync();
|
||||
return Ok(countries);
|
||||
}
|
||||
|
||||
[HttpGet("civilities")]
|
||||
[ProducesResponseType(typeof(List<CivilityDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<CivilityDto>>> GetCivilities()
|
||||
{
|
||||
var civilities = await setupService.GetCivilitiesAsync();
|
||||
return Ok(civilities);
|
||||
}
|
||||
|
||||
[HttpGet("contact-types")]
|
||||
[ProducesResponseType(typeof(List<ContactTypeDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<ContactTypeDto>>> GetContactTypes()
|
||||
{
|
||||
var contactTypes = await setupService.GetContactTypesAsync();
|
||||
return Ok(contactTypes);
|
||||
}
|
||||
|
||||
[HttpGet("payment-terms")]
|
||||
[ProducesResponseType(typeof(List<PaymentTermDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<List<PaymentTermDto>>> GetPaymentTerms()
|
||||
{
|
||||
var paymentTerms = await setupService.GetPaymentTermsAsync();
|
||||
return Ok(paymentTerms);
|
||||
}
|
||||
|
||||
[HttpGet("company")]
|
||||
[ProducesResponseType(typeof(CompanyDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<CompanyDto>> GetCompany()
|
||||
{
|
||||
var company = await setupService.GetCompanyAsync();
|
||||
return Ok(company);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<List<ClientDto>> GetClientsAsync(
|
||||
int limit = 50,
|
||||
int page = 1)
|
||||
public async Task<List<ClientDto>> GetClientsAsync(int limit = 50, int page = 1)
|
||||
{
|
||||
var endpoint = $"thirdparties?limit={limit}&page={page - 1}";
|
||||
|
||||
var clients = await dolibarrApiClient.GetCollectionAsync<ClientResponse>(endpoint);
|
||||
var clients = await apiClient.GetCollectionAsync<ClientResponse>(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<ContactResponse>(contactsEndpoint);
|
||||
var contacts = await apiClient.GetCollectionAsync<ContactResponse>($"contacts?thirdparty_ids={clientIds}");
|
||||
var contactDtos = contacts.Select(ContactMapper.MapToContactDto).ToList();
|
||||
|
||||
return clients.Select(c => ClientMapper.MapToClientDto(c, contactDtos)).ToList();
|
||||
}
|
||||
|
||||
public async Task<ClientDetailDto> GetClientAsync(int id)
|
||||
{
|
||||
var client = await apiClient.GetResourceAsync<ClientResponse>($"thirdparties/{id}");
|
||||
|
||||
List<ContactDto> contactDtos = new();
|
||||
try
|
||||
{
|
||||
var contacts = await apiClient.GetCollectionAsync<ContactResponse>($"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<int> CreateClientAsync(CreateClientDto dto)
|
||||
{
|
||||
var requestBody = new Dictionary<string, object?>
|
||||
{
|
||||
["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<ClientResponse>($"thirdparties/{id}");
|
||||
|
||||
var requestBody = new Dictionary<string, object?>
|
||||
{
|
||||
["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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -65,6 +65,15 @@ public class DolibarrApiClient(HttpClient httpClient, DolibarrTokenCacheService
|
|||
await EnsureSuccessOrThrowAsync(response, endpoint);
|
||||
}
|
||||
|
||||
public async Task<string> 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();
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ public interface IDolibarrApiClient
|
|||
Task<string> PostAsync(string endpoint, object requestBody);
|
||||
Task<string> PutAsync(string endpoint, object requestBody);
|
||||
Task DeleteAsync(string endpoint);
|
||||
Task<string> GetAsyncRaw(string endpoint);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<List<ContactDto>> 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<ContactResponse>(endpoint);
|
||||
return data.Select(ContactMapper.MapToContactDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<ContactDetailDto> GetContactAsync(int id)
|
||||
{
|
||||
var data = await apiClient.GetResourceAsync<ContactResponse>($"contacts/{id}");
|
||||
return ContactMapper.MapToContactDetailDto(data);
|
||||
}
|
||||
|
||||
public async Task<ContactDetailDto> GetContactByEmailAsync(string email)
|
||||
{
|
||||
var data = await apiClient.GetCollectionAsync<ContactResponse>(
|
||||
$"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<int> CreateContactAsync(CreateContactDto dto)
|
||||
{
|
||||
var requestBody = new Dictionary<string, object?>
|
||||
{
|
||||
["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<ContactResponse>($"contacts/{id}");
|
||||
|
||||
var requestBody = new Dictionary<string, object?>
|
||||
{
|
||||
["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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<DocumentItem>> GetDocumentsAsync(string modulePart, string refOrSocid)
|
||||
{
|
||||
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 =>
|
||||
{
|
||||
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<DolibarrPdfResponse>(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; }
|
||||
}
|
||||
|
|
@ -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<InvoiceDetailResponse>($"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<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($"invoices/{invoiceId}/lines/{lineId}", requestBody);
|
||||
}
|
||||
|
||||
public async Task<List<InvoiceDto>> GetInvoiceTemplatesAsync()
|
||||
{
|
||||
var dataList = await apiClient.GetCollectionAsync<InvoiceResponse>("invoices/templates");
|
||||
return dataList.Select(InvoiceMapper.MapToInvoiceDto).ToList();
|
||||
}
|
||||
|
||||
private async Task<Dictionary<int, string>> GetClientNamesAsync(List<int> clientIds)
|
||||
{
|
||||
var uniqueIds = clientIds.Distinct().ToList();
|
||||
|
|
|
|||
|
|
@ -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<List<PaymentTypeDto>> GetPaymentTypesAsync()
|
||||
{
|
||||
var data = await apiClient.GetCollectionAsync<PaymentTypeResponse>("setup/dictionary/payment_types");
|
||||
return data.Select(SetupMapper.MapToPaymentTypeDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<CountryDto>> GetCountriesAsync()
|
||||
{
|
||||
var data = await apiClient.GetCollectionAsync<CountryResponse>("setup/dictionary/countries");
|
||||
return data.Select(SetupMapper.MapToCountryDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<CivilityDto>> GetCivilitiesAsync()
|
||||
{
|
||||
var data = await apiClient.GetCollectionAsync<CivilityResponse>("setup/dictionary/civilities");
|
||||
return data.Select(SetupMapper.MapToCivilityDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<ContactTypeDto>> GetContactTypesAsync()
|
||||
{
|
||||
var data = await apiClient.GetCollectionAsync<ContactTypeResponse>("setup/dictionary/contact_types");
|
||||
return data.Select(SetupMapper.MapToContactTypeDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<PaymentTermDto>> GetPaymentTermsAsync()
|
||||
{
|
||||
var data = await apiClient.GetCollectionAsync<PaymentTermResponse>("setup/dictionary/payment_terms");
|
||||
return data.Select(SetupMapper.MapToPaymentTermDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<CompanyDto> GetCompanyAsync()
|
||||
{
|
||||
var data = await apiClient.GetResourceAsync<CompanyResponse>("setup/company");
|
||||
return SetupMapper.MapToCompanyDto(data);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue