feat: módulo banco completo (GET/POST/PUT cuentas y movimientos), campos TotalHt/TotalTax en facturas

This commit is contained in:
Алекс 2026-05-15 21:50:16 +02:00
parent 8f0fff3953
commit f2030977ff
13 changed files with 350 additions and 0 deletions

View File

@ -0,0 +1,83 @@
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 BankController(BankService bankService) : ControllerBase
{
[HttpGet("accounts")]
[ProducesResponseType(typeof(List<BankAccountDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<List<BankAccountDto>>> GetAccounts()
{
var accounts = await bankService.GetAccountsAsync();
return Ok(accounts);
}
[HttpGet("accounts/{id:int}")]
[ProducesResponseType(typeof(BankAccountDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<BankAccountDto>> GetAccount([Range(1, int.MaxValue)] int id)
{
var account = await bankService.GetAccountAsync(id);
return Ok(account);
}
[HttpGet("accounts/{id:int}/balance")]
[ProducesResponseType(typeof(double), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<double>> GetBalance([Range(1, int.MaxValue)] int id)
{
var balance = await bankService.GetBalanceAsync(id);
return Ok(balance);
}
[HttpGet("accounts/{id:int}/lines")]
[ProducesResponseType(typeof(List<BankLineDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<List<BankLineDto>>> GetAccountLines([Range(1, int.MaxValue)] int id)
{
var lines = await bankService.GetAccountLinesAsync(id);
return Ok(lines);
}
[HttpGet("movements")]
[ProducesResponseType(typeof(List<BankLineDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<List<BankLineDto>>> GetMovements([FromQuery] int limit = 20)
{
var movements = await bankService.GetMovementsAsync(limit);
return Ok(movements);
}
[HttpPut("accounts/{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> UpdateAccount([Range(1, int.MaxValue)] int id, [FromBody] UpdateBankAccountDto dto)
{
await bankService.UpdateAccountAsync(id, dto);
return NoContent();
}
[HttpPost("accounts")]
[ProducesResponseType(typeof(int), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<int>> CreateAccount([FromBody] CreateBankAccountDto dto)
{
var id = await bankService.CreateAccountAsync(dto);
return CreatedAtAction(nameof(GetAccount), new { id }, id);
}
}

View File

@ -0,0 +1,15 @@
namespace DoliMiddlewareApi.Dtos.Dolibarr;
public class BankAccountResponse
{
public string? id { get; set; }
public string? @ref { get; set; }
public string? label { get; set; }
public string? number { get; set; }
public string? iban_prefix { get; set; }
public string? bic { get; set; }
public string? currency_code { get; set; }
public string? bank { get; set; }
public double? solde { get; set; }
public string? clos { get; set; }
}

View File

@ -0,0 +1,15 @@
namespace DoliMiddlewareApi.Dtos.Dolibarr;
public class BankLineResponse
{
public string? id { get; set; }
public string? rowid { get; set; }
public string? label { get; set; }
public double? amount { get; set; }
public long? dateo { get; set; }
public long? datev { get; set; }
public string? fk_account { get; set; }
public string? num_releve { get; set; }
public string? type { get; set; }
public string? num_chq { get; set; }
}

View File

@ -9,6 +9,8 @@ public class InvoiceResponse
public long? date_lim_reglement { get; set; } public long? date_lim_reglement { get; set; }
public string? socid { get; set; } public string? socid { get; set; }
public string? total_ht { get; set; }
public string? total_tva { get; set; }
public string? total_ttc { get; set; } public string? total_ttc { get; set; }
public string? remaintopay { get; set; } public string? remaintopay { get; set; }
public string? statut { get; set; } public string? statut { get; set; }

View File

@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
namespace DoliMiddlewareApi.Dtos.command;
public class CreateBankAccountDto
{
[Required]
public required string Label { get; set; }
[Required]
public required string Ref { get; set; }
/// <summary>0 = Savings, 1 = Current/Checking, 2 = Cash</summary>
public int Type { get; set; } = 1;
public string CurrencyCode { get; set; } = "EUR";
[Required, Range(1, int.MaxValue)]
public int CountryId { get; set; }
public string? AccountNumber { get; set; }
public string? Iban { get; set; }
public string? Bic { get; set; }
public string? Bank { get; set; }
}

View File

@ -0,0 +1,14 @@
namespace DoliMiddlewareApi.Dtos.command;
public class UpdateBankAccountDto
{
public string? Label { get; set; }
public string? Ref { get; set; }
public int? Type { get; set; }
public string? CurrencyCode { get; set; }
public int? CountryId { get; set; }
public string? AccountNumber { get; set; }
public string? Iban { get; set; }
public string? Bic { get; set; }
public string? Bank { get; set; }
}

View File

@ -0,0 +1,15 @@
namespace DoliMiddlewareApi.Dtos.query;
public class BankAccountDto
{
public int Id { get; set; }
public required string Label { get; set; }
public string? Ref { get; set; }
public string? AccountNumber { get; set; }
public string? Iban { get; set; }
public string? Bic { get; set; }
public string? Bank { get; set; }
public string? CurrencyCode { get; set; }
public double Balance { get; set; }
public bool IsClosed { get; set; }
}

View File

@ -0,0 +1,13 @@
namespace DoliMiddlewareApi.Dtos.query;
public class BankLineDto
{
public int Id { get; set; }
public string? Label { get; set; }
public double Amount { get; set; }
public DateTime? Date { get; set; }
public DateTime? DateValue { get; set; }
public int AccountId { get; set; }
public string? BankStatement { get; set; }
public string? Type { get; set; }
}

View File

@ -7,6 +7,8 @@ public class InvoiceDto
public DateTime? Date { get; set; } public DateTime? Date { get; set; }
public DateTime? ExpireDate { get; set; } public DateTime? ExpireDate { get; set; }
public int ClientId { get; set; } public int ClientId { get; set; }
public decimal? TotalHt { get; set; }
public decimal? TotalTax { get; set; }
public decimal? Total { get; set; } public decimal? Total { get; set; }
public decimal? RemainToPay { get; set; } public decimal? RemainToPay { get; set; }
public string Status { get; set; } public string Status { get; set; }

View File

@ -0,0 +1,49 @@
using DoliMiddlewareApi.Dtos.Dolibarr;
using DoliMiddlewareApi.Dtos.query;
namespace DoliMiddlewareApi.Mappers;
public static class BankMapper
{
public static BankAccountDto MapToAccountDto(BankAccountResponse r)
{
return new BankAccountDto
{
Id = int.TryParse(r.id, out var id) ? id : 0,
Label = r.label ?? r.@ref ?? "Cuenta sin nombre",
Ref = r.@ref,
AccountNumber = r.number,
Iban = r.iban_prefix,
Bic = r.bic,
Bank = r.bank,
CurrencyCode = r.currency_code ?? "EUR",
Balance = r.solde ?? 0,
IsClosed = r.clos == "1",
};
}
public static BankLineDto MapToLineDto(BankLineResponse r, int accountId)
{
var lineId = int.TryParse(r.id ?? r.rowid, out var id) ? id : 0;
DateTime? dateo = r.dateo.HasValue
? DateTimeOffset.FromUnixTimeSeconds(r.dateo.Value).DateTime
: null;
DateTime? datev = r.datev.HasValue
? DateTimeOffset.FromUnixTimeSeconds(r.datev.Value).DateTime
: null;
return new BankLineDto
{
Id = lineId,
Label = r.label,
Amount = r.amount ?? 0,
Date = dateo,
DateValue = datev,
AccountId = int.TryParse(r.fk_account, out var accId) ? accId : accountId,
BankStatement = r.num_releve,
Type = r.type,
};
}
}

View File

@ -26,6 +26,14 @@ public static class InvoiceMapper
ClientId = int.TryParse(invoiceResponse.socid, out int clientId) ? clientId : 0, ClientId = int.TryParse(invoiceResponse.socid, out int clientId) ? clientId : 0,
TotalHt = decimal.TryParse(invoiceResponse.total_ht, NumberStyles.Any, CultureInfo.InvariantCulture,
out decimal ht)
? Math.Round(ht, 2)
: null,
TotalTax = decimal.TryParse(invoiceResponse.total_tva, NumberStyles.Any, CultureInfo.InvariantCulture,
out decimal tva)
? Math.Round(tva, 2)
: null,
Total = decimal.TryParse(invoiceResponse.total_ttc, NumberStyles.Any, CultureInfo.InvariantCulture, Total = decimal.TryParse(invoiceResponse.total_ttc, NumberStyles.Any, CultureInfo.InvariantCulture,
out decimal total) out decimal total)
? Math.Round(total, 2) ? Math.Round(total, 2)

View File

@ -163,6 +163,9 @@ builder.Services.AddScoped<DocumentService>();
// Servicio de negocio (setup/diccionarios) // Servicio de negocio (setup/diccionarios)
builder.Services.AddScoped<SetupService>(); builder.Services.AddScoped<SetupService>();
// Servicio de negocio (banco)
builder.Services.AddScoped<BankService>();
// Servicio de aplicación (orquesta login + cache) // Servicio de aplicación (orquesta login + cache)
builder.Services.AddScoped<AuthApplicationService>(); builder.Services.AddScoped<AuthApplicationService>();

View File

@ -0,0 +1,111 @@
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 BankService(IDolibarrApiClient apiClient)
{
public async Task<List<BankAccountDto>> GetAccountsAsync()
{
var accounts = await apiClient.GetCollectionAsync<BankAccountResponse>("bankaccounts?sortfield=t.rowid&sortorder=ASC&limit=100");
return accounts.Select(BankMapper.MapToAccountDto).ToList();
}
public async Task<BankAccountDto> GetAccountAsync(int id)
{
var account = await apiClient.GetResourceAsync<BankAccountResponse>($"bankaccounts/{id}");
return BankMapper.MapToAccountDto(account);
}
public async Task<double> GetBalanceAsync(int id)
{
var result = await apiClient.GetResourceAsync<BankAccountResponse>($"bankaccounts/{id}/balance");
return result.solde ?? 0;
}
public async Task<List<BankLineDto>> GetMovementsAsync(int limit = 20)
{
// Fetch all accounts first, then their lines in parallel
List<BankAccountResponse> accounts;
try
{
accounts = await apiClient.GetCollectionAsync<BankAccountResponse>("bankaccounts?sortfield=t.rowid&sortorder=ASC&limit=100");
}
catch (ApiException)
{
return [];
}
if (accounts.Count == 0)
return [];
var lineTasks = accounts
.Where(a => int.TryParse(a.id, out _))
.Select(async a =>
{
var accountId = int.Parse(a.id!);
try
{
var lines = await apiClient.GetCollectionAsync<BankLineResponse>($"bankaccounts/{accountId}/lines");
return lines.Select(l => BankMapper.MapToLineDto(l, accountId));
}
catch
{
return Enumerable.Empty<BankLineDto>();
}
});
var results = await Task.WhenAll(lineTasks);
return results
.SelectMany(lines => lines)
.OrderByDescending(l => l.Date ?? l.DateValue ?? DateTime.MinValue)
.Take(limit)
.ToList();
}
public async Task<List<BankLineDto>> GetAccountLinesAsync(int accountId)
{
var lines = await apiClient.GetCollectionAsync<BankLineResponse>($"bankaccounts/{accountId}/lines");
return lines.Select(l => BankMapper.MapToLineDto(l, accountId)).ToList();
}
public async Task UpdateAccountAsync(int id, UpdateBankAccountDto dto)
{
var requestBody = new Dictionary<string, object?>();
if (dto.Label != null) requestBody["label"] = dto.Label;
if (dto.Ref != null) requestBody["ref"] = dto.Ref;
if (dto.Type != null) requestBody["type"] = dto.Type;
if (dto.CurrencyCode != null) requestBody["currency_code"] = dto.CurrencyCode;
if (dto.CountryId != null) requestBody["country_id"] = dto.CountryId;
if (dto.AccountNumber!= null) requestBody["number"] = dto.AccountNumber;
if (dto.Iban != null) requestBody["iban_prefix"] = dto.Iban;
if (dto.Bic != null) requestBody["bic"] = dto.Bic;
if (dto.Bank != null) requestBody["bank"] = dto.Bank;
await apiClient.PutAsync($"bankaccounts/{id}", requestBody);
}
public async Task<int> CreateAccountAsync(CreateBankAccountDto dto)
{
var requestBody = new Dictionary<string, object?>
{
["ref"] = dto.Ref,
["label"] = dto.Label,
["type"] = dto.Type,
["currency_code"] = dto.CurrencyCode,
["country_id"] = dto.CountryId,
["number"] = dto.AccountNumber,
["iban_prefix"] = dto.Iban,
["bic"] = dto.Bic,
["bank"] = dto.Bank,
["clos"] = 0,
};
var response = await apiClient.PostAsync("bankaccounts", requestBody);
return int.TryParse(response, out var id) ? id : 0;
}
}