diff --git a/DoliMiddlewareApi/Controllers/BankController.cs b/DoliMiddlewareApi/Controllers/BankController.cs new file mode 100644 index 0000000..9611e5c --- /dev/null +++ b/DoliMiddlewareApi/Controllers/BankController.cs @@ -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), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> 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> 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> GetBalance([Range(1, int.MaxValue)] int id) + { + var balance = await bankService.GetBalanceAsync(id); + return Ok(balance); + } + + [HttpGet("accounts/{id:int}/lines")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> GetAccountLines([Range(1, int.MaxValue)] int id) + { + var lines = await bankService.GetAccountLinesAsync(id); + return Ok(lines); + } + + [HttpGet("movements")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] + public async Task>> 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 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> CreateAccount([FromBody] CreateBankAccountDto dto) + { + var id = await bankService.CreateAccountAsync(dto); + return CreatedAtAction(nameof(GetAccount), new { id }, id); + } +} diff --git a/DoliMiddlewareApi/Dtos/Dolibarr/BankAccountResponse.cs b/DoliMiddlewareApi/Dtos/Dolibarr/BankAccountResponse.cs new file mode 100644 index 0000000..0210a4c --- /dev/null +++ b/DoliMiddlewareApi/Dtos/Dolibarr/BankAccountResponse.cs @@ -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; } +} diff --git a/DoliMiddlewareApi/Dtos/Dolibarr/BankLineResponse.cs b/DoliMiddlewareApi/Dtos/Dolibarr/BankLineResponse.cs new file mode 100644 index 0000000..0251f26 --- /dev/null +++ b/DoliMiddlewareApi/Dtos/Dolibarr/BankLineResponse.cs @@ -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; } +} diff --git a/DoliMiddlewareApi/Dtos/Dolibarr/InvoiceResponse.cs b/DoliMiddlewareApi/Dtos/Dolibarr/InvoiceResponse.cs index 8ccfd48..89d31c7 100644 --- a/DoliMiddlewareApi/Dtos/Dolibarr/InvoiceResponse.cs +++ b/DoliMiddlewareApi/Dtos/Dolibarr/InvoiceResponse.cs @@ -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; } diff --git a/DoliMiddlewareApi/Dtos/command/CreateBankAccountDto.cs b/DoliMiddlewareApi/Dtos/command/CreateBankAccountDto.cs new file mode 100644 index 0000000..b0f3bee --- /dev/null +++ b/DoliMiddlewareApi/Dtos/command/CreateBankAccountDto.cs @@ -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; } + /// 0 = Savings, 1 = Current/Checking, 2 = Cash + 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; } +} diff --git a/DoliMiddlewareApi/Dtos/command/UpdateBankAccountDto.cs b/DoliMiddlewareApi/Dtos/command/UpdateBankAccountDto.cs new file mode 100644 index 0000000..830fd42 --- /dev/null +++ b/DoliMiddlewareApi/Dtos/command/UpdateBankAccountDto.cs @@ -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; } +} diff --git a/DoliMiddlewareApi/Dtos/query/BankAccountDto.cs b/DoliMiddlewareApi/Dtos/query/BankAccountDto.cs new file mode 100644 index 0000000..ccff299 --- /dev/null +++ b/DoliMiddlewareApi/Dtos/query/BankAccountDto.cs @@ -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; } +} diff --git a/DoliMiddlewareApi/Dtos/query/BankLineDto.cs b/DoliMiddlewareApi/Dtos/query/BankLineDto.cs new file mode 100644 index 0000000..83ddbd0 --- /dev/null +++ b/DoliMiddlewareApi/Dtos/query/BankLineDto.cs @@ -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; } +} diff --git a/DoliMiddlewareApi/Dtos/query/InvoiceDto.cs b/DoliMiddlewareApi/Dtos/query/InvoiceDto.cs index 92af20d..5caea13 100644 --- a/DoliMiddlewareApi/Dtos/query/InvoiceDto.cs +++ b/DoliMiddlewareApi/Dtos/query/InvoiceDto.cs @@ -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; } diff --git a/DoliMiddlewareApi/Mappers/BankMapper.cs b/DoliMiddlewareApi/Mappers/BankMapper.cs new file mode 100644 index 0000000..8c48399 --- /dev/null +++ b/DoliMiddlewareApi/Mappers/BankMapper.cs @@ -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, + }; + } +} diff --git a/DoliMiddlewareApi/Mappers/InvoiceMapper.cs b/DoliMiddlewareApi/Mappers/InvoiceMapper.cs index 1305621..16b394a 100644 --- a/DoliMiddlewareApi/Mappers/InvoiceMapper.cs +++ b/DoliMiddlewareApi/Mappers/InvoiceMapper.cs @@ -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) diff --git a/DoliMiddlewareApi/Program.cs b/DoliMiddlewareApi/Program.cs index 1ac8588..2e106f4 100644 --- a/DoliMiddlewareApi/Program.cs +++ b/DoliMiddlewareApi/Program.cs @@ -163,6 +163,9 @@ builder.Services.AddScoped(); // Servicio de negocio (setup/diccionarios) builder.Services.AddScoped(); +// Servicio de negocio (banco) +builder.Services.AddScoped(); + // Servicio de aplicación (orquesta login + cache) builder.Services.AddScoped(); diff --git a/DoliMiddlewareApi/Services/BankService.cs b/DoliMiddlewareApi/Services/BankService.cs new file mode 100644 index 0000000..6bfe754 --- /dev/null +++ b/DoliMiddlewareApi/Services/BankService.cs @@ -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> GetAccountsAsync() + { + var accounts = await apiClient.GetCollectionAsync("bankaccounts?sortfield=t.rowid&sortorder=ASC&limit=100"); + return accounts.Select(BankMapper.MapToAccountDto).ToList(); + } + + public async Task GetAccountAsync(int id) + { + var account = await apiClient.GetResourceAsync($"bankaccounts/{id}"); + return BankMapper.MapToAccountDto(account); + } + + public async Task GetBalanceAsync(int id) + { + var result = await apiClient.GetResourceAsync($"bankaccounts/{id}/balance"); + return result.solde ?? 0; + } + + public async Task> GetMovementsAsync(int limit = 20) + { + // Fetch all accounts first, then their lines in parallel + List accounts; + try + { + accounts = await apiClient.GetCollectionAsync("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($"bankaccounts/{accountId}/lines"); + return lines.Select(l => BankMapper.MapToLineDto(l, accountId)); + } + catch + { + return Enumerable.Empty(); + } + }); + + 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> GetAccountLinesAsync(int accountId) + { + var lines = await apiClient.GetCollectionAsync($"bankaccounts/{accountId}/lines"); + return lines.Select(l => BankMapper.MapToLineDto(l, accountId)).ToList(); + } + + public async Task UpdateAccountAsync(int id, UpdateBankAccountDto dto) + { + var requestBody = new Dictionary(); + 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 CreateAccountAsync(CreateBankAccountDto dto) + { + var requestBody = new Dictionary + { + ["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; + } +}