feat: módulo banco completo (GET/POST/PUT cuentas y movimientos), campos TotalHt/TotalTax en facturas
This commit is contained in:
parent
8f0fff3953
commit
f2030977ff
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ public class InvoiceResponse
|
|||
public long? date_lim_reglement { 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? remaintopay { get; set; }
|
||||
public string? statut { get; set; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ public class InvoiceDto
|
|||
public DateTime? Date { get; set; }
|
||||
public DateTime? ExpireDate { get; set; }
|
||||
public int ClientId { get; set; }
|
||||
public decimal? TotalHt { get; set; }
|
||||
public decimal? TotalTax { get; set; }
|
||||
public decimal? Total { get; set; }
|
||||
public decimal? RemainToPay { get; set; }
|
||||
public string Status { get; set; }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,14 @@ public static class InvoiceMapper
|
|||
|
||||
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,
|
||||
out decimal total)
|
||||
? Math.Round(total, 2)
|
||||
|
|
|
|||
|
|
@ -163,6 +163,9 @@ builder.Services.AddScoped<DocumentService>();
|
|||
// Servicio de negocio (setup/diccionarios)
|
||||
builder.Services.AddScoped<SetupService>();
|
||||
|
||||
// Servicio de negocio (banco)
|
||||
builder.Services.AddScoped<BankService>();
|
||||
|
||||
// Servicio de aplicación (orquesta login + cache)
|
||||
builder.Services.AddScoped<AuthApplicationService>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue