diff --git a/.idea/.idea.Fin-Backend/.idea/dataSources.xml b/.idea/.idea.Fin-Backend/.idea/dataSources.xml
index cc31fb4..9ed83d1 100644
--- a/.idea/.idea.Fin-Backend/.idea/dataSources.xml
+++ b/.idea/.idea.Fin-Backend/.idea/dataSources.xml
@@ -6,6 +6,11 @@
true
org.postgresql.Driver
jdbc:postgresql://localhost:5432/postgres
+
+
+
+
+
$ProjectFileDir$
diff --git a/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml b/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml
new file mode 100644
index 0000000..c970994
--- /dev/null
+++ b/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Fin-Backend.sln.DotSettings.user b/Fin-Backend.sln.DotSettings.user
index 1d58c68..ca2fb04 100644
--- a/Fin-Backend.sln.DotSettings.user
+++ b/Fin-Backend.sln.DotSettings.user
@@ -1,9 +1,10 @@
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
- <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
- <Solution />
-</SessionState>
+ ForceIncluded
+
+
\ No newline at end of file
diff --git a/Fin.Api/FinancialInstitutions/FinancialInstitutionController.cs b/Fin.Api/FinancialInstitutions/FinancialInstitutionController.cs
index 1b4c205..b08f72a 100644
--- a/Fin.Api/FinancialInstitutions/FinancialInstitutionController.cs
+++ b/Fin.Api/FinancialInstitutions/FinancialInstitutionController.cs
@@ -1,6 +1,8 @@
using Fin.Application.FinancialInstitutions;
+using Fin.Application.FinancialInstitutions.Dtos;
using Fin.Domain.FinancialInstitutions.Dtos;
using Fin.Domain.Global.Classes;
+using Fin.Infrastructure.Authentications.Constants;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -24,6 +26,7 @@ public async Task> Get([FromRoute] Guid
}
[HttpPost]
+ [Authorize(Roles = AuthenticationRoles.Admin)]
public async Task> Create([FromBody] FinancialInstitutionInput input)
{
var institution = await service.Create(input, autoSave: true);
@@ -31,6 +34,7 @@ public async Task> Create([FromBody] Fi
}
[HttpPut("{id:guid}")]
+ [Authorize(Roles = AuthenticationRoles.Admin)]
public async Task Update([FromRoute] Guid id, [FromBody] FinancialInstitutionInput input)
{
var updated = await service.Update(id, input, autoSave: true);
@@ -38,6 +42,7 @@ public async Task Update([FromRoute] Guid id, [FromBody] Financial
}
[HttpDelete("{id:guid}")]
+ [Authorize(Roles = AuthenticationRoles.Admin)]
public async Task Delete([FromRoute] Guid id)
{
var deleted = await service.Delete(id, autoSave: true);
@@ -45,6 +50,7 @@ public async Task Delete([FromRoute] Guid id)
}
[HttpPatch("{id:guid}/toggle-inactive")]
+ [Authorize(Roles = AuthenticationRoles.Admin)]
public async Task ToggleInactive([FromRoute] Guid id)
{
var toggled = await service.ToggleInactive(id, autoSave: true);
diff --git a/Fin.Api/Program.cs b/Fin.Api/Program.cs
index ec44e2a..d74c9bb 100644
--- a/Fin.Api/Program.cs
+++ b/Fin.Api/Program.cs
@@ -4,6 +4,7 @@
using Fin.Infrastructure.Extensions;
using Fin.Infrastructure.Seeders.Extensions;
using Hangfire;
+using NSwag;
var builder = WebApplication.CreateBuilder(args);
@@ -11,7 +12,18 @@
builder.Services
.AddInfrastructure(builder.Configuration)
- .AddOpenApiDocument()
+ .AddOpenApiDocument(config =>
+ {
+ config.Title = "FinApp API";
+ config.Version = "v1";
+
+ config.AddSecurity("Bearer", [], new OpenApiSecurityScheme
+ {
+ Type = OpenApiSecuritySchemeType.Http,
+ Scheme = "bearer",
+ BearerFormat = "JWT",
+ });
+ })
.AddCors(options =>
{
options.AddPolicy("AllowAngularLocalhost",
diff --git a/Fin.Api/TitleCategories/TitleCategoryController.cs b/Fin.Api/TitleCategories/TitleCategoryController.cs
index 3a66689..e27e322 100644
--- a/Fin.Api/TitleCategories/TitleCategoryController.cs
+++ b/Fin.Api/TitleCategories/TitleCategoryController.cs
@@ -1,5 +1,6 @@
using Fin.Application.TitleCategories;
using Fin.Application.TitleCategories.Dtos;
+using Fin.Application.TitleCategories.Enums;
using Fin.Domain.Global.Classes;
using Fin.Domain.TitleCategories.Dtos;
using Microsoft.AspNetCore.Authorization;
@@ -9,46 +10,52 @@ namespace Fin.Api.TitleCategories;
[Route("title-categories")]
[Authorize]
-public class TitleCategoryController(ITitleCategoryService service): ControllerBase
+public class TitleCategoryController(ITitleCategoryService service) : ControllerBase
{
[HttpGet]
public async Task> GetList([FromQuery] TitleCategoryGetListInput 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();
+ return category != null ? Ok(category) : NotFound();
}
-
+
[HttpPost]
public async Task> Create([FromBody] TitleCategoryInput input)
{
- var category = await service.Create(input, autoSave: true);
- return category != null ? Created($"categories/{category.Id}", category) : UnprocessableEntity();
+ 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] TitleCategoryInput input)
{
- var updated = await service.Update(id, input, autoSave: true);
- return updated ? Ok() : UnprocessableEntity();
+ var validationResult = await service.Update(id, input, autoSave: true);
+ return validationResult.Success
+ ? Ok()
+ : validationResult.ErrorCode == TitleCategoryCreateOrUpdateErrorCode.TitleCategoryNotFound
+ ? NotFound(validationResult)
+ : UnprocessableEntity(validationResult);
}
-
+
[HttpPut("toggle-inactivated/{id:guid}")]
public async Task ToggleInactivated([FromRoute] Guid id)
{
var updated = await service.ToggleInactive(id, autoSave: true);
- return updated ? Ok() : UnprocessableEntity();
+ return updated ? Ok() : NotFound();
}
-
+
[HttpDelete("{id:guid}")]
public async Task Delete([FromRoute] Guid id)
{
var deleted = await service.Delete(id, autoSave: true);
- return deleted ? Ok() : UnprocessableEntity();
+ return deleted ? Ok() : NotFound();
}
}
\ No newline at end of file
diff --git a/Fin.Api/Wallets/WalletController.cs b/Fin.Api/Wallets/WalletController.cs
new file mode 100644
index 0000000..9cb941c
--- /dev/null
+++ b/Fin.Api/Wallets/WalletController.cs
@@ -0,0 +1,63 @@
+using Fin.Application.Wallets.Dtos;
+using Fin.Application.Wallets.Enums;
+using Fin.Application.Wallets.Services;
+using Fin.Domain.Global.Classes;
+using Fin.Domain.Wallets.Dtos;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Fin.Api.Wallets;
+
+[Route("wallets")]
+[Authorize]
+public class WalletController(IWalletService service) : ControllerBase
+{
+ [HttpGet]
+ public async Task> GetList([FromQuery] WalletGetListInput 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] WalletInput 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] WalletInput input)
+ {
+ var validationResult = await service.Update(id, input, autoSave: true);
+ return validationResult.Success ? Ok() :
+ validationResult.ErrorCode == WalletCreateOrUpdateErrorCode.WalletNotFound ? 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 == WalletToggleInactiveErrorCode.WalletNotFound ? 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 == WalletDeleteErrorCode.WalletNotFound ? NotFound(validationResult) :
+ UnprocessableEntity(validationResult);
+ }
+}
\ No newline at end of file
diff --git a/Fin.Application/FinancialInstitutions/Dtos/FinancialInstitutionGetListInput.cs b/Fin.Application/FinancialInstitutions/Dtos/FinancialInstitutionGetListInput.cs
index 7bfb950..acec3c3 100644
--- a/Fin.Application/FinancialInstitutions/Dtos/FinancialInstitutionGetListInput.cs
+++ b/Fin.Application/FinancialInstitutions/Dtos/FinancialInstitutionGetListInput.cs
@@ -1,6 +1,8 @@
using Fin.Domain.FinancialInstitutions.Enums;
using Fin.Domain.Global.Classes;
+namespace Fin.Application.FinancialInstitutions.Dtos;
+
public class FinancialInstitutionGetListInput : PagedFilteredAndSortedInput
{
public bool? Inactive { get; set; }
diff --git a/Fin.Application/FinancialInstitutions/FinancialInstitutionService.cs b/Fin.Application/FinancialInstitutions/FinancialInstitutionService.cs
index d00dfb4..4c26dda 100644
--- a/Fin.Application/FinancialInstitutions/FinancialInstitutionService.cs
+++ b/Fin.Application/FinancialInstitutions/FinancialInstitutionService.cs
@@ -1,3 +1,4 @@
+using Fin.Application.FinancialInstitutions.Dtos;
using Fin.Domain.FinancialInstitutions.Dtos;
using Fin.Domain.FinancialInstitutions.Entities;
using Fin.Domain.Global.Classes;
@@ -12,12 +13,12 @@ namespace Fin.Application.FinancialInstitutions;
public interface IFinancialInstitutionService
{
- Task Get(Guid id);
- Task> GetList(FinancialInstitutionGetListInput input);
- Task Create(FinancialInstitutionInput input, bool autoSave = false);
- Task Update(Guid id, FinancialInstitutionInput input, bool autoSave = false);
- Task Delete(Guid id, bool autoSave = false);
- Task ToggleInactive(Guid id, bool autoSave = false);
+ public Task Get(Guid id);
+ public Task> GetList(FinancialInstitutionGetListInput input);
+ public Task Create(FinancialInstitutionInput input, bool autoSave = false);
+ public Task Update(Guid id, FinancialInstitutionInput input, bool autoSave = false);
+ public Task Delete(Guid id, bool autoSave = false);
+ public Task ToggleInactive(Guid id, bool autoSave = false);
}
public class FinancialInstitutionService(
@@ -70,9 +71,10 @@ public async Task Update(Guid id, FinancialInstitutionInput input, bool au
public async Task Delete(Guid id, bool autoSave = false)
{
var institution = await repository.Query()
+ .Include(f => f.Wallets)
.FirstOrDefaultAsync(f => f.Id == id);
- if (institution == null) return false;
-
+ if (institution == null || institution.Wallets.Any()) return false;
+
await repository.DeleteAsync(institution, autoSave);
return true;
}
@@ -80,8 +82,9 @@ public async Task Delete(Guid id, bool autoSave = false)
public async Task ToggleInactive(Guid id, bool autoSave = false)
{
var institution = await repository.Query()
+ .Include(f => f.Wallets)
.FirstOrDefaultAsync(f => f.Id == id);
- if (institution == null) return false;
+ if (institution == null || (!institution.Inactive && institution.Wallets.Any(w => !w.Inactivated))) return false;
institution.ToggleInactive();
diff --git a/Fin.Application/Globals/Dtos/ValidationResultDto.cs b/Fin.Application/Globals/Dtos/ValidationResultDto.cs
index 722c422..388284f 100644
--- a/Fin.Application/Globals/Dtos/ValidationResultDto.cs
+++ b/Fin.Application/Globals/Dtos/ValidationResultDto.cs
@@ -8,10 +8,11 @@ public class ValidationResultDto
public E? ErrorCode { get; set; }
}
-public class ValidationResultDto
+public class ValidationResultDto
{
public D? Data { get; set; }
public string Message { get; set; }
public bool Success { get; set; }
public Enum? ErrorCode { get; set; }
}
+
diff --git a/Fin.Application/Menus/MenuService.cs b/Fin.Application/Menus/MenuService.cs
index 81997b4..23426df 100644
--- a/Fin.Application/Menus/MenuService.cs
+++ b/Fin.Application/Menus/MenuService.cs
@@ -55,7 +55,7 @@ public async Task> GetListForSideNav()
public async Task Create(MenuInput input, bool autoSave = false)
{
- ValidarInput(input);
+ ValidateInput(input);
var menu = new Menu(input);
await repository.AddAsync(menu, autoSave);
return new MenuOutput(menu);
@@ -63,7 +63,7 @@ public async Task Create(MenuInput input, bool autoSave = false)
public async Task Update(Guid id, MenuInput input, bool autoSave = false)
{
- ValidarInput(input);
+ ValidateInput(input);
var menu = await repository.Query()
.FirstOrDefaultAsync(u => u.Id == id);
if (menu == null) return false;
@@ -84,7 +84,7 @@ public async Task Delete(Guid id, bool autoSave = false)
return true;
}
- private static void ValidarInput( MenuInput input)
+ private static void ValidateInput( MenuInput input)
{
if (string.IsNullOrWhiteSpace(input.FrontRoute))
throw new BadHttpRequestException("FrontRoute is required");
diff --git a/Fin.Application/TitleCategories/Enums/TitleCategoryCreateOrUpdateErrorCode.cs b/Fin.Application/TitleCategories/Enums/TitleCategoryCreateOrUpdateErrorCode.cs
new file mode 100644
index 0000000..3a427c9
--- /dev/null
+++ b/Fin.Application/TitleCategories/Enums/TitleCategoryCreateOrUpdateErrorCode.cs
@@ -0,0 +1,13 @@
+namespace Fin.Application.TitleCategories.Enums;
+
+public enum TitleCategoryCreateOrUpdateErrorCode
+{
+ NameIsRequired = 0,
+ NameAlreadyInUse = 1,
+ NameTooLong = 2,
+ ColorIsRequired = 3,
+ ColorTooLong = 4,
+ IconIsRequired = 5,
+ IconTooLong = 6,
+ TitleCategoryNotFound = 7
+}
\ No newline at end of file
diff --git a/Fin.Application/TitleCategories/TitleCategoryService.cs b/Fin.Application/TitleCategories/TitleCategoryService.cs
index d32ffff..3903504 100644
--- a/Fin.Application/TitleCategories/TitleCategoryService.cs
+++ b/Fin.Application/TitleCategories/TitleCategoryService.cs
@@ -1,11 +1,12 @@
-using Fin.Application.TitleCategories.Dtos;
+using Fin.Application.Globals.Dtos;
+using Fin.Application.TitleCategories.Dtos;
+using Fin.Application.TitleCategories.Enums;
using Fin.Domain.Global.Classes;
using Fin.Domain.TitleCategories.Dtos;
using Fin.Domain.TitleCategories.Entities;
using Fin.Infrastructure.AutoServices.Interfaces;
using Fin.Infrastructure.Database.Extensions;
using Fin.Infrastructure.Database.Repositories;
-using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
namespace Fin.Application.TitleCategories;
@@ -14,8 +15,8 @@ public interface ITitleCategoryService
{
public Task Get(Guid id);
public Task> GetList(TitleCategoryGetListInput input);
- public Task Create(TitleCategoryInput input, bool autoSave = false);
- public Task Update(Guid id, TitleCategoryInput input, bool autoSave = false);
+ public Task> Create(TitleCategoryInput input, bool autoSave = false);
+ public Task> Update(Guid id, TitleCategoryInput input, bool autoSave = false);
public Task Delete(Guid id, bool autoSave = false);
public Task ToggleInactive(Guid id, bool autoSave = false);
}
@@ -42,25 +43,28 @@ public async Task> GetList(TitleCategoryGetList
.ToPagedResult(input);
}
- public async Task Create(TitleCategoryInput input, bool autoSave = false)
+ public async Task> Create(TitleCategoryInput input, bool autoSave = false)
{
- ValidarInput(input);
+ var validation = await ValidateInput(input);
+ if (!validation.Success) return validation;
+
var titleCategory = new TitleCategory(input);
await repository.AddAsync(titleCategory, autoSave);
- return new TitleCategoryOutput(titleCategory);
+ validation.Data = new TitleCategoryOutput(titleCategory);
+ return validation;
}
- public async Task Update(Guid id, TitleCategoryInput input, bool autoSave = false)
+ public async Task> Update(Guid id, TitleCategoryInput input, bool autoSave = false)
{
- ValidarInput(input);
- var titleCategory = await repository.Query()
- .FirstOrDefaultAsync(u => u.Id == id);
- if (titleCategory == null) return false;
-
+ var validation = await ValidateInput(input, id);
+ if (!validation.Success) return validation;
+
+ var titleCategory = await repository.Query().FirstAsync(u => u.Id == id);
titleCategory.Update(input);
await repository.UpdateAsync(titleCategory, autoSave);
-
- return true;
+
+ validation.Data = true;
+ return validation;
}
public async Task Delete(Guid id, bool autoSave = false)
@@ -85,13 +89,70 @@ public async Task ToggleInactive(Guid id, bool autoSave = false)
return true;
}
- private static void ValidarInput( TitleCategoryInput input)
+ private async Task> ValidateInput( TitleCategoryInput input, Guid? editingId = null)
{
+ var validationResult = new ValidationResultDto();
+
+ if (editingId.HasValue)
+ {
+ var titleExists = await repository.Query()
+ .AnyAsync(n => n.Id == editingId.Value);
+ if (!titleExists)
+ {
+ validationResult.ErrorCode = TitleCategoryCreateOrUpdateErrorCode.TitleCategoryNotFound;
+ validationResult.Message = "Title category not found to edit.";
+ return validationResult;
+ }
+ }
+
if (string.IsNullOrWhiteSpace(input.Color))
- throw new BadHttpRequestException("FrontRoute is required");
- if (string.IsNullOrWhiteSpace(input.Name))
- throw new BadHttpRequestException("Name is required");
+ {
+ validationResult.ErrorCode = TitleCategoryCreateOrUpdateErrorCode.ColorIsRequired;
+ validationResult.Message = "Color is required.";
+ return validationResult;
+ }
+ if (input.Color.Length > 20)
+ {
+ validationResult.ErrorCode = TitleCategoryCreateOrUpdateErrorCode.ColorTooLong;
+ validationResult.Message = "Color is too long. Max 20 characters.";
+ return validationResult;
+ }
+
if (string.IsNullOrWhiteSpace(input.Icon))
- throw new BadHttpRequestException("Icon is required");
+ {
+ validationResult.ErrorCode = TitleCategoryCreateOrUpdateErrorCode.IconIsRequired;
+ validationResult.Message = "Icon is required.";
+ return validationResult;
+ }
+ if (input.Icon.Length > 20)
+ {
+ validationResult.ErrorCode = TitleCategoryCreateOrUpdateErrorCode.IconTooLong;
+ validationResult.Message = "Icon is too long. Max 20 characters.";
+ return validationResult;
+ }
+
+ if (string.IsNullOrWhiteSpace(input.Name))
+ {
+ validationResult.ErrorCode = TitleCategoryCreateOrUpdateErrorCode.NameIsRequired;
+ validationResult.Message = "Name is required.";
+ return validationResult;
+ }
+ if (input.Name.Length > 100)
+ {
+ validationResult.ErrorCode = TitleCategoryCreateOrUpdateErrorCode.NameTooLong;
+ validationResult.Message = "Name is too long. Max 100 characters.";
+ return validationResult;
+ }
+ var nameAlredInUse = await repository.Query()
+ .AnyAsync(n => n.Name == input.Name && (!editingId.HasValue || n.Id != editingId));
+ if (nameAlredInUse)
+ {
+ validationResult.ErrorCode = TitleCategoryCreateOrUpdateErrorCode.NameAlreadyInUse;
+ validationResult.Message = "Name is already in use.";
+ return validationResult;
+ }
+
+ validationResult.Success = true;
+ return validationResult;
}
}
\ No newline at end of file
diff --git a/Fin.Application/Users/Services/UserCreateService.cs b/Fin.Application/Users/Services/UserCreateService.cs
index 77c5c15..3fd654f 100644
--- a/Fin.Application/Users/Services/UserCreateService.cs
+++ b/Fin.Application/Users/Services/UserCreateService.cs
@@ -9,6 +9,8 @@
using Fin.Domain.Users.Dtos;
using Fin.Domain.Users.Entities;
using Fin.Domain.Users.Factories;
+using Fin.Domain.Wallets.Dtos;
+using Fin.Domain.Wallets.Entities;
using Fin.Infrastructure.Authentications.Constants;
using Fin.Infrastructure.AutoServices.Interfaces;
using Fin.Infrastructure.Constants;
@@ -37,6 +39,7 @@ public class UserCreateService : IUserCreateService, IAutoTransient
private readonly IRepository _credentialRepository;
private readonly IRepository _userRepository;
private readonly IRepository _tenantRepository;
+ private readonly IRepository _walletRepository;
private readonly IRepository _notificationSettingsRepository;
private readonly IRepository _userRememberUseSettingRepository;
@@ -59,7 +62,7 @@ public UserCreateService(
IEmailSenderService emailSender,
IConfirmationCodeGenerator codeGenerator,
IRepository notificationSettingsRepository,
- IRepository userRememberUseSettingRepository, IUnitOfWork unitOfWork)
+ IRepository userRememberUseSettingRepository, IUnitOfWork unitOfWork, IRepository walletRepository)
{
_credentialRepository = credentialRepository;
_dateTimeProvider = dateTimeProvider;
@@ -70,6 +73,7 @@ public UserCreateService(
_notificationSettingsRepository = notificationSettingsRepository;
_userRememberUseSettingRepository = userRememberUseSettingRepository;
_unitOfWork = unitOfWork;
+ _walletRepository = walletRepository;
_tenantRepository = tenantRepository;
_userRepository = userRepository;
@@ -200,33 +204,7 @@ public async Task> CreateUser(string creationToken,
var user = new User(input, now);
var credential = UserCredentialFactory.Create(user.Id, process.EncryptedEmail, process.EncryptedPassword, UserCredentialFactoryType.Password);
- var tenant = new Tenant(now);
- user.Tenants.Add(tenant);
-
- var notificationSetting = new UserNotificationSettings(user.Id, tenant.Id);
- var rememberUseSetting = new UserRememberUseSetting(user.Id, tenant.Id);
-
- await using (await _unitOfWork.BeginTransactionAsync())
- {
- await _tenantRepository.AddAsync(tenant);
- await _userRepository.AddAsync(user);
- await _credentialRepository.AddAsync(credential);
- await _userRememberUseSettingRepository.AddAsync(rememberUseSetting);
- await _notificationSettingsRepository.AddAsync(notificationSetting);
- await _unitOfWork.CommitAsync();
- }
-
- await _cache.RemoveAsync(GenerateProcessCacheKey(creationToken));
-
- user.Tenants.First().Users = null;
- user.Credential.User = null;
-
- return new ValidationResultDto
- {
- Success = true,
- Data = new UserDto(user),
- Message = "Created user"
- };
+ return await ExecuteCreateUser(creationToken, user, credential);
}
public async Task> CreateUser(string googleId, string email, UserUpdateOrCreateInput input)
@@ -244,15 +222,29 @@ public async Task> CreateUser(string googleId, stri
var user = new User(input, now);
var credential = UserCredentialFactory.Create(user.Id, encryptedEmail, googleId, UserCredentialFactoryType.Google);
- var tenant = new Tenant(now);
+ return await ExecuteCreateUser(null, user, credential);
+ }
+
+ private async Task> ExecuteCreateUser(string creationToken, User user, UserCredential credential)
+ {
+ var tenant = new Tenant(user.CreatedAt);
user.Tenants.Add(tenant);
-
+
var notificationSetting = new UserNotificationSettings(user.Id, tenant.Id);
var rememberUseSetting = new UserRememberUseSetting(user.Id, tenant.Id);
-
+ var firstWallet = new Wallet(new WalletInput
+ {
+ Color = "#fdc570",
+ Icon = "wallet",
+ Name = "Wallet",
+ InitialBalance = 0
+ });
+
await using (await _unitOfWork.BeginTransactionAsync())
{
await _tenantRepository.AddAsync(tenant);
+ firstWallet.TenantId = tenant.Id;
+ await _walletRepository.AddAsync(firstWallet);
await _userRepository.AddAsync(user);
await _credentialRepository.AddAsync(credential);
await _userRememberUseSettingRepository.AddAsync(rememberUseSetting);
@@ -260,8 +252,12 @@ public async Task> CreateUser(string googleId, stri
await _unitOfWork.CommitAsync();
}
+ if (!string.IsNullOrWhiteSpace(creationToken))
+ await _cache.RemoveAsync(GenerateProcessCacheKey(creationToken));
+
user.Tenants.First().Users = null;
user.Credential.User = null;
+
return new ValidationResultDto
{
Success = true,
diff --git a/Fin.Application/Wallets/Dtos/WalletGetListInput.cs b/Fin.Application/Wallets/Dtos/WalletGetListInput.cs
new file mode 100644
index 0000000..c02c655
--- /dev/null
+++ b/Fin.Application/Wallets/Dtos/WalletGetListInput.cs
@@ -0,0 +1,8 @@
+using Fin.Domain.Global.Classes;
+
+namespace Fin.Application.Wallets.Dtos;
+
+public class WalletGetListInput: PagedFilteredAndSortedInput
+{
+ public bool? Inactivated { get; set; }
+}
\ No newline at end of file
diff --git a/Fin.Application/Wallets/Enums/WalletCreateOrUpdateErrorCode.cs b/Fin.Application/Wallets/Enums/WalletCreateOrUpdateErrorCode.cs
new file mode 100644
index 0000000..56e59e2
--- /dev/null
+++ b/Fin.Application/Wallets/Enums/WalletCreateOrUpdateErrorCode.cs
@@ -0,0 +1,15 @@
+namespace Fin.Application.Wallets.Enums;
+
+public enum WalletCreateOrUpdateErrorCode
+{
+ NameIsRequired = 0,
+ NameAlreadyInUse = 1,
+ NameTooLong = 2,
+ ColorIsRequired = 3,
+ ColorTooLong = 4,
+ IconIsRequired = 5,
+ IconTooLong = 6,
+ WalletNotFound = 7,
+ FinancialInstitutionNotFound = 8,
+ FinancialInstitutionInactivated = 9
+}
\ No newline at end of file
diff --git a/Fin.Application/Wallets/Enums/WalletDeleteErrorCode.cs b/Fin.Application/Wallets/Enums/WalletDeleteErrorCode.cs
new file mode 100644
index 0000000..e51230b
--- /dev/null
+++ b/Fin.Application/Wallets/Enums/WalletDeleteErrorCode.cs
@@ -0,0 +1,9 @@
+namespace Fin.Application.Wallets.Enums;
+
+public enum WalletDeleteErrorCode
+{
+ WalletNotFound = 0,
+ WalletInUseByTitles = 1,
+ WalletInUseByCreditCards = 2,
+ WalletInUseByCreditCardsAndTitle = 3,
+}
\ No newline at end of file
diff --git a/Fin.Application/Wallets/Enums/WalletToggleInactiveErrorCode.cs b/Fin.Application/Wallets/Enums/WalletToggleInactiveErrorCode.cs
new file mode 100644
index 0000000..5d34c50
--- /dev/null
+++ b/Fin.Application/Wallets/Enums/WalletToggleInactiveErrorCode.cs
@@ -0,0 +1,7 @@
+namespace Fin.Application.Wallets.Enums;
+
+public enum WalletToggleInactiveErrorCode
+{
+ WalletNotFound = 0,
+ WalletInUseByActivatedCreditCards = 1
+}
\ No newline at end of file
diff --git a/Fin.Application/Wallets/Services/WalletService.cs b/Fin.Application/Wallets/Services/WalletService.cs
new file mode 100644
index 0000000..5cf93f0
--- /dev/null
+++ b/Fin.Application/Wallets/Services/WalletService.cs
@@ -0,0 +1,95 @@
+using Fin.Application.Globals.Dtos;
+using Fin.Application.Wallets.Dtos;
+using Fin.Application.Wallets.Enums;
+using Fin.Domain.Global.Classes;
+using Fin.Domain.Wallets.Dtos;
+using Fin.Domain.Wallets.Entities;
+using Fin.Infrastructure.AutoServices.Interfaces;
+using Fin.Infrastructure.Database.Extensions;
+using Fin.Infrastructure.Database.Repositories;
+using Microsoft.EntityFrameworkCore;
+
+namespace Fin.Application.Wallets.Services;
+
+public interface IWalletService
+{
+ public Task Get(Guid id);
+ public Task> GetList(WalletGetListInput input);
+ public Task> Create(WalletInput input, bool autoSave = false);
+ public Task> Update(Guid id, WalletInput input, bool autoSave = false);
+ public Task> Delete(Guid id, bool autoSave = false);
+ public Task> ToggleInactive(Guid id, bool autoSave = false);
+}
+
+public class WalletService(
+ IRepository repository,
+ IWalletValidationService validationService
+ ) : IWalletService, IAutoTransient
+{
+ public async Task Get(Guid id)
+ {
+ var entity = await repository.Query(false).FirstOrDefaultAsync(n => n.Id == id);
+ return entity != null ? new WalletOutput(entity) : null;
+ }
+
+ public async Task> GetList(WalletGetListInput input)
+ {
+ return await repository.Query(false)
+ .WhereIf(input.Inactivated.HasValue, n => n.Inactivated == input.Inactivated.Value)
+ .OrderBy(m => m.Inactivated)
+ .ThenBy(m => m.Name)
+ .ApplyFilterAndSorter(input)
+ .Select(n => new WalletOutput(n))
+ .ToPagedResult(input);
+ }
+
+ public async Task> Create(WalletInput input, bool autoSave = false)
+ {
+ var validation = await validationService.ValidateInput(input);
+ if (!validation.Success) return validation;
+
+ var wallet = new Wallet(input);
+ await repository.AddAsync(wallet, autoSave);
+ validation.Data = new WalletOutput(wallet);
+ return validation;
+ }
+
+ public async Task> Update(Guid id, WalletInput input, bool autoSave = false)
+ {
+ var validation = await validationService.ValidateInput(input, id);
+ if (!validation.Success) return validation;
+
+ var wallet = await repository.Query().FirstAsync(u => u.Id == id);
+ wallet.Update(input);
+ // TODO reprocesses CurrentBalance
+ await repository.UpdateAsync(wallet, 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 wallet = await repository.Query().FirstAsync(u => u.Id == id);
+ await repository.DeleteAsync(wallet, 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 wallet = await repository.Query().FirstAsync(u => u.Id == id);
+ wallet.ToggleInactivated();
+ await repository.UpdateAsync(wallet, autoSave);
+
+ validation.Data = true;
+ return validation;
+ }
+}
\ No newline at end of file
diff --git a/Fin.Application/Wallets/Services/WalletValidationService.cs b/Fin.Application/Wallets/Services/WalletValidationService.cs
new file mode 100644
index 0000000..1e44eec
--- /dev/null
+++ b/Fin.Application/Wallets/Services/WalletValidationService.cs
@@ -0,0 +1,151 @@
+using Fin.Application.FinancialInstitutions;
+using Fin.Application.Globals.Dtos;
+using Fin.Application.Wallets.Enums;
+using Fin.Domain.Wallets.Dtos;
+using Fin.Domain.Wallets.Entities;
+using Fin.Infrastructure.AutoServices.Interfaces;
+using Fin.Infrastructure.Database.Repositories;
+using Microsoft.EntityFrameworkCore;
+
+namespace Fin.Application.Wallets.Services;
+
+public interface IWalletValidationService
+{
+ public Task> ValidateToggleInactive(Guid walletId);
+ public Task> ValidateDelete(Guid walletId);
+
+ public Task> ValidateInput(WalletInput input,
+ Guid? editingId = null);
+}
+
+public class WalletValidationService(
+ IRepository repository,
+ IFinancialInstitutionService financialInstitutionService
+) : IWalletValidationService, IAutoTransient
+{
+ public async Task> ValidateToggleInactive(Guid walletId)
+ {
+ var validationResult = new ValidationResultDto();
+
+ var wallet = await repository.Query(tracking: false).FirstOrDefaultAsync(n => n.Id == walletId);
+ if (wallet is null)
+ {
+ validationResult.ErrorCode = WalletToggleInactiveErrorCode.WalletNotFound;
+ validationResult.Message = "Wallet not found to toogle inactive.";
+ return validationResult;
+ }
+
+ // TODO here validate relations
+
+ validationResult.Success = true;
+ return validationResult;
+ }
+
+ public async Task> ValidateDelete(Guid walletId)
+ {
+ var validationResult = new ValidationResultDto();
+
+ var walletExists = await repository.Query().AnyAsync(n => n.Id == walletId);
+ if (!walletExists)
+ {
+ validationResult.ErrorCode = WalletDeleteErrorCode.WalletNotFound;
+ validationResult.Message = "Wallet not found to delete.";
+ return validationResult;
+ }
+
+ // TODO here validate relations
+
+ validationResult.Success = true;
+ return validationResult;
+ }
+
+ public async Task> ValidateInput(WalletInput input,
+ Guid? editingId = null)
+ {
+ var validationResult = new ValidationResultDto();
+
+ if (editingId.HasValue)
+ {
+ var walletExists = await repository.Query()
+ .AnyAsync(n => n.Id == editingId.Value);
+ if (!walletExists)
+ {
+ validationResult.ErrorCode = WalletCreateOrUpdateErrorCode.WalletNotFound;
+ validationResult.Message = "Wallet not found to edit.";
+ return validationResult;
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(input.Color))
+ {
+ validationResult.ErrorCode = WalletCreateOrUpdateErrorCode.ColorIsRequired;
+ validationResult.Message = "Color is required.";
+ return validationResult;
+ }
+
+ if (input.Color.Length > 20)
+ {
+ validationResult.ErrorCode = WalletCreateOrUpdateErrorCode.ColorTooLong;
+ validationResult.Message = "Color is too long. Max 20 characters.";
+ return validationResult;
+ }
+
+ if (string.IsNullOrWhiteSpace(input.Icon))
+ {
+ validationResult.ErrorCode = WalletCreateOrUpdateErrorCode.IconIsRequired;
+ validationResult.Message = "Icon is required.";
+ return validationResult;
+ }
+
+ if (input.Icon.Length > 20)
+ {
+ validationResult.ErrorCode = WalletCreateOrUpdateErrorCode.IconTooLong;
+ validationResult.Message = "Icon is too long. Max 20 characters.";
+ return validationResult;
+ }
+
+ if (string.IsNullOrWhiteSpace(input.Name))
+ {
+ validationResult.ErrorCode = WalletCreateOrUpdateErrorCode.NameIsRequired;
+ validationResult.Message = "Name is required.";
+ return validationResult;
+ }
+
+ if (input.Name.Length > 100)
+ {
+ validationResult.ErrorCode = WalletCreateOrUpdateErrorCode.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 = WalletCreateOrUpdateErrorCode.NameAlreadyInUse;
+ validationResult.Message = "Name is already in use.";
+ return validationResult;
+ }
+
+ if (input.FinancialInstitutionId.HasValue)
+ {
+ var financialInstitution = await financialInstitutionService.Get(input.FinancialInstitutionId.Value);
+ if (financialInstitution is null)
+ {
+ validationResult.ErrorCode = WalletCreateOrUpdateErrorCode.FinancialInstitutionNotFound;
+ validationResult.Message = "Financial institution not found.";
+ return validationResult;
+ }
+
+ if (financialInstitution.Inactive)
+ {
+ validationResult.ErrorCode = WalletCreateOrUpdateErrorCode.FinancialInstitutionInactivated;
+ validationResult.Message = "Financial institution is inactive.";
+ return validationResult;
+ }
+ }
+
+ validationResult.Success = true;
+ return validationResult;
+ }
+}
\ No newline at end of file
diff --git a/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs b/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs
index aca544f..87cbbed 100644
--- a/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs
+++ b/Fin.Domain/FinancialInstitutions/Entities/FinancialInstitution.cs
@@ -1,6 +1,7 @@
using Fin.Domain.FinancialInstitutions.Dtos;
using Fin.Domain.FinancialInstitutions.Enums;
using Fin.Domain.Global.Interfaces;
+using Fin.Domain.Wallets.Entities;
namespace Fin.Domain.FinancialInstitutions.Entities;
@@ -13,11 +14,14 @@ public class FinancialInstitution : IAuditedEntity
public string Color { get; set; }
public bool Inactive { get; set; }
+ public virtual ICollection Wallets { 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 FinancialInstitution()
{
}
diff --git a/Fin.Domain/Wallets/Dtos/WalletInput.cs b/Fin.Domain/Wallets/Dtos/WalletInput.cs
new file mode 100644
index 0000000..76be974
--- /dev/null
+++ b/Fin.Domain/Wallets/Dtos/WalletInput.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations;
+
+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/Dtos/WalletOutput.cs b/Fin.Domain/Wallets/Dtos/WalletOutput.cs
new file mode 100644
index 0000000..b1f417a
--- /dev/null
+++ b/Fin.Domain/Wallets/Dtos/WalletOutput.cs
@@ -0,0 +1,19 @@
+using Fin.Domain.Wallets.Entities;
+
+namespace Fin.Domain.Wallets.Dtos;
+
+public class WalletOutput(Wallet wallet)
+{
+ public Guid Id { get; set; } = wallet.Id;
+ public string Name { get; set; } = wallet.Name;
+ public string Color { get; set; } = wallet.Color;
+ public string Icon { get; set; } = wallet.Icon;
+ public bool Inactivated { get; set; } = wallet.Inactivated;
+ public Guid? FinancialInstitutionId { get; set; } = wallet.FinancialInstitutionId;
+ public decimal InitialBalance { get; set; } = wallet.InitialBalance;
+ public decimal CurrentBalance { get; set; } = wallet.CurrentBalance;
+
+ public WalletOutput(): this(new Wallet())
+ {
+ }
+}
\ No newline at end of file
diff --git a/Fin.Domain/Wallets/Entities/Wallet.cs b/Fin.Domain/Wallets/Entities/Wallet.cs
new file mode 100644
index 0000000..9a12f9f
--- /dev/null
+++ b/Fin.Domain/Wallets/Entities/Wallet.cs
@@ -0,0 +1,52 @@
+using Fin.Domain.FinancialInstitutions.Entities;
+using Fin.Domain.Global.Interfaces;
+using Fin.Domain.Wallets.Dtos;
+
+namespace Fin.Domain.Wallets.Entities;
+
+public class Wallet: IAuditedTenantEntity
+{
+ 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 string Name { get; private set; }
+ public string Color { get; private set; }
+ public string Icon { get; private set; }
+ public bool Inactivated { get; private set; }
+
+ public Guid? FinancialInstitutionId { get; private set; }
+ public virtual FinancialInstitution FinancialInstitution { get; }
+
+ public decimal InitialBalance { get; private set; }
+ public decimal CurrentBalance { get; set; }
+
+ public Wallet()
+ {
+ }
+
+ public Wallet(WalletInput wallet)
+ {
+ Name = wallet.Name;
+ Color = wallet.Color;
+ Icon = wallet.Icon;
+ FinancialInstitutionId = wallet.FinancialInstitutionId;
+ InitialBalance = wallet.InitialBalance;
+ CurrentBalance = wallet.InitialBalance;
+ }
+
+ public void Update(WalletInput wallet)
+ {
+ Name = wallet.Name;
+ Color = wallet.Color;
+ Icon = wallet.Icon;
+ FinancialInstitutionId = wallet.FinancialInstitutionId;
+ InitialBalance = wallet.InitialBalance;
+ // Here we don't update CurrentBalance because It's titles must be reprocesses
+ }
+
+ public void ToggleInactivated() => Inactivated = !Inactivated;
+}
\ No newline at end of file
diff --git a/Fin.Infrastructure/Database/Configurations/TitleCategories/TitleCategoryConfiguration.cs b/Fin.Infrastructure/Database/Configurations/TitleCategories/TitleCategoryConfiguration.cs
index 555d78b..1c91c66 100644
--- a/Fin.Infrastructure/Database/Configurations/TitleCategories/TitleCategoryConfiguration.cs
+++ b/Fin.Infrastructure/Database/Configurations/TitleCategories/TitleCategoryConfiguration.cs
@@ -13,5 +13,7 @@ public void Configure(EntityTypeBuilder builder)
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();
}
}
\ No newline at end of file
diff --git a/Fin.Infrastructure/Database/Configurations/Wallets/WalletConfiguration.cs b/Fin.Infrastructure/Database/Configurations/Wallets/WalletConfiguration.cs
new file mode 100644
index 0000000..3200381
--- /dev/null
+++ b/Fin.Infrastructure/Database/Configurations/Wallets/WalletConfiguration.cs
@@ -0,0 +1,31 @@
+using Fin.Domain.Wallets.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Fin.Infrastructure.Database.Configurations.Wallets;
+
+public class WalletConfiguration: 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.InitialBalance)
+ .HasColumnType("numeric(19,4)")
+ .HasPrecision(19, 4);
+
+ builder
+ .HasOne(wallet => wallet.FinancialInstitution)
+ .WithMany(financialInstitution => financialInstitution.Wallets)
+ .HasForeignKey(wallet => wallet.FinancialInstitutionId)
+ .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 a91f6bb..eb8edbf 100644
--- a/Fin.Infrastructure/Database/FinDbContext.cs
+++ b/Fin.Infrastructure/Database/FinDbContext.cs
@@ -6,6 +6,7 @@
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;
@@ -30,6 +31,8 @@ public class FinDbContext : DbContext
public DbSet