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:
JavMB 2026-05-15 18:58:35 +02:00
parent 32028e21e1
commit fbbbce3574
15 changed files with 593 additions and 30 deletions

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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
);

View File

@ -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);

View File

@ -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}");
}
}

View File

@ -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();

View File

@ -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);
}

View File

@ -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}");
}
}

View File

@ -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; }
}

View File

@ -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();

View File

@ -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);
}
}