diff --git a/.idea/.idea.Fin-Backend/.idea/dataSources.xml b/.idea/.idea.Fin-Backend/.idea/dataSources.xml
index 9ed83d1..ff0e1a0 100644
--- a/.idea/.idea.Fin-Backend/.idea/dataSources.xml
+++ b/.idea/.idea.Fin-Backend/.idea/dataSources.xml
@@ -4,6 +4,7 @@
postgresql
true
+ true
org.postgresql.Driver
jdbc:postgresql://localhost:5432/postgres
@@ -13,5 +14,12 @@
$ProjectFileDir$
+
+ postgresql
+ true
+ org.postgresql.Driver
+ jdbc:postgresql://localhost:5432/postgres
+ $ProjectFileDir$
+
\ No newline at end of file
diff --git a/Fin-Backend.sln.DotSettings.user b/Fin-Backend.sln.DotSettings.user
index ca2fb04..6ef6c84 100644
--- a/Fin-Backend.sln.DotSettings.user
+++ b/Fin-Backend.sln.DotSettings.user
@@ -4,6 +4,9 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
+ <Solution />
+</SessionState>
diff --git a/Fin.Api/CreditCards/CreditCardController.cs b/Fin.Api/CreditCards/CreditCardController.cs
new file mode 100644
index 0000000..3244aac
--- /dev/null
+++ b/Fin.Api/CreditCards/CreditCardController.cs
@@ -0,0 +1,63 @@
+using Fin.Application.CreditCards.Dtos;
+using Fin.Application.CreditCards.Enums;
+using Fin.Application.CreditCards.Services;
+using Fin.Domain.Global.Classes;
+using Fin.Domain.CreditCards.Dtos;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Fin.Api.CreditCards;
+
+[Route("credit-cards")]
+[Authorize]
+public class CreditCardController(ICreditCardService service) : ControllerBase
+{
+ [HttpGet]
+ public async Task> GetList([FromQuery] CreditCardGetListInput input)
+ {
+ return await service.GetList(input);
+ }
+
+ [HttpGet("{id:guid}")]
+ public async Task> Get([FromRoute] Guid id)
+ {
+ var category = await service.Get(id);
+ return category != null ? Ok(category) : NotFound();
+ }
+
+ [HttpPost]
+ public async Task> Create([FromBody] CreditCardInput input)
+ {
+ var validationResult = await service.Create(input, autoSave: true);
+ return validationResult.Success
+ ? Created($"categories/{validationResult.Data?.Id}", validationResult.Data)
+ : UnprocessableEntity(validationResult);
+ }
+
+ [HttpPut("{id:guid}")]
+ public async Task Update([FromRoute] Guid id, [FromBody] CreditCardInput input)
+ {
+ var validationResult = await service.Update(id, input, autoSave: true);
+ return validationResult.Success ? Ok() :
+ validationResult.ErrorCode == CreditCardCreateOrUpdateErrorCode.CreditCardNotFound ? NotFound(validationResult) :
+ UnprocessableEntity(validationResult);
+ }
+
+ [HttpPut("toggle-inactivated/{id:guid}")]
+ public async Task ToggleInactivated([FromRoute] Guid id)
+ {
+ var validationResult = await service.ToggleInactive(id, autoSave: true);
+ return validationResult.Success ? Ok() :
+ validationResult.ErrorCode == CreditCardToggleInactiveErrorCode.CreditCardNotFound ? NotFound(validationResult) :
+ UnprocessableEntity(validationResult);
+ }
+
+ [HttpDelete("{id:guid}")]
+ public async Task Delete([FromRoute] Guid id)
+ {
+ var validationResult = await service.Delete(id, autoSave: true);
+ return validationResult.Success ? Ok() :
+ validationResult.ErrorCode == CreditCardDeleteErrorCode.CreditCardNotFound ? NotFound(validationResult) :
+ UnprocessableEntity(validationResult);
+ }
+}
\ No newline at end of file
diff --git a/Fin.Application/CreditCards/Dtos/CreditCardGetListInput.cs b/Fin.Application/CreditCards/Dtos/CreditCardGetListInput.cs
new file mode 100644
index 0000000..e050beb
--- /dev/null
+++ b/Fin.Application/CreditCards/Dtos/CreditCardGetListInput.cs
@@ -0,0 +1,11 @@
+using Fin.Domain.Global.Classes;
+
+namespace Fin.Application.CreditCards.Dtos;
+
+public class CreditCardGetListInput: PagedFilteredAndSortedInput
+{
+ public bool? Inactivated { get; set; }
+ public List DebitWalletIds { get; set; } = [];
+ public List FinancialInstitutionIds { get; set; } = [];
+ public List CardBrandIds { get; set; } = [];
+}
\ No newline at end of file
diff --git a/Fin.Application/CreditCards/Enums/CreditCardCreateOrUpdateErrorCode.cs b/Fin.Application/CreditCards/Enums/CreditCardCreateOrUpdateErrorCode.cs
new file mode 100644
index 0000000..dbd1cc4
--- /dev/null
+++ b/Fin.Application/CreditCards/Enums/CreditCardCreateOrUpdateErrorCode.cs
@@ -0,0 +1,25 @@
+namespace Fin.Application.CreditCards.Enums;
+
+public enum CreditCardCreateOrUpdateErrorCode
+{
+ NameIsRequired = 0,
+ NameAlreadyInUse = 1,
+ NameTooLong = 2,
+ ColorIsRequired = 3,
+ ColorTooLong = 4,
+ IconIsRequired = 5,
+ IconTooLong = 6,
+ CreditCardNotFound = 7,
+
+ FinancialInstitutionNotFound = 8,
+ FinancialInstitutionInactivated = 9,
+
+ DebitWalletNotFound = 10,
+ DebitWalletInactivated = 11,
+
+ CardBrandNotFound = 12,
+
+ LimitMinValueZero = 13,
+ DueDayOutOfRange = 14,
+ ClosingDayOutOfRange = 15
+}
\ No newline at end of file
diff --git a/Fin.Application/CreditCards/Enums/CreditCardDeleteErrorCode.cs b/Fin.Application/CreditCards/Enums/CreditCardDeleteErrorCode.cs
new file mode 100644
index 0000000..12feeb3
--- /dev/null
+++ b/Fin.Application/CreditCards/Enums/CreditCardDeleteErrorCode.cs
@@ -0,0 +1,7 @@
+namespace Fin.Application.CreditCards.Enums;
+
+public enum CreditCardDeleteErrorCode
+{
+ CreditCardNotFound = 0,
+ CreditCardInUse = 1
+}
\ No newline at end of file
diff --git a/Fin.Application/CreditCards/Enums/CreditCardToggleInactiveErrorCode.cs b/Fin.Application/CreditCards/Enums/CreditCardToggleInactiveErrorCode.cs
new file mode 100644
index 0000000..09bfacf
--- /dev/null
+++ b/Fin.Application/CreditCards/Enums/CreditCardToggleInactiveErrorCode.cs
@@ -0,0 +1,6 @@
+namespace Fin.Application.CreditCards.Enums;
+
+public enum CreditCardToggleInactiveErrorCode
+{
+ CreditCardNotFound = 0
+}
\ No newline at end of file
diff --git a/Fin.Application/CreditCards/Services/CreditCardService.cs b/Fin.Application/CreditCards/Services/CreditCardService.cs
new file mode 100644
index 0000000..b3b185e
--- /dev/null
+++ b/Fin.Application/CreditCards/Services/CreditCardService.cs
@@ -0,0 +1,97 @@
+using Fin.Application.Globals.Dtos;
+using Fin.Application.CreditCards.Dtos;
+using Fin.Application.CreditCards.Enums;
+using Fin.Domain.Global.Classes;
+using Fin.Domain.CreditCards.Dtos;
+using Fin.Domain.CreditCards.Entities;
+using Fin.Infrastructure.AutoServices.Interfaces;
+using Fin.Infrastructure.Database.Extensions;
+using Fin.Infrastructure.Database.Repositories;
+using Microsoft.EntityFrameworkCore;
+
+namespace Fin.Application.CreditCards.Services;
+
+public interface ICreditCardService
+{
+ public Task Get(Guid id);
+ public Task> GetList(CreditCardGetListInput input);
+ public Task> Create(CreditCardInput input, bool autoSave = false);
+ public Task> Update(Guid id, CreditCardInput input, bool autoSave = false);
+ public Task> Delete(Guid id, bool autoSave = false);
+ public Task> ToggleInactive(Guid id, bool autoSave = false);
+}
+
+public class CreditCardService(
+ IRepository repository,
+ ICreditCardValidationService validationService
+ ) : ICreditCardService, IAutoTransient
+{
+ public async Task Get(Guid id)
+ {
+ var entity = await repository.Query(false).FirstOrDefaultAsync(n => n.Id == id);
+ return entity != null ? new CreditCardOutput(entity) : null;
+ }
+
+ public async Task> GetList(CreditCardGetListInput input)
+ {
+ return await repository.Query(false)
+ .WhereIf(input.Inactivated.HasValue, n => n.Inactivated == input.Inactivated.Value)
+ .WhereIf(input.CardBrandIds.Any(), creditCard => input.CardBrandIds.Contains(creditCard.CardBrandId))
+ .WhereIf(input.DebitWalletIds.Any(), creditCard => input.DebitWalletIds.Contains(creditCard.DebitWalletId))
+ .WhereIf(input.FinancialInstitutionIds.Any(), creditCard => input.FinancialInstitutionIds.Contains(creditCard.FinancialInstitutionId))
+ .OrderBy(m => m.Inactivated)
+ .ThenBy(m => m.Name)
+ .ApplyFilterAndSorter(input)
+ .Select(n => new CreditCardOutput(n))
+ .ToPagedResult(input);
+ }
+
+ public async Task> Create(CreditCardInput input, bool autoSave = false)
+ {
+ var validation = await validationService.ValidateInput(input);
+ if (!validation.Success) return validation;
+
+ var creditCard = new CreditCard(input);
+ await repository.AddAsync(creditCard, autoSave);
+ validation.Data = new CreditCardOutput(creditCard);
+ return validation;
+ }
+
+ public async Task> Update(Guid id, CreditCardInput input, bool autoSave = false)
+ {
+ var validation = await validationService.ValidateInput(input, id);
+ if (!validation.Success) return validation;
+
+ var creditCard = await repository.Query().FirstAsync(u => u.Id == id);
+ creditCard.Update(input);
+ await repository.UpdateAsync(creditCard, autoSave);
+
+ validation.Data = true;
+ return validation;
+ }
+
+ public async Task> Delete(Guid id, bool autoSave = false)
+ {
+ var validation = await validationService.ValidateDelete(id);
+ if (!validation.Success) return validation;
+
+ var creditCard = await repository.Query().FirstAsync(u => u.Id == id);
+ await repository.DeleteAsync(creditCard, autoSave);
+
+ validation.Success = true;
+ return validation;
+ }
+
+ public async Task> ToggleInactive(Guid id, bool autoSave = false)
+ {
+ var validation = await validationService.ValidateToggleInactive(id);
+ if (!validation.Success) return validation;
+
+ var creditCard = await repository.Query().FirstAsync(u => u.Id == id);
+ creditCard.ToggleInactivated();
+ await repository.UpdateAsync(creditCard, autoSave);
+
+ validation.Data = true;
+ return validation;
+ }
+}
\ No newline at end of file
diff --git a/Fin.Application/CreditCards/Services/CreditCardValidationService.cs b/Fin.Application/CreditCards/Services/CreditCardValidationService.cs
new file mode 100644
index 0000000..abbaed3
--- /dev/null
+++ b/Fin.Application/CreditCards/Services/CreditCardValidationService.cs
@@ -0,0 +1,190 @@
+using Fin.Application.CardBrands;
+using Fin.Application.FinancialInstitutions;
+using Fin.Application.Globals.Dtos;
+using Fin.Application.CreditCards.Enums;
+using Fin.Application.Wallets.Services;
+using Fin.Domain.CreditCards.Dtos;
+using Fin.Domain.CreditCards.Entities;
+using Fin.Infrastructure.AutoServices.Interfaces;
+using Fin.Infrastructure.Database.Repositories;
+using Microsoft.EntityFrameworkCore;
+
+namespace Fin.Application.CreditCards.Services;
+
+public interface ICreditCardValidationService
+{
+ public Task> ValidateToggleInactive(Guid creditCardId);
+ public Task> ValidateDelete(Guid creditCardId);
+
+ public Task> ValidateInput(CreditCardInput input,
+ Guid? editingId = null);
+}
+
+public class CreditCardValidationService(
+ IRepository repository,
+ IFinancialInstitutionService financialInstitutionService,
+ IWalletService walletService,
+ ICardBrandService cardBrandService
+) : ICreditCardValidationService, IAutoTransient
+{
+ public async Task> ValidateToggleInactive(Guid creditCardId)
+ {
+ var validationResult = new ValidationResultDto();
+
+ var creditCard = await repository.Query(tracking: false).FirstOrDefaultAsync(n => n.Id == creditCardId);
+ if (creditCard is null)
+ {
+ validationResult.ErrorCode = CreditCardToggleInactiveErrorCode.CreditCardNotFound;
+ validationResult.Message = "CreditCard not found to toggle inactive.";
+ return validationResult;
+ }
+
+ validationResult.Success = true;
+ return validationResult;
+ }
+
+ public async Task> ValidateDelete(Guid creditCardId)
+ {
+ var validationResult = new ValidationResultDto();
+
+ var creditCardExists = await repository.Query().AnyAsync(n => n.Id == creditCardId);
+ if (!creditCardExists)
+ {
+ validationResult.ErrorCode = CreditCardDeleteErrorCode.CreditCardNotFound;
+ validationResult.Message = "CreditCard not found to delete.";
+ return validationResult;
+ }
+
+ // TODO here validate relations
+
+ validationResult.Success = true;
+ return validationResult;
+ }
+
+ public async Task> ValidateInput(CreditCardInput input,
+ Guid? editingId = null)
+ {
+ var validationResult = new ValidationResultDto();
+
+ if (editingId.HasValue)
+ {
+ var creditCardExists = await repository.Query()
+ .AnyAsync(n => n.Id == editingId.Value);
+ if (!creditCardExists)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.CreditCardNotFound;
+ validationResult.Message = "CreditCard not found to edit.";
+ return validationResult;
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(input.Color))
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.ColorIsRequired;
+ validationResult.Message = "Color is required.";
+ return validationResult;
+ }
+
+ if (input.Color.Length > 20)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.ColorTooLong;
+ validationResult.Message = "Color is too long. Max 20 characters.";
+ return validationResult;
+ }
+
+ if (string.IsNullOrWhiteSpace(input.Icon))
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.IconIsRequired;
+ validationResult.Message = "Icon is required.";
+ return validationResult;
+ }
+
+ if (input.Icon.Length > 20)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.IconTooLong;
+ validationResult.Message = "Icon is too long. Max 20 characters.";
+ return validationResult;
+ }
+
+ if (string.IsNullOrWhiteSpace(input.Name))
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.NameIsRequired;
+ validationResult.Message = "Name is required.";
+ return validationResult;
+ }
+
+ if (input.Name.Length > 100)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.NameTooLong;
+ validationResult.Message = "Name is too long. Max 100 characters.";
+ return validationResult;
+ }
+
+ var nameAlredInUse = await repository.Query()
+ .AnyAsync(n => n.Name.ToLower() == input.Name.ToLower() && (!editingId.HasValue || n.Id != editingId));
+ if (nameAlredInUse)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.NameAlreadyInUse;
+ validationResult.Message = "Name is already in use.";
+ return validationResult;
+ }
+
+ var financialInstitution = await financialInstitutionService.Get(input.FinancialInstitutionId);
+ if (financialInstitution is null)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.FinancialInstitutionNotFound;
+ validationResult.Message = "Financial institution not found.";
+ return validationResult;
+ }
+ if (financialInstitution.Inactive)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.FinancialInstitutionInactivated;
+ validationResult.Message = "Financial institution is inactive.";
+ return validationResult;
+ }
+
+ var wallet = await walletService.Get(input.DebitWalletId);
+ if (wallet is null)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.DebitWalletNotFound;
+ validationResult.Message = "Debit wallet not found.";
+ return validationResult;
+ }
+ if (wallet.Inactivated)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.DebitWalletInactivated;
+ validationResult.Message = "Debit wallet is inactive.";
+ return validationResult;
+ }
+
+ var cardBrand = await cardBrandService.Get(input.CardBrandId);
+ if (cardBrand is null)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.CardBrandNotFound;
+ validationResult.Message = "CardBrand not found.";
+ return validationResult;
+ }
+
+ if (input.Limit <= 0)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.LimitMinValueZero;
+ validationResult.Message = "Limit must be greater than zero.";
+ return validationResult;
+ }
+ if (1 > input.DueDay || input.DueDay > 31)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.DueDayOutOfRange;
+ validationResult.Message = "Due day is out of range. >= 1 and <= 31";
+ return validationResult;
+ }
+ if (1 > input.ClosingDay || input.ClosingDay > 31)
+ {
+ validationResult.ErrorCode = CreditCardCreateOrUpdateErrorCode.ClosingDayOutOfRange;
+ validationResult.Message = "Closing day is out of range. >= 1 and <= 31";
+ return validationResult;
+ }
+
+ validationResult.Success = true;
+ return validationResult;
+ }
+}
\ No newline at end of file
diff --git a/Fin.Application/Wallets/Services/WalletValidationService.cs b/Fin.Application/Wallets/Services/WalletValidationService.cs
index 1e44eec..c2a9e6e 100644
--- a/Fin.Application/Wallets/Services/WalletValidationService.cs
+++ b/Fin.Application/Wallets/Services/WalletValidationService.cs
@@ -1,6 +1,7 @@
using Fin.Application.FinancialInstitutions;
using Fin.Application.Globals.Dtos;
using Fin.Application.Wallets.Enums;
+using Fin.Domain.CreditCards.Entities;
using Fin.Domain.Wallets.Dtos;
using Fin.Domain.Wallets.Entities;
using Fin.Infrastructure.AutoServices.Interfaces;
@@ -19,7 +20,8 @@ public Task> ValidateInput
}
public class WalletValidationService(
- IRepository repository,
+ IRepository walletRepository,
+ IRepository creditCardRepository,
IFinancialInstitutionService financialInstitutionService
) : IWalletValidationService, IAutoTransient
{
@@ -27,15 +29,21 @@ public async Task> Vali
{
var validationResult = new ValidationResultDto();
- var wallet = await repository.Query(tracking: false).FirstOrDefaultAsync(n => n.Id == walletId);
+ var wallet = await walletRepository.Query(tracking: false).FirstOrDefaultAsync(n => n.Id == walletId);
if (wallet is null)
{
validationResult.ErrorCode = WalletToggleInactiveErrorCode.WalletNotFound;
- validationResult.Message = "Wallet not found to toogle inactive.";
+ validationResult.Message = "Wallet not found to toggle inactive.";
return validationResult;
}
- // TODO here validate relations
+ var walletInUseByActivatedCreditCard = await creditCardRepository.Query().AnyAsync(n => n.DebitWalletId == walletId && !n.Inactivated);
+ if (walletInUseByActivatedCreditCard)
+ {
+ validationResult.ErrorCode = WalletToggleInactiveErrorCode.WalletInUseByActivatedCreditCards;
+ validationResult.Message = "Wallet in use by activated credit cards.";
+ return validationResult;
+ }
validationResult.Success = true;
return validationResult;
@@ -45,13 +53,21 @@ public async Task> ValidateDele
{
var validationResult = new ValidationResultDto();
- var walletExists = await repository.Query().AnyAsync(n => n.Id == walletId);
+ var walletExists = await walletRepository.Query().AnyAsync(n => n.Id == walletId);
if (!walletExists)
{
validationResult.ErrorCode = WalletDeleteErrorCode.WalletNotFound;
validationResult.Message = "Wallet not found to delete.";
return validationResult;
}
+
+ var walletInUseByCreditCard = await creditCardRepository.Query().AnyAsync(n => n.DebitWalletId == walletId);
+ if (walletInUseByCreditCard)
+ {
+ validationResult.ErrorCode = WalletDeleteErrorCode.WalletInUseByCreditCards;
+ validationResult.Message = "Wallet in use by credit cards.";
+ return validationResult;
+ }
// TODO here validate relations
@@ -66,7 +82,7 @@ public async Task> Validat
if (editingId.HasValue)
{
- var walletExists = await repository.Query()
+ var walletExists = await walletRepository.Query()
.AnyAsync(n => n.Id == editingId.Value);
if (!walletExists)
{
@@ -118,7 +134,7 @@ public async Task> Validat
return validationResult;
}
- var nameAlredInUse = await repository.Query()
+ var nameAlredInUse = await walletRepository.Query()
.AnyAsync(n => n.Name.ToLower() == input.Name.ToLower() && (!editingId.HasValue || n.Id != editingId));
if (nameAlredInUse)
{
diff --git a/Fin.Domain/CardBrands/Entities/CardBrand.cs b/Fin.Domain/CardBrands/Entities/CardBrand.cs
index 64e951d..48b8989 100644
--- a/Fin.Domain/CardBrands/Entities/CardBrand.cs
+++ b/Fin.Domain/CardBrands/Entities/CardBrand.cs
@@ -1,4 +1,5 @@
using Fin.Domain.CardBrands.Dtos;
+using Fin.Domain.CreditCards.Entities;
using Fin.Domain.Global.Interfaces;
namespace Fin.Domain.CardBrands.Entities;
@@ -9,6 +10,8 @@ public class CardBrand: IAuditedEntity
public string Icon { get; set; }
public string Color { get; set; }
+ public virtual ICollection CreditCards { get; set; }
+
public Guid Id { get; set; }
public Guid CreatedBy { get; set; }
public Guid UpdatedBy { get; set; }
diff --git a/Fin.Domain/CreditCards/Dtos/CreditCardInput.cs b/Fin.Domain/CreditCards/Dtos/CreditCardInput.cs
new file mode 100644
index 0000000..a11251c
--- /dev/null
+++ b/Fin.Domain/CreditCards/Dtos/CreditCardInput.cs
@@ -0,0 +1,14 @@
+namespace Fin.Domain.CreditCards.Dtos;
+
+public class CreditCardInput
+{
+ public string Name { get; set; }
+ public string Color { get; set; }
+ public string Icon { get; set; }
+ public decimal Limit { get; set; }
+ public int DueDay { get; set; }
+ public int ClosingDay { get; set; }
+ public Guid DebitWalletId { get; set; }
+ public Guid CardBrandId { get; set; }
+ public Guid FinancialInstitutionId { get; set; }
+}
\ No newline at end of file
diff --git a/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs b/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs
new file mode 100644
index 0000000..a1f2e7b
--- /dev/null
+++ b/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs
@@ -0,0 +1,22 @@
+using Fin.Domain.CreditCards.Entities;
+
+namespace Fin.Domain.CreditCards.Dtos;
+
+public class CreditCardOutput(CreditCard card)
+{
+ public Guid Id { get; set; } = card.Id;
+ public string Name { get; set; } = card.Name;
+ public string Color { get; set; } = card.Color;
+ public string Icon { get; set; } = card.Icon;
+ public decimal Limit { get; set; } = card.Limit;
+ public int DueDay { get; set; } = card.DueDay;
+ public int ClosingDay { get; set; } = card.ClosingDay;
+ public Guid DebitWalletId { get; set; } = card.DebitWalletId;
+ public Guid CardBrandId { get; set; } = card.CardBrandId;
+ public Guid FinancialInstitutionId { get; set; } = card.FinancialInstitutionId;
+ public bool Inactivated { get; private set; } = card.Inactivated;
+
+ public CreditCardOutput(): this(new CreditCard())
+ {
+ }
+}
\ No newline at end of file
diff --git a/Fin.Domain/CreditCards/Entities/CreditCard.cs b/Fin.Domain/CreditCards/Entities/CreditCard.cs
new file mode 100644
index 0000000..81f94fd
--- /dev/null
+++ b/Fin.Domain/CreditCards/Entities/CreditCard.cs
@@ -0,0 +1,66 @@
+using Fin.Domain.CardBrands.Entities;
+using Fin.Domain.CreditCards.Dtos;
+using Fin.Domain.FinancialInstitutions.Entities;
+using Fin.Domain.Global.Interfaces;
+using Fin.Domain.Wallets.Entities;
+
+namespace Fin.Domain.CreditCards.Entities;
+
+public class CreditCard: IAuditedTenantEntity
+{
+ public string Name { get; private set; }
+ public string Color { get; private set; }
+ public string Icon { get; private set; }
+ public decimal Limit { get; private set; }
+ public int DueDay { get; private set; }
+ public int ClosingDay { get; private set; }
+ public bool Inactivated { get; private set; }
+
+ public Guid DebitWalletId { get; private set; }
+ public virtual Wallet DebitWallet { get; set; }
+
+ public Guid CardBrandId { get; private set; }
+ public virtual CardBrand CardBrand { get; set; }
+
+ public Guid FinancialInstitutionId { get; set; }
+ public virtual FinancialInstitution FinancialInstitution { get; set; }
+
+ public Guid Id { get; set; }
+ public Guid CreatedBy { get; set; }
+ public Guid UpdatedBy { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime UpdatedAt { get; set; }
+ public Guid TenantId { get; set; }
+
+ public CreditCard()
+ {
+ }
+
+ public CreditCard(CreditCardInput input)
+ {
+ Name = input.Name;
+ Color = input.Color;
+ Icon = input.Icon;
+ Limit = input.Limit;
+ DueDay = input.DueDay;
+ ClosingDay = input.ClosingDay;
+ DebitWalletId = input.DebitWalletId;
+ CardBrandId = input.CardBrandId;
+ FinancialInstitutionId = input.FinancialInstitutionId;
+ }
+
+ public void Update(CreditCardInput input)
+ {
+ Name = input.Name;
+ Color = input.Color;
+ Icon = input.Icon;
+ Limit = input.Limit;
+ DueDay = input.DueDay;
+ ClosingDay = input.ClosingDay;
+ DebitWalletId = input.DebitWalletId;
+ CardBrandId = input.CardBrandId;
+ FinancialInstitutionId = input.FinancialInstitutionId;
+ }
+
+ public void ToggleInactivated() => Inactivated = !Inactivated;
+}
\ No newline at end of file
diff --git a/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs b/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs
index 87cbbed..f7d2dd3 100644
--- a/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs
+++ b/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs
@@ -1,3 +1,4 @@
+using Fin.Domain.CreditCards.Entities;
using Fin.Domain.FinancialInstitutions.Dtos;
using Fin.Domain.FinancialInstitutions.Enums;
using Fin.Domain.Global.Interfaces;
@@ -15,6 +16,7 @@ public class FinancialInstitution : IAuditedEntity
public bool Inactive { get; set; }
public virtual ICollection Wallets { get; set; }
+ public virtual ICollection CreditCards { get; set; }
public Guid Id { get; set; }
public Guid CreatedBy { get; set; }
diff --git a/Fin.Domain/Wallets/Dtos/WalletInput.cs b/Fin.Domain/Wallets/Dtos/WalletInput.cs
index 76be974..54c6df9 100644
--- a/Fin.Domain/Wallets/Dtos/WalletInput.cs
+++ b/Fin.Domain/Wallets/Dtos/WalletInput.cs
@@ -4,17 +4,9 @@ namespace Fin.Domain.Wallets.Dtos;
public class WalletInput
{
- [Required]
public string Name { get; set; }
-
- [Required]
public string Color { get; set; }
-
- [Required]
public string Icon { get; set; }
-
public Guid? FinancialInstitutionId { get; set; }
-
- [Required]
public decimal InitialBalance { get; set; }
}
\ No newline at end of file
diff --git a/Fin.Domain/Wallets/Entities/Wallet.cs b/Fin.Domain/Wallets/Entities/Wallet.cs
index 9a12f9f..613bdee 100644
--- a/Fin.Domain/Wallets/Entities/Wallet.cs
+++ b/Fin.Domain/Wallets/Entities/Wallet.cs
@@ -1,3 +1,4 @@
+using Fin.Domain.CreditCards.Entities;
using Fin.Domain.FinancialInstitutions.Entities;
using Fin.Domain.Global.Interfaces;
using Fin.Domain.Wallets.Dtos;
@@ -19,10 +20,13 @@ public class Wallet: IAuditedTenantEntity
public bool Inactivated { get; private set; }
public Guid? FinancialInstitutionId { get; private set; }
- public virtual FinancialInstitution FinancialInstitution { get; }
+ public virtual FinancialInstitution FinancialInstitution { get; set; }
public decimal InitialBalance { get; private set; }
public decimal CurrentBalance { get; set; }
+
+
+ public virtual ICollection CreditCards { get; set; }
public Wallet()
{
diff --git a/Fin.Infrastructure/Database/Configurations/CreditCards/CreditCardConfiguration.cs b/Fin.Infrastructure/Database/Configurations/CreditCards/CreditCardConfiguration.cs
new file mode 100644
index 0000000..1ff6919
--- /dev/null
+++ b/Fin.Infrastructure/Database/Configurations/CreditCards/CreditCardConfiguration.cs
@@ -0,0 +1,45 @@
+using Fin.Domain.CreditCards.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Fin.Infrastructure.Database.Configurations.CreditCards;
+
+public class CreditCardConfiguration: IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(x => x.Id);
+
+ builder.Property(x => x.Name).HasMaxLength(100).IsRequired();
+ builder.Property(x => x.Icon).HasMaxLength(20).IsRequired();
+ builder.Property(x => x.Color).HasMaxLength(20).IsRequired();
+
+ builder.HasIndex(x => new {x.Name, x.TenantId}).IsUnique();
+
+ builder
+ .Property(p => p.Limit)
+ .HasColumnType("numeric(19,4)")
+ .HasPrecision(19, 4);
+
+ builder
+ .HasOne(creditCard => creditCard.FinancialInstitution)
+ .WithMany(financialInstitution => financialInstitution.CreditCards)
+ .HasForeignKey(creditCard => creditCard.FinancialInstitutionId)
+ .IsRequired(false)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder
+ .HasOne(creditCard => creditCard.CardBrand)
+ .WithMany(cardBrand => cardBrand.CreditCards)
+ .HasForeignKey(creditCard => creditCard.CardBrandId)
+ .IsRequired(false)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ builder
+ .HasOne(creditCard => creditCard.DebitWallet)
+ .WithMany(debitWallet => debitWallet.CreditCards)
+ .HasForeignKey(creditCard => creditCard.DebitWalletId)
+ .IsRequired(false)
+ .OnDelete(DeleteBehavior.Restrict);
+ }
+}
\ No newline at end of file
diff --git a/Fin.Infrastructure/Database/FinDbContext.cs b/Fin.Infrastructure/Database/FinDbContext.cs
index 9b268d0..f45101a 100644
--- a/Fin.Infrastructure/Database/FinDbContext.cs
+++ b/Fin.Infrastructure/Database/FinDbContext.cs
@@ -1,15 +1,14 @@
using System.Reflection;
using Fin.Domain.CardBrands.Entities;
+using Fin.Domain.CreditCards.Entities;
using Fin.Domain.Global.Interfaces;
using Fin.Domain.Menus.Entities;
-using Fin.Domain.Notifications;
using Fin.Domain.Notifications.Entities;
using Fin.Domain.Tenants.Entities;
using Fin.Domain.TitleCategories.Entities;
using Fin.Domain.Users.Entities;
using Fin.Domain.Wallets.Entities;
using Fin.Infrastructure.AmbientDatas;
-using Fin.Infrastructure.Database.Configurations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -34,6 +33,7 @@ public class FinDbContext : DbContext
public DbSet CardBrands { get; set; }
public DbSet TitleCategories { get; set; }
public DbSet Wallets { get; set; }
+ public DbSet CreditCards { get; set; }
private readonly IAmbientData _ambientData;
diff --git a/Fin.Infrastructure/Migrations/20251011180701_adding_creditcard.Designer.cs b/Fin.Infrastructure/Migrations/20251011180701_adding_creditcard.Designer.cs
new file mode 100644
index 0000000..3fe8a26
--- /dev/null
+++ b/Fin.Infrastructure/Migrations/20251011180701_adding_creditcard.Designer.cs
@@ -0,0 +1,831 @@
+//
+using System;
+using Fin.Infrastructure.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Fin.Infrastructure.Migrations
+{
+ [DbContext(typeof(FinDbContext))]
+ [Migration("20251011180701_adding_creditcard")]
+ partial class adding_creditcard
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("public")
+ .HasAnnotation("ProductVersion", "9.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Fin.Domain.CardBrands.Entities.CardBrand", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Color")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("Icon")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.ToTable("CardBrands", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.CreditCards.Entities.CreditCard", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CardBrandId")
+ .HasColumnType("uuid");
+
+ b.Property("ClosingDay")
+ .HasColumnType("integer");
+
+ b.Property("Color")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("DebitWalletId")
+ .HasColumnType("uuid");
+
+ b.Property("DueDay")
+ .HasColumnType("integer");
+
+ b.Property("FinancialInstitutionId")
+ .HasColumnType("uuid");
+
+ b.Property("Icon")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Inactivated")
+ .HasColumnType("boolean");
+
+ b.Property("Limit")
+ .HasPrecision(19, 4)
+ .HasColumnType("numeric(19,4)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CardBrandId");
+
+ b.HasIndex("DebitWalletId");
+
+ b.HasIndex("FinancialInstitutionId");
+
+ b.HasIndex("Name", "TenantId")
+ .IsUnique();
+
+ b.ToTable("CreditCards", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.FinancialInstitutions.Entities.FinancialInstitution", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Code")
+ .HasMaxLength(15)
+ .HasColumnType("character varying(15)");
+
+ b.Property("Color")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("Icon")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Inactive")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .IsUnicode(true)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Type")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.ToTable("FinancialInstitution", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Menus.Entities.Menu", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Color")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("FrontRoute")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Icon")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("KeyWords")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("OnlyForAdmin")
+ .HasColumnType("boolean");
+
+ b.Property("Position")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.ToTable("Menus", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Notifications.Entities.Notification", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Continuous")
+ .HasColumnType("boolean");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("HtmlBody")
+ .HasColumnType("text");
+
+ b.Property("Link")
+ .HasColumnType("text");
+
+ b.Property("NormalizedTextBody")
+ .HasColumnType("text");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("text");
+
+ b.Property("Severity")
+ .HasColumnType("integer");
+
+ b.Property("StartToDelivery")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("StopToDelivery")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("TextBody")
+ .HasColumnType("text");
+
+ b.Property("Title")
+ .HasMaxLength(250)
+ .HasColumnType("character varying(250)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("Ways")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Notifications", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Notifications.Entities.NotificationUserDelivery", b =>
+ {
+ b.Property("NotificationId")
+ .HasColumnType("uuid");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.Property("BackgroundJobId")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Delivery")
+ .HasColumnType("boolean");
+
+ b.Property("Visualized")
+ .HasColumnType("boolean");
+
+ b.HasKey("NotificationId", "UserId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("NotificationUserDeliveries", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserNotificationSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AllowedWays")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("FirebaseTokens")
+ .HasColumnType("text");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("UserNotificationSettings", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserRememberUseSetting", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("NotifyOn")
+ .HasColumnType("interval");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.Property("Ways")
+ .HasColumnType("text");
+
+ b.Property("WeekDays")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("UserRememberUseSettings", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Tenants.Entities.Tenant", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Locale")
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("Timezone")
+ .HasMaxLength(30)
+ .HasColumnType("character varying(30)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.ToTable("Tenants", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Tenants.Entities.TenantUser", b =>
+ {
+ b.Property("TenantId")
+ .HasColumnType("uuid");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("TenantId", "UserId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("TenantUsers", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.TitleCategories.Entities.TitleCategory", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Color")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("Icon")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Inactivated")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid");
+
+ b.Property("Type")
+ .HasColumnType("smallint");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name", "TenantId")
+ .IsUnique();
+
+ b.ToTable("TitleCategories", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Users.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("BirthDate")
+ .HasColumnType("date");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DisplayName")
+ .HasMaxLength(150)
+ .HasColumnType("character varying(150)");
+
+ b.Property("FirstName")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Gender")
+ .HasColumnType("integer");
+
+ b.Property("ImagePublicUrl")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("IsActivity")
+ .HasColumnType("boolean");
+
+ b.Property("IsAdmin")
+ .HasColumnType("boolean");
+
+ b.Property("LastName")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Users.Entities.UserCredential", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("EncryptedEmail")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("EncryptedPassword")
+ .HasMaxLength(300)
+ .HasColumnType("character varying(300)");
+
+ b.Property("FailLoginAttempts")
+ .HasColumnType("integer");
+
+ b.Property("GoogleId")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("ResetToken")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EncryptedEmail")
+ .IsUnique();
+
+ b.HasIndex("GoogleId")
+ .IsUnique();
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("Credentials", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Users.Entities.UserDeleteRequest", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Aborted")
+ .HasColumnType("boolean");
+
+ b.Property("AbortedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("DeleteEffectivatedAt")
+ .HasColumnType("date");
+
+ b.Property("DeleteRequestedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("UserAbortedId")
+ .HasColumnType("uuid");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserAbortedId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("UserDeleteRequests", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Wallets.Entities.Wallet", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Color")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("CurrentBalance")
+ .HasColumnType("numeric");
+
+ b.Property("FinancialInstitutionId")
+ .HasColumnType("uuid");
+
+ b.Property("Icon")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Inactivated")
+ .HasColumnType("boolean");
+
+ b.Property("InitialBalance")
+ .HasPrecision(19, 4)
+ .HasColumnType("numeric(19,4)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FinancialInstitutionId");
+
+ b.HasIndex("Name", "TenantId")
+ .IsUnique();
+
+ b.ToTable("Wallets", "public");
+ });
+
+ modelBuilder.Entity("Fin.Domain.CreditCards.Entities.CreditCard", b =>
+ {
+ b.HasOne("Fin.Domain.CardBrands.Entities.CardBrand", "CardBrand")
+ .WithMany("CreditCards")
+ .HasForeignKey("CardBrandId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("Fin.Domain.Wallets.Entities.Wallet", "DebitWallet")
+ .WithMany("CreditCards")
+ .HasForeignKey("DebitWalletId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("Fin.Domain.FinancialInstitutions.Entities.FinancialInstitution", "FinancialInstitution")
+ .WithMany("CreditCards")
+ .HasForeignKey("FinancialInstitutionId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("CardBrand");
+
+ b.Navigation("DebitWallet");
+
+ b.Navigation("FinancialInstitution");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Notifications.Entities.NotificationUserDelivery", b =>
+ {
+ b.HasOne("Fin.Domain.Notifications.Entities.Notification", "Notification")
+ .WithMany("UserDeliveries")
+ .HasForeignKey("NotificationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Fin.Domain.Users.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Notification");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserNotificationSettings", b =>
+ {
+ b.HasOne("Fin.Domain.Users.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserRememberUseSetting", b =>
+ {
+ b.HasOne("Fin.Domain.Users.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Tenants.Entities.TenantUser", b =>
+ {
+ b.HasOne("Fin.Domain.Tenants.Entities.Tenant", null)
+ .WithMany()
+ .HasForeignKey("TenantId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Fin.Domain.Users.Entities.User", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Fin.Domain.Users.Entities.UserCredential", b =>
+ {
+ b.HasOne("Fin.Domain.Users.Entities.User", "User")
+ .WithOne("Credential")
+ .HasForeignKey("Fin.Domain.Users.Entities.UserCredential", "UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Users.Entities.UserDeleteRequest", b =>
+ {
+ b.HasOne("Fin.Domain.Users.Entities.User", "UserAborted")
+ .WithMany()
+ .HasForeignKey("UserAbortedId");
+
+ b.HasOne("Fin.Domain.Users.Entities.User", "User")
+ .WithMany("DeleteRequests")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+
+ b.Navigation("UserAborted");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Wallets.Entities.Wallet", b =>
+ {
+ b.HasOne("Fin.Domain.FinancialInstitutions.Entities.FinancialInstitution", "FinancialInstitution")
+ .WithMany("Wallets")
+ .HasForeignKey("FinancialInstitutionId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("FinancialInstitution");
+ });
+
+ modelBuilder.Entity("Fin.Domain.CardBrands.Entities.CardBrand", b =>
+ {
+ b.Navigation("CreditCards");
+ });
+
+ modelBuilder.Entity("Fin.Domain.FinancialInstitutions.Entities.FinancialInstitution", b =>
+ {
+ b.Navigation("CreditCards");
+
+ b.Navigation("Wallets");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Notifications.Entities.Notification", b =>
+ {
+ b.Navigation("UserDeliveries");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Users.Entities.User", b =>
+ {
+ b.Navigation("Credential");
+
+ b.Navigation("DeleteRequests");
+ });
+
+ modelBuilder.Entity("Fin.Domain.Wallets.Entities.Wallet", b =>
+ {
+ b.Navigation("CreditCards");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Fin.Infrastructure/Migrations/20251011180701_adding_creditcard.cs b/Fin.Infrastructure/Migrations/20251011180701_adding_creditcard.cs
new file mode 100644
index 0000000..8a61d02
--- /dev/null
+++ b/Fin.Infrastructure/Migrations/20251011180701_adding_creditcard.cs
@@ -0,0 +1,96 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Fin.Infrastructure.Migrations
+{
+ ///
+ public partial class adding_creditcard : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "CreditCards",
+ schema: "public",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ Color = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ Icon = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ Limit = table.Column(type: "numeric(19,4)", precision: 19, scale: 4, nullable: false),
+ DueDay = table.Column(type: "integer", nullable: false),
+ ClosingDay = table.Column(type: "integer", nullable: false),
+ Inactivated = table.Column(type: "boolean", nullable: false),
+ DebitWalletId = table.Column(type: "uuid", nullable: false),
+ CardBrandId = table.Column(type: "uuid", nullable: false),
+ FinancialInstitutionId = table.Column(type: "uuid", nullable: false),
+ CreatedBy = table.Column(type: "uuid", nullable: false),
+ UpdatedBy = table.Column(type: "uuid", nullable: false),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ TenantId = table.Column(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_CreditCards", x => x.Id);
+ table.ForeignKey(
+ name: "FK_CreditCards_CardBrands_CardBrandId",
+ column: x => x.CardBrandId,
+ principalSchema: "public",
+ principalTable: "CardBrands",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ table.ForeignKey(
+ name: "FK_CreditCards_FinancialInstitution_FinancialInstitutionId",
+ column: x => x.FinancialInstitutionId,
+ principalSchema: "public",
+ principalTable: "FinancialInstitution",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ table.ForeignKey(
+ name: "FK_CreditCards_Wallets_DebitWalletId",
+ column: x => x.DebitWalletId,
+ principalSchema: "public",
+ principalTable: "Wallets",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_CreditCards_CardBrandId",
+ schema: "public",
+ table: "CreditCards",
+ column: "CardBrandId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_CreditCards_DebitWalletId",
+ schema: "public",
+ table: "CreditCards",
+ column: "DebitWalletId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_CreditCards_FinancialInstitutionId",
+ schema: "public",
+ table: "CreditCards",
+ column: "FinancialInstitutionId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_CreditCards_Name_TenantId",
+ schema: "public",
+ table: "CreditCards",
+ columns: new[] { "Name", "TenantId" },
+ unique: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "CreditCards",
+ schema: "public");
+ }
+ }
+}
diff --git a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs
index 7c5b1ef..e26effc 100644
--- a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs
+++ b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs
@@ -60,6 +60,78 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("CardBrands", "public");
});
+ modelBuilder.Entity("Fin.Domain.CreditCards.Entities.CreditCard", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CardBrandId")
+ .HasColumnType("uuid");
+
+ b.Property("ClosingDay")
+ .HasColumnType("integer");
+
+ b.Property("Color")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("DebitWalletId")
+ .HasColumnType("uuid");
+
+ b.Property("DueDay")
+ .HasColumnType("integer");
+
+ b.Property("FinancialInstitutionId")
+ .HasColumnType("uuid");
+
+ b.Property("Icon")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Inactivated")
+ .HasColumnType("boolean");
+
+ b.Property("Limit")
+ .HasPrecision(19, 4)
+ .HasColumnType("numeric(19,4)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("TenantId")
+ .HasColumnType("uuid");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CardBrandId");
+
+ b.HasIndex("DebitWalletId");
+
+ b.HasIndex("FinancialInstitutionId");
+
+ b.HasIndex("Name", "TenantId")
+ .IsUnique();
+
+ b.ToTable("CreditCards", "public");
+ });
+
modelBuilder.Entity("Fin.Domain.FinancialInstitutions.Entities.FinancialInstitution", b =>
{
b.Property("Id")
@@ -604,6 +676,30 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("Wallets", "public");
});
+ modelBuilder.Entity("Fin.Domain.CreditCards.Entities.CreditCard", b =>
+ {
+ b.HasOne("Fin.Domain.CardBrands.Entities.CardBrand", "CardBrand")
+ .WithMany("CreditCards")
+ .HasForeignKey("CardBrandId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("Fin.Domain.Wallets.Entities.Wallet", "DebitWallet")
+ .WithMany("CreditCards")
+ .HasForeignKey("DebitWalletId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("Fin.Domain.FinancialInstitutions.Entities.FinancialInstitution", "FinancialInstitution")
+ .WithMany("CreditCards")
+ .HasForeignKey("FinancialInstitutionId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("CardBrand");
+
+ b.Navigation("DebitWallet");
+
+ b.Navigation("FinancialInstitution");
+ });
+
modelBuilder.Entity("Fin.Domain.Notifications.Entities.NotificationUserDelivery", b =>
{
b.HasOne("Fin.Domain.Notifications.Entities.Notification", "Notification")
@@ -698,8 +794,15 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Navigation("FinancialInstitution");
});
+ modelBuilder.Entity("Fin.Domain.CardBrands.Entities.CardBrand", b =>
+ {
+ b.Navigation("CreditCards");
+ });
+
modelBuilder.Entity("Fin.Domain.FinancialInstitutions.Entities.FinancialInstitution", b =>
{
+ b.Navigation("CreditCards");
+
b.Navigation("Wallets");
});
@@ -714,6 +817,11 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Navigation("DeleteRequests");
});
+
+ modelBuilder.Entity("Fin.Domain.Wallets.Entities.Wallet", b =>
+ {
+ b.Navigation("CreditCards");
+ });
#pragma warning restore 612, 618
}
}
diff --git a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs
index ced8b95..e00e410 100644
--- a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs
+++ b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs
@@ -72,6 +72,28 @@ public async Task SeedAsync()
OnlyForAdmin = true,
Position = MenuPosition.LeftTop,
KeyWords = "notifications, notificação, notificações"
+ },
+ new()
+ {
+ Id = Guid.Parse("7826C06C-7F7D-4D92-BAFD-68B5D5F247A9"),
+ FrontRoute = "/admin/card-brand",
+ Name = "finCore.features.cardBrand.title",
+ Color = "#6d28d9",
+ Icon = "credit-card",
+ OnlyForAdmin = true,
+ Position = MenuPosition.LeftTop,
+ KeyWords = "card brand, bandeira, cartao"
+ },
+ new()
+ {
+ Id = Guid.Parse("090183AC-2FBC-4DCE-BA22-CDD46B2C7494"),
+ FrontRoute = "/credit-cards",
+ Name = "finCore.features.creditCard.title",
+ Color = "#6d28d9",
+ Icon = "credit-card",
+ OnlyForAdmin = false,
+ Position = MenuPosition.LeftTop,
+ KeyWords = "credit card, cartao de crédito, cartão de credito, cartao"
}
};
var defaultMenusIds = defaultMenus.Select(x => x.Id).ToList();
diff --git a/Fin.Test/CreditCards/Services/CreditCardControllerTest.cs b/Fin.Test/CreditCards/Services/CreditCardControllerTest.cs
new file mode 100644
index 0000000..236b96d
--- /dev/null
+++ b/Fin.Test/CreditCards/Services/CreditCardControllerTest.cs
@@ -0,0 +1,316 @@
+using Fin.Api.CreditCards;
+using Fin.Application.CreditCards.Dtos;
+using Fin.Application.CreditCards.Enums;
+using Fin.Application.CreditCards.Services;
+using Fin.Application.Globals.Dtos;
+using Fin.Domain.CreditCards.Dtos;
+using Fin.Domain.Global.Classes;
+using FluentAssertions;
+using Microsoft.AspNetCore.Mvc;
+using Moq;
+
+namespace Fin.Test.CreditCards.Services;
+
+public class CreditCardControllerTest : TestUtils.BaseTest
+{
+ private readonly Mock _serviceMock;
+ private readonly CreditCardController _controller;
+
+ public CreditCardControllerTest()
+ {
+ _serviceMock = new Mock();
+ _controller = new CreditCardController(_serviceMock.Object);
+ }
+
+ #region GetList
+
+ [Fact]
+ public async Task GetList_ShouldReturnPagedOutput()
+ {
+ // Arrange
+ var input = new CreditCardGetListInput();
+ var expectedOutput = new PagedOutput(1,
+ [
+ new CreditCardOutput { Id = TestUtils.Guids[0], Name = TestUtils.Strings[1] }
+ ]);
+
+ _serviceMock.Setup(s => s.GetList(input)).ReturnsAsync(expectedOutput);
+
+ // Act
+ var result = await _controller.GetList(input);
+
+ // Assert
+ result.Should().BeEquivalentTo(expectedOutput);
+ }
+
+ #endregion
+
+ #region Get
+
+ [Fact]
+ public async Task Get_ShouldReturnOk_WhenCreditCardExists()
+ {
+ // Arrange
+ var cardId = TestUtils.Guids[0];
+ var expectedCard = new CreditCardOutput { Id = cardId, Name = TestUtils.Strings[1] };
+ _serviceMock.Setup(s => s.Get(cardId)).ReturnsAsync(expectedCard);
+
+ // Act
+ var result = await _controller.Get(cardId);
+
+ // Assert
+ result.Result.Should().BeOfType()
+ .Which.Value.Should().Be(expectedCard);
+ }
+
+ [Fact]
+ public async Task Get_ShouldReturnNotFound_WhenCreditCardDoesNotExist()
+ {
+ // Arrange
+ var cardId = TestUtils.Guids[0];
+ _serviceMock.Setup(s => s.Get(cardId)).ReturnsAsync((CreditCardOutput)null);
+
+ // Act
+ var result = await _controller.Get(cardId);
+
+ // Assert
+ result.Result.Should().BeOfType();
+ }
+
+ #endregion
+
+ #region Create
+
+ [Fact]
+ public async Task Create_ShouldReturnCreated_WhenInputIsValid()
+ {
+ // Arrange
+ var input = new CreditCardInput { Name = TestUtils.Strings[1], Limit = 5000m };
+ var createdCard = new CreditCardOutput { Id = TestUtils.Guids[0], Name = TestUtils.Strings[1] };
+ var successResult = new ValidationResultDto
+ {
+ Success = true,
+ Data = createdCard
+ };
+ _serviceMock.Setup(s => s.Create(input, true)).ReturnsAsync(successResult);
+
+ // Act
+ var result = await _controller.Create(input);
+
+ // Assert
+ result.Result.Should().BeOfType()
+ .Which.Value.Should().Be(createdCard);
+
+ // NOTE: The controller uses 'categories/{validationResult.Data?.Id}' as location, replicating the provided controller code.
+ (result.Result as CreatedResult)?.Location.Should().Be($"categories/{createdCard.Id}");
+ }
+
+ [Fact]
+ public async Task Create_ShouldReturnUnprocessableEntity_WhenValidationFails()
+ {
+ // Arrange
+ var input = new CreditCardInput();
+ var failureResult = new ValidationResultDto
+ {
+ Success = false,
+ ErrorCode = CreditCardCreateOrUpdateErrorCode.NameIsRequired,
+ Message = "Name is required."
+ };
+ _serviceMock.Setup(s => s.Create(input, true)).ReturnsAsync(failureResult);
+
+ // Act
+ var result = await _controller.Create(input);
+
+ // Assert
+ var unprocessableEntityResult = result.Result.Should().BeOfType().Subject;
+ unprocessableEntityResult.Value.Should().BeEquivalentTo(failureResult);
+ }
+
+ #endregion
+
+ #region Update
+
+ [Fact]
+ public async Task Update_ShouldReturnOk_WhenUpdateSucceeds()
+ {
+ // Arrange
+ var cardId = TestUtils.Guids[0];
+ var input = new CreditCardInput { Name = TestUtils.Strings[1], Limit = 5000m };
+ var successResult = new ValidationResultDto { Success = true, Data = true };
+ _serviceMock.Setup(s => s.Update(cardId, input, true)).ReturnsAsync(successResult);
+
+ // Act
+ var result = await _controller.Update(cardId, input);
+
+ // Assert
+ result.Should().BeOfType();
+ }
+
+ [Fact]
+ public async Task Update_ShouldReturnNotFound_WhenCreditCardDoesNotExist()
+ {
+ // Arrange
+ var cardId = TestUtils.Guids[0];
+ var input = new CreditCardInput();
+ var notFoundResult = new ValidationResultDto
+ {
+ Success = false,
+ ErrorCode = CreditCardCreateOrUpdateErrorCode.CreditCardNotFound,
+ Message = "CreditCard not found to edit."
+ };
+ _serviceMock.Setup(s => s.Update(cardId, input, true)).ReturnsAsync(notFoundResult);
+
+ // Act
+ var result = await _controller.Update(cardId, input);
+
+ // Assert
+ var notFoundObjectResult = result.Should().BeOfType().Subject;
+ notFoundObjectResult.Value.Should().BeEquivalentTo(notFoundResult);
+ }
+
+ [Fact]
+ public async Task Update_ShouldReturnUnprocessableEntity_WhenValidationFails()
+ {
+ // Arrange
+ var cardId = TestUtils.Guids[0];
+ var input = new CreditCardInput();
+ var failureResult = new ValidationResultDto
+ {
+ Success = false,
+ ErrorCode = CreditCardCreateOrUpdateErrorCode.NameAlreadyInUse,
+ Message = "Name is already in use."
+ };
+ _serviceMock.Setup(s => s.Update(cardId, input, true)).ReturnsAsync(failureResult);
+
+ // Act
+ var result = await _controller.Update(cardId, input);
+
+ // Assert
+ var unprocessableEntityResult = result.Should().BeOfType().Subject;
+ unprocessableEntityResult.Value.Should().BeEquivalentTo(failureResult);
+ }
+
+ #endregion
+
+ #region ToggleInactivated
+
+ [Fact]
+ public async Task ToggleInactivated_ShouldReturnOk_WhenToggleSucceeds()
+ {
+ // Arrange
+ var cardId = TestUtils.Guids[0];
+ var successResult = new ValidationResultDto { Success = true, Data = true };
+ _serviceMock.Setup(s => s.ToggleInactive(cardId, true)).ReturnsAsync(successResult);
+
+ // Act
+ var result = await _controller.ToggleInactivated(cardId);
+
+ // Assert
+ result.Should().BeOfType();
+ }
+
+ [Fact]
+ public async Task ToggleInactivated_ShouldReturnNotFound_WhenCreditCardDoesNotExist()
+ {
+ // Arrange
+ var cardId = TestUtils.Guids[0];
+ var notFoundResult = new ValidationResultDto