feat: Implementar endpoint PUT para actualizar facturas draft, limitado a campos permitidos por Dolibarr (ref, notas, estado, fecha expiración) y unificar campo 'number' para consistencia

This commit is contained in:
javiermengual 2026-01-02 20:20:58 +01:00
parent 1fd331ac17
commit a06ac2bdd9
7 changed files with 427 additions and 11 deletions

View File

@ -50,4 +50,16 @@ public class InvoicesController : ControllerBase
var invoiceId= await _invoiceService.CreateInvoiceAsync(createInvoiceDto);
return CreatedAtAction(nameof(GetInvoice), new { id = invoiceId }, invoiceId);
}
[HttpPut("{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> UpdateInvoice([Range(1, int.MaxValue)] int id, [FromBody] UpdateInvoiceDto updateInvoiceDto)
{
await _invoiceService.UpdateInvoiceAsync(id, updateInvoiceDto);
return NoContent();
}
}

View File

@ -12,4 +12,7 @@ public class InvoiceResponse
public string? total_ttc { get; set; }
public string? remaintopay { get; set; }
public string? statut { get; set; }
public string? note_public { get; set; }
public string? note_private { get; set; }
}

View File

@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
namespace DoliMiddlewareApi.Dtos.command;
public class UpdateInvoiceDto
{
// ClientId y Date no se cambian en PUT (Dolibarr no permite)
public DateTime? ExpireDate { get; set; }
[StringLength(100)]
public string? Number { get; set; } // Cambiado de Reference a Number para consistencia con GET
public string? NotePublic { get; set; }
public string? NotePrivate { get; set; }
public string Status { get; set; } = "draft";
// Lines quitadas - usa POST /lines para añadir líneas
}

View File

@ -41,18 +41,14 @@ public class DolibarrApiClient
// porque devuelve el id como response
return await response.Content.ReadAsStringAsync();
}
public async Task<string> PutAsync(string endpoint, object requestBody)
{
var response = await _httpClient.PutAsJsonAsync(endpoint, requestBody);
await EnsureSuccessOrThrowAsync(response, endpoint);
return await response.Content.ReadAsStringAsync();
}

View File

@ -2,6 +2,7 @@ using System.Globalization;
using DoliMiddlewareApi.Dtos;
using DoliMiddlewareApi.Dtos.command;
using DoliMiddlewareApi.Dtos.Dolibarr;
using DoliMiddlewareApi.Exceptions;
using DoliMiddlewareApi.Mappers;
using DoliMiddlewareApi.Services.Clients;
@ -66,4 +67,22 @@ public class InvoiceService
return int.Parse(responseBody);
}
public async Task UpdateInvoiceAsync(int id, UpdateInvoiceDto dto)
{
// GET el JSON completo
var current = await _apiClient.GetResourceAsync<InvoiceDetailResponse>($"invoices/{id}");
if (current.statut != "0") throw new ForbiddenException("Solo drafts");
// Modifica solo campos que Dolibarr permite en PUT
if (dto.Number != null) current.@ref = dto.Number;
if (dto.NotePublic != null) current.note_public = dto.NotePublic;
if (dto.NotePrivate != null) current.note_private = dto.NotePrivate;
current.statut = dto.Status == "unpaid" ? "1" : "0";
if (dto.ExpireDate.HasValue) current.date_lim_reglement = ((DateTimeOffset)dto.ExpireDate.Value).ToUnixTimeSeconds();
// No tocar: date, socid, lines (Dolibarr no los cambia en PUT)
await _apiClient.PutAsync($"invoices/{id}", current);
}
}

33
generate-openapi.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
set -e
echo "🚀 Generando openapi.json..."
# Variables
PORT=5123
PROJECT_DIR="DoliMiddlewareApi"
OUTPUT_FILE="openapi.json"
# Levantar la API en background
cd "$PROJECT_DIR"
dotnet run --urls "http://localhost:$PORT" > /dev/null 2>&1 &
PID=$!
# Esperar a que arranque
echo "⏳ Esperando a que la API arranque..."
sleep 8
# Descargar el OpenAPI spec
echo "📥 Descargando openapi.json..."
curl -s "http://localhost:$PORT/openapi/v1.json" -o "../$OUTPUT_FILE"
# Matar el proceso
kill $PID 2>/dev/null || true
echo "✅ openapi.json generado exitosamente en: $OUTPUT_FILE"
echo ""
echo "📦 Para generar cliente TypeScript:"
echo " npx @openapitools/openapi-generator-cli generate -i openapi.json -g typescript-fetch -o ./client"
echo ""
echo "📦 Para importar en Postman:"
echo " File > Import > Selecciona openapi.json"

333
openapi.json Normal file
View File

@ -0,0 +1,333 @@
{
"openapi": "3.0.4",
"info": {
"title": "DoliMiddlewareApi",
"version": "1.0"
},
"paths": {
"/api/Invoices": {
"get": {
"tags": [
"Invoices"
],
"parameters": [
{
"name": "limit",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 50
}
},
{
"name": "page",
"in": "query",
"schema": {
"maximum": 2147483647,
"minimum": 1,
"type": "integer",
"format": "int32",
"default": 1
}
},
{
"name": "status",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/InvoiceDto"
}
}
},
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/InvoiceDto"
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/InvoiceDto"
}
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
},
"/api/Invoices/{id}": {
"get": {
"tags": [
"Invoices"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/InvoiceDetailDto"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvoiceDetailDto"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/InvoiceDetailDto"
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"InvoiceDetailDto": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"number": {
"type": "string",
"nullable": true
},
"date": {
"type": "string",
"format": "date-time",
"nullable": true
},
"expireDate": {
"type": "string",
"format": "date-time",
"nullable": true
},
"clientId": {
"type": "integer",
"format": "int32"
},
"total": {
"type": "number",
"format": "double",
"nullable": true
},
"remainToPay": {
"type": "number",
"format": "double",
"nullable": true
},
"status": {
"type": "string",
"nullable": true
},
"lines": {
"type": "array",
"items": {
"$ref": "#/components/schemas/InvoiceLineDto"
},
"nullable": true
}
},
"additionalProperties": false
},
"InvoiceDto": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"number": {
"type": "string",
"nullable": true
},
"date": {
"type": "string",
"format": "date-time",
"nullable": true
},
"expireDate": {
"type": "string",
"format": "date-time",
"nullable": true
},
"clientId": {
"type": "integer",
"format": "int32"
},
"total": {
"type": "number",
"format": "double",
"nullable": true
},
"remainToPay": {
"type": "number",
"format": "double",
"nullable": true
},
"status": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"InvoiceLineDto": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int32"
},
"description": {
"type": "string",
"nullable": true
},
"quantity": {
"type": "number",
"format": "double"
},
"unitPrice": {
"type": "number",
"format": "double"
},
"taxRate": {
"type": "number",
"format": "double"
},
"total": {
"type": "number",
"format": "double"
}
},
"additionalProperties": false
},
"ProblemDetails": {
"type": "object",
"properties": {
"type": {
"type": "string",
"nullable": true
},
"title": {
"type": "string",
"nullable": true
},
"status": {
"type": "integer",
"format": "int32",
"nullable": true
},
"detail": {
"type": "string",
"nullable": true
},
"instance": {
"type": "string",
"nullable": true
}
},
"additionalProperties": { }
}
}
},
"tags": [
{
"name": "Invoices"
}
]
}