From eecabd6d06bf255b77d16480ae611fd447b900a4 Mon Sep 17 00:00:00 2001 From: luis Date: Mon, 6 Oct 2025 22:12:45 -0300 Subject: [PATCH 01/15] creating cardbrand backend --- Fin.Api/CardBrands/CardBrandController.cs | 52 ++ .../CardBrands/CardBrandService.cs | 91 +++ Fin.Domain/CardBrands/Dtos/CardBrandInput.cs | 15 + Fin.Domain/CardBrands/Dtos/CardBrandOutput.cs | 23 + Fin.Domain/CardBrands/Entities/CardBrand.cs | 36 ++ Fin.Domain/Fin.Domain.csproj | 4 + .../CardBrands/CardBrandConfiguration.cs | 16 + Fin.Infrastructure/Database/FinDbContext.cs | 2 + .../20251007010904_CardBrand.Designer.cs | 550 ++++++++++++++++++ .../Migrations/20251007010904_CardBrand.cs | 42 ++ .../Migrations/FinDbContextModelSnapshot.cs | 37 ++ 11 files changed, 868 insertions(+) create mode 100644 Fin.Api/CardBrands/CardBrandController.cs create mode 100644 Fin.Application/CardBrands/CardBrandService.cs create mode 100644 Fin.Domain/CardBrands/Dtos/CardBrandInput.cs create mode 100644 Fin.Domain/CardBrands/Dtos/CardBrandOutput.cs create mode 100644 Fin.Domain/CardBrands/Entities/CardBrand.cs create mode 100644 Fin.Infrastructure/Database/Configurations/CardBrands/CardBrandConfiguration.cs create mode 100644 Fin.Infrastructure/Migrations/20251007010904_CardBrand.Designer.cs create mode 100644 Fin.Infrastructure/Migrations/20251007010904_CardBrand.cs diff --git a/Fin.Api/CardBrands/CardBrandController.cs b/Fin.Api/CardBrands/CardBrandController.cs new file mode 100644 index 0000000..3ae78bb --- /dev/null +++ b/Fin.Api/CardBrands/CardBrandController.cs @@ -0,0 +1,52 @@ +using Fin.Application.CardBrands; +using Fin.Domain.Global.Classes; +using Fin.Domain.CardBrands.Dtos; +using Fin.Infrastructure.Authentications.Constants; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Fin.Api.CardBrands; + +[Route("card-brand")] +[Authorize] +public class CardBrandController(ICardBrandService service): ControllerBase +{ + [HttpGet] + public async Task> GetList([FromQuery] PagedFilteredAndSortedInput input) + { + return await service.GetList(input); + } + + + [HttpGet("{id:guid}")] + [Authorize(Roles = AuthenticationRoles.Admin)] + public async Task> Get([FromRoute] Guid id) + { + var menu = await service.Get(id); + return menu != null ? Ok(menu) : NotFound(); + } + + [HttpPost] + [Authorize(Roles = AuthenticationRoles.Admin)] + public async Task> Create([FromBody] CardBrandInput input) + { + var menu = await service.Create(input, autoSave: true); + return menu != null ? Created($"card-brand/{menu.Id}", menu) : UnprocessableEntity(); + } + + [HttpPut("{id:guid}")] + [Authorize(Roles = AuthenticationRoles.Admin)] + public async Task Update([FromRoute] Guid id, [FromBody] CardBrandInput input) + { + var updated = await service.Update(id, input, autoSave: true); + return updated ? Ok() : UnprocessableEntity(); + } + + [HttpDelete("{id:guid}")] + [Authorize(Roles = AuthenticationRoles.Admin)] + public async Task Delete([FromRoute] Guid id) + { + var deleted = await service.Delete(id, autoSave: true); + return deleted ? Ok() : UnprocessableEntity(); + } +} \ No newline at end of file diff --git a/Fin.Application/CardBrands/CardBrandService.cs b/Fin.Application/CardBrands/CardBrandService.cs new file mode 100644 index 0000000..fdf84a6 --- /dev/null +++ b/Fin.Application/CardBrands/CardBrandService.cs @@ -0,0 +1,91 @@ +using Fin.Application.CardBrands; +using Fin.Domain.Global.Classes; +using Fin.Domain.CardBrands.Dtos; +using Fin.Domain.CardBrands.Entities; +using Fin.Infrastructure.AmbientDatas; +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.CardBrands; + +public interface ICardBrandService +{ + public Task Get(Guid id); + public Task> GetList(PagedFilteredAndSortedInput input); + public Task Create(CardBrandInput input, bool autoSave = false); + public Task Update(Guid id, CardBrandInput input, bool autoSave = false); + public Task Delete(Guid id, bool autoSave = false); + public Task> GetListForSideNav(); +} + +public class CardBrandService( + IRepository repository, + IAmbientData ambientData + ) : ICardBrandService, IAutoTransient +{ + public async Task Get(Guid id) + { + var entity = await repository.Query() + .FirstOrDefaultAsync(n => n.Id == id); + return entity != null ? new CardBrandOutput(entity) : null; + } + + public async Task> GetList(PagedFilteredAndSortedInput input) + { + return await repository.Query(false) + .OrderBy(m => m.Name) + .ApplyFilterAndSorter(input) + .Select(n => new CardBrandOutput(n)) + .ToPagedResult(input); + } + + public async Task> GetListForSideNav() + { + return await repository.Query(false) + .OrderBy(m => m.Name) + .Select(m => new CardBrandOutput(m)) + .ToListAsync(); + } + + public async Task Create(CardBrandInput input, bool autoSave = false) + { + ValidarInput(input); + var cardBrand = new CardBrand(input); + await repository.AddAsync(cardBrand, autoSave); + return new CardBrandOutput(cardBrand); + } + + public async Task Update(Guid id, CardBrandInput input, bool autoSave = false) + { + ValidarInput(input); + var cardBrand = await repository.Query() + .FirstOrDefaultAsync(u => u.Id == id); + if (cardBrand == null) return false; + + cardBrand.Update(input); + await repository.UpdateAsync(cardBrand, autoSave); + + return true; + } + + public async Task Delete(Guid id, bool autoSave = false) + { + var cardBrand = await repository.Query() + .FirstOrDefaultAsync(u => u.Id == id); + if (cardBrand == null) return false; + + await repository.DeleteAsync(cardBrand, autoSave); + return true; + } + + private static void ValidarInput(CardBrandInput input) + { + if (string.IsNullOrWhiteSpace(input.Name)) + throw new BadHttpRequestException("Name is required"); + if (string.IsNullOrWhiteSpace(input.Icon)) + throw new BadHttpRequestException("Icon is required"); + } +} \ No newline at end of file diff --git a/Fin.Domain/CardBrands/Dtos/CardBrandInput.cs b/Fin.Domain/CardBrands/Dtos/CardBrandInput.cs new file mode 100644 index 0000000..e0855e4 --- /dev/null +++ b/Fin.Domain/CardBrands/Dtos/CardBrandInput.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using Fin.Domain.Menus.Enums; + +namespace Fin.Domain.CardBrands.Dtos; + +public class CardBrandInput +{ + [Required] + public string Name { get; set; } + + [Required] + public string Icon { get; set; } + public string Color { get; set; } + +} \ No newline at end of file diff --git a/Fin.Domain/CardBrands/Dtos/CardBrandOutput.cs b/Fin.Domain/CardBrands/Dtos/CardBrandOutput.cs new file mode 100644 index 0000000..beb7243 --- /dev/null +++ b/Fin.Domain/CardBrands/Dtos/CardBrandOutput.cs @@ -0,0 +1,23 @@ + +using Fin.Domain.CardBrands.Entities; + +public class CardBrandOutput +{ + public Guid Id { get; set; } + public string Name { get; set; } + public string Icon { get; set; } + public string Color { get; set; } + + + public CardBrandOutput() + { + } + + public CardBrandOutput(CardBrand input) + { + Id = input.Id; + Name = input.Name; + Icon = input.Icon; + Color = input.Color; + } +} \ No newline at end of file diff --git a/Fin.Domain/CardBrands/Entities/CardBrand.cs b/Fin.Domain/CardBrands/Entities/CardBrand.cs new file mode 100644 index 0000000..64e951d --- /dev/null +++ b/Fin.Domain/CardBrands/Entities/CardBrand.cs @@ -0,0 +1,36 @@ +using Fin.Domain.CardBrands.Dtos; +using Fin.Domain.Global.Interfaces; + +namespace Fin.Domain.CardBrands.Entities; + +public class CardBrand: IAuditedEntity +{ + public string Name { get; set; } + public string Icon { get; set; } + public string Color { 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 CardBrand() + { + } + + public CardBrand(CardBrandInput input) + { + Name = input.Name; + Icon = input.Icon; + Color = input.Color; + } + + public void Update(CardBrandInput input) + { + Name = input.Name; + Icon = input.Icon; + Color = input.Color; + + } +} diff --git a/Fin.Domain/Fin.Domain.csproj b/Fin.Domain/Fin.Domain.csproj index 7b8dbdc..6005f43 100644 --- a/Fin.Domain/Fin.Domain.csproj +++ b/Fin.Domain/Fin.Domain.csproj @@ -5,4 +5,8 @@ enable + + + + diff --git a/Fin.Infrastructure/Database/Configurations/CardBrands/CardBrandConfiguration.cs b/Fin.Infrastructure/Database/Configurations/CardBrands/CardBrandConfiguration.cs new file mode 100644 index 0000000..0735b7e --- /dev/null +++ b/Fin.Infrastructure/Database/Configurations/CardBrands/CardBrandConfiguration.cs @@ -0,0 +1,16 @@ +using Fin.Domain.CardBrands.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Fin.Infrastructure.Database.Configurations.CardBrands; + +public class CardBrandConfiguration: 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); + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Database/FinDbContext.cs b/Fin.Infrastructure/Database/FinDbContext.cs index 527c06c..85c3050 100644 --- a/Fin.Infrastructure/Database/FinDbContext.cs +++ b/Fin.Infrastructure/Database/FinDbContext.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Fin.Domain.CardBrands.Entities; using Fin.Domain.Global.Interfaces; using Fin.Domain.Menus.Entities; using Fin.Domain.Notifications; @@ -27,6 +28,7 @@ public class FinDbContext : DbContext public DbSet NotificationUserDeliveries { get; set; } public DbSet Menus { get; set; } + public DbSet CardBrands { get; set; } private readonly IAmbientData _ambientData; diff --git a/Fin.Infrastructure/Migrations/20251007010904_CardBrand.Designer.cs b/Fin.Infrastructure/Migrations/20251007010904_CardBrand.Designer.cs new file mode 100644 index 0000000..2f47e17 --- /dev/null +++ b/Fin.Infrastructure/Migrations/20251007010904_CardBrand.Designer.cs @@ -0,0 +1,550 @@ +// +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("20251007010904_CardBrand")] + partial class CardBrand + { + /// + 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.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.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.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.Notifications.Entities.Notification", b => + { + b.Navigation("UserDeliveries"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.User", b => + { + b.Navigation("Credential"); + + b.Navigation("DeleteRequests"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Fin.Infrastructure/Migrations/20251007010904_CardBrand.cs b/Fin.Infrastructure/Migrations/20251007010904_CardBrand.cs new file mode 100644 index 0000000..8d22dab --- /dev/null +++ b/Fin.Infrastructure/Migrations/20251007010904_CardBrand.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Fin.Infrastructure.Migrations +{ + /// + public partial class CardBrand : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CardBrands", + schema: "public", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Icon = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Color = table.Column(type: "character varying(20)", maxLength: 20, nullable: true), + 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) + }, + constraints: table => + { + table.PrimaryKey("PK_CardBrands", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CardBrands", + schema: "public"); + } + } +} diff --git a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs index 1e384e8..37c164e 100644 --- a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs +++ b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs @@ -23,6 +23,43 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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.Menus.Entities.Menu", b => { b.Property("Id") From 350aa4315f6fca0310f4a3fe939906565585029e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Sat, 11 Oct 2025 14:27:30 -0300 Subject: [PATCH 02/15] FIN-36 WIP credit card --- Fin.Domain/CreditCards/CreditCard.cs | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Fin.Domain/CreditCards/CreditCard.cs diff --git a/Fin.Domain/CreditCards/CreditCard.cs b/Fin.Domain/CreditCards/CreditCard.cs new file mode 100644 index 0000000..927c082 --- /dev/null +++ b/Fin.Domain/CreditCards/CreditCard.cs @@ -0,0 +1,33 @@ +using Fin.Domain.FinancialInstitutions.Entities; +using Fin.Domain.Global.Interfaces; +using Fin.Domain.Wallets.Entities; + +namespace Fin.Domain.CreditCards; + +public class CreditCard: IAuditedTenantEntity +{ + 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 ClossingDay { get; set; } + public bool Inactivated { get; set; } + + public Guid DebitWalletId { get; set; } + public virtual Wallet DebitWallet { get; set; } + + public virtual Guid FlagId { get; set; } + // public Flag Flag { 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; } +} \ No newline at end of file From 3c13204465da37dfc7cec1fc80a12eb57e51c250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Sat, 11 Oct 2025 14:31:08 -0300 Subject: [PATCH 03/15] FIN-30 removing migrations --- .../.idea/data_source_mapping.xml | 6 - .../20251007010904_CardBrand.Designer.cs | 550 ------------------ .../Migrations/20251007010904_CardBrand.cs | 42 -- .../Migrations/FinDbContextModelSnapshot.cs | 142 ++++- 4 files changed, 139 insertions(+), 601 deletions(-) delete mode 100644 .idea/.idea.Fin-Backend/.idea/data_source_mapping.xml delete mode 100644 Fin.Infrastructure/Migrations/20251007010904_CardBrand.Designer.cs delete mode 100644 Fin.Infrastructure/Migrations/20251007010904_CardBrand.cs diff --git a/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml b/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml deleted file mode 100644 index 3e9887f..0000000 --- a/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Fin.Infrastructure/Migrations/20251007010904_CardBrand.Designer.cs b/Fin.Infrastructure/Migrations/20251007010904_CardBrand.Designer.cs deleted file mode 100644 index 2f47e17..0000000 --- a/Fin.Infrastructure/Migrations/20251007010904_CardBrand.Designer.cs +++ /dev/null @@ -1,550 +0,0 @@ -// -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("20251007010904_CardBrand")] - partial class CardBrand - { - /// - 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.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.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.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.Notifications.Entities.Notification", b => - { - b.Navigation("UserDeliveries"); - }); - - modelBuilder.Entity("Fin.Domain.Users.Entities.User", b => - { - b.Navigation("Credential"); - - b.Navigation("DeleteRequests"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Fin.Infrastructure/Migrations/20251007010904_CardBrand.cs b/Fin.Infrastructure/Migrations/20251007010904_CardBrand.cs deleted file mode 100644 index 8d22dab..0000000 --- a/Fin.Infrastructure/Migrations/20251007010904_CardBrand.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Fin.Infrastructure.Migrations -{ - /// - public partial class CardBrand : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "CardBrands", - schema: "public", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Icon = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), - Color = table.Column(type: "character varying(20)", maxLength: 20, nullable: true), - 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) - }, - constraints: table => - { - table.PrimaryKey("PK_CardBrands", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "CardBrands", - schema: "public"); - } - } -} diff --git a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs index 37c164e..9127044 100644 --- a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs +++ b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs @@ -23,13 +23,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("Fin.Domain.CardBrands.Entities.CardBrand", b => + 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)"); @@ -44,11 +49,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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"); @@ -57,7 +69,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("CardBrands", "public"); + b.ToTable("FinancialInstitution", "public"); }); modelBuilder.Entity("Fin.Domain.Menus.Entities.Menu", b => @@ -314,6 +326,56 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -446,6 +508,65 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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.Notifications.Entities.NotificationUserDelivery", b => { b.HasOne("Fin.Domain.Notifications.Entities.Notification", "Notification") @@ -530,6 +651,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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.FinancialInstitutions.Entities.FinancialInstitution", b => + { + b.Navigation("Wallets"); + }); + modelBuilder.Entity("Fin.Domain.Notifications.Entities.Notification", b => { b.Navigation("UserDeliveries"); @@ -544,4 +680,4 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning restore 612, 618 } } -} +} \ No newline at end of file From 75f4b10ff8eb07cdcdd46b5a96b42b2bc29fd714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Sat, 11 Oct 2025 14:34:55 -0300 Subject: [PATCH 04/15] FIN-30 adding migrations --- .../.idea/data_source_mapping.xml | 7 - ...0251011173326_adding_cardbrand.Designer.cs | 723 ++++++++++++++++++ .../20251011173326_adding_cardbrand.cs | 42 + .../Migrations/FinDbContextModelSnapshot.cs | 39 +- 4 files changed, 803 insertions(+), 8 deletions(-) delete mode 100644 .idea/.idea.Fin-Backend/.idea/data_source_mapping.xml create mode 100644 Fin.Infrastructure/Migrations/20251011173326_adding_cardbrand.Designer.cs create mode 100644 Fin.Infrastructure/Migrations/20251011173326_adding_cardbrand.cs diff --git a/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml b/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml deleted file mode 100644 index c970994..0000000 --- a/.idea/.idea.Fin-Backend/.idea/data_source_mapping.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Fin.Infrastructure/Migrations/20251011173326_adding_cardbrand.Designer.cs b/Fin.Infrastructure/Migrations/20251011173326_adding_cardbrand.Designer.cs new file mode 100644 index 0000000..07b3c2f --- /dev/null +++ b/Fin.Infrastructure/Migrations/20251011173326_adding_cardbrand.Designer.cs @@ -0,0 +1,723 @@ +// +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("20251011173326_adding_cardbrand")] + partial class adding_cardbrand + { + /// + 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.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.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.FinancialInstitutions.Entities.FinancialInstitution", b => + { + 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"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Fin.Infrastructure/Migrations/20251011173326_adding_cardbrand.cs b/Fin.Infrastructure/Migrations/20251011173326_adding_cardbrand.cs new file mode 100644 index 0000000..c68881d --- /dev/null +++ b/Fin.Infrastructure/Migrations/20251011173326_adding_cardbrand.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Fin.Infrastructure.Migrations +{ + /// + public partial class adding_cardbrand : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CardBrands", + schema: "public", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Icon = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Color = table.Column(type: "character varying(20)", maxLength: 20, nullable: true), + 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) + }, + constraints: table => + { + table.PrimaryKey("PK_CardBrands", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CardBrands", + schema: "public"); + } + } +} diff --git a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs index 9127044..7c5b1ef 100644 --- a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs +++ b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs @@ -23,6 +23,43 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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.FinancialInstitutions.Entities.FinancialInstitution", b => { b.Property("Id") @@ -680,4 +717,4 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning restore 612, 618 } } -} \ No newline at end of file +} From ddf2ace66309249f9608c44e5d6a11dbc95be745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Sat, 11 Oct 2025 14:55:36 -0300 Subject: [PATCH 05/15] FIN-36 adding credit card --- Fin.Domain/CreditCards/CreditCard.cs | 33 ---------- .../CreditCards/Dtos/CreditCardInput.cs | 14 ++++ .../CreditCards/Dtos/CreditCardOutput.cs | 20 ++++++ Fin.Domain/CreditCards/Entities/CreditCard.cs | 66 +++++++++++++++++++ Fin.Domain/Wallets/Dtos/WalletInput.cs | 3 + Fin.Infrastructure/Database/FinDbContext.cs | 4 +- 6 files changed, 105 insertions(+), 35 deletions(-) delete mode 100644 Fin.Domain/CreditCards/CreditCard.cs create mode 100644 Fin.Domain/CreditCards/Dtos/CreditCardInput.cs create mode 100644 Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs create mode 100644 Fin.Domain/CreditCards/Entities/CreditCard.cs diff --git a/Fin.Domain/CreditCards/CreditCard.cs b/Fin.Domain/CreditCards/CreditCard.cs deleted file mode 100644 index 927c082..0000000 --- a/Fin.Domain/CreditCards/CreditCard.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Fin.Domain.FinancialInstitutions.Entities; -using Fin.Domain.Global.Interfaces; -using Fin.Domain.Wallets.Entities; - -namespace Fin.Domain.CreditCards; - -public class CreditCard: IAuditedTenantEntity -{ - 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 ClossingDay { get; set; } - public bool Inactivated { get; set; } - - public Guid DebitWalletId { get; set; } - public virtual Wallet DebitWallet { get; set; } - - public virtual Guid FlagId { get; set; } - // public Flag Flag { 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; } -} \ No newline at end of file diff --git a/Fin.Domain/CreditCards/Dtos/CreditCardInput.cs b/Fin.Domain/CreditCards/Dtos/CreditCardInput.cs new file mode 100644 index 0000000..9d6b347 --- /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 ClossingDay { 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..86926dc --- /dev/null +++ b/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs @@ -0,0 +1,20 @@ +using Fin.Domain.CreditCards.Entities; + +namespace Fin.Domain.CreditCards.Dtos; + +public class CreditCardOutput(CreditCard card) +{ + 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 ClossingDay { get; set; } = card.ClossingDay; + public Guid DebitWalletId { get; set; } = card.DebitWalletId; + public Guid CardBrandId { get; set; } = card.CardBrandId; + public Guid FinancialInstitutionId { get; set; } = card.FinancialInstitutionId; + + 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..4dba827 --- /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 ClossingDay { 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; private 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; + ClossingDay = input.ClossingDay; + 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; + ClossingDay = input.ClossingDay; + 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/Wallets/Dtos/WalletInput.cs b/Fin.Domain/Wallets/Dtos/WalletInput.cs index 76be974..8f29a79 100644 --- a/Fin.Domain/Wallets/Dtos/WalletInput.cs +++ b/Fin.Domain/Wallets/Dtos/WalletInput.cs @@ -5,12 +5,15 @@ namespace Fin.Domain.Wallets.Dtos; public class WalletInput { [Required] + [MaxLength(100)] public string Name { get; set; } [Required] + [MaxLength(20)] public string Color { get; set; } [Required] + [MaxLength(20)] public string Icon { get; set; } public Guid? FinancialInstitutionId { get; set; } 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; From 161ff3a44e81f49cc1dc5f4657e1791cbabd992d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Sat, 11 Oct 2025 15:06:34 -0300 Subject: [PATCH 06/15] FIN-36 config credit card --- Fin.Domain/CardBrands/Entities/CardBrand.cs | 3 ++ .../CreditCards/Dtos/CreditCardInput.cs | 2 +- .../CreditCards/Dtos/CreditCardOutput.cs | 2 +- Fin.Domain/CreditCards/Entities/CreditCard.cs | 6 +-- .../Entities/FinancialInstitution.cs | 2 + Fin.Domain/Wallets/Entities/Wallet.cs | 4 ++ .../CreditCards/CreditCardConfiguration.cs | 45 +++++++++++++++++++ 7 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 Fin.Infrastructure/Database/Configurations/CreditCards/CreditCardConfiguration.cs 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 index 9d6b347..a11251c 100644 --- a/Fin.Domain/CreditCards/Dtos/CreditCardInput.cs +++ b/Fin.Domain/CreditCards/Dtos/CreditCardInput.cs @@ -7,7 +7,7 @@ public class CreditCardInput public string Icon { get; set; } public decimal Limit { get; set; } public int DueDay { get; set; } - public int ClossingDay { get; set; } + public int ClosingDay { get; set; } public Guid DebitWalletId { get; set; } public Guid CardBrandId { get; set; } public Guid FinancialInstitutionId { get; set; } diff --git a/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs b/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs index 86926dc..6db561b 100644 --- a/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs +++ b/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs @@ -9,7 +9,7 @@ public class CreditCardOutput(CreditCard card) public string Icon { get; set; } = card.Icon; public decimal Limit { get; set; } = card.Limit; public int DueDay { get; set; } = card.DueDay; - public int ClossingDay { get; set; } = card.ClossingDay; + 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; diff --git a/Fin.Domain/CreditCards/Entities/CreditCard.cs b/Fin.Domain/CreditCards/Entities/CreditCard.cs index 4dba827..8349a52 100644 --- a/Fin.Domain/CreditCards/Entities/CreditCard.cs +++ b/Fin.Domain/CreditCards/Entities/CreditCard.cs @@ -13,7 +13,7 @@ public class CreditCard: IAuditedTenantEntity public string Icon { get; private set; } public decimal Limit { get; private set; } public int DueDay { get; private set; } - public int ClossingDay { get; private set; } + public int ClosingDay { get; private set; } public bool Inactivated { get; private set; } public Guid DebitWalletId { get; private set; } @@ -43,7 +43,7 @@ public CreditCard(CreditCardInput input) Icon = input.Icon; Limit = input.Limit; DueDay = input.DueDay; - ClossingDay = input.ClossingDay; + ClosingDay = input.ClosingDay; DebitWalletId = input.DebitWalletId; CardBrandId = input.CardBrandId; FinancialInstitutionId = input.FinancialInstitutionId; @@ -56,7 +56,7 @@ public void Update(CreditCardInput input) Icon = input.Icon; Limit = input.Limit; DueDay = input.DueDay; - ClossingDay = input.ClossingDay; + ClosingDay = input.ClosingDay; DebitWalletId = input.DebitWalletId; CardBrandId = input.CardBrandId; FinancialInstitutionId = input.FinancialInstitutionId; 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/Entities/Wallet.cs b/Fin.Domain/Wallets/Entities/Wallet.cs index 9a12f9f..f81930d 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; @@ -23,6 +24,9 @@ public class Wallet: IAuditedTenantEntity 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 From 95e9a19e8772d8d77f550f6b5718efedc877281d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Sat, 11 Oct 2025 15:22:17 -0300 Subject: [PATCH 07/15] FIN-36 adding migrations --- ...251011180701_adding_creditcard.Designer.cs | 831 ++++++++++++++++++ .../20251011180701_adding_creditcard.cs | 96 ++ .../Migrations/FinDbContextModelSnapshot.cs | 108 +++ 3 files changed, 1035 insertions(+) create mode 100644 Fin.Infrastructure/Migrations/20251011180701_adding_creditcard.Designer.cs create mode 100644 Fin.Infrastructure/Migrations/20251011180701_adding_creditcard.cs 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 } } From 4ebabf5b1842d890e2817d9e5ca2fb08ead30504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Sat, 11 Oct 2025 16:41:18 -0300 Subject: [PATCH 08/15] FIN-36 adding credit card service --- .../Dtos/CreditCardGetListInput.cs | 11 ++ .../CreditCardCreateOrUpdateErrorCode.cs | 21 +++ .../Enums/CreditCardDeleteErrorCode.cs | 7 + .../CreditCardToggleInactiveErrorCode.cs | 6 + .../CreditCards/Services/CreditCardService.cs | 97 ++++++++++ .../Services/CreditCardValidationService.cs | 171 ++++++++++++++++++ .../Services/WalletValidationService.cs | 30 ++- .../Services/WalletValidationServiceTest.cs | 7 +- 8 files changed, 341 insertions(+), 9 deletions(-) create mode 100644 Fin.Application/CreditCards/Dtos/CreditCardGetListInput.cs create mode 100644 Fin.Application/CreditCards/Enums/CreditCardCreateOrUpdateErrorCode.cs create mode 100644 Fin.Application/CreditCards/Enums/CreditCardDeleteErrorCode.cs create mode 100644 Fin.Application/CreditCards/Enums/CreditCardToggleInactiveErrorCode.cs create mode 100644 Fin.Application/CreditCards/Services/CreditCardService.cs create mode 100644 Fin.Application/CreditCards/Services/CreditCardValidationService.cs 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..9236c1d --- /dev/null +++ b/Fin.Application/CreditCards/Enums/CreditCardCreateOrUpdateErrorCode.cs @@ -0,0 +1,21 @@ +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, +} \ 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..68790bf --- /dev/null +++ b/Fin.Application/CreditCards/Services/CreditCardValidationService.cs @@ -0,0 +1,171 @@ +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; + } + + 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.Test/Wallets/Services/WalletValidationServiceTest.cs b/Fin.Test/Wallets/Services/WalletValidationServiceTest.cs index 1e49ba3..3dd2024 100644 --- a/Fin.Test/Wallets/Services/WalletValidationServiceTest.cs +++ b/Fin.Test/Wallets/Services/WalletValidationServiceTest.cs @@ -1,6 +1,7 @@ using Fin.Application.FinancialInstitutions; using Fin.Application.Wallets.Enums; using Fin.Application.Wallets.Services; +using Fin.Domain.CreditCards.Entities; using Fin.Domain.FinancialInstitutions.Dtos; using Fin.Domain.Wallets.Dtos; using Fin.Domain.Wallets.Entities; @@ -16,7 +17,7 @@ public class WalletValidationServiceTest : TestUtils.BaseTestWithContext private WalletValidationService GetService(Resources resources) { - return new WalletValidationService(resources.WalletRepository, resources.FakeFinancialInstitution.Object); + return new WalletValidationService(resources.WalletRepository, resources.CreditCardRepository, resources.FakeFinancialInstitution.Object); } private Resources GetResources() @@ -24,6 +25,7 @@ private Resources GetResources() return new Resources { WalletRepository = GetRepository(), + CreditCardRepository = GetRepository(), FakeFinancialInstitution = new Mock() }; } @@ -31,6 +33,7 @@ private Resources GetResources() private class Resources { public IRepository WalletRepository { get; set; } + public IRepository CreditCardRepository { get; set; } public Mock FakeFinancialInstitution { get; set; } } @@ -68,7 +71,7 @@ public async Task ValidateToggleInactive_ShouldReturnFailure_WhenWalletNotFound( result.Should().NotBeNull(); result.Success.Should().BeFalse(); result.ErrorCode.Should().Be(WalletToggleInactiveErrorCode.WalletNotFound); - result.Message.Should().Be("Wallet not found to toogle inactive."); + result.Message.Should().Be("Wallet not found to toggle inactive."); } #endregion From b05d9f19ea4cab704fe7c4fcf41cbc8e5df5c945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Sat, 11 Oct 2025 16:43:24 -0300 Subject: [PATCH 09/15] FIN-36 added credit card test --- Fin-Backend.sln.DotSettings.user | 3 + Fin.Api/CreditCards/CreditCardController.cs | 63 +++++++++++++++++++ .../CreditCards/Dtos/CreditCardOutput.cs | 1 + 3 files changed, 67 insertions(+) create mode 100644 Fin.Api/CreditCards/CreditCardController.cs 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..9d238c2 --- /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("creditCards")] +[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.Domain/CreditCards/Dtos/CreditCardOutput.cs b/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs index 6db561b..f149ad7 100644 --- a/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs +++ b/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs @@ -4,6 +4,7 @@ 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; From 5e747c4d0f55889317891325bc4f963614aa3c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Sat, 11 Oct 2025 18:34:39 -0300 Subject: [PATCH 10/15] FIN-36 adding credit card tests --- Fin.Domain/CreditCards/Entities/CreditCard.cs | 2 +- Fin.Domain/Wallets/Dtos/WalletInput.cs | 11 - Fin.Domain/Wallets/Entities/Wallet.cs | 2 +- .../Services/CreditCardControllerTest.cs | 316 ++++++++++ .../Services/CreditCardServiceTest.cs | 545 ++++++++++++++++++ .../CreditCardValidationServiceTest.cs | 454 +++++++++++++++ Fin.Test/TestUtils.cs | 77 ++- 7 files changed, 1383 insertions(+), 24 deletions(-) create mode 100644 Fin.Test/CreditCards/Services/CreditCardControllerTest.cs create mode 100644 Fin.Test/CreditCards/Services/CreditCardServiceTest.cs create mode 100644 Fin.Test/CreditCards/Services/CreditCardValidationServiceTest.cs diff --git a/Fin.Domain/CreditCards/Entities/CreditCard.cs b/Fin.Domain/CreditCards/Entities/CreditCard.cs index 8349a52..81f94fd 100644 --- a/Fin.Domain/CreditCards/Entities/CreditCard.cs +++ b/Fin.Domain/CreditCards/Entities/CreditCard.cs @@ -23,7 +23,7 @@ public class CreditCard: IAuditedTenantEntity public virtual CardBrand CardBrand { get; set; } public Guid FinancialInstitutionId { get; set; } - public virtual FinancialInstitution FinancialInstitution { get; private set; } + public virtual FinancialInstitution FinancialInstitution { 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 8f29a79..54c6df9 100644 --- a/Fin.Domain/Wallets/Dtos/WalletInput.cs +++ b/Fin.Domain/Wallets/Dtos/WalletInput.cs @@ -4,20 +4,9 @@ namespace Fin.Domain.Wallets.Dtos; public class WalletInput { - [Required] - [MaxLength(100)] public string Name { get; set; } - - [Required] - [MaxLength(20)] public string Color { get; set; } - - [Required] - [MaxLength(20)] 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 f81930d..613bdee 100644 --- a/Fin.Domain/Wallets/Entities/Wallet.cs +++ b/Fin.Domain/Wallets/Entities/Wallet.cs @@ -20,7 +20,7 @@ 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; } 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 + { + Success = false, + ErrorCode = CreditCardToggleInactiveErrorCode.CreditCardNotFound, + Message = "CreditCard not found." + }; + _serviceMock.Setup(s => s.ToggleInactive(cardId, true)).ReturnsAsync(notFoundResult); + + // Act + var result = await _controller.ToggleInactivated(cardId); + + // Assert + var notFoundObjectResult = result.Should().BeOfType().Subject; + notFoundObjectResult.Value.Should().BeEquivalentTo(notFoundResult); + } + + [Fact] + public async Task ToggleInactivated_ShouldReturnUnprocessableEntity_WhenValidationFails() + { + // Arrange + var cardId = TestUtils.Guids[0]; + var failureResult = new ValidationResultDto + { + Success = false, + ErrorCode = CreditCardToggleInactiveErrorCode.CreditCardNotFound, // Reusing error code for a general failure scenario + Message = "General validation failed." + }; + _serviceMock.Setup(s => s.ToggleInactive(cardId, true)).ReturnsAsync(failureResult); + + // Act + var result = await _controller.ToggleInactivated(cardId); + + // Assert + var unprocessableEntityResult = result.Should().BeOfType().Subject; + unprocessableEntityResult.Value.Should().BeEquivalentTo(failureResult); + } + + #endregion + + #region Delete + + [Fact] + public async Task Delete_ShouldReturnOk_WhenDeleteSucceeds() + { + // Arrange + var cardId = TestUtils.Guids[0]; + var successResult = new ValidationResultDto { Success = true, Data = true }; + _serviceMock.Setup(s => s.Delete(cardId, true)).ReturnsAsync(successResult); + + // Act + var result = await _controller.Delete(cardId); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task Delete_ShouldReturnNotFound_WhenCreditCardDoesNotExist() + { + // Arrange + var cardId = TestUtils.Guids[0]; + var notFoundResult = new ValidationResultDto + { + Success = false, + ErrorCode = CreditCardDeleteErrorCode.CreditCardNotFound, + Message = "CreditCard not found." + }; + _serviceMock.Setup(s => s.Delete(cardId, true)).ReturnsAsync(notFoundResult); + + // Act + var result = await _controller.Delete(cardId); + + // Assert + var notFoundObjectResult = result.Should().BeOfType().Subject; + notFoundObjectResult.Value.Should().BeEquivalentTo(notFoundResult); + } + + [Fact] + public async Task Delete_ShouldReturnUnprocessableEntity_WhenValidationFails() + { + // Arrange + var cardId = TestUtils.Guids[0]; + var failureResult = new ValidationResultDto + { + Success = false, + ErrorCode = CreditCardDeleteErrorCode.CreditCardInUse, // Reusing error code for a general failure scenario + Message = "Cannot delete due to related transactions." + }; + _serviceMock.Setup(s => s.Delete(cardId, true)).ReturnsAsync(failureResult); + + // Act + var result = await _controller.Delete(cardId); + + // Assert + var unprocessableEntityResult = result.Should().BeOfType().Subject; + unprocessableEntityResult.Value.Should().BeEquivalentTo(failureResult); + } + + #endregion +} \ No newline at end of file diff --git a/Fin.Test/CreditCards/Services/CreditCardServiceTest.cs b/Fin.Test/CreditCards/Services/CreditCardServiceTest.cs new file mode 100644 index 0000000..49e2896 --- /dev/null +++ b/Fin.Test/CreditCards/Services/CreditCardServiceTest.cs @@ -0,0 +1,545 @@ +using Fin.Application.CreditCards.Dtos; +using Fin.Application.CreditCards.Enums; +using Fin.Application.CreditCards.Services; +using Fin.Application.Globals.Dtos; +using Fin.Domain.CardBrands.Entities; +using Fin.Domain.CreditCards.Dtos; +using Fin.Domain.CreditCards.Entities; +using Fin.Domain.Wallets.Entities; +using Fin.Infrastructure.Database.Repositories; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Moq; + +namespace Fin.Test.CreditCards.Services; + +public class CreditCardServiceTest : TestUtils.BaseTestWithContext +{ + private readonly Mock _validationServiceMock; + + public CreditCardServiceTest() + { + _validationServiceMock = new Mock(); + } + + private CreditCardService GetService(Resources resources) + { + return new CreditCardService(resources.CreditCardRepository, _validationServiceMock.Object); + } + + private async Task GetResources() + { + var resource = new Resources + { + CreditCardRepository = GetRepository(), + DebitWalletId = TestUtils.Guids[9], + CardBrandId = TestUtils.Guids[8], + FinancialInstitutionId = TestUtils.Guids[7] + }; + + var financialInstitution = TestUtils.FinancialInstitutions[0]; + financialInstitution.Id = resource.FinancialInstitutionId; + var cardBrand = TestUtils.CardBrands[0]; + cardBrand.Id = resource.CardBrandId; + var wallet = TestUtils.Wallets[0]; + wallet.Id = resource.DebitWalletId; + wallet.FinancialInstitution = financialInstitution; + + await GetRepository().AddAsync(wallet, true); + await GetRepository().AddAsync(cardBrand, true); + + return resource; + } + + private CreditCard CreateCreditCard(CreditCardInput creditCard, Resources resources) + { + creditCard.FinancialInstitutionId = resources.FinancialInstitutionId; + creditCard.DebitWalletId = resources.DebitWalletId; + creditCard.CardBrandId = resources.CardBrandId; + creditCard.Color = TestUtils.Strings[8]; + creditCard.Icon = TestUtils.Strings[9]; + return new CreditCard(creditCard); + } + + private class Resources + { + public IRepository CreditCardRepository { get; set; } + public Guid FinancialInstitutionId { get; set; } + public Guid DebitWalletId { get; set; } + public Guid CardBrandId { get; set; } + } + + #region Get + + [Fact] + public async Task Get_ShouldReturnCreditCard_WhenExists() + { + var resources = await GetResources(); + var service = GetService(resources); + var input = new CreditCardInput + { + Name = "Visa Test", Limit = 5000, DueDay = 15, ClosingDay = 5, Color = TestUtils.Strings[9], Icon = TestUtils.Strings[9] + }; + var creditCard = CreateCreditCard(input, resources); + await resources.CreditCardRepository.AddAsync(creditCard, true); + + var result = await service.Get(creditCard.Id); + + result.Should().NotBeNull(); + result.Id.Should().Be(creditCard.Id); + result.Name.Should().Be(input.Name); + } + + [Fact] + public async Task Get_ShouldReturnNull_WhenNotExists() + { + var resources = await GetResources(); + var service = GetService(resources); + + var result = await service.Get(TestUtils.Guids[9]); + + result.Should().BeNull(); + } + + #endregion + + #region GetList + + [Fact] + public async Task GetList_ShouldReturnPagedResult_WithoutFilter() + { + var resources = await GetResources(); + var service = GetService(resources); + var brandId = TestUtils.Guids[0]; + var walletId = TestUtils.Guids[1]; + var fiId = TestUtils.Guids[2]; + + await resources.CreditCardRepository.AddAsync( + CreateCreditCard(new CreditCardInput + { + Name = "C", Limit = 100, DueDay = 1, ClosingDay = 1, CardBrandId = brandId, DebitWalletId = walletId, + FinancialInstitutionId = fiId + }, resources), true); + await resources.CreditCardRepository.AddAsync( + CreateCreditCard(new CreditCardInput + { + Name = "A", Limit = 100, DueDay = 1, ClosingDay = 1, CardBrandId = brandId, DebitWalletId = walletId, + FinancialInstitutionId = fiId + }, resources), true); + await resources.CreditCardRepository.AddAsync( + CreateCreditCard(new CreditCardInput + { + Name = "B", Limit = 100, DueDay = 1, ClosingDay = 1, CardBrandId = brandId, DebitWalletId = walletId, + FinancialInstitutionId = fiId + }, resources), true); + + var input = new CreditCardGetListInput { MaxResultCount = 2, SkipCount = 0 }; + + var result = await service.GetList(input); + + result.Should().NotBeNull(); + result.TotalCount.Should().Be(3); + result.Items.Should().HaveCount(2); + result.Items.First().Name.Should().Be("A"); + result.Items.Last().Name.Should().Be("B"); + } + + [Fact] + public async Task GetList_ShouldFilterByInactivatedTrue() + { + var resources = await GetResources(); + var service = GetService(resources); + var brandId = resources.CardBrandId; + var walletId = resources.DebitWalletId; + var fiId = resources.FinancialInstitutionId; + + await resources.CreditCardRepository.AddAsync( + CreateCreditCard(new CreditCardInput + { + Name = "Active1", Limit = 100, DueDay = 1, ClosingDay = 1, CardBrandId = brandId, + DebitWalletId = walletId, FinancialInstitutionId = fiId, Color = TestUtils.Strings[9], Icon = TestUtils.Strings[9] + }, resources), true); + + var inactive = CreateCreditCard(new CreditCardInput + { + Name = "Inactive1", Limit = 100, DueDay = 1, ClosingDay = 1, CardBrandId = brandId, + DebitWalletId = walletId, FinancialInstitutionId = fiId, Color = TestUtils.Strings[9], Icon = TestUtils.Strings[9] + }, resources); + inactive.ToggleInactivated(); + await resources.CreditCardRepository.AddAsync(inactive, true); + + var inactive2 = CreateCreditCard(new CreditCardInput + { + Name = "Inactive2", Limit = 100, DueDay = 1, ClosingDay = 1, CardBrandId = brandId, + DebitWalletId = walletId, FinancialInstitutionId = fiId, Color = TestUtils.Strings[9], Icon = TestUtils.Strings[9] + }, resources); + inactive2.ToggleInactivated(); + await resources.CreditCardRepository.AddAsync(inactive2, true); + + var input = new CreditCardGetListInput { Inactivated = true, MaxResultCount = 10, SkipCount = 0 }; + + var result = await service.GetList(input); + + result.Should().NotBeNull(); + result.TotalCount.Should().Be(2); + result.Items.Should().HaveCount(2); + result.Items.First().Name.Should().Be("Inactive1"); + result.Items.Last().Name.Should().Be("Inactive2"); + } + + [Fact] + public async Task GetList_ShouldFilterByMultipleIds() + { + var resources = await GetResources(); + var service = GetService(resources); + var brandId0 = TestUtils.Guids[0]; + var brandId1 = TestUtils.Guids[1]; + var walletId2 = TestUtils.Guids[2]; + var walletId3 = TestUtils.Guids[3]; + var fiId4 = TestUtils.Guids[4]; + var fiId5 = TestUtils.Guids[5]; + + await resources.CreditCardRepository.AddAsync( + CreateCreditCard(new CreditCardInput + { + Name = "CardA", Limit = 100, DueDay = 1, ClosingDay = 1, CardBrandId = brandId0, + DebitWalletId = walletId2, FinancialInstitutionId = fiId4 + }, resources), true); + await resources.CreditCardRepository.AddAsync( + CreateCreditCard(new CreditCardInput + { + Name = "CardB", Limit = 100, DueDay = 1, ClosingDay = 1, CardBrandId = brandId1, + DebitWalletId = walletId3, FinancialInstitutionId = fiId4 + }, resources), true); + await resources.CreditCardRepository.AddAsync( + CreateCreditCard(new CreditCardInput + { + Name = "CardC", Limit = 100, DueDay = 1, ClosingDay = 1, CardBrandId = brandId0, + DebitWalletId = walletId3, FinancialInstitutionId = fiId5 + }, resources), true); + + var input = new CreditCardGetListInput + { + CardBrandIds = [brandId0], + DebitWalletIds = [walletId3], + FinancialInstitutionIds = [fiId4], + MaxResultCount = 10, + SkipCount = 0 + }; + + var result = await service.GetList(input); + + result.Should().NotBeNull(); + result.TotalCount.Should().Be(0); + result.Items.Should().BeEmpty(); + } + + [Fact] + public async Task GetList_ShouldFilterCorrectly_WhenOneFilterIsMatch() + { + var resources = await GetResources(); + var service = GetService(resources); + var brandId0 = resources.CardBrandId; + var brandId1 = TestUtils.Guids[5]; + var walletId2 = resources.DebitWalletId; + var fiId4 = resources.FinancialInstitutionId; + + var cardBrand1 = TestUtils.CardBrands[1]; + cardBrand1.Id = brandId1; + await GetRepository().AddAsync(cardBrand1); + + await resources.CreditCardRepository.AddAsync( + CreateCreditCard(new CreditCardInput + { + Name = "CardA", Limit = 100, DueDay = 1, ClosingDay = 1, CardBrandId = brandId0, + DebitWalletId = walletId2, FinancialInstitutionId = fiId4 + }, resources), true); + await resources.CreditCardRepository.AddAsync( + new CreditCard(new CreditCardInput + { + Name = "CardB", Limit = 100, DueDay = 1, ClosingDay = 1, CardBrandId = brandId1, + DebitWalletId = walletId2, FinancialInstitutionId = fiId4, Icon = TestUtils.Strings[1], Color = TestUtils.Strings[1], + }), true); + + var input = new CreditCardGetListInput + { + CardBrandIds = [brandId1], + MaxResultCount = 10, + SkipCount = 0 + }; + + var result = await service.GetList(input); + + result.Should().NotBeNull(); + result.TotalCount.Should().Be(1); + result.Items.Should().HaveCount(1); + result.Items.First().Name.Should().Be("CardB"); + } + + #endregion + + #region Create + + [Fact] + public async Task Create_ShouldReturnSuccessAndCreditCard_WhenInputIsValid() + { + var resources = await GetResources(); + var service = GetService(resources); + var input = new CreditCardInput + { + Name = "Mastercard Black", + Color = "#000000", + Icon = "cc-mastercard", + Limit = 15000.50m, + DueDay = 15, + ClosingDay = 5, + CardBrandId = resources.CardBrandId, + DebitWalletId = resources.DebitWalletId, + FinancialInstitutionId = resources.FinancialInstitutionId + }; + + var successValidation = new ValidationResultDto + { Success = true }; + _validationServiceMock.Setup(v => v.ValidateInput(input, null)) + .ReturnsAsync(successValidation); + + var result = await service.Create(input, true); + + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + result.Data.Should().NotBeNull(); + + var dbCreditCard = await resources.CreditCardRepository.Query(false) + .FirstOrDefaultAsync(a => a.Id == result.Data.Id); + dbCreditCard.Should().NotBeNull(); + dbCreditCard.Name.Should().Be(input.Name); + dbCreditCard.Limit.Should().Be(input.Limit); + dbCreditCard.ClosingDay.Should().Be(input.ClosingDay); + dbCreditCard.DebitWalletId.Should().Be(input.DebitWalletId); + } + + [Fact] + public async Task Create_ShouldReturnFailure_WhenValidationFails() + { + var resources = await GetResources(); + var service = GetService(resources); + var input = new CreditCardInput { Name = null }; + + var failureValidation = new ValidationResultDto + { + Success = false, + ErrorCode = CreditCardCreateOrUpdateErrorCode.NameIsRequired, + Message = "Name is required." + }; + _validationServiceMock.Setup(v => v.ValidateInput(input, null)) + .ReturnsAsync(failureValidation); + + var result = await service.Create(input, true); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.NameIsRequired); + result.Data.Should().BeNull(); + + (await resources.CreditCardRepository.Query(false).CountAsync()).Should().Be(0); + } + + #endregion + + #region Update + + [Fact] + public async Task Update_ShouldReturnSuccess_WhenInputIsValid() + { + var resources = await GetResources(); + var service = GetService(resources); + var originalInput = new CreditCardInput + { + Name = "Old Card", Limit = 1000, DueDay = 1, ClosingDay = 25, CardBrandId = TestUtils.Guids[0], + DebitWalletId = TestUtils.Guids[1], FinancialInstitutionId = TestUtils.Guids[2] + }; + var creditCard = CreateCreditCard(originalInput, resources); + await resources.CreditCardRepository.AddAsync(creditCard, true); + + var updatedInput = new CreditCardInput + { + Name = "New Card Name", + Color = "#FFFFFF", + Icon = "cc-visa", + Limit = 5000.75m, + DueDay = 10, + ClosingDay = 30, + CardBrandId = resources.CardBrandId, + DebitWalletId = resources.DebitWalletId, + FinancialInstitutionId = resources.FinancialInstitutionId + }; + + var successValidation = new ValidationResultDto { Success = true }; + _validationServiceMock.Setup(v => v.ValidateInput(updatedInput, creditCard.Id)) + .ReturnsAsync(successValidation); + + var result = await service.Update(creditCard.Id, updatedInput, true); + + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + result.Data.Should().BeTrue(); + + var dbCreditCard = await resources.CreditCardRepository.Query(false).FirstAsync(a => a.Id == creditCard.Id); + dbCreditCard.Name.Should().Be(updatedInput.Name); + dbCreditCard.Limit.Should().Be(updatedInput.Limit); + dbCreditCard.DueDay.Should().Be(updatedInput.DueDay); + dbCreditCard.ClosingDay.Should().Be(updatedInput.ClosingDay); + dbCreditCard.CardBrandId.Should().Be(updatedInput.CardBrandId); + dbCreditCard.DebitWalletId.Should().Be(updatedInput.DebitWalletId); + } + + [Fact] + public async Task Update_ShouldReturnFailure_WhenValidationFails() + { + var resources = await GetResources(); + var service = GetService(resources); + var originalInput = new CreditCardInput + { + Name = "Old Card", Limit = 1000, DueDay = 1, ClosingDay = 25, CardBrandId = TestUtils.Guids[0], + DebitWalletId = TestUtils.Guids[1], FinancialInstitutionId = TestUtils.Guids[2] + }; + var creditCard = CreateCreditCard(originalInput, resources); + await resources.CreditCardRepository.AddAsync(creditCard, true); + + var updatedInput = new CreditCardInput { Name = TestUtils.Strings[4], Limit = 5000 }; + + var failureValidation = new ValidationResultDto + { + Success = false, + ErrorCode = CreditCardCreateOrUpdateErrorCode.CardBrandNotFound, + Message = "Card Brand not found." + }; + _validationServiceMock.Setup(v => v.ValidateInput(updatedInput, creditCard.Id)) + .ReturnsAsync(failureValidation); + + var result = await service.Update(creditCard.Id, updatedInput, true); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.CardBrandNotFound); + result.Data.Should().BeFalse(); + + var dbCreditCard = await resources.CreditCardRepository.Query(false).FirstAsync(a => a.Id == creditCard.Id); + dbCreditCard.Name.Should().Be(originalInput.Name); + } + + #endregion + + #region Delete + + [Fact] + public async Task Delete_ShouldReturnSuccess_WhenValid() + { + var resources = await GetResources(); + var service = GetService(resources); + var creditCard = CreateCreditCard(new CreditCardInput + { + Name = TestUtils.Strings[0], Limit = 1000, DueDay = 1, ClosingDay = 25, Color = TestUtils.Strings[9], Icon = TestUtils.Strings[9] + }, resources); + await resources.CreditCardRepository.AddAsync(creditCard, true); + + var successValidation = new ValidationResultDto { Success = true }; + _validationServiceMock.Setup(v => v.ValidateDelete(creditCard.Id)).ReturnsAsync(successValidation); + + var result = await service.Delete(creditCard.Id, true); + + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + (await resources.CreditCardRepository.Query(false).FirstOrDefaultAsync(a => a.Id == creditCard.Id)).Should() + .BeNull(); + } + + [Fact] + public async Task Delete_ShouldReturnFailure_WhenValidationFails() + { + var resources = await GetResources(); + var service = GetService(resources); + var creditCard = CreateCreditCard(new CreditCardInput + { + Name = TestUtils.Strings[0], Limit = 1000, DueDay = 1, ClosingDay = 25, Color = TestUtils.Strings[9], Icon = TestUtils.Strings[9] + }, resources); + await resources.CreditCardRepository.AddAsync(creditCard, true); + + var failureValidation = new ValidationResultDto + { + Success = false, + ErrorCode = CreditCardDeleteErrorCode.CreditCardInUse, + Message = "Cannot delete credit card with transactions." + }; + _validationServiceMock.Setup(v => v.ValidateDelete(creditCard.Id)).ReturnsAsync(failureValidation); + + var result = await service.Delete(creditCard.Id, true); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardDeleteErrorCode.CreditCardInUse); + (await resources.CreditCardRepository.Query(false).FirstOrDefaultAsync(a => a.Id == creditCard.Id)).Should() + .NotBeNull(); + } + + #endregion + + #region ToggleInactive + + [Fact] + public async Task ToggleInactive_ShouldReturnSuccess_WhenValidAndDeactivate() + { + var resources = await GetResources(); + var service = GetService(resources); + var creditCard = CreateCreditCard(new CreditCardInput + { + Name = TestUtils.Strings[0], Limit = 1000, DueDay = 1, ClosingDay = 25, CardBrandId = TestUtils.Guids[0], + DebitWalletId = TestUtils.Guids[1], FinancialInstitutionId = TestUtils.Guids[2] + }, resources); + await resources.CreditCardRepository.AddAsync(creditCard, true); + creditCard.Inactivated.Should().BeFalse(); + + var successValidation = new ValidationResultDto { Success = true }; + _validationServiceMock.Setup(v => v.ValidateToggleInactive(creditCard.Id)).ReturnsAsync(successValidation); + + var result = await service.ToggleInactive(creditCard.Id, true); + + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + result.Data.Should().BeTrue(); + var dbCreditCard = await resources.CreditCardRepository.Query(false).FirstAsync(a => a.Id == creditCard.Id); + dbCreditCard.Inactivated.Should().BeTrue(); + } + + [Fact] + public async Task ToggleInactive_ShouldReturnFailure_WhenValidationFails() + { + var resources = await GetResources(); + var service = GetService(resources); + var creditCard = CreateCreditCard(new CreditCardInput + { + Name = TestUtils.Strings[0], Limit = 1000, DueDay = 1, ClosingDay = 25, CardBrandId = TestUtils.Guids[0], + DebitWalletId = TestUtils.Guids[1], FinancialInstitutionId = TestUtils.Guids[2] + }, resources); + await resources.CreditCardRepository.AddAsync(creditCard, true); + creditCard.Inactivated.Should().BeFalse(); + + var failureValidation = new ValidationResultDto + { + Success = false, + ErrorCode = CreditCardToggleInactiveErrorCode.CreditCardNotFound, + Message = "Credit Card not found." + }; + _validationServiceMock.Setup(v => v.ValidateToggleInactive(creditCard.Id)).ReturnsAsync(failureValidation); + + var result = await service.ToggleInactive(creditCard.Id, true); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardToggleInactiveErrorCode.CreditCardNotFound); + var dbCreditCard = await resources.CreditCardRepository.Query(false).FirstAsync(a => a.Id == creditCard.Id); + dbCreditCard.Inactivated.Should().BeFalse(); + } + + #endregion +} \ No newline at end of file diff --git a/Fin.Test/CreditCards/Services/CreditCardValidationServiceTest.cs b/Fin.Test/CreditCards/Services/CreditCardValidationServiceTest.cs new file mode 100644 index 0000000..bab2aea --- /dev/null +++ b/Fin.Test/CreditCards/Services/CreditCardValidationServiceTest.cs @@ -0,0 +1,454 @@ +using Fin.Application.CardBrands; +using Fin.Application.FinancialInstitutions; +using Fin.Application.Globals.Dtos; +using Fin.Application.CreditCards.Enums; +using Fin.Application.CreditCards.Services; +using Fin.Application.Wallets.Services; +using Fin.Domain.CreditCards.Dtos; +using Fin.Domain.CreditCards.Entities; +using Fin.Domain.FinancialInstitutions.Dtos; +using Fin.Domain.Wallets.Dtos; +using Fin.Infrastructure.Database.Repositories; +using FluentAssertions; +using Moq; +using Microsoft.EntityFrameworkCore; + +namespace Fin.Test.CreditCards.Services; + +public class CreditCardValidationServiceTest : TestUtils.BaseTestWithContext +{ + private CreditCardValidationService GetService(Resources resources) + { + return new CreditCardValidationService( + resources.CreditCardRepository, + resources.FakeFinancialInstitution.Object, + resources.FakeWalletService.Object, + resources.FakeCardBrandService.Object + ); + } + + private Resources GetResources() + { + return new Resources + { + CreditCardRepository = GetRepository(), + FakeFinancialInstitution = new Mock(), + FakeWalletService = new Mock(), + FakeCardBrandService = new Mock() + }; + } + + private class Resources + { + public IRepository CreditCardRepository { get; set; } + public Mock FakeFinancialInstitution { get; set; } + public Mock FakeWalletService { get; set; } + public Mock FakeCardBrandService { get; set; } + } + + #region ValidateToggleInactive + + [Fact] + public async Task ValidateToggleInactive_ShouldReturnSuccess_WhenCreditCardExists() + { + var resources = GetResources(); + var service = GetService(resources); + var input = new CreditCardInput { Name = TestUtils.Strings[0], Color = TestUtils.Strings[1], Icon = TestUtils.Strings[2], CardBrandId = TestUtils.Guids[0], DebitWalletId = TestUtils.Guids[1], FinancialInstitutionId = TestUtils.Guids[2] }; + var creditCard = new CreditCard(input); + creditCard.CardBrand = TestUtils.CardBrands[0]; + creditCard.FinancialInstitution = TestUtils.FinancialInstitutions[0]; + creditCard.DebitWallet = TestUtils.Wallets[0]; + creditCard.DebitWallet.FinancialInstitution = creditCard.FinancialInstitution; + + await resources.CreditCardRepository.AddAsync(creditCard, true); + + var result = await service.ValidateToggleInactive(creditCard.Id); + + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + } + + [Fact] + public async Task ValidateToggleInactive_ShouldReturnFailure_WhenCreditCardNotFound() + { + var resources = GetResources(); + var service = GetService(resources); + var nonExistentId = TestUtils.Guids[9]; + + var result = await service.ValidateToggleInactive(nonExistentId); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardToggleInactiveErrorCode.CreditCardNotFound); + result.Message.Should().Be("CreditCard not found to toggle inactive."); + } + + #endregion + + #region ValidateDelete + + [Fact] + public async Task ValidateDelete_ShouldReturnSuccess_WhenCreditCardExists() + { + var resources = GetResources(); + var service = GetService(resources); + var input = new CreditCardInput { Name = TestUtils.Strings[0], Color = TestUtils.Strings[1], Icon = TestUtils.Strings[2], CardBrandId = TestUtils.Guids[0], DebitWalletId = TestUtils.Guids[1], FinancialInstitutionId = TestUtils.Guids[2] }; + var creditCard = new CreditCard(input); + creditCard.CardBrand = TestUtils.CardBrands[0]; + creditCard.FinancialInstitution = TestUtils.FinancialInstitutions[0]; + creditCard.DebitWallet = TestUtils.Wallets[0]; + creditCard.DebitWallet.FinancialInstitution = creditCard.FinancialInstitution; + + await resources.CreditCardRepository.AddAsync(creditCard, true); + + var result = await service.ValidateDelete(creditCard.Id); + + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + } + + [Fact] + public async Task ValidateDelete_ShouldReturnFailure_WhenCreditCardNotFound() + { + var resources = GetResources(); + var service = GetService(resources); + var nonExistentId = TestUtils.Guids[9]; + + var result = await service.ValidateDelete(nonExistentId); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardDeleteErrorCode.CreditCardNotFound); + result.Message.Should().Be("CreditCard not found to delete."); + } + + // NOTE: 'TODO here validate relations' exists in the service, but since the implementation is missing, we only test the existing logic. + + #endregion + + #region ValidateInput (Create and Update) + + private CreditCardInput GetValidInput() => new() + { + Name = "New Card", + Color = "#FFFFFF", + Icon = "fa-credit-card", + Limit = 1000m, + DueDay = 15, + ClosingDay = 5, + CardBrandId = TestUtils.Guids[0], + DebitWalletId = TestUtils.Guids[1], + FinancialInstitutionId = TestUtils.Guids[2] + }; + + private void SetupDependenciesSuccess(Resources resources) + { + var fiId = TestUtils.Guids[2]; + var walletId = TestUtils.Guids[1]; + var brandId = TestUtils.Guids[0]; + + var activeInstitution = new FinancialInstitutionOutput { Id = fiId, Inactive = false }; + resources.FakeFinancialInstitution.Setup(s => s.Get(fiId)).ReturnsAsync(activeInstitution); + + var activeWallet = new WalletOutput { Id = walletId, Inactivated = false }; + resources.FakeWalletService.Setup(s => s.Get(walletId)).ReturnsAsync(activeWallet); + + resources.FakeCardBrandService.Setup(s => s.Get(brandId)).ReturnsAsync(new CardBrandOutput { Id = brandId }); + } + + [Fact] + public async Task ValidateInput_Create_ShouldReturnSuccess_WhenValid() + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + SetupDependenciesSuccess(resources); + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + } + + [Fact] + public async Task ValidateInput_Update_ShouldReturnSuccess_WhenValid() + { + var resources = GetResources(); + var service = GetService(resources); + var originalInput = GetValidInput(); + var creditCard = new CreditCard(originalInput); + creditCard.CardBrand = TestUtils.CardBrands[0]; + creditCard.FinancialInstitution = TestUtils.FinancialInstitutions[0]; + creditCard.DebitWallet = TestUtils.Wallets[0]; + creditCard.DebitWallet.FinancialInstitution = creditCard.FinancialInstitution; + + await resources.CreditCardRepository.AddAsync(creditCard, true); + var input = GetValidInput(); + SetupDependenciesSuccess(resources); + + var result = await service.ValidateInput(input, creditCard.Id); + + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + } + + [Fact] + public async Task ValidateInput_Update_ShouldReturnFailure_WhenCreditCardNotFound() + { + var resources = GetResources(); + var service = GetService(resources); + var nonExistentId = TestUtils.Guids[9]; + var input = GetValidInput(); + + var result = await service.ValidateInput(input, nonExistentId); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.CreditCardNotFound); + result.Message.Should().Be("CreditCard not found to edit."); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task ValidateInput_ShouldReturnFailure_WhenNameIsRequired(string name) + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + input.Name = name; + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.NameIsRequired); + result.Message.Should().Be("Name is required."); + } + + [Fact] + public async Task ValidateInput_ShouldReturnFailure_WhenNameTooLong() + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + input.Name = new string('A', 101); + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.NameTooLong); + result.Message.Should().Be("Name is too long. Max 100 characters."); + } + + [Fact] + public async Task ValidateInput_ShouldReturnFailure_WhenNameAlreadyInUseOnCreate() + { + var resources = GetResources(); + var service = GetService(resources); + var existingName = TestUtils.Strings[0]; + + var creditCard = new CreditCard(new CreditCardInput + { + Name = existingName, CardBrandId = TestUtils.Guids[9], DebitWalletId = TestUtils.Guids[9], + Color = TestUtils.Strings[9], + Icon = TestUtils.Strings[9], + }); + creditCard.CardBrand = TestUtils.CardBrands[0]; + creditCard.FinancialInstitution = TestUtils.FinancialInstitutions[0]; + creditCard.DebitWallet = TestUtils.Wallets[0]; + creditCard.DebitWallet.FinancialInstitution = creditCard.FinancialInstitution; + + await resources.CreditCardRepository.AddAsync(creditCard, true); + + var input = GetValidInput(); + input.Name = existingName; + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.NameAlreadyInUse); + result.Message.Should().Be("Name is already in use."); + } + + [Fact] + public async Task ValidateInput_ShouldReturnFailure_WhenFinancialInstitutionNotFound() + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + + resources.FakeFinancialInstitution.Setup(s => s.Get(input.FinancialInstitutionId)).ReturnsAsync((FinancialInstitutionOutput)null); + resources.FakeWalletService.Setup(s => s.Get(input.DebitWalletId)).ReturnsAsync(new WalletOutput()); + resources.FakeCardBrandService.Setup(s => s.Get(input.CardBrandId)).ReturnsAsync(new CardBrandOutput()); + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.FinancialInstitutionNotFound); + result.Message.Should().Be("Financial institution not found."); + } + + [Fact] + public async Task ValidateInput_ShouldReturnFailure_WhenFinancialInstitutionInactivated() + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + + var inactiveInstitution = new FinancialInstitutionOutput { Id = input.FinancialInstitutionId, Inactive = true }; + resources.FakeFinancialInstitution.Setup(s => s.Get(input.FinancialInstitutionId)).ReturnsAsync(inactiveInstitution); + resources.FakeWalletService.Setup(s => s.Get(input.DebitWalletId)).ReturnsAsync(new WalletOutput()); + resources.FakeCardBrandService.Setup(s => s.Get(input.CardBrandId)).ReturnsAsync(new CardBrandOutput()); + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.FinancialInstitutionInactivated); + result.Message.Should().Be("Financial institution is inactive."); + } + + [Fact] + public async Task ValidateInput_ShouldReturnFailure_WhenDebitWalletNotFound() + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + + var activeInstitution = new FinancialInstitutionOutput { Id = input.FinancialInstitutionId, Inactive = false }; + resources.FakeFinancialInstitution.Setup(s => s.Get(input.FinancialInstitutionId)).ReturnsAsync(activeInstitution); + resources.FakeWalletService.Setup(s => s.Get(input.DebitWalletId)).ReturnsAsync((WalletOutput)null); + resources.FakeCardBrandService.Setup(s => s.Get(input.CardBrandId)).ReturnsAsync(new CardBrandOutput()); + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.DebitWalletNotFound); + result.Message.Should().Be("Debit wallet not found."); + } + + [Fact] + public async Task ValidateInput_ShouldReturnFailure_WhenDebitWalletInactivated() + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + + var activeInstitution = new FinancialInstitutionOutput { Id = input.FinancialInstitutionId, Inactive = false }; + resources.FakeFinancialInstitution.Setup(s => s.Get(input.FinancialInstitutionId)).ReturnsAsync(activeInstitution); + + var inactiveWallet = new WalletOutput { Id = input.DebitWalletId, Inactivated = true }; + resources.FakeWalletService.Setup(s => s.Get(input.DebitWalletId)).ReturnsAsync(inactiveWallet); + resources.FakeCardBrandService.Setup(s => s.Get(input.CardBrandId)).ReturnsAsync(new CardBrandOutput()); + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.DebitWalletInactivated); + result.Message.Should().Be("Debit wallet is inactive."); + } + + [Fact] + public async Task ValidateInput_ShouldReturnFailure_WhenCardBrandNotFound() + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + + var activeInstitution = new FinancialInstitutionOutput { Id = input.FinancialInstitutionId, Inactive = false }; + resources.FakeFinancialInstitution.Setup(s => s.Get(input.FinancialInstitutionId)).ReturnsAsync(activeInstitution); + + var activeWallet = new WalletOutput { Id = input.DebitWalletId, Inactivated = false }; + resources.FakeWalletService.Setup(s => s.Get(input.DebitWalletId)).ReturnsAsync(activeWallet); + + resources.FakeCardBrandService.Setup(s => s.Get(input.CardBrandId)).ReturnsAsync((CardBrandOutput)null); + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.CardBrandNotFound); + result.Message.Should().Be("CardBrand not found."); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task ValidateInput_ShouldReturnFailure_WhenColorIsRequired(string color) + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + input.Color = color; + SetupDependenciesSuccess(resources); + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.ColorIsRequired); + result.Message.Should().Be("Color is required."); + } + + [Fact] + public async Task ValidateInput_ShouldReturnFailure_WhenColorTooLong() + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + input.Color = new string('A', 21); + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.ColorTooLong); + result.Message.Should().Be("Color is too long. Max 20 characters."); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task ValidateInput_ShouldReturnFailure_WhenIconIsRequired(string icon) + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + input.Icon = icon; + SetupDependenciesSuccess(resources); + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.IconIsRequired); + result.Message.Should().Be("Icon is required."); + } + + [Fact] + public async Task ValidateInput_ShouldReturnFailure_WhenIconTooLong() + { + var resources = GetResources(); + var service = GetService(resources); + var input = GetValidInput(); + input.Icon = new string('A', 21); + + var result = await service.ValidateInput(input); + + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(CreditCardCreateOrUpdateErrorCode.IconTooLong); + result.Message.Should().Be("Icon is too long. Max 20 characters."); + } + + #endregion +} \ No newline at end of file diff --git a/Fin.Test/TestUtils.cs b/Fin.Test/TestUtils.cs index 91db448..a76717c 100644 --- a/Fin.Test/TestUtils.cs +++ b/Fin.Test/TestUtils.cs @@ -1,13 +1,16 @@ -using Fin.Domain.Global.Interfaces; +using Fin.Domain.CardBrands.Entities; +using Fin.Domain.FinancialInstitutions.Entities; +using Fin.Domain.FinancialInstitutions.Enums; using Fin.Domain.Tenants.Entities; using Fin.Domain.Users.Entities; +using Fin.Domain.Wallets.Dtos; +using Fin.Domain.Wallets.Entities; using Fin.Infrastructure.AmbientDatas; using Fin.Infrastructure.Database; using Fin.Infrastructure.Database.Repositories; using Fin.Infrastructure.DateTimes; using Fin.Infrastructure.UnitOfWorks; using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; using Moq; namespace Fin.Test; @@ -29,27 +32,29 @@ protected async Task ConfigureLoggedAmbientAsync(bool isAdmin = true) await Task.CompletedTask; } } - - public class BaseTestWithContext: BaseTest, IDisposable + + public class BaseTestWithContext : BaseTest, IDisposable { protected readonly FinDbContext Context; protected readonly UnitOfWork UnitOfWork; private readonly SqliteConnection _connection; private readonly string _dbFilePath; - + protected BaseTestWithContext() { var dateTimeProviderMockForContext = new Mock(); - Context = TestDbContextFactory.Create(out _connection, out _dbFilePath, AmbientData, dateTimeProviderMockForContext.Object, useFile: true); + Context = TestDbContextFactory.Create(out _connection, out _dbFilePath, AmbientData, + dateTimeProviderMockForContext.Object, useFile: true); UnitOfWork = new UnitOfWork(Context); } public void Dispose() { Context.Dispose(); - TestDbContextFactory.Destroy(_connection, _dbFilePath);; + TestDbContextFactory.Destroy(_connection, _dbFilePath); + ; } - + protected IRepository GetRepository() where T : class { return new Repository(Context); @@ -60,7 +65,7 @@ protected IRepository GetRepository() where T : class var user = new User { Id = Guids[0], - Tenants = [ new Tenant() ], + Tenants = [new Tenant()], Credential = new UserCredential() }; if (isAdmin) user.MakeAdmin(); @@ -70,7 +75,7 @@ protected IRepository GetRepository() where T : class AmbientData.SetData(user.Tenants.First().Id, user.Id, user.DisplayName, user.IsAdmin); } } - + public static List Guids => [ Guid.Parse("3f5e2a76-9c4d-45f7-b798-8412ad4cfb6d"), @@ -84,7 +89,7 @@ protected IRepository GetRepository() where T : class Guid.Parse("fd933db3-74d9-423b-bdb5-9c16fa91d1d7"), Guid.Parse("6f4d9ef4-3211-46b2-abe1-c87ef6a39db7") ]; - + public static List Strings => [ "alpha-923", @@ -99,6 +104,20 @@ protected IRepository GetRepository() where T : class "Zebra@Night" ]; + public static List Decimals => + [ + 100.00m, + 45.50m, + 0.00m, + -12.75m, + 99999.99m, + 1.234567m, + 1000m, + -500.00m, + 123456.78m, + 2.5m + ]; + public static List UtcDateTimes => [ new(2023, 01, 01, 0, 0, 0, DateTimeKind.Utc), @@ -126,4 +145,40 @@ protected IRepository GetRepository() where T : class new(10, 10, 10), // 10:10:10 new(7, 20, 5) ]; + + public static List CardBrands => + [ + new() { Name = Strings[0], Color = Strings[1], Icon = Strings[2] }, + new() { Name = Strings[2], Color = Strings[3], Icon = Strings[4] }, + new() { Name = Strings[4], Color = Strings[5], Icon = Strings[6] }, + new() { Name = Strings[6], Color = Strings[7], Icon = Strings[8] }, + new() { Name = Strings[8], Color = Strings[9], Icon = Strings[0] } + ]; + + public static List FinancialInstitutions => + [ + new() { Name = Strings[0], Color = Strings[1], Icon = Strings[2], Type = FinancialInstitutionType.Bank }, + new() { Name = Strings[2], Color = Strings[3], Icon = Strings[4], Type = FinancialInstitutionType.DigitalBank }, + new() { Name = Strings[4], Color = Strings[5], Icon = Strings[6], Type = FinancialInstitutionType.FoodCard }, + new() { Name = Strings[6], Color = Strings[7], Icon = Strings[8], Type = FinancialInstitutionType.DigitalBank }, + new() { Name = Strings[8], Color = Strings[9], Icon = Strings[0], Type = FinancialInstitutionType.Bank } + ]; + + public static List WalletsInputs => + [ + new() { Name = Strings[0], Color = Strings[1], Icon = Strings[2], InitialBalance = Decimals[0] }, + new() { Name = Strings[2], Color = Strings[3], Icon = Strings[4], InitialBalance = Decimals[1] }, + new() { Name = Strings[4], Color = Strings[5], Icon = Strings[6], InitialBalance = Decimals[2] }, + new() { Name = Strings[6], Color = Strings[7], Icon = Strings[8], InitialBalance = Decimals[3] }, + new() { Name = Strings[8], Color = Strings[9], Icon = Strings[0], InitialBalance = Decimals[4] } + ]; + + public static List Wallets => + [ + new(WalletsInputs[0]), + new(WalletsInputs[1]), + new(WalletsInputs[2]), + new(WalletsInputs[3]), + new(WalletsInputs[4]) + ]; } \ No newline at end of file From 19c19ed4b2ba1c7315a8aad10317ed30d0cf32d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Sat, 11 Oct 2025 19:25:23 -0300 Subject: [PATCH 11/15] FIN-36 adding more validations to create ou update credit card --- .idea/.idea.Fin-Backend/.idea/dataSources.xml | 7 +++++++ .../CreditCardCreateOrUpdateErrorCode.cs | 4 ++++ .../Services/CreditCardValidationService.cs | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/.idea/.idea.Fin-Backend/.idea/dataSources.xml b/.idea/.idea.Fin-Backend/.idea/dataSources.xml index 9ed83d1..85dcd68 100644 --- a/.idea/.idea.Fin-Backend/.idea/dataSources.xml +++ b/.idea/.idea.Fin-Backend/.idea/dataSources.xml @@ -13,5 +13,12 @@ $ProjectFileDir$ + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/postgres + $ProjectFileDir$ + \ No newline at end of file diff --git a/Fin.Application/CreditCards/Enums/CreditCardCreateOrUpdateErrorCode.cs b/Fin.Application/CreditCards/Enums/CreditCardCreateOrUpdateErrorCode.cs index 9236c1d..dbd1cc4 100644 --- a/Fin.Application/CreditCards/Enums/CreditCardCreateOrUpdateErrorCode.cs +++ b/Fin.Application/CreditCards/Enums/CreditCardCreateOrUpdateErrorCode.cs @@ -18,4 +18,8 @@ public enum CreditCardCreateOrUpdateErrorCode DebitWalletInactivated = 11, CardBrandNotFound = 12, + + LimitMinValueZero = 13, + DueDayOutOfRange = 14, + ClosingDayOutOfRange = 15 } \ No newline at end of file diff --git a/Fin.Application/CreditCards/Services/CreditCardValidationService.cs b/Fin.Application/CreditCards/Services/CreditCardValidationService.cs index 68790bf..abbaed3 100644 --- a/Fin.Application/CreditCards/Services/CreditCardValidationService.cs +++ b/Fin.Application/CreditCards/Services/CreditCardValidationService.cs @@ -165,6 +165,25 @@ public async Task> Val 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; } From 0195efc744618f23733d21329b145a8db0d9a9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Sat, 11 Oct 2025 19:33:30 -0300 Subject: [PATCH 12/15] FIN-36 adusting credit card controller url --- Fin.Api/CreditCards/CreditCardController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fin.Api/CreditCards/CreditCardController.cs b/Fin.Api/CreditCards/CreditCardController.cs index 9d238c2..3244aac 100644 --- a/Fin.Api/CreditCards/CreditCardController.cs +++ b/Fin.Api/CreditCards/CreditCardController.cs @@ -8,7 +8,7 @@ namespace Fin.Api.CreditCards; -[Route("creditCards")] +[Route("credit-cards")] [Authorize] public class CreditCardController(ICreditCardService service) : ControllerBase { From b638b59f51e49faf0ac1e517a44c8117f2cd8863 Mon Sep 17 00:00:00 2001 From: luis Date: Sun, 12 Oct 2025 18:14:38 -0300 Subject: [PATCH 13/15] corrigindo seeder de cardbrand --- .idea/.idea.Fin-Backend/.idea/dataSources.xml | 9 +++++++-- .../Seeders/Seeders/DefaultMenusSeeder.cs | 11 +++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.idea/.idea.Fin-Backend/.idea/dataSources.xml b/.idea/.idea.Fin-Backend/.idea/dataSources.xml index 5f87bc2..1d15709 100644 --- a/.idea/.idea.Fin-Backend/.idea/dataSources.xml +++ b/.idea/.idea.Fin-Backend/.idea/dataSources.xml @@ -1,12 +1,17 @@ - + postgresql true true org.postgresql.Driver - jdbc:postgresql://localhost:5432/postgres?password=postgres&user=postgres + jdbc:postgresql://localhost:5432/fin_app?password=fin_app&user=fin_app + + + + + $ProjectFileDir$ diff --git a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs index e211704..e5f7c12 100644 --- a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs +++ b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs @@ -39,6 +39,17 @@ public async Task SeedAsync() OnlyForAdmin = true, Position = MenuPosition.LeftTop, KeyWords = "notifications, notificação, notificações" + }, + new() + { + Id = Guid.Parse("01999256-1baa-76bb-be49-6e209249c828"), + FrontRoute = "/admin/card-brand", + Name = "finCore.features.cardBrand.title", + Color = "#6d28d9", + Icon = "credit-card", + OnlyForAdmin = true, + Position = MenuPosition.LeftTop, + KeyWords = "card brand, bandeira, cartao" } }; var defaultMenusIds = defaultMenus.Select(x => x.Id).ToList(); From a88f35f4e72ef0792106db1f90c9c9b37e531bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Tue, 14 Oct 2025 20:20:16 -0300 Subject: [PATCH 14/15] FIN-36 adding seeder --- .../Seeders/Seeders/DefaultMenusSeeder.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs index 340b018..58975d1 100644 --- a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs +++ b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs @@ -75,7 +75,7 @@ public async Task SeedAsync() }, new() { - Id = Guid.Parse("01999256-1baa-76bb-be49-6e209249c828"), + Id = Guid.Parse("7826C06C-7F7D-4D92-BAFD-68B5D5F247A9"), FrontRoute = "/admin/card-brand", Name = "finCore.features.cardBrand.title", Color = "#6d28d9", @@ -83,6 +83,17 @@ public async Task SeedAsync() OnlyForAdmin = true, Position = MenuPosition.LeftTop, KeyWords = "card brand, bandeira, cartao" + }, + new() + { + Id = Guid.Parse("090183AC-2FBC-4DCE-BA22-CDD46B2C7494"), + FrontRoute = "/admin/credit-card", + 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(); From 0bf9ef405a5d9673d8fb11058715c1c5fe7780f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Rafael?= <¨rafaelkaua97@gmail.com¨> Date: Tue, 21 Oct 2025 20:03:49 -0300 Subject: [PATCH 15/15] FIN-36 adjusts --- Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs | 1 + Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs b/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs index f149ad7..a1f2e7b 100644 --- a/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs +++ b/Fin.Domain/CreditCards/Dtos/CreditCardOutput.cs @@ -14,6 +14,7 @@ public class CreditCardOutput(CreditCard card) 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()) { diff --git a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs index 58975d1..e00e410 100644 --- a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs +++ b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs @@ -87,7 +87,7 @@ public async Task SeedAsync() new() { Id = Guid.Parse("090183AC-2FBC-4DCE-BA22-CDD46B2C7494"), - FrontRoute = "/admin/credit-card", + FrontRoute = "/credit-cards", Name = "finCore.features.creditCard.title", Color = "#6d28d9", Icon = "credit-card",