From 07d727a6ab6c58a6c5a05fd9214dc6e69c2fa5a0 Mon Sep 17 00:00:00 2001 From: RafaelKC Date: Sun, 9 Nov 2025 19:54:01 -0300 Subject: [PATCH 01/15] FIN-31 adding person entity --- Fin.Domain/Global/Interfaces/IAudited.cs | 9 +++++ .../Global/Interfaces/IAuditedEntity.cs | 6 +-- Fin.Domain/Global/Interfaces/ITenant.cs | 6 +++ Fin.Domain/Global/Interfaces/ITenantEntity.cs | 4 +- Fin.Domain/People/Dtos/PersonInput.cs | 6 +++ Fin.Domain/People/Dtos/PersonOutput.cs | 10 +++++ Fin.Domain/People/Dtos/TitlePersonInput.cs | 7 ++++ Fin.Domain/People/Dtos/TitlePersonOutput.cs | 9 +++++ Fin.Domain/People/Entities/Person.cs | 40 +++++++++++++++++++ Fin.Domain/People/Entities/TitlePerson.cs | 38 ++++++++++++++++++ Fin.Domain/Titles/Entities/Title.cs | 4 ++ .../People/PeopleConfiguration.cs | 31 ++++++++++++++ .../People/TitlePersonConfiguration.cs | 17 ++++++++ Fin.Infrastructure/Database/FinDbContext.cs | 4 +- .../Interceptors/AuditedEntityInterceptor.cs | 2 +- .../Interceptors/TenantEntityInterceptor.cs | 2 +- 16 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 Fin.Domain/Global/Interfaces/IAudited.cs create mode 100644 Fin.Domain/Global/Interfaces/ITenant.cs create mode 100644 Fin.Domain/People/Dtos/PersonInput.cs create mode 100644 Fin.Domain/People/Dtos/PersonOutput.cs create mode 100644 Fin.Domain/People/Dtos/TitlePersonInput.cs create mode 100644 Fin.Domain/People/Dtos/TitlePersonOutput.cs create mode 100644 Fin.Domain/People/Entities/Person.cs create mode 100644 Fin.Domain/People/Entities/TitlePerson.cs create mode 100644 Fin.Infrastructure/Database/Configurations/People/PeopleConfiguration.cs create mode 100644 Fin.Infrastructure/Database/Configurations/People/TitlePersonConfiguration.cs diff --git a/Fin.Domain/Global/Interfaces/IAudited.cs b/Fin.Domain/Global/Interfaces/IAudited.cs new file mode 100644 index 0000000..b944f93 --- /dev/null +++ b/Fin.Domain/Global/Interfaces/IAudited.cs @@ -0,0 +1,9 @@ +namespace Fin.Domain.Global.Interfaces; + +public interface IAudited +{ + public Guid CreatedBy { get; set; } + public Guid UpdatedBy { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/Fin.Domain/Global/Interfaces/IAuditedEntity.cs b/Fin.Domain/Global/Interfaces/IAuditedEntity.cs index 570e202..185b345 100644 --- a/Fin.Domain/Global/Interfaces/IAuditedEntity.cs +++ b/Fin.Domain/Global/Interfaces/IAuditedEntity.cs @@ -1,9 +1,5 @@ namespace Fin.Domain.Global.Interfaces; -public interface IAuditedEntity: IEntity +public interface IAuditedEntity: IEntity, IAudited { - public Guid CreatedBy { get; set; } - public Guid UpdatedBy { get; set; } - public DateTime CreatedAt { get; set; } - public DateTime UpdatedAt { get; set; } } \ No newline at end of file diff --git a/Fin.Domain/Global/Interfaces/ITenant.cs b/Fin.Domain/Global/Interfaces/ITenant.cs new file mode 100644 index 0000000..0966103 --- /dev/null +++ b/Fin.Domain/Global/Interfaces/ITenant.cs @@ -0,0 +1,6 @@ +namespace Fin.Domain.Global.Interfaces; + +public interface ITenant +{ + public Guid TenantId { get; set; } +} \ No newline at end of file diff --git a/Fin.Domain/Global/Interfaces/ITenantEntity.cs b/Fin.Domain/Global/Interfaces/ITenantEntity.cs index a26541f..3617a83 100644 --- a/Fin.Domain/Global/Interfaces/ITenantEntity.cs +++ b/Fin.Domain/Global/Interfaces/ITenantEntity.cs @@ -1,6 +1,6 @@ namespace Fin.Domain.Global.Interfaces; -public interface ITenantEntity: IEntity +public interface ITenantEntity: IEntity, ITenant { - public Guid TenantId { get; set; } + } \ No newline at end of file diff --git a/Fin.Domain/People/Dtos/PersonInput.cs b/Fin.Domain/People/Dtos/PersonInput.cs new file mode 100644 index 0000000..4472f8a --- /dev/null +++ b/Fin.Domain/People/Dtos/PersonInput.cs @@ -0,0 +1,6 @@ +namespace Fin.Domain.People.Dtos; + +public class PersonInput +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/Fin.Domain/People/Dtos/PersonOutput.cs b/Fin.Domain/People/Dtos/PersonOutput.cs new file mode 100644 index 0000000..16736ed --- /dev/null +++ b/Fin.Domain/People/Dtos/PersonOutput.cs @@ -0,0 +1,10 @@ +using Fin.Domain.People.Entities; + +namespace Fin.Domain.People.Dtos; + +public class PersonOutput(Person person) +{ + public Guid Id { get; private set; } = person.Id; + public string Name { get; private set; } = person.Name; + public bool Inactivated { get; private set; } = person.Inactivated; +} \ No newline at end of file diff --git a/Fin.Domain/People/Dtos/TitlePersonInput.cs b/Fin.Domain/People/Dtos/TitlePersonInput.cs new file mode 100644 index 0000000..b594987 --- /dev/null +++ b/Fin.Domain/People/Dtos/TitlePersonInput.cs @@ -0,0 +1,7 @@ +namespace Fin.Domain.People.Dtos; + +public class TitlePersonInput +{ + public Guid PersonId { get; set; } + public decimal Percentage { get; set; } +} \ No newline at end of file diff --git a/Fin.Domain/People/Dtos/TitlePersonOutput.cs b/Fin.Domain/People/Dtos/TitlePersonOutput.cs new file mode 100644 index 0000000..343e37a --- /dev/null +++ b/Fin.Domain/People/Dtos/TitlePersonOutput.cs @@ -0,0 +1,9 @@ +using Fin.Domain.People.Entities; + +namespace Fin.Domain.People.Dtos; + +public class TitlePersonOutput(TitlePerson titlePerson) +{ + public Guid PersonId { get; set; } = titlePerson.PersonId; + public decimal Percentage {get; set;} = titlePerson.Percentage; +} \ No newline at end of file diff --git a/Fin.Domain/People/Entities/Person.cs b/Fin.Domain/People/Entities/Person.cs new file mode 100644 index 0000000..bea8b17 --- /dev/null +++ b/Fin.Domain/People/Entities/Person.cs @@ -0,0 +1,40 @@ +using Fin.Domain.Global.Interfaces; +using Fin.Domain.People.Dtos; +using Fin.Domain.Titles.Entities; + +namespace Fin.Domain.People.Entities; + +public class Person: IAuditedTenantEntity +{ + public string Name { get; private set; } + public bool Inactivated { 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 virtual ICollection Titles { get; set; } = []; + public virtual ICollection<TitlePerson> TitlePeople { get; set; } = []; + + public Person() + { + } + + public Person(PersonInput input) + { + Name = input.Name; + } + + public void Update(PersonInput input) + { + Name = input.Name; + } + + public void ToggleInactivated() + { + Inactivated = !Inactivated; + } +} \ No newline at end of file diff --git a/Fin.Domain/People/Entities/TitlePerson.cs b/Fin.Domain/People/Entities/TitlePerson.cs new file mode 100644 index 0000000..11e2709 --- /dev/null +++ b/Fin.Domain/People/Entities/TitlePerson.cs @@ -0,0 +1,38 @@ +using Fin.Domain.Global.Interfaces; +using Fin.Domain.People.Dtos; +using Fin.Domain.Titles.Entities; + +namespace Fin.Domain.People.Entities; + +public class TitlePerson: ITenant, IAudited +{ + public Guid PersonId { get; private set; } + public virtual Person Person { get; set; } + + public Guid TitleId { get; private set; } + public virtual Title Title { get; set; } + + public decimal Percentage {get; private set;} + + public TitlePerson() + { + } + + public TitlePerson(Guid titleId, TitlePersonInput titlePerson) + { + TitleId = titleId; + PersonId = titlePerson.PersonId; + Percentage = titlePerson.Percentage; + } + + public void Update(decimal percentage) + { + Percentage = percentage; + } + + public Guid TenantId { get; set; } + public Guid CreatedBy { get; set; } + public Guid UpdatedBy { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/Fin.Domain/Titles/Entities/Title.cs b/Fin.Domain/Titles/Entities/Title.cs index 2faa694..d285268 100644 --- a/Fin.Domain/Titles/Entities/Title.cs +++ b/Fin.Domain/Titles/Entities/Title.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using Fin.Domain.Global.Interfaces; +using Fin.Domain.People.Entities; using Fin.Domain.TitleCategories.Entities; using Fin.Domain.Titles.Dtos; using Fin.Domain.Titles.Enums; @@ -25,6 +26,9 @@ public class Title: IAuditedTenantEntity public ICollection<TitleCategory> TitleCategories { get; set; } = []; public ICollection<TitleTitleCategory> TitleTitleCategories { get; set; } = []; + public virtual ICollection<Person> People { get; set; } = []; + public virtual ICollection<TitlePerson> TitlePeople { get; set; } = []; + public Guid Id { get; set; } public Guid CreatedBy { get; set; } diff --git a/Fin.Infrastructure/Database/Configurations/People/PeopleConfiguration.cs b/Fin.Infrastructure/Database/Configurations/People/PeopleConfiguration.cs new file mode 100644 index 0000000..706ed1d --- /dev/null +++ b/Fin.Infrastructure/Database/Configurations/People/PeopleConfiguration.cs @@ -0,0 +1,31 @@ +using Fin.Domain.People.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Fin.Infrastructure.Database.Configurations.People; + +public class PeopleConfiguration: IEntityTypeConfiguration<Person> +{ + public void Configure(EntityTypeBuilder<Person> builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(100).IsRequired(); + builder.HasIndex(x => new {x.Name, x.TenantId}).IsUnique(); + + builder + .HasMany(x => x.Titles) + .WithMany(x => x.People) + .UsingEntity<TitlePerson>( + l => l + .HasOne(ttc => ttc.Title) + .WithMany(title => title.TitlePeople) + .HasForeignKey(e => e.TitleId) + .OnDelete(DeleteBehavior.Cascade), + r => r + .HasOne(ttc => ttc.Person) + .WithMany(category => category.TitlePeople) + .HasForeignKey(e => e.PersonId) + .OnDelete(DeleteBehavior.Cascade) + ); + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Database/Configurations/People/TitlePersonConfiguration.cs b/Fin.Infrastructure/Database/Configurations/People/TitlePersonConfiguration.cs new file mode 100644 index 0000000..a534c61 --- /dev/null +++ b/Fin.Infrastructure/Database/Configurations/People/TitlePersonConfiguration.cs @@ -0,0 +1,17 @@ +using Fin.Domain.People.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Fin.Infrastructure.Database.Configurations.People; + +public class TitlePersonConfiguration: IEntityTypeConfiguration<TitlePerson> +{ + public void Configure(EntityTypeBuilder<TitlePerson> builder) + { + builder.HasKey(x => new { x.PersonId, x.TitleId }); + + builder.Property(x => x.Percentage) + .HasPrecision(5, 2) + .IsRequired(); + } +} \ No newline at end of file diff --git a/Fin.Infrastructure/Database/FinDbContext.cs b/Fin.Infrastructure/Database/FinDbContext.cs index c1c2d5e..fcd9635 100644 --- a/Fin.Infrastructure/Database/FinDbContext.cs +++ b/Fin.Infrastructure/Database/FinDbContext.cs @@ -76,7 +76,7 @@ private void ApplyTenantFilter(ModelBuilder modelBuilder) { foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { - if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType)) + if (typeof(ITenant).IsAssignableFrom(entityType.ClrType)) { var method = typeof(FinDbContext) .GetMethod(nameof(SetTenantFilter), BindingFlags.NonPublic | BindingFlags.Instance) @@ -87,7 +87,7 @@ private void ApplyTenantFilter(ModelBuilder modelBuilder) } } - private void SetTenantFilter<TEntity>(ModelBuilder modelBuilder) where TEntity : class, ITenantEntity + private void SetTenantFilter<TEntity>(ModelBuilder modelBuilder) where TEntity : class, ITenant { if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") return; modelBuilder.Entity<TEntity>().HasQueryFilter(e => _ambientData.IsLogged && e.TenantId == _ambientData.TenantId); diff --git a/Fin.Infrastructure/Database/Interceptors/AuditedEntityInterceptor.cs b/Fin.Infrastructure/Database/Interceptors/AuditedEntityInterceptor.cs index 7589945..18a3bf9 100644 --- a/Fin.Infrastructure/Database/Interceptors/AuditedEntityInterceptor.cs +++ b/Fin.Infrastructure/Database/Interceptors/AuditedEntityInterceptor.cs @@ -31,7 +31,7 @@ public override ValueTask<InterceptionResult<int>> SavingChangesAsync( var hasUserId = userId != Guid.Empty; - foreach (var entry in context.ChangeTracker.Entries<IAuditedEntity>()) + foreach (var entry in context.ChangeTracker.Entries<IAudited>()) { if (entry.State == EntityState.Added) { diff --git a/Fin.Infrastructure/Database/Interceptors/TenantEntityInterceptor.cs b/Fin.Infrastructure/Database/Interceptors/TenantEntityInterceptor.cs index 9b624d3..76fff85 100644 --- a/Fin.Infrastructure/Database/Interceptors/TenantEntityInterceptor.cs +++ b/Fin.Infrastructure/Database/Interceptors/TenantEntityInterceptor.cs @@ -28,7 +28,7 @@ public override ValueTask<InterceptionResult<int>> SavingChangesAsync( if (!hasTenantId) return base.SavingChangesAsync(eventData, result, cancellationToken); - foreach (var entry in context.ChangeTracker.Entries<ITenantEntity>()) + foreach (var entry in context.ChangeTracker.Entries<ITenant>()) { if (entry.State == EntityState.Added) { From 18873c9342ce876143ca38df978b4add4fe269c4 Mon Sep 17 00:00:00 2001 From: RafaelKC <rafaelkaua97@gmail.com> Date: Sun, 9 Nov 2025 19:57:01 -0300 Subject: [PATCH 02/15] FIN-31 adding person migrations --- .../20251109225437_Person.Designer.cs | 1035 +++++++++++++++++ .../Migrations/20251109225437_Person.cs | 92 ++ .../Migrations/FinDbContextModelSnapshot.cs | 97 ++ 3 files changed, 1224 insertions(+) create mode 100644 Fin.Infrastructure/Migrations/20251109225437_Person.Designer.cs create mode 100644 Fin.Infrastructure/Migrations/20251109225437_Person.cs diff --git a/Fin.Infrastructure/Migrations/20251109225437_Person.Designer.cs b/Fin.Infrastructure/Migrations/20251109225437_Person.Designer.cs new file mode 100644 index 0000000..9e6f64e --- /dev/null +++ b/Fin.Infrastructure/Migrations/20251109225437_Person.Designer.cs @@ -0,0 +1,1035 @@ +// <auto-generated /> +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("20251109225437_Person")] + partial class Person + { + /// <inheritdoc /> + 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<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<string>("Color") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<string>("Icon") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("CardBrands", "public"); + }); + + modelBuilder.Entity("Fin.Domain.CreditCards.Entities.CreditCard", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<Guid>("CardBrandId") + .HasColumnType("uuid"); + + b.Property<int>("ClosingDay") + .HasColumnType("integer"); + + b.Property<string>("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<Guid>("DebitWalletId") + .HasColumnType("uuid"); + + b.Property<int>("DueDay") + .HasColumnType("integer"); + + b.Property<Guid>("FinancialInstitutionId") + .HasColumnType("uuid"); + + b.Property<string>("Icon") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<bool>("Inactivated") + .HasColumnType("boolean"); + + b.Property<decimal>("Limit") + .HasPrecision(19, 4) + .HasColumnType("numeric(19,4)"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("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<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<string>("Code") + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.Property<string>("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<string>("Icon") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<bool>("Inactive") + .HasColumnType("boolean"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(true) + .HasColumnType("character varying(100)"); + + b.Property<int>("Type") + .HasColumnType("integer"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("FinancialInstitution", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Menus.Entities.Menu", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<string>("Color") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<string>("FrontRoute") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<string>("Icon") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<string>("KeyWords") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<bool>("OnlyForAdmin") + .HasColumnType("boolean"); + + b.Property<int>("Position") + .HasColumnType("integer"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Menus", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.Notification", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<bool>("Continuous") + .HasColumnType("boolean"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<string>("HtmlBody") + .HasColumnType("text"); + + b.Property<string>("Link") + .HasColumnType("text"); + + b.Property<string>("NormalizedTextBody") + .HasColumnType("text"); + + b.Property<string>("NormalizedTitle") + .HasColumnType("text"); + + b.Property<int>("Severity") + .HasColumnType("integer"); + + b.Property<DateTime>("StartToDelivery") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime?>("StopToDelivery") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("TextBody") + .HasColumnType("text"); + + b.Property<string>("Title") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.Property<string>("Ways") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Notifications", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.NotificationUserDelivery", b => + { + b.Property<Guid>("NotificationId") + .HasColumnType("uuid"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid"); + + b.Property<string>("BackgroundJobId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<bool>("Delivery") + .HasColumnType("boolean"); + + b.Property<bool>("Visualized") + .HasColumnType("boolean"); + + b.HasKey("NotificationId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationUserDeliveries", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserNotificationSettings", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<string>("AllowedWays") + .HasColumnType("text"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<bool>("Enabled") + .HasColumnType("boolean"); + + b.Property<string>("FirebaseTokens") + .HasColumnType("text"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserNotificationSettings", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Notifications.Entities.UserRememberUseSetting", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<TimeSpan>("NotifyOn") + .HasColumnType("interval"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid"); + + b.Property<string>("Ways") + .HasColumnType("text"); + + b.Property<string>("WeekDays") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRememberUseSettings", "public"); + }); + + modelBuilder.Entity("Fin.Domain.People.Entities.Person", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<bool>("Inactivated") + .HasColumnType("boolean"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Name", "TenantId") + .IsUnique(); + + b.ToTable("Person", "public"); + }); + + modelBuilder.Entity("Fin.Domain.People.Entities.TitlePerson", b => + { + b.Property<Guid>("PersonId") + .HasColumnType("uuid"); + + b.Property<Guid>("TitleId") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<decimal>("Percentage") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("PersonId", "TitleId"); + + b.HasIndex("TitleId"); + + b.ToTable("TitlePerson", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Tenants.Entities.Tenant", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Locale") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property<string>("Timezone") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tenants", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Tenants.Entities.TenantUser", b => + { + b.Property<Guid>("TenantId") + .HasColumnType("uuid"); + + b.Property<Guid>("UserId") + .HasColumnType("uuid"); + + b.HasKey("TenantId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("TenantUsers", "public"); + }); + + modelBuilder.Entity("Fin.Domain.TitleCategories.Entities.TitleCategory", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<string>("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<string>("Icon") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<bool>("Inactivated") + .HasColumnType("boolean"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Name", "TenantId") + .IsUnique(); + + b.ToTable("TitleCategories", "public"); + }); + + modelBuilder.Entity("Fin.Domain.TitleCategories.Entities.TitleTitleCategory", b => + { + b.Property<Guid>("TitleCategoryId") + .HasColumnType("uuid"); + + b.Property<Guid>("TitleId") + .HasColumnType("uuid"); + + b.HasKey("TitleCategoryId", "TitleId"); + + b.HasIndex("TitleId"); + + b.ToTable("TitleTitleCategories", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Titles.Entities.Title", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<DateTime>("Date") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("Description") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<decimal>("PreviousBalance") + .HasPrecision(19, 4) + .HasColumnType("numeric(19,4)"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid"); + + b.Property<byte>("Type") + .HasColumnType("smallint"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.Property<decimal>("Value") + .HasPrecision(19, 4) + .HasColumnType("numeric(19,4)"); + + b.Property<Guid>("WalletId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WalletId"); + + b.ToTable("Titles", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.User", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<DateOnly?>("BirthDate") + .HasColumnType("date"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("DisplayName") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property<string>("FirstName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<int>("Gender") + .HasColumnType("integer"); + + b.Property<string>("ImagePublicUrl") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property<bool>("IsActivity") + .HasColumnType("boolean"); + + b.Property<bool>("IsAdmin") + .HasColumnType("boolean"); + + b.Property<string>("LastName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Users", "public"); + }); + + modelBuilder.Entity("Fin.Domain.Users.Entities.UserCredential", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<string>("EncryptedEmail") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property<string>("EncryptedPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property<int>("FailLoginAttempts") + .HasColumnType("integer"); + + b.Property<string>("GoogleId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property<string>("ResetToken") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property<Guid>("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<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<bool>("Aborted") + .HasColumnType("boolean"); + + b.Property<DateTime?>("AbortedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<DateOnly>("DeleteEffectivatedAt") + .HasColumnType("date"); + + b.Property<DateTime>("DeleteRequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.Property<Guid?>("UserAbortedId") + .HasColumnType("uuid"); + + b.Property<Guid>("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<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<string>("Color") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<Guid?>("FinancialInstitutionId") + .HasColumnType("uuid"); + + b.Property<string>("Icon") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property<bool>("Inactivated") + .HasColumnType("boolean"); + + b.Property<decimal>("InitialBalance") + .HasPrecision(19, 4) + .HasColumnType("numeric(19,4)"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("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) + .IsRequired(); + + b.HasOne("Fin.Domain.Wallets.Entities.Wallet", "DebitWallet") + .WithMany("CreditCards") + .HasForeignKey("DebitWalletId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Fin.Domain.FinancialInstitutions.Entities.FinancialInstitution", "FinancialInstitution") + .WithMany("CreditCards") + .HasForeignKey("FinancialInstitutionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.People.Entities.TitlePerson", b => + { + b.HasOne("Fin.Domain.People.Entities.Person", "Person") + .WithMany("TitlePeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Fin.Domain.Titles.Entities.Title", "Title") + .WithMany("TitlePeople") + .HasForeignKey("TitleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + + b.Navigation("Title"); + }); + + 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.TitleCategories.Entities.TitleTitleCategory", b => + { + b.HasOne("Fin.Domain.TitleCategories.Entities.TitleCategory", "TitleCategory") + .WithMany("TitleTitleCategories") + .HasForeignKey("TitleCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Fin.Domain.Titles.Entities.Title", "Title") + .WithMany("TitleTitleCategories") + .HasForeignKey("TitleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Title"); + + b.Navigation("TitleCategory"); + }); + + modelBuilder.Entity("Fin.Domain.Titles.Entities.Title", b => + { + b.HasOne("Fin.Domain.Wallets.Entities.Wallet", "Wallet") + .WithMany("Titles") + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Wallet"); + }); + + 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.People.Entities.Person", b => + { + b.Navigation("TitlePeople"); + }); + + modelBuilder.Entity("Fin.Domain.TitleCategories.Entities.TitleCategory", b => + { + b.Navigation("TitleTitleCategories"); + }); + + modelBuilder.Entity("Fin.Domain.Titles.Entities.Title", b => + { + b.Navigation("TitlePeople"); + + b.Navigation("TitleTitleCategories"); + }); + + 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"); + + b.Navigation("Titles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Fin.Infrastructure/Migrations/20251109225437_Person.cs b/Fin.Infrastructure/Migrations/20251109225437_Person.cs new file mode 100644 index 0000000..1539ebd --- /dev/null +++ b/Fin.Infrastructure/Migrations/20251109225437_Person.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Fin.Infrastructure.Migrations +{ + /// <inheritdoc /> + public partial class Person : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Person", + schema: "public", + columns: table => new + { + Id = table.Column<Guid>(type: "uuid", nullable: false), + Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false), + Inactivated = table.Column<bool>(type: "boolean", nullable: false), + CreatedBy = table.Column<Guid>(type: "uuid", nullable: false), + UpdatedBy = table.Column<Guid>(type: "uuid", nullable: false), + CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + TenantId = table.Column<Guid>(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Person", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TitlePerson", + schema: "public", + columns: table => new + { + PersonId = table.Column<Guid>(type: "uuid", nullable: false), + TitleId = table.Column<Guid>(type: "uuid", nullable: false), + Percentage = table.Column<decimal>(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false), + TenantId = table.Column<Guid>(type: "uuid", nullable: false), + CreatedBy = table.Column<Guid>(type: "uuid", nullable: false), + UpdatedBy = table.Column<Guid>(type: "uuid", nullable: false), + CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TitlePerson", x => new { x.PersonId, x.TitleId }); + table.ForeignKey( + name: "FK_TitlePerson_Person_PersonId", + column: x => x.PersonId, + principalSchema: "public", + principalTable: "Person", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TitlePerson_Titles_TitleId", + column: x => x.TitleId, + principalSchema: "public", + principalTable: "Titles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Person_Name_TenantId", + schema: "public", + table: "Person", + columns: new[] { "Name", "TenantId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_TitlePerson_TitleId", + schema: "public", + table: "TitlePerson", + column: "TitleId"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TitlePerson", + schema: "public"); + + migrationBuilder.DropTable( + name: "Person", + schema: "public"); + } + } +} diff --git a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs index 3aacbe7..2e25d6f 100644 --- a/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs +++ b/Fin.Infrastructure/Migrations/FinDbContextModelSnapshot.cs @@ -395,6 +395,77 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("UserRememberUseSettings", "public"); }); + modelBuilder.Entity("Fin.Domain.People.Entities.Person", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<bool>("Inactivated") + .HasColumnType("boolean"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Name", "TenantId") + .IsUnique(); + + b.ToTable("Person", "public"); + }); + + modelBuilder.Entity("Fin.Domain.People.Entities.TitlePerson", b => + { + b.Property<Guid>("PersonId") + .HasColumnType("uuid"); + + b.Property<Guid>("TitleId") + .HasColumnType("uuid"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("CreatedBy") + .HasColumnType("uuid"); + + b.Property<decimal>("Percentage") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property<Guid>("TenantId") + .HasColumnType("uuid"); + + b.Property<DateTime>("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property<Guid>("UpdatedBy") + .HasColumnType("uuid"); + + b.HasKey("PersonId", "TitleId"); + + b.HasIndex("TitleId"); + + b.ToTable("TitlePerson", "public"); + }); + modelBuilder.Entity("Fin.Domain.Tenants.Entities.Tenant", b => { b.Property<Guid>("Id") @@ -806,6 +877,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Fin.Domain.People.Entities.TitlePerson", b => + { + b.HasOne("Fin.Domain.People.Entities.Person", "Person") + .WithMany("TitlePeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Fin.Domain.Titles.Entities.Title", "Title") + .WithMany("TitlePeople") + .HasForeignKey("TitleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + + b.Navigation("Title"); + }); + modelBuilder.Entity("Fin.Domain.Tenants.Entities.TenantUser", b => { b.HasOne("Fin.Domain.Tenants.Entities.Tenant", null) @@ -906,6 +996,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("UserDeliveries"); }); + modelBuilder.Entity("Fin.Domain.People.Entities.Person", b => + { + b.Navigation("TitlePeople"); + }); + modelBuilder.Entity("Fin.Domain.TitleCategories.Entities.TitleCategory", b => { b.Navigation("TitleTitleCategories"); @@ -913,6 +1008,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Fin.Domain.Titles.Entities.Title", b => { + b.Navigation("TitlePeople"); + b.Navigation("TitleTitleCategories"); }); From 3ec80f7dbb8052194d773541ec6e3380b16f32d8 Mon Sep 17 00:00:00 2001 From: RafaelKC <rafaelkaua97@gmail.com> Date: Sun, 9 Nov 2025 20:11:17 -0300 Subject: [PATCH 03/15] FIN-31 WIP udpating Title CRUD to support person --- Fin.Domain/Titles/Dtos/TitleInput.cs | 2 + Fin.Domain/Titles/Dtos/TitleOutput.cs | 3 + Fin.Domain/Titles/Entities/Title.cs | 69 ++++++++++++++----- .../TitleCategoryConfiguration.cs | 1 - 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/Fin.Domain/Titles/Dtos/TitleInput.cs b/Fin.Domain/Titles/Dtos/TitleInput.cs index 3658a82..15abe65 100644 --- a/Fin.Domain/Titles/Dtos/TitleInput.cs +++ b/Fin.Domain/Titles/Dtos/TitleInput.cs @@ -1,3 +1,4 @@ +using Fin.Domain.People.Dtos; using Fin.Domain.Titles.Enums; namespace Fin.Domain.Titles.Dtos; @@ -10,4 +11,5 @@ public class TitleInput public DateTime Date { get; set; } public Guid WalletId { get; set; } public List<Guid> TitleCategoriesIds { get; set; } = []; + public List<TitlePersonInput> TitlePeople { get; set; } = []; } \ No newline at end of file diff --git a/Fin.Domain/Titles/Dtos/TitleOutput.cs b/Fin.Domain/Titles/Dtos/TitleOutput.cs index 0cc98e3..ee922fa 100644 --- a/Fin.Domain/Titles/Dtos/TitleOutput.cs +++ b/Fin.Domain/Titles/Dtos/TitleOutput.cs @@ -1,3 +1,4 @@ +using Fin.Domain.People.Dtos; using Fin.Domain.Titles.Entities; using Fin.Domain.Titles.Enums; @@ -16,6 +17,8 @@ public class TitleOutput(Title title) public Guid WalletId { get; set; } = title.WalletId; public List<Guid> TitleCategoriesIds { get; set; } = title.TitleCategories .Select(x => x.Id).ToList(); + public List<TitlePersonOutput> TitlePeople { get; set; } = title.TitlePeople + .Select(x => new TitlePersonOutput(x)).ToList(); public TitleOutput(): this(new Title()) { diff --git a/Fin.Domain/Titles/Entities/Title.cs b/Fin.Domain/Titles/Entities/Title.cs index d285268..e23bc53 100644 --- a/Fin.Domain/Titles/Entities/Title.cs +++ b/Fin.Domain/Titles/Entities/Title.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using Fin.Domain.Global.Interfaces; +using Fin.Domain.People.Dtos; using Fin.Domain.People.Entities; using Fin.Domain.TitleCategories.Entities; using Fin.Domain.Titles.Dtos; @@ -26,8 +27,8 @@ public class Title: IAuditedTenantEntity public ICollection<TitleCategory> TitleCategories { get; set; } = []; public ICollection<TitleTitleCategory> TitleTitleCategories { get; set; } = []; - public virtual ICollection<Person> People { get; set; } = []; - public virtual ICollection<TitlePerson> TitlePeople { get; set; } = []; + public ICollection<Person> People { get; set; } = []; + public ICollection<TitlePerson> TitlePeople { get; set; } = []; public Guid Id { get; set; } @@ -46,7 +47,12 @@ public Title() { Id = Guid.NewGuid(); - UpdateBasicProperties(input, previousBalance); + Value = input.Value; + Type = input.Type; + Description = input.Description.Trim(); + Date = input.Date; + WalletId = input.WalletId; + PreviousBalance = previousBalance; TitleTitleCategories = new Collection<TitleTitleCategory>( input.TitleCategoriesIds @@ -54,12 +60,21 @@ public Title() .Select(categoryId => new TitleTitleCategory(categoryId, Id)) .ToList() ); + + TitlePeople = new Collection<TitlePerson>( + input.TitlePeople.DistinctBy(x => x.PersonId) + .Select(x => new TitlePerson(Id, x)) + .ToList()); } - public List<TitleTitleCategory> UpdateAndReturnCategoriesToRemove(TitleInput input, decimal previousBalance) + public void Update(TitleInput input, decimal previousBalance) { - UpdateBasicProperties(input, previousBalance); - return SyncCategories(input.TitleCategoriesIds); + Value = input.Value; + Type = input.Type; + Description = input.Description.Trim(); + Date = input.Date; + WalletId = input.WalletId; + PreviousBalance = previousBalance; } public bool MustReprocess(TitleInput input) @@ -70,17 +85,7 @@ public bool MustReprocess(TitleInput input) || input.WalletId != WalletId; } - private void UpdateBasicProperties(TitleInput input, decimal previousBalance) - { - Value = input.Value; - Type = input.Type; - Description = input.Description.Trim(); - Date = input.Date; - WalletId = input.WalletId; - PreviousBalance = previousBalance; - } - - private List<TitleTitleCategory> SyncCategories(List<Guid> newCategoryIds) + public List<TitleTitleCategory> SyncCategoriesAndReturnToRemove(List<Guid> newCategoryIds) { var updatedCategories = newCategoryIds.Select(userId => new TitleTitleCategory(userId, Id)).ToList(); @@ -106,4 +111,34 @@ private List<TitleTitleCategory> SyncCategories(List<Guid> newCategoryIds) return categoriesToDelete; } + + public List<TitlePerson> SyncPeopleAndReturnToRemove(List<TitlePersonInput> titlePersonInputs) + { + var updatedPeople = titlePersonInputs.Select(titlePerson => new TitlePerson(Id, titlePerson)).ToList(); + + var titlePeopleToDelete = new List<TitlePerson>(); + foreach (var currentPerson in TitlePeople) + { + var index = updatedPeople.FindIndex(c => c.PersonId == currentPerson.PersonId); + if (index != -1) + { + currentPerson.Update(updatedPeople[index].Percentage); + } + titlePeopleToDelete.Add(currentPerson); + } + + foreach (var currentDelivery in titlePeopleToDelete) + { + TitlePeople.Remove(currentDelivery); + } + + foreach (var updatePerson in updatedPeople) + { + var index = TitlePeople.ToList().FindIndex(c => c.PersonId == updatePerson.PersonId); + if (index != -1) continue; + TitlePeople.Add(updatePerson); + } + + return titlePeopleToDelete; + } } \ No newline at end of file diff --git a/Fin.Infrastructure/Database/Configurations/TitleCategories/TitleCategoryConfiguration.cs b/Fin.Infrastructure/Database/Configurations/TitleCategories/TitleCategoryConfiguration.cs index b132321..69a7c3c 100644 --- a/Fin.Infrastructure/Database/Configurations/TitleCategories/TitleCategoryConfiguration.cs +++ b/Fin.Infrastructure/Database/Configurations/TitleCategories/TitleCategoryConfiguration.cs @@ -1,5 +1,4 @@ using Fin.Domain.TitleCategories.Entities; -using Fin.Domain.Titles.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; From f0f3169cd8f719935dce685f79c2523b38a79dab Mon Sep 17 00:00:00 2001 From: RafaelKC <rafaelkaua97@gmail.com> Date: Mon, 10 Nov 2025 18:05:07 -0300 Subject: [PATCH 04/15] FIN-31 adicionado CRUD do people --- Fin.Api/People/TitleCategoryController.cs | 61 +++++++++ .../People/Dtos/PersonGetListInput.cs | 8 ++ .../Enums/PersonCreateOrUpdateErrorCode.cs | 18 +++ .../People/Enums/PersonDeleteErrorCode.cs | 7 + .../People/TitleCategoryService.cs | 127 ++++++++++++++++++ .../Enums/TitleCreateOrUpdateErrorCode.cs | 10 ++ .../Titles/Services/TitleService.cs | 3 +- .../Titles/Services/TitleUpdateHelpService.cs | 28 ++-- .../TitleInputPeopleValidation.cs | 82 +++++++++++ Fin.Domain/Titles/Entities/Title.cs | 1 + Fin.Test/Titles/TitleServiceTest.cs | 2 +- Fin.Test/Titles/TitleUpdateHelpServiceTest.cs | 2 +- 12 files changed, 335 insertions(+), 14 deletions(-) create mode 100644 Fin.Api/People/TitleCategoryController.cs create mode 100644 Fin.Application/People/Dtos/PersonGetListInput.cs create mode 100644 Fin.Application/People/Enums/PersonCreateOrUpdateErrorCode.cs create mode 100644 Fin.Application/People/Enums/PersonDeleteErrorCode.cs create mode 100644 Fin.Application/People/TitleCategoryService.cs create mode 100644 Fin.Application/Titles/Validations/UpdateOrCrestes/TitleInputPeopleValidation.cs diff --git a/Fin.Api/People/TitleCategoryController.cs b/Fin.Api/People/TitleCategoryController.cs new file mode 100644 index 0000000..02d9256 --- /dev/null +++ b/Fin.Api/People/TitleCategoryController.cs @@ -0,0 +1,61 @@ +using Fin.Application.People; +using Fin.Application.People.Dtos; +using Fin.Application.People.Enums; +using Fin.Domain.Global.Classes; +using Fin.Domain.People.Dtos; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Fin.Api.People; + +[Route("people")] +[Authorize] +public class PersonController(IPersonService service) : ControllerBase +{ + [HttpGet] + public async Task<PagedOutput<PersonOutput>> GetList([FromQuery] PersonGetListInput input) + { + return await service.GetList(input); + } + + [HttpGet("{id:guid}")] + public async Task<ActionResult<PersonOutput>> Get([FromRoute] Guid id) + { + var category = await service.Get(id); + return category != null ? Ok(category) : NotFound(); + } + + [HttpPost] + public async Task<ActionResult<PersonOutput>> Create([FromBody] PersonInput 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<ActionResult> Update([FromRoute] Guid id, [FromBody] PersonInput input) + { + var validationResult = await service.Update(id, input, autoSave: true); + return validationResult.Success + ? Ok() + : validationResult.ErrorCode == PersonCreateOrUpdateErrorCode.PersonNotFound + ? NotFound(validationResult) + : UnprocessableEntity(validationResult); + } + + [HttpPut("toggle-inactivated/{id:guid}")] + public async Task<ActionResult> ToggleInactivated([FromRoute] Guid id) + { + var updated = await service.ToggleInactive(id, autoSave: true); + return updated ? Ok() : NotFound(); + } + + [HttpDelete("{id:guid}")] + public async Task<ActionResult> Delete([FromRoute] Guid id) + { + var validation = await service.Delete(id, autoSave: true); + return validation.Success ? NoContent() : validation.ErrorCode == PersonDeleteErrorCode.PersonNotFound ? NotFound(validation): UnprocessableEntity(validation); + } +} \ No newline at end of file diff --git a/Fin.Application/People/Dtos/PersonGetListInput.cs b/Fin.Application/People/Dtos/PersonGetListInput.cs new file mode 100644 index 0000000..04024e0 --- /dev/null +++ b/Fin.Application/People/Dtos/PersonGetListInput.cs @@ -0,0 +1,8 @@ +using Fin.Domain.Global.Classes; + +namespace Fin.Application.People.Dtos; + +public class PersonGetListInput: PagedFilteredAndSortedInput +{ + public bool? Inactivated { get; set; } +} \ No newline at end of file diff --git a/Fin.Application/People/Enums/PersonCreateOrUpdateErrorCode.cs b/Fin.Application/People/Enums/PersonCreateOrUpdateErrorCode.cs new file mode 100644 index 0000000..47a800d --- /dev/null +++ b/Fin.Application/People/Enums/PersonCreateOrUpdateErrorCode.cs @@ -0,0 +1,18 @@ +using Fin.Infrastructure.Errors; + +namespace Fin.Application.People.Enums; + +public enum PersonCreateOrUpdateErrorCode +{ + [ErrorMessage("Name is required")] + NameIsRequired = 0, + + [ErrorMessage("Name already in use")] + NameAlreadyInUse = 1, + + [ErrorMessage("Name max lenght 100")] + NameTooLong = 2, + + [ErrorMessage("Name max lenght 100")] + PersonNotFound = 4 +} \ No newline at end of file diff --git a/Fin.Application/People/Enums/PersonDeleteErrorCode.cs b/Fin.Application/People/Enums/PersonDeleteErrorCode.cs new file mode 100644 index 0000000..9d220f5 --- /dev/null +++ b/Fin.Application/People/Enums/PersonDeleteErrorCode.cs @@ -0,0 +1,7 @@ +namespace Fin.Application.People.Enums; + +public enum PersonDeleteErrorCode +{ + PersonInUse = 0, + PersonNotFound = 1 +} \ No newline at end of file diff --git a/Fin.Application/People/TitleCategoryService.cs b/Fin.Application/People/TitleCategoryService.cs new file mode 100644 index 0000000..72bde43 --- /dev/null +++ b/Fin.Application/People/TitleCategoryService.cs @@ -0,0 +1,127 @@ +using Fin.Application.Globals.Dtos; +using Fin.Application.People.Dtos; +using Fin.Application.People.Enums; +using Fin.Domain.Global.Classes; +using Fin.Domain.People.Dtos; +using Fin.Domain.People.Entities; +using Fin.Infrastructure.AutoServices.Interfaces; +using Fin.Infrastructure.Database.Extensions; +using Fin.Infrastructure.Database.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace Fin.Application.People; + +public interface IPersonService +{ + public Task<PersonOutput> Get(Guid id); + public Task<PagedOutput<PersonOutput>> GetList(PersonGetListInput input); + public Task<ValidationResultDto<PersonOutput, PersonCreateOrUpdateErrorCode>> Create(PersonInput input, bool autoSave = false); + public Task<ValidationResultDto<bool, PersonCreateOrUpdateErrorCode>> Update(Guid id, PersonInput input, bool autoSave = false); + public Task<ValidationResultDto<bool, PersonDeleteErrorCode>> Delete(Guid id, bool autoSave = false); + public Task<bool> ToggleInactive(Guid id, bool autoSave = false); +} + +public class PersonService( + IRepository<Person> repository + ) : IPersonService, IAutoTransient +{ + public async Task<PersonOutput> Get(Guid id) + { + var entity = await repository.AsNoTracking().FirstOrDefaultAsync(n => n.Id == id); + return entity != null ? new PersonOutput(entity) : null; + } + + public async Task<PagedOutput<PersonOutput>> GetList(PersonGetListInput input) + { + return await repository + .AsNoTracking() + .WhereIf(input.Inactivated.HasValue, n => n.Inactivated == input.Inactivated.Value) + .OrderBy(m => m.Inactivated) + .ThenBy(m => m.Name) + .ApplyFilterAndSorter(input) + .Select(n => new PersonOutput(n)) + .ToPagedResult(input); + } + + public async Task<ValidationResultDto<PersonOutput, PersonCreateOrUpdateErrorCode>> Create(PersonInput input, bool autoSave = false) + { + var validation = await ValidateInput<PersonOutput>(input); + if (!validation.Success) return validation; + + var person = new Person(input); + await repository.AddAsync(person, autoSave); + validation.Data = new PersonOutput(person); + return validation; + } + + public async Task<ValidationResultDto<bool, PersonCreateOrUpdateErrorCode>> Update(Guid id, PersonInput input, bool autoSave = false) + { + var validation = await ValidateInput<bool>(input, id); + if (!validation.Success) return validation; + + var person = await repository.FirstAsync(u => u.Id == id); + person.Update(input); + await repository.UpdateAsync(person, autoSave); + + validation.Data = true; + return validation; + } + + public async Task<ValidationResultDto<bool, PersonDeleteErrorCode>> Delete(Guid id, bool autoSave = false) + { + var validation = new ValidationResultDto<bool, PersonDeleteErrorCode>(); + + var person = await repository + .Include(u => u.TitlePeople) + .FirstOrDefaultAsync(u => u.Id == id); + if (person == null) return validation.WithError(PersonDeleteErrorCode.PersonNotFound); + if (person.TitlePeople != null && person.TitlePeople.Any()) validation.WithError(PersonDeleteErrorCode.PersonInUse); + + await repository.DeleteAsync(person, autoSave); + return validation.WithSuccess(true); + } + + public async Task<bool> ToggleInactive(Guid id, bool autoSave = false) + { + var person = await repository + .FirstOrDefaultAsync(u => u.Id == id); + if (person == null) return false; + + person.ToggleInactivated(); + await repository.UpdateAsync(person, autoSave); + + return true; + } + + private async Task<ValidationResultDto<T,PersonCreateOrUpdateErrorCode>> ValidateInput<T>( PersonInput input, Guid? editingId = null) + { + var validationResult = new ValidationResultDto<T,PersonCreateOrUpdateErrorCode>(); + + if (editingId.HasValue) + { + var titleExists = await repository.AnyAsync(n => n.Id == editingId.Value); + if (!titleExists) + return validationResult.WithError(PersonCreateOrUpdateErrorCode.PersonNotFound); + } + + if (string.IsNullOrWhiteSpace(input.Name)) + { + return validationResult.WithError(PersonCreateOrUpdateErrorCode.NameIsRequired); + } + if (input.Name.Length > 100) + { + return validationResult.WithError(PersonCreateOrUpdateErrorCode.NameTooLong); + } + var nameAlredInUse = await repository + .AnyAsync(n => n.Name == input.Name && (!editingId.HasValue || n.Id != editingId)); + if (nameAlredInUse) + { + validationResult.ErrorCode = PersonCreateOrUpdateErrorCode.NameAlreadyInUse; + validationResult.Message = "Name is already in use."; + return validationResult; + } + + validationResult.Success = true; + return validationResult; + } +} \ No newline at end of file diff --git a/Fin.Application/Titles/Enums/TitleCreateOrUpdateErrorCode.cs b/Fin.Application/Titles/Enums/TitleCreateOrUpdateErrorCode.cs index 4671e8b..559b464 100644 --- a/Fin.Application/Titles/Enums/TitleCreateOrUpdateErrorCode.cs +++ b/Fin.Application/Titles/Enums/TitleCreateOrUpdateErrorCode.cs @@ -36,4 +36,14 @@ public enum TitleCreateOrUpdateErrorCode [ErrorMessage("Duplicated title in same date time until minute.")] DuplicateTitleInSameDateTimeMinute = 10, + + [ErrorMessage("Some people was not found")] + SomePeopleNotFound = 11, + + [ErrorMessage("Some people is inactive")] + SomePeopleInactive = 12, + + [ErrorMessage("Financial split between people must be greater than or equal to 0 and less than or equal to 100")] + PeopleSplitRange = 13, + } \ No newline at end of file diff --git a/Fin.Application/Titles/Services/TitleService.cs b/Fin.Application/Titles/Services/TitleService.cs index 377ad2a..3a903b2 100644 --- a/Fin.Application/Titles/Services/TitleService.cs +++ b/Fin.Application/Titles/Services/TitleService.cs @@ -92,6 +92,7 @@ public async Task<ValidationResultDto<bool, TitleCreateOrUpdateErrorCode>> Updat var title = await titleRepository .Include(title => title.TitleTitleCategories) + .Include(title => title.TitlePeople) .FirstAsync(title => title.Id == id, cancellationToken); var mustReprocess = title.MustReprocess(input); @@ -99,7 +100,7 @@ public async Task<ValidationResultDto<bool, TitleCreateOrUpdateErrorCode>> Updat await using (var scope = await unitOfWork.BeginTransactionAsync(cancellationToken)) { - await updateHelpService.UpdateTitleAndCategories(title, input, context.CategoriesToRemove, cancellationToken); + await updateHelpService.PerformUpdateTitle(title, input, context, cancellationToken); if (mustReprocess) await updateHelpService.ReprocessAffectedWallets(title, context, autoSave: false, cancellationToken); if (autoSave) await scope.CompleteAsync(cancellationToken); } diff --git a/Fin.Application/Titles/Services/TitleUpdateHelpService.cs b/Fin.Application/Titles/Services/TitleUpdateHelpService.cs index efb4de5..99d5109 100644 --- a/Fin.Application/Titles/Services/TitleUpdateHelpService.cs +++ b/Fin.Application/Titles/Services/TitleUpdateHelpService.cs @@ -1,4 +1,5 @@ using Fin.Application.Wallets.Services; +using Fin.Domain.People.Entities; using Fin.Domain.TitleCategories.Entities; using Fin.Domain.Titles.Dtos; using Fin.Domain.Titles.Entities; @@ -10,10 +11,10 @@ namespace Fin.Application.Titles.Services; public interface ITitleUpdateHelpService { - Task UpdateTitleAndCategories( + Task PerformUpdateTitle( Title title, TitleInput input, - List<TitleTitleCategory> categoriesToRemove, + UpdateTitleContext context, CancellationToken cancellationToken); Task<UpdateTitleContext> PrepareUpdateContext( @@ -55,20 +56,22 @@ Task<List<Title>> GetTitlesForReprocessing( public class TitleUpdateHelpService( IRepository<Title> titleRepository, IRepository<TitleTitleCategory> titleTitleCategoryRepository, + IRepository<TitlePerson> titlePeopleRepository, IWalletBalanceService balanceService ): ITitleUpdateHelpService, IAutoTransient { - public async Task UpdateTitleAndCategories( + public async Task PerformUpdateTitle( Title title, TitleInput input, - List<TitleTitleCategory> categoriesToRemove, + UpdateTitleContext context, CancellationToken cancellationToken) { await titleRepository.UpdateAsync(title, cancellationToken); - foreach (var category in categoriesToRemove) - { + foreach (var category in context.CategoriesToRemove) await titleTitleCategoryRepository.DeleteAsync(category, cancellationToken); - } + foreach (var person in context.PeopleToRemove) + await titlePeopleRepository.DeleteAsync(person, cancellationToken); + } public async Task<UpdateTitleContext> PrepareUpdateContext( @@ -81,13 +84,16 @@ public async Task<UpdateTitleContext> PrepareUpdateContext( ? await CalculatePreviousBalance(title, input, cancellationToken) : title.PreviousBalance; - var categoriesToRemove = title.UpdateAndReturnCategoriesToRemove(input, previousBalance); + title.Update(input, previousBalance); + var categoriesToRemove = title.SyncCategoriesAndReturnToRemove(input.TitleCategoriesIds); + var peopleToRemove = title.SyncPeopleAndReturnToRemove(input.TitlePeople); return new UpdateTitleContext( PreviousWalletId: title.WalletId, PreviousDate: title.Date, PreviousBalance: title.PreviousBalance, - CategoriesToRemove: categoriesToRemove + CategoriesToRemove: categoriesToRemove, + PeopleToRemove: peopleToRemove ); } @@ -182,6 +188,6 @@ public record UpdateTitleContext( Guid PreviousWalletId, DateTime PreviousDate, decimal PreviousBalance, - List<TitleTitleCategory> CategoriesToRemove -); + List<TitleTitleCategory> CategoriesToRemove, + List<TitlePerson> PeopleToRemove); diff --git a/Fin.Application/Titles/Validations/UpdateOrCrestes/TitleInputPeopleValidation.cs b/Fin.Application/Titles/Validations/UpdateOrCrestes/TitleInputPeopleValidation.cs new file mode 100644 index 0000000..bf44fe2 --- /dev/null +++ b/Fin.Application/Titles/Validations/UpdateOrCrestes/TitleInputPeopleValidation.cs @@ -0,0 +1,82 @@ +using Fin.Application.Titles.Enums; +using Fin.Domain.People.Dtos; +using Fin.Domain.People.Entities; +using Fin.Domain.Titles.Dtos; +using Fin.Domain.Titles.Entities; +using Fin.Infrastructure.AutoServices.Interfaces; +using Fin.Infrastructure.Database.Repositories; +using Fin.Infrastructure.ValidationsPipeline; +using Microsoft.EntityFrameworkCore; + +namespace Fin.Application.Titles.Validations.UpdateOrCrestes; + +public class TitleInputPeopleValidation( + IRepository<Title> titleRepository, + IRepository<Person> personRepository + ): IValidationRule<TitleInput, TitleCreateOrUpdateErrorCode, List<Guid>>, IAutoTransient +{ + public async Task<ValidationPipelineOutput<TitleCreateOrUpdateErrorCode, List<Guid>>> ValidateAsync(TitleInput input, Guid? editingId = null, CancellationToken cancellationToken = default) + { + var validation = new ValidationPipelineOutput<TitleCreateOrUpdateErrorCode, List<Guid>>(); + + var people = await personRepository + .Where(person => input.TitlePeople.Select(tp => tp.PersonId).Contains(person.Id)) + .ToListAsync(cancellationToken); + + ValidatePeopleExistence(input, people, validation); + if (!validation.Success) return validation; + + var titleEditing = !editingId.HasValue ? null : await titleRepository + .Include(title => title.TitlePeople) + .FirstOrDefaultAsync(title => title.Id == editingId.Value, cancellationToken); + ValidatePeopleStatus(titleEditing, people, validation); + if (!validation.Success) return validation; + + ValidatePeopleSplitRange(input.TitlePeople, validation); + + return validation; + } + + private void ValidatePeopleExistence( + TitleInput input, + List<Person> people, + ValidationPipelineOutput<TitleCreateOrUpdateErrorCode, List<Guid>> validation) + { + var foundPeopleIds = people.Select(person => person.Id).ToList(); + var notFoundPeople = input.TitlePeople + .Select(tp => tp.PersonId) + .Except(foundPeopleIds) + .ToList(); + + if (notFoundPeople.Any()) + validation.AddError(TitleCreateOrUpdateErrorCode.SomePeopleNotFound, notFoundPeople); + } + + private void ValidatePeopleStatus( + Title? titleEditing, + List<Person> people, + ValidationPipelineOutput<TitleCreateOrUpdateErrorCode, List<Guid>> validation) + { + var previousPeopleIds = titleEditing?.TitlePeople? + .Select(tc => tc.PersonId)? + .ToList() ?? new List<Guid>(); + + var inactivePeopleIds = people + .Where(person => person.Inactivated + && !previousPeopleIds.Contains(person.Id)) + .Select(person => person.Id) + .ToList(); + + if (inactivePeopleIds.Any()) + validation.AddError(TitleCreateOrUpdateErrorCode.SomePeopleInactive, inactivePeopleIds); + } + + private void ValidatePeopleSplitRange( + List<TitlePersonInput> people, + ValidationPipelineOutput<TitleCreateOrUpdateErrorCode, List<Guid>> validation) + { + var splitSum = people.Sum(p => p.Percentage); + if (splitSum > 100 && splitSum < 0) + validation.AddError(TitleCreateOrUpdateErrorCode.PeopleSplitRange); + } +} \ No newline at end of file diff --git a/Fin.Domain/Titles/Entities/Title.cs b/Fin.Domain/Titles/Entities/Title.cs index e23bc53..c955723 100644 --- a/Fin.Domain/Titles/Entities/Title.cs +++ b/Fin.Domain/Titles/Entities/Title.cs @@ -24,6 +24,7 @@ public class Title: IAuditedTenantEntity public decimal EffectiveValue => (Value * (Type == TitleType.Expense ? -1 : 1)); public virtual Wallet Wallet { get; set; } + public ICollection<TitleCategory> TitleCategories { get; set; } = []; public ICollection<TitleTitleCategory> TitleTitleCategories { get; set; } = []; diff --git a/Fin.Test/Titles/TitleServiceTest.cs b/Fin.Test/Titles/TitleServiceTest.cs index 16dab1a..d50aded 100644 --- a/Fin.Test/Titles/TitleServiceTest.cs +++ b/Fin.Test/Titles/TitleServiceTest.cs @@ -457,7 +457,7 @@ public async Task Update_ShouldReturnSuccess_WhenInputIsValid() .ReturnsAsync(context); _updateHelpServiceMock - .Setup(u => u.UpdateTitleAndCategories( + .Setup(u => u.PerformUpdateTitle( It.IsAny<Title>(), updateInput, It.IsAny<List<TitleTitleCategory>>(), diff --git a/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs b/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs index 11e194d..b508203 100644 --- a/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs +++ b/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs @@ -75,7 +75,7 @@ public async Task UpdateTitleAndCategories_ShouldUpdateTitleAndRemoveCategories( title.UpdateAndReturnCategoriesToRemove(updateInput, 1000m); // Act - await service.UpdateTitleAndCategories(title, updateInput, categoriesToRemove, CancellationToken.None); + await service.PerformUpdateTitle(title, updateInput, categoriesToRemove, CancellationToken.None); await Context.SaveChangesAsync(); // Assert From 7194f3bd304de3236af936d3428cd21f309ee6c6 Mon Sep 17 00:00:00 2001 From: RafaelKC <rafaelkaua97@gmail.com> Date: Tue, 11 Nov 2025 22:54:18 -0300 Subject: [PATCH 05/15] FIN-31 adding api home --- Fin.Api/Program.cs | 16 +- Fin.Api/wwwroot/index.html | 286 ++++++++++++++++++ .../Services/HealthCheckService.cs | 3 +- Fin.Infrastructure/Constants/AppConstants.cs | 1 + 4 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 Fin.Api/wwwroot/index.html diff --git a/Fin.Api/Program.cs b/Fin.Api/Program.cs index d74c9bb..c6baa27 100644 --- a/Fin.Api/Program.cs +++ b/Fin.Api/Program.cs @@ -9,6 +9,7 @@ var builder = WebApplication.CreateBuilder(args); var frontEndUrl = builder.Configuration.GetSection(AppConstants.FrontUrlConfigKey).Get<string>(); +var version = builder.Configuration.GetSection(AppConstants.VersionConfigKey).Get<string>(); builder.Services .AddInfrastructure(builder.Configuration) @@ -41,6 +42,14 @@ var app = builder.Build(); +if (!string.IsNullOrWhiteSpace(version)) +{ + var versionPathBase = version; + if (!versionPathBase.StartsWith("/")) versionPathBase = $"/{versionPathBase}"; + app.UsePathBase(versionPathBase); +} + + if (app.Environment.IsDevelopment()) { app.UseOpenApi(); @@ -55,11 +64,14 @@ await app.UseDbMigrations(); await app.UseSeeders(); -app.UseAuthentication(); -app.UseAuthorization(); +app.UseDefaultFiles(); +app.UseStaticFiles(); app.UseHsts(); app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); + app.MapControllers(); app.Run(); \ No newline at end of file diff --git a/Fin.Api/wwwroot/index.html b/Fin.Api/wwwroot/index.html new file mode 100644 index 0000000..5ce8e71 --- /dev/null +++ b/Fin.Api/wwwroot/index.html @@ -0,0 +1,286 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>FinApp - API + + + + +
+
+ +

FinApp - API

+

Backend running successfully!

+
+ +
+
+ Checking status... +
+ + + + + + +
+ + + + \ No newline at end of file diff --git a/Fin.Application/HealthChecks/Services/HealthCheckService.cs b/Fin.Application/HealthChecks/Services/HealthCheckService.cs index 94a4dd1..ecb6e90 100644 --- a/Fin.Application/HealthChecks/Services/HealthCheckService.cs +++ b/Fin.Application/HealthChecks/Services/HealthCheckService.cs @@ -1,6 +1,7 @@ using Fin.Application.HealthChecks.Dtos; using Fin.Infrastructure.AutoServices; using Fin.Infrastructure.AutoServices.Interfaces; +using Fin.Infrastructure.Constants; using Fin.Infrastructure.DateTimes; using Microsoft.Extensions.Configuration; @@ -19,7 +20,7 @@ public HealthCheckOutput GetHealthCheck() return new HealthCheckOutput { Status = "OK", - Version = configuration["ApiSettings:Version"] ?? "", + Version = configuration[AppConstants.VersionConfigKey] ?? "", Timestamp = dateTimeProvider.UtcNow() }; } diff --git a/Fin.Infrastructure/Constants/AppConstants.cs b/Fin.Infrastructure/Constants/AppConstants.cs index c9f27db..326a0af 100644 --- a/Fin.Infrastructure/Constants/AppConstants.cs +++ b/Fin.Infrastructure/Constants/AppConstants.cs @@ -4,4 +4,5 @@ public static class AppConstants { public const string AppName = "Fin App"; public const string FrontUrlConfigKey = "ApiSettings:FrontendConfigs:Url"; + public const string VersionConfigKey = "ApiSettings:Version"; } \ No newline at end of file From 171cc42ed044eb989ebefc0de8a7b6c766636bf3 Mon Sep 17 00:00:00 2001 From: RafaelKC Date: Wed, 12 Nov 2025 14:06:38 -0300 Subject: [PATCH 06/15] FIN-31 adjusting index page --- Fin.Api/Properties/launchSettings.json | 4 ++-- Fin.Api/wwwroot/index.html | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Fin.Api/Properties/launchSettings.json b/Fin.Api/Properties/launchSettings.json index 45a24e9..7ec1a1e 100644 --- a/Fin.Api/Properties/launchSettings.json +++ b/Fin.Api/Properties/launchSettings.json @@ -4,7 +4,7 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, "applicationUrl": "http://localhost:5045", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -13,7 +13,7 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, "applicationUrl": "https://localhost:7122", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/Fin.Api/wwwroot/index.html b/Fin.Api/wwwroot/index.html index 5ce8e71..31b3cf9 100644 --- a/Fin.Api/wwwroot/index.html +++ b/Fin.Api/wwwroot/index.html @@ -233,8 +233,8 @@

FinApp - API

From d88d0a76e28892c99003bd313276a6eab38b55f1 Mon Sep 17 00:00:00 2001 From: RafaelKC Date: Sat, 22 Nov 2025 03:22:26 -0300 Subject: [PATCH 08/15] FIN-31 adjustes person on title --- .../People/Enums/PersonCreateOrUpdateErrorCode.cs | 2 +- Fin.Application/People/Enums/PersonDeleteErrorCode.cs | 5 +++++ Fin.Application/Titles/Services/TitleService.cs | 6 ++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Fin.Application/People/Enums/PersonCreateOrUpdateErrorCode.cs b/Fin.Application/People/Enums/PersonCreateOrUpdateErrorCode.cs index 47a800d..21593f2 100644 --- a/Fin.Application/People/Enums/PersonCreateOrUpdateErrorCode.cs +++ b/Fin.Application/People/Enums/PersonCreateOrUpdateErrorCode.cs @@ -13,6 +13,6 @@ public enum PersonCreateOrUpdateErrorCode [ErrorMessage("Name max lenght 100")] NameTooLong = 2, - [ErrorMessage("Name max lenght 100")] + [ErrorMessage("Person not found.")] PersonNotFound = 4 } \ No newline at end of file diff --git a/Fin.Application/People/Enums/PersonDeleteErrorCode.cs b/Fin.Application/People/Enums/PersonDeleteErrorCode.cs index 9d220f5..0be0a69 100644 --- a/Fin.Application/People/Enums/PersonDeleteErrorCode.cs +++ b/Fin.Application/People/Enums/PersonDeleteErrorCode.cs @@ -1,7 +1,12 @@ +using Fin.Infrastructure.Errors; + namespace Fin.Application.People.Enums; public enum PersonDeleteErrorCode { + [ErrorMessage("Person in use.")] PersonInUse = 0, + + [ErrorMessage("Person not found.")] PersonNotFound = 1 } \ No newline at end of file diff --git a/Fin.Application/Titles/Services/TitleService.cs b/Fin.Application/Titles/Services/TitleService.cs index 3a903b2..d692dc8 100644 --- a/Fin.Application/Titles/Services/TitleService.cs +++ b/Fin.Application/Titles/Services/TitleService.cs @@ -43,8 +43,9 @@ IValidationPipelineOrchestrator validation { public async Task Get(Guid id, CancellationToken cancellationToken = default) { - var entity = await titleRepository.Query(false) + var entity = await titleRepository .Include(title => title.TitleCategories) + .Include(title => title.TitlePeople) .FirstOrDefaultAsync(n => n.Id == id, cancellationToken); return entity != null ? new TitleOutput(entity) : null; } @@ -52,8 +53,9 @@ public async Task Get(Guid id, CancellationToken cancellationToken public async Task> GetList(TitleGetListInput input, CancellationToken cancellationToken = default) { - return await titleRepository.Query(false) + return await titleRepository .Include(title => title.TitleCategories) + .Include(title => title.TitlePeople) .WhereIf(input.Type.HasValue, n => n.Type == input.Type) .WhereIf(input.WalletIds.Any(), title => input.WalletIds.Contains(title.WalletId)) .WhereIf(input.CategoryIds.Any() && input.CategoryOperator == MultiplyFilterOperator.And, title => From 9a86d90f83d150ae1bf7d2614f20feb2c879fe57 Mon Sep 17 00:00:00 2001 From: RafaelKC Date: Sun, 23 Nov 2025 00:14:10 -0300 Subject: [PATCH 09/15] FIN-31 adding person filer on titles and adjuted person delete --- .../{TitleCategoryController.cs => PersonController.cs} | 0 Fin.Application/People/TitleCategoryService.cs | 2 +- Fin.Application/Titles/Dtos/TitleGetListInput.cs | 2 ++ Fin.Application/Titles/Services/TitleService.cs | 7 +++++++ 4 files changed, 10 insertions(+), 1 deletion(-) rename Fin.Api/People/{TitleCategoryController.cs => PersonController.cs} (100%) diff --git a/Fin.Api/People/TitleCategoryController.cs b/Fin.Api/People/PersonController.cs similarity index 100% rename from Fin.Api/People/TitleCategoryController.cs rename to Fin.Api/People/PersonController.cs diff --git a/Fin.Application/People/TitleCategoryService.cs b/Fin.Application/People/TitleCategoryService.cs index 72bde43..a3a0bb8 100644 --- a/Fin.Application/People/TitleCategoryService.cs +++ b/Fin.Application/People/TitleCategoryService.cs @@ -75,7 +75,7 @@ public async Task> Delete(Guid .Include(u => u.TitlePeople) .FirstOrDefaultAsync(u => u.Id == id); if (person == null) return validation.WithError(PersonDeleteErrorCode.PersonNotFound); - if (person.TitlePeople != null && person.TitlePeople.Any()) validation.WithError(PersonDeleteErrorCode.PersonInUse); + if (person.TitlePeople != null && person.TitlePeople.Any()) return validation.WithError(PersonDeleteErrorCode.PersonInUse); await repository.DeleteAsync(person, autoSave); return validation.WithSuccess(true); diff --git a/Fin.Application/Titles/Dtos/TitleGetListInput.cs b/Fin.Application/Titles/Dtos/TitleGetListInput.cs index aa4b713..862a7f7 100644 --- a/Fin.Application/Titles/Dtos/TitleGetListInput.cs +++ b/Fin.Application/Titles/Dtos/TitleGetListInput.cs @@ -8,6 +8,8 @@ public class TitleGetListInput: PagedFilteredAndSortedInput { public List CategoryIds { get; set; } = []; public MultiplyFilterOperator CategoryOperator { get; set; } + public List PersonIds { get; set; } = []; + public MultiplyFilterOperator PersonOperator { get; set; } public List WalletIds { get; set; } = []; public TitleType? Type { get; set; } } \ No newline at end of file diff --git a/Fin.Application/Titles/Services/TitleService.cs b/Fin.Application/Titles/Services/TitleService.cs index d692dc8..558062a 100644 --- a/Fin.Application/Titles/Services/TitleService.cs +++ b/Fin.Application/Titles/Services/TitleService.cs @@ -58,10 +58,17 @@ public async Task> GetList(TitleGetListInput input, .Include(title => title.TitlePeople) .WhereIf(input.Type.HasValue, n => n.Type == input.Type) .WhereIf(input.WalletIds.Any(), title => input.WalletIds.Contains(title.WalletId)) + .WhereIf(input.CategoryIds.Any() && input.CategoryOperator == MultiplyFilterOperator.And, title => input.CategoryIds.All(id => title.TitleCategories.Any(c => c.Id == id))) .WhereIf(input.CategoryIds.Any() && input.CategoryOperator == MultiplyFilterOperator.Or, title => title.TitleCategories.Any(titleCategory => input.CategoryIds.Contains(titleCategory.Id))) + + .WhereIf(input.PersonIds.Any() && input.PersonOperator == MultiplyFilterOperator.And, title => + input.PersonIds.All(id => title.People.Any(c => c.Id == id))) + .WhereIf(input.PersonIds.Any() && input.PersonOperator == MultiplyFilterOperator.Or, + title => title.People.Any(titleCategory => input.PersonIds.Contains(titleCategory.Id))) + .ApplyDefaultTitleOrder() .ApplyFilterAndSorter(input) .Select(n => new TitleOutput(n)) From 3cacd190bc4558a057d6c7e908d0f3ff22cbb803 Mon Sep 17 00:00:00 2001 From: RafaelKC Date: Mon, 24 Nov 2025 20:33:53 -0300 Subject: [PATCH 10/15] FIN-31 added person menu to defult seeder --- .../Seeders/Seeders/DefaultMenusSeeder.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs index 357d3e8..92632fd 100644 --- a/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs +++ b/Fin.Infrastructure/Seeders/Seeders/DefaultMenusSeeder.cs @@ -105,6 +105,17 @@ public async Task SeedAsync() OnlyForAdmin = false, Position = MenuPosition.LeftTop, KeyWords = "titles, títulos, lançamentos, gostos, recebidos" + }, + new() + { + Id = Guid.Parse("019aa9aa-55c4-72e5-931e-eb9a973670c8"), + FrontRoute = "/people", + Name = "finCore.features.person.title", + Color = "#fdc570", + Icon = "user", + OnlyForAdmin = false, + Position = MenuPosition.LeftTop, + KeyWords = "person, people, pessoas" } }; var defaultMenusIds = defaultMenus.Select(x => x.Id).ToList(); From 1bb2b4ff051b37d450a6544ad2f5fc8f07602635 Mon Sep 17 00:00:00 2001 From: RafaelKC Date: Mon, 24 Nov 2025 20:45:16 -0300 Subject: [PATCH 11/15] FIN-31 deleting existing tests --- .../Titles/Services/TitleService.cs | 2 +- .../Titles/Services/TitleUpdateHelpService.cs | 2 -- Fin.Test/Titles/TitleServiceTest.cs | 7 ++--- Fin.Test/Titles/TitleTest.cs | 23 ++++++++-------- Fin.Test/Titles/TitleUpdateHelpServiceTest.cs | 27 ++++++++++++++----- 5 files changed, 37 insertions(+), 24 deletions(-) diff --git a/Fin.Application/Titles/Services/TitleService.cs b/Fin.Application/Titles/Services/TitleService.cs index 558062a..0aed1c8 100644 --- a/Fin.Application/Titles/Services/TitleService.cs +++ b/Fin.Application/Titles/Services/TitleService.cs @@ -109,7 +109,7 @@ public async Task> Updat await using (var scope = await unitOfWork.BeginTransactionAsync(cancellationToken)) { - await updateHelpService.PerformUpdateTitle(title, input, context, cancellationToken); + await updateHelpService.PerformUpdateTitle(title, context, cancellationToken); if (mustReprocess) await updateHelpService.ReprocessAffectedWallets(title, context, autoSave: false, cancellationToken); if (autoSave) await scope.CompleteAsync(cancellationToken); } diff --git a/Fin.Application/Titles/Services/TitleUpdateHelpService.cs b/Fin.Application/Titles/Services/TitleUpdateHelpService.cs index 99d5109..a3bedb1 100644 --- a/Fin.Application/Titles/Services/TitleUpdateHelpService.cs +++ b/Fin.Application/Titles/Services/TitleUpdateHelpService.cs @@ -13,7 +13,6 @@ public interface ITitleUpdateHelpService { Task PerformUpdateTitle( Title title, - TitleInput input, UpdateTitleContext context, CancellationToken cancellationToken); @@ -62,7 +61,6 @@ IWalletBalanceService balanceService { public async Task PerformUpdateTitle( Title title, - TitleInput input, UpdateTitleContext context, CancellationToken cancellationToken) { diff --git a/Fin.Test/Titles/TitleServiceTest.cs b/Fin.Test/Titles/TitleServiceTest.cs index d50aded..d53a6c7 100644 --- a/Fin.Test/Titles/TitleServiceTest.cs +++ b/Fin.Test/Titles/TitleServiceTest.cs @@ -2,6 +2,7 @@ using Fin.Application.Titles.Enums; using Fin.Application.Titles.Services; using Fin.Application.Wallets.Services; +using Fin.Domain.People.Entities; using Fin.Domain.TitleCategories.Entities; using Fin.Domain.Titles.Dtos; using Fin.Domain.Titles.Entities; @@ -445,7 +446,8 @@ public async Task Update_ShouldReturnSuccess_WhenInputIsValid() PreviousWalletId: wallet.Id, PreviousDate: title.Date, PreviousBalance: title.PreviousBalance, - CategoriesToRemove: new List() + CategoriesToRemove: new List(), + PeopleToRemove: new List() ); _updateHelpServiceMock @@ -459,8 +461,7 @@ public async Task Update_ShouldReturnSuccess_WhenInputIsValid() _updateHelpServiceMock .Setup(u => u.PerformUpdateTitle( It.IsAny(), - updateInput, - It.IsAny<List<TitleTitleCategory>>(), + It.IsAny<UpdateTitleContext>(), It.IsAny<CancellationToken>())) .Returns(Task.CompletedTask); diff --git a/Fin.Test/Titles/TitleTest.cs b/Fin.Test/Titles/TitleTest.cs index 755ad15..bd22844 100644 --- a/Fin.Test/Titles/TitleTest.cs +++ b/Fin.Test/Titles/TitleTest.cs @@ -237,10 +237,10 @@ public void EffectiveValue_ShouldBeNegative_ForExpense() #endregion - #region UpdateAndReturnCategoriesToRemove + #region Update [Fact] - public void UpdateAndReturnCategoriesToRemove_ShouldUpdateBasicProperties() + public void Update_ShouldUpdateBasicProperties() { // Arrange var initialInput = new TitleInput @@ -266,7 +266,7 @@ public void UpdateAndReturnCategoriesToRemove_ShouldUpdateBasicProperties() var newPreviousBalance = 150m; // Act - var result = title.UpdateAndReturnCategoriesToRemove(updateInput, newPreviousBalance); + title.Update(updateInput, newPreviousBalance); // Assert title.Value.Should().Be(200m); @@ -275,11 +275,10 @@ public void UpdateAndReturnCategoriesToRemove_ShouldUpdateBasicProperties() title.Date.Should().Be(updateInput.Date); title.WalletId.Should().Be(TestUtils.Guids[2]); title.PreviousBalance.Should().Be(150m); - result.Should().NotBeNull(); } [Fact] - public void UpdateAndReturnCategoriesToRemove_ShouldAddNewCategories() + public void SyncCategoriesAndReturnToRemove_ShouldAddNewCategories() { // Arrange var initialInput = new TitleInput @@ -304,7 +303,7 @@ public void UpdateAndReturnCategoriesToRemove_ShouldAddNewCategories() }; // Act - var result = title.UpdateAndReturnCategoriesToRemove(updateInput, 0); + var result = title.SyncCategoriesAndReturnToRemove(updateInput.TitleCategoriesIds); // Assert title.TitleTitleCategories.Should().HaveCount(3); @@ -315,7 +314,7 @@ public void UpdateAndReturnCategoriesToRemove_ShouldAddNewCategories() } [Fact] - public void UpdateAndReturnCategoriesToRemove_ShouldRemoveCategories() + public void SyncCategoriesAndReturnToRemove_ShouldRemoveCategories() { // Arrange var initialInput = new TitleInput @@ -340,7 +339,7 @@ public void UpdateAndReturnCategoriesToRemove_ShouldRemoveCategories() }; // Act - var result = title.UpdateAndReturnCategoriesToRemove(updateInput, 0); + var result = title.SyncCategoriesAndReturnToRemove(updateInput.TitleCategoriesIds); // Assert title.TitleTitleCategories.Should().HaveCount(1); @@ -351,7 +350,7 @@ public void UpdateAndReturnCategoriesToRemove_ShouldRemoveCategories() } [Fact] - public void UpdateAndReturnCategoriesToRemove_ShouldKeepExistingCategories() + public void SyncCategoriesAndReturnToRemove_ShouldKeepExistingCategories() { // Arrange var initialInput = new TitleInput @@ -376,7 +375,7 @@ public void UpdateAndReturnCategoriesToRemove_ShouldKeepExistingCategories() }; // Act - var result = title.UpdateAndReturnCategoriesToRemove(updateInput, 0); + var result = title.SyncCategoriesAndReturnToRemove(updateInput.TitleCategoriesIds); // Assert title.TitleTitleCategories.Should().HaveCount(2); @@ -386,7 +385,7 @@ public void UpdateAndReturnCategoriesToRemove_ShouldKeepExistingCategories() } [Fact] - public void UpdateAndReturnCategoriesToRemove_ShouldRemoveAllCategories() + public void Update_ShouldRemoveAllCategories() { // Arrange var initialInput = new TitleInput @@ -411,7 +410,7 @@ public void UpdateAndReturnCategoriesToRemove_ShouldRemoveAllCategories() }; // Act - var result = title.UpdateAndReturnCategoriesToRemove(updateInput, 0); + var result = title.SyncCategoriesAndReturnToRemove(updateInput.TitleCategoriesIds); // Assert title.TitleTitleCategories.Should().BeEmpty(); diff --git a/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs b/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs index b508203..b1966a1 100644 --- a/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs +++ b/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs @@ -1,5 +1,6 @@ using Fin.Application.Titles.Services; using Fin.Application.Wallets.Services; +using Fin.Domain.People.Entities; using Fin.Domain.TitleCategories.Entities; using Fin.Domain.Titles.Dtos; using Fin.Domain.Titles.Entities; @@ -43,7 +44,6 @@ public async Task UpdateTitleAndCategories_ShouldUpdateTitleAndRemoveCategories( var titleCategory1 = TestUtils.TitleCategories[0]; var titleCategory2 = TestUtils.TitleCategories[1]; - var titleCategory3 = TestUtils.TitleCategories[2]; await resources.TitleCategoryRepository.AddAsync(titleCategory1, autoSave: true); await resources.TitleCategoryRepository.AddAsync(titleCategory2, autoSave: true); @@ -58,6 +58,7 @@ public async Task UpdateTitleAndCategories_ShouldUpdateTitleAndRemoveCategories( TitleCategoriesIds = new List<Guid> { titleCategory1.Id, titleCategory2.Id } }, 1000m); await resources.TitleRepository.AddAsync(title, autoSave: true); + var previousBalance = title.PreviousBalance; // Create categories to remove var categoriesToRemove = title.TitleTitleCategories.Take(1).ToList(); @@ -72,10 +73,19 @@ public async Task UpdateTitleAndCategories_ShouldUpdateTitleAndRemoveCategories( TitleCategoriesIds = new List<Guid> { titleCategory2.Id } }; - title.UpdateAndReturnCategoriesToRemove(updateInput, 1000m); + title.Update(updateInput, 1000m); + title.SyncCategoriesAndReturnToRemove(updateInput.TitleCategoriesIds); + var udpateContext = new UpdateTitleContext( + wallet.Id, + TestUtils.UtcDateTimes[0], + previousBalance, + categoriesToRemove, + new List<TitlePerson>() + ); + // Act - await service.PerformUpdateTitle(title, updateInput, categoriesToRemove, CancellationToken.None); + await service.PerformUpdateTitle(title, udpateContext, CancellationToken.None); await Context.SaveChangesAsync(); // Assert @@ -600,7 +610,8 @@ public async Task ReprocessAffectedWallets_ShouldReprocessBothWallets_WhenWallet PreviousWalletId: wallet1.Id, // Was in wallet1 PreviousDate: TestUtils.UtcDateTimes[0], PreviousBalance: 1000m, - CategoriesToRemove: new List<TitleTitleCategory>() + CategoriesToRemove: new List<TitleTitleCategory>(), + PeopleToRemove: new List<TitlePerson>() ); _balanceServiceMock @@ -657,7 +668,8 @@ public async Task ReprocessAffectedWallets_ShouldReprocessOnlyCurrentWallet_When PreviousWalletId: wallet.Id, // Same wallet PreviousDate: TestUtils.UtcDateTimes[0], PreviousBalance: 1000m, - CategoriesToRemove: new List<TitleTitleCategory>() + CategoriesToRemove: new List<TitleTitleCategory>(), + PeopleToRemove: new List<TitlePerson>() ); _balanceServiceMock @@ -689,6 +701,7 @@ private TitleUpdateHelpService GetService(Resources resources) return new TitleUpdateHelpService( resources.TitleRepository, resources.TitleTitleCategoryRepository, + resources.TitlePersonsRepository, _balanceServiceMock.Object ); } @@ -700,7 +713,8 @@ private Resources GetResources() TitleRepository = GetRepository<Title>(), TitleTitleCategoryRepository = GetRepository<TitleTitleCategory>(), TitleCategoryRepository = GetRepository<TitleCategory>(), - WalletRepository = GetRepository<Wallet>() + WalletRepository = GetRepository<Wallet>(), + TitlePersonsRepository = GetRepository<TitlePerson>() }; } @@ -709,6 +723,7 @@ private class Resources public IRepository<Title> TitleRepository { get; set; } public IRepository<TitleTitleCategory> TitleTitleCategoryRepository { get; set; } public IRepository<TitleCategory> TitleCategoryRepository { get; set; } + public IRepository<TitlePerson> TitlePersonsRepository { get; set; } public IRepository<Wallet> WalletRepository { get; set; } } } \ No newline at end of file From eb68de882aa6179a38d42471af4320bebf5a9d31 Mon Sep 17 00:00:00 2001 From: RafaelKC <rafaelkaua97@gmail.com> Date: Mon, 24 Nov 2025 21:22:57 -0300 Subject: [PATCH 12/15] FIN-31 adding Person tests --- Fin.Test/People/PersonControllerTest.cs | 417 +++++++++++++++ Fin.Test/People/PersonEntityTest.cs | 135 +++++ Fin.Test/People/PersonServiceTest.cs | 657 ++++++++++++++++++++++++ 3 files changed, 1209 insertions(+) create mode 100644 Fin.Test/People/PersonControllerTest.cs create mode 100644 Fin.Test/People/PersonEntityTest.cs create mode 100644 Fin.Test/People/PersonServiceTest.cs diff --git a/Fin.Test/People/PersonControllerTest.cs b/Fin.Test/People/PersonControllerTest.cs new file mode 100644 index 0000000..75cb936 --- /dev/null +++ b/Fin.Test/People/PersonControllerTest.cs @@ -0,0 +1,417 @@ +using Fin.Api.People; +using Fin.Application.Globals.Dtos; +using Fin.Application.People; +using Fin.Application.People.Dtos; +using Fin.Application.People.Enums; +using Fin.Domain.Global.Classes; +using Fin.Domain.People.Dtos; +using Fin.Domain.People.Entities; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Moq; + +namespace Fin.Test.People; + +public class PersonControllerTest : TestUtils.BaseTest +{ + private readonly Mock<IPersonService> _serviceMock; + private readonly PersonController _controller; + + public PersonControllerTest() + { + _serviceMock = new Mock<IPersonService>(); + _controller = new PersonController(_serviceMock.Object); + } + + #region GetList + + [Fact] + public async Task GetList_ShouldReturnPagedResult() + { + // Arrange + var input = new PersonGetListInput { MaxResultCount = 10, SkipCount = 0 }; + var expectedResult = new PagedOutput<PersonOutput> + { + Items = new List<PersonOutput> + { + new(new Person(new PersonInput { Name = TestUtils.Strings[0] })), + new(new Person(new PersonInput { Name = TestUtils.Strings[1] })) + }, + TotalCount = 2 + }; + + _serviceMock + .Setup(s => s.GetList(input)) + .ReturnsAsync(expectedResult); + + // Act + var result = await _controller.GetList(input); + + // Assert + result.Should().NotBeNull(); + result.Items.Should().HaveCount(2); + result.TotalCount.Should().Be(2); + } + + [Fact] + public async Task GetList_ShouldReturnEmpty_WhenNoData() + { + // Arrange + var input = new PersonGetListInput { MaxResultCount = 10, SkipCount = 0 }; + var expectedResult = new PagedOutput<PersonOutput> + { + Items = new List<PersonOutput>(), + TotalCount = 0 + }; + + _serviceMock + .Setup(s => s.GetList(input)) + .ReturnsAsync(expectedResult); + + // Act + var result = await _controller.GetList(input); + + // Assert + result.Should().NotBeNull(); + result.Items.Should().BeEmpty(); + result.TotalCount.Should().Be(0); + } + + #endregion + + #region Get + + [Fact] + public async Task Get_ShouldReturnOk_WhenPersonExists() + { + // Arrange + var personId = TestUtils.Guids[0]; + var expectedPerson = new PersonOutput( + new Person(new PersonInput { Name = TestUtils.Strings[0] }) + { Id = personId }); + + _serviceMock + .Setup(s => s.Get(personId)) + .ReturnsAsync(expectedPerson); + + // Act + var result = await _controller.Get(personId); + + // Assert + result.Result.Should().BeOfType<OkObjectResult>() + .Which.Value.Should().Be(expectedPerson); + } + + [Fact] + public async Task Get_ShouldReturnNotFound_WhenPersonDoesNotExist() + { + // Arrange + var personId = TestUtils.Guids[0]; + _serviceMock + .Setup(s => s.Get(personId)) + .ReturnsAsync((PersonOutput)null); + + // Act + var result = await _controller.Get(personId); + + // Assert + result.Result.Should().BeOfType<NotFoundResult>(); + } + + #endregion + + #region Create + + [Fact] + public async Task Create_ShouldReturnCreated_WhenInputIsValid() + { + // Arrange + var input = new PersonInput { Name = TestUtils.Strings[0] }; + var createdPerson = new PersonOutput( + new Person(input) { Id = TestUtils.Guids[0] }); + + var successResult = new ValidationResultDto<PersonOutput, PersonCreateOrUpdateErrorCode> + { + Success = true, + Data = createdPerson + }; + + _serviceMock + .Setup(s => s.Create(input, true)) + .ReturnsAsync(successResult); + + // Act + var result = await _controller.Create(input); + + // Assert + result.Result.Should().BeOfType<CreatedResult>() + .Which.Value.Should().Be(createdPerson); + + var createdResult = result.Result as CreatedResult; + createdResult.Location.Should().Be($"categories/{createdPerson.Id}"); + } + + [Fact] + public async Task Create_ShouldReturnUnprocessableEntity_WhenValidationFails() + { + // Arrange + var input = new PersonInput { Name = null }; + var failureResult = new ValidationResultDto<PersonOutput, PersonCreateOrUpdateErrorCode> + { + Success = false, + ErrorCode = PersonCreateOrUpdateErrorCode.NameIsRequired, + Message = "Name is required" + }; + + _serviceMock + .Setup(s => s.Create(input, true)) + .ReturnsAsync(failureResult); + + // Act + var result = await _controller.Create(input); + + // Assert + var unprocessableResult = result.Result + .Should().BeOfType<UnprocessableEntityObjectResult>() + .Subject; + unprocessableResult.Value.Should().BeEquivalentTo(failureResult); + } + + [Fact] + public async Task Create_ShouldReturnUnprocessableEntity_WhenNameAlreadyInUse() + { + // Arrange + var input = new PersonInput { Name = TestUtils.Strings[0] }; + var failureResult = new ValidationResultDto<PersonOutput, PersonCreateOrUpdateErrorCode> + { + Success = false, + ErrorCode = PersonCreateOrUpdateErrorCode.NameAlreadyInUse, + Message = "Name is already in use." + }; + + _serviceMock + .Setup(s => s.Create(input, true)) + .ReturnsAsync(failureResult); + + // Act + var result = await _controller.Create(input); + + // Assert + result.Result.Should().BeOfType<UnprocessableEntityObjectResult>(); + } + + #endregion + + #region Update + + [Fact] + public async Task Update_ShouldReturnOk_WhenUpdateIsSuccessful() + { + // Arrange + var personId = TestUtils.Guids[0]; + var input = new PersonInput { Name = TestUtils.Strings[1] }; + var successResult = new ValidationResultDto<bool, PersonCreateOrUpdateErrorCode> + { + Success = true, + Data = true + }; + + _serviceMock + .Setup(s => s.Update(personId, input, true)) + .ReturnsAsync(successResult); + + // Act + var result = await _controller.Update(personId, input); + + // Assert + result.Should().BeOfType<OkResult>(); + } + + [Fact] + public async Task Update_ShouldReturnNotFound_WhenPersonDoesNotExist() + { + // Arrange + var personId = TestUtils.Guids[0]; + var input = new PersonInput { Name = TestUtils.Strings[0] }; + var failureResult = new ValidationResultDto<bool, PersonCreateOrUpdateErrorCode> + { + Success = false, + ErrorCode = PersonCreateOrUpdateErrorCode.PersonNotFound + }; + + _serviceMock + .Setup(s => s.Update(personId, input, true)) + .ReturnsAsync(failureResult); + + // Act + var result = await _controller.Update(personId, input); + + // Assert + var notFoundResult = result.Should().BeOfType<NotFoundObjectResult>().Subject; + notFoundResult.Value.Should().BeEquivalentTo(failureResult); + } + + [Fact] + public async Task Update_ShouldReturnUnprocessableEntity_WhenValidationFails() + { + // Arrange + var personId = TestUtils.Guids[0]; + var input = new PersonInput { Name = null }; + var failureResult = new ValidationResultDto<bool, PersonCreateOrUpdateErrorCode> + { + Success = false, + ErrorCode = PersonCreateOrUpdateErrorCode.NameIsRequired, + Message = "Name is required" + }; + + _serviceMock + .Setup(s => s.Update(personId, input, true)) + .ReturnsAsync(failureResult); + + // Act + var result = await _controller.Update(personId, input); + + // Assert + var unprocessableResult = result + .Should().BeOfType<UnprocessableEntityObjectResult>() + .Subject; + unprocessableResult.Value.Should().BeEquivalentTo(failureResult); + } + + [Fact] + public async Task Update_ShouldReturnUnprocessableEntity_WhenNameAlreadyInUse() + { + // Arrange + var personId = TestUtils.Guids[0]; + var input = new PersonInput { Name = TestUtils.Strings[0] }; + var failureResult = new ValidationResultDto<bool, PersonCreateOrUpdateErrorCode> + { + Success = false, + ErrorCode = PersonCreateOrUpdateErrorCode.NameAlreadyInUse, + Message = "Name is already in use." + }; + + _serviceMock + .Setup(s => s.Update(personId, input, true)) + .ReturnsAsync(failureResult); + + // Act + var result = await _controller.Update(personId, input); + + // Assert + result.Should().BeOfType<UnprocessableEntityObjectResult>(); + } + + #endregion + + #region ToggleInactivated + + [Fact] + public async Task ToggleInactivated_ShouldReturnOk_WhenPersonExists() + { + // Arrange + var personId = TestUtils.Guids[0]; + _serviceMock + .Setup(s => s.ToggleInactive(personId, true)) + .ReturnsAsync(true); + + // Act + var result = await _controller.ToggleInactivated(personId); + + // Assert + result.Should().BeOfType<OkResult>(); + } + + [Fact] + public async Task ToggleInactivated_ShouldReturnNotFound_WhenPersonDoesNotExist() + { + // Arrange + var personId = TestUtils.Guids[0]; + _serviceMock + .Setup(s => s.ToggleInactive(personId, true)) + .ReturnsAsync(false); + + // Act + var result = await _controller.ToggleInactivated(personId); + + // Assert + result.Should().BeOfType<NotFoundResult>(); + } + + #endregion + + #region Delete + + [Fact] + public async Task Delete_ShouldReturnNoContent_WhenPersonDeleted() + { + // Arrange + var personId = TestUtils.Guids[0]; + var successResult = new ValidationResultDto<bool, PersonDeleteErrorCode> + { + Success = true, + Data = true + }; + + _serviceMock + .Setup(s => s.Delete(personId, true)) + .ReturnsAsync(successResult); + + // Act + var result = await _controller.Delete(personId); + + // Assert + result.Should().BeOfType<NoContentResult>(); + } + + [Fact] + public async Task Delete_ShouldReturnNotFound_WhenPersonDoesNotExist() + { + // Arrange + var personId = TestUtils.Guids[0]; + var failureResult = new ValidationResultDto<bool, PersonDeleteErrorCode> + { + Success = false, + ErrorCode = PersonDeleteErrorCode.PersonNotFound + }; + + _serviceMock + .Setup(s => s.Delete(personId, true)) + .ReturnsAsync(failureResult); + + // Act + var result = await _controller.Delete(personId); + + // Assert + var notFoundResult = result.Should().BeOfType<NotFoundObjectResult>().Subject; + notFoundResult.Value.Should().BeEquivalentTo(failureResult); + } + + [Fact] + public async Task Delete_ShouldReturnUnprocessableEntity_WhenPersonInUse() + { + // Arrange + var personId = TestUtils.Guids[0]; + var failureResult = new ValidationResultDto<bool, PersonDeleteErrorCode> + { + Success = false, + ErrorCode = PersonDeleteErrorCode.PersonInUse, + Message = "Person in use." + }; + + _serviceMock + .Setup(s => s.Delete(personId, true)) + .ReturnsAsync(failureResult); + + // Act + var result = await _controller.Delete(personId); + + // Assert + var unprocessableResult = result + .Should().BeOfType<UnprocessableEntityObjectResult>() + .Subject; + unprocessableResult.Value.Should().BeEquivalentTo(failureResult); + } + + #endregion +} \ No newline at end of file diff --git a/Fin.Test/People/PersonEntityTest.cs b/Fin.Test/People/PersonEntityTest.cs new file mode 100644 index 0000000..2176490 --- /dev/null +++ b/Fin.Test/People/PersonEntityTest.cs @@ -0,0 +1,135 @@ +using Fin.Domain.People.Dtos; +using Fin.Domain.People.Entities; +using FluentAssertions; + +namespace Fin.Test.People; + +public class PersonEntityTest +{ + #region Constructor + + [Fact] + public void Constructor_ShouldInitializeWithInput() + { + // Arrange + var input = new PersonInput + { + Name = TestUtils.Strings[0] + }; + + // Act + var person = new Person(input); + + // Assert + person.Should().NotBeNull(); + person.Name.Should().Be(TestUtils.Strings[0]); + person.Inactivated.Should().BeFalse(); + } + + [Fact] + public void Constructor_ShouldInitializeWithEmptyCollections() + { + // Arrange + var input = new PersonInput + { + Name = TestUtils.Strings[0] + }; + + // Act + var person = new Person(input); + + // Assert + person.Titles.Should().NotBeNull(); + person.Titles.Should().BeEmpty(); + person.TitlePeople.Should().NotBeNull(); + person.TitlePeople.Should().BeEmpty(); + } + + #endregion + + #region Update + + [Fact] + public void Update_ShouldUpdateName() + { + // Arrange + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + var updateInput = new PersonInput { Name = TestUtils.Strings[1] }; + + // Act + person.Update(updateInput); + + // Assert + person.Name.Should().Be(TestUtils.Strings[1]); + } + + [Fact] + public void Update_ShouldNotChangeInactivatedStatus() + { + // Arrange + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + person.ToggleInactivated(); + var updateInput = new PersonInput { Name = TestUtils.Strings[1] }; + + // Act + person.Update(updateInput); + + // Assert + person.Name.Should().Be(TestUtils.Strings[1]); + person.Inactivated.Should().BeTrue(); + } + + #endregion + + #region ToggleInactivated + + [Fact] + public void ToggleInactivated_ShouldChangeFromFalseToTrue() + { + // Arrange + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + person.Inactivated.Should().BeFalse(); + + // Act + person.ToggleInactivated(); + + // Assert + person.Inactivated.Should().BeTrue(); + } + + [Fact] + public void ToggleInactivated_ShouldChangeFromTrueToFalse() + { + // Arrange + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + person.ToggleInactivated(); + person.Inactivated.Should().BeTrue(); + + // Act + person.ToggleInactivated(); + + // Assert + person.Inactivated.Should().BeFalse(); + } + + [Fact] + public void ToggleInactivated_ShouldToggleMultipleTimes() + { + // Arrange + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + + // Act & Assert + person.Inactivated.Should().BeFalse(); + + person.ToggleInactivated(); + person.Inactivated.Should().BeTrue(); + + person.ToggleInactivated(); + person.Inactivated.Should().BeFalse(); + + person.ToggleInactivated(); + person.Inactivated.Should().BeTrue(); + } + + #endregion +} \ No newline at end of file diff --git a/Fin.Test/People/PersonServiceTest.cs b/Fin.Test/People/PersonServiceTest.cs new file mode 100644 index 0000000..9d569af --- /dev/null +++ b/Fin.Test/People/PersonServiceTest.cs @@ -0,0 +1,657 @@ +using Fin.Application.People; +using Fin.Application.People.Dtos; +using Fin.Application.People.Enums; +using Fin.Domain.People.Dtos; +using Fin.Domain.People.Entities; +using Fin.Domain.Titles.Entities; +using Fin.Domain.Wallets.Dtos; +using Fin.Domain.Wallets.Entities; +using Fin.Infrastructure.Database.Repositories; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; + +namespace Fin.Test.People; + +public class PersonServiceTest : TestUtils.BaseTestWithContext +{ + #region Get + + [Fact] + public async Task Get_ShouldReturnPerson_WhenExists() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + await resources.Repository.AddAsync(person, autoSave: true); + + // Act + var result = await service.Get(person.Id); + + // Assert + result.Should().NotBeNull(); + result.Id.Should().Be(person.Id); + result.Name.Should().Be(TestUtils.Strings[0]); + result.Inactivated.Should().BeFalse(); + } + + [Fact] + public async Task Get_ShouldReturnNull_WhenNotExists() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + // Act + var result = await service.Get(TestUtils.Guids[0]); + + // Assert + result.Should().BeNull(); + } + + #endregion + + #region GetList + + [Fact] + public async Task GetList_ShouldReturnPagedResult_WhenHasData() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + await resources.Repository.AddAsync( + new Person(new PersonInput { Name = "Person A" }), autoSave: true); + await resources.Repository.AddAsync( + new Person(new PersonInput { Name = "Person B" }), autoSave: true); + + var input = new PersonGetListInput { MaxResultCount = 10, SkipCount = 0 }; + + // Act + var result = await service.GetList(input); + + // Assert + result.Should().NotBeNull(); + result.Items.Should().HaveCount(2); + result.TotalCount.Should().Be(2); + } + + [Fact] + public async Task GetList_ShouldReturnEmpty_WhenNoData() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var input = new PersonGetListInput { MaxResultCount = 10, SkipCount = 0 }; + + // Act + var result = await service.GetList(input); + + // Assert + result.Should().NotBeNull(); + result.Items.Should().BeEmpty(); + result.TotalCount.Should().Be(0); + } + + [Fact] + public async Task GetList_ShouldOrderByInactivatedThenName() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var personC = new Person(new PersonInput { Name = "C Person" }); + var personA = new Person(new PersonInput { Name = "A Person" }); + var personB = new Person(new PersonInput { Name = "B Person" }); + personB.ToggleInactivated(); + + await resources.Repository.AddAsync(personC, autoSave: true); + await resources.Repository.AddAsync(personA, autoSave: true); + await resources.Repository.AddAsync(personB, autoSave: true); + + var input = new PersonGetListInput { MaxResultCount = 10, SkipCount = 0 }; + + // Act + var result = await service.GetList(input); + + // Assert + result.Items.Should().HaveCount(3); + result.Items[0].Name.Should().Be("A Person"); + result.Items[0].Inactivated.Should().BeFalse(); + result.Items[1].Name.Should().Be("C Person"); + result.Items[1].Inactivated.Should().BeFalse(); + result.Items[2].Name.Should().Be("B Person"); + result.Items[2].Inactivated.Should().BeTrue(); + } + + [Fact] + public async Task GetList_ShouldFilterByInactivated_WhenTrue() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var activePerson = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + var inactivePerson = new Person(new PersonInput { Name = TestUtils.Strings[1] }); + inactivePerson.ToggleInactivated(); + + await resources.Repository.AddAsync(activePerson, autoSave: true); + await resources.Repository.AddAsync(inactivePerson, autoSave: true); + + var input = new PersonGetListInput + { + MaxResultCount = 10, + SkipCount = 0, + Inactivated = true + }; + + // Act + var result = await service.GetList(input); + + // Assert + result.Items.Should().HaveCount(1); + result.Items[0].Name.Should().Be(TestUtils.Strings[1]); + result.Items[0].Inactivated.Should().BeTrue(); + } + + [Fact] + public async Task GetList_ShouldFilterByInactivated_WhenFalse() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var activePerson = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + var inactivePerson = new Person(new PersonInput { Name = TestUtils.Strings[1] }); + inactivePerson.ToggleInactivated(); + + await resources.Repository.AddAsync(activePerson, autoSave: true); + await resources.Repository.AddAsync(inactivePerson, autoSave: true); + + var input = new PersonGetListInput + { + MaxResultCount = 10, + SkipCount = 0, + Inactivated = false + }; + + // Act + var result = await service.GetList(input); + + // Assert + result.Items.Should().HaveCount(1); + result.Items[0].Name.Should().Be(TestUtils.Strings[0]); + result.Items[0].Inactivated.Should().BeFalse(); + } + + [Fact] + public async Task GetList_ShouldReturnAll_WhenInactivatedIsNull() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var activePerson = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + var inactivePerson = new Person(new PersonInput { Name = TestUtils.Strings[1] }); + inactivePerson.ToggleInactivated(); + + await resources.Repository.AddAsync(activePerson, autoSave: true); + await resources.Repository.AddAsync(inactivePerson, autoSave: true); + + var input = new PersonGetListInput + { + MaxResultCount = 10, + SkipCount = 0, + Inactivated = null + }; + + // Act + var result = await service.GetList(input); + + // Assert + result.Items.Should().HaveCount(2); + } + + #endregion + + #region Create + + [Fact] + public async Task Create_ShouldReturnSuccess_WhenInputIsValid() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var input = new PersonInput { Name = TestUtils.Strings[0] }; + + // Act + var result = await service.Create(input, autoSave: true); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + result.Data.Should().NotBeNull(); + result.Data.Name.Should().Be(TestUtils.Strings[0]); + result.Data.Inactivated.Should().BeFalse(); + + var dbPerson = await resources.Repository.AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == result.Data.Id); + + dbPerson.Should().NotBeNull(); + dbPerson.Name.Should().Be(TestUtils.Strings[0]); + } + + [Fact] + public async Task Create_ShouldReturnFailure_WhenNameIsRequired() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var input = new PersonInput { Name = null }; + + // Act + var result = await service.Create(input, autoSave: true); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(PersonCreateOrUpdateErrorCode.NameIsRequired); + result.Data.Should().BeNull(); + + var count = await resources.Repository.AsNoTracking().CountAsync(); + count.Should().Be(0); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task Create_ShouldReturnFailure_WhenNameIsNullOrWhiteSpace(string name) + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var input = new PersonInput { Name = name }; + + // Act + var result = await service.Create(input, autoSave: true); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(PersonCreateOrUpdateErrorCode.NameIsRequired); + } + + [Fact] + public async Task Create_ShouldReturnFailure_WhenNameTooLong() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var input = new PersonInput { Name = new string('A', 101) }; + + // Act + var result = await service.Create(input, autoSave: true); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(PersonCreateOrUpdateErrorCode.NameTooLong); + } + + [Fact] + public async Task Create_ShouldReturnFailure_WhenNameAlreadyInUse() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var existingPerson = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + await resources.Repository.AddAsync(existingPerson, autoSave: true); + + var input = new PersonInput { Name = TestUtils.Strings[0] }; + + // Act + var result = await service.Create(input, autoSave: true); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(PersonCreateOrUpdateErrorCode.NameAlreadyInUse); + result.Message.Should().Be("Name is already in use."); + } + + #endregion + + #region Update + + [Fact] + public async Task Update_ShouldReturnSuccess_WhenInputIsValid() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + await resources.Repository.AddAsync(person, autoSave: true); + + var input = new PersonInput { Name = TestUtils.Strings[1] }; + + // Act + var result = await service.Update(person.Id, input, autoSave: true); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + result.Data.Should().BeTrue(); + + var dbPerson = await resources.Repository.AsNoTracking() + .FirstAsync(p => p.Id == person.Id); + + dbPerson.Name.Should().Be(TestUtils.Strings[1]); + } + + [Fact] + public async Task Update_ShouldReturnFailure_WhenPersonNotFound() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var input = new PersonInput { Name = TestUtils.Strings[0] }; + + // Act + var result = await service.Update(TestUtils.Guids[0], input, autoSave: true); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(PersonCreateOrUpdateErrorCode.PersonNotFound); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task Update_ShouldReturnFailure_WhenNameIsNullOrWhiteSpace(string name) + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + await resources.Repository.AddAsync(person, autoSave: true); + + var input = new PersonInput { Name = name }; + + // Act + var result = await service.Update(person.Id, input, autoSave: true); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(PersonCreateOrUpdateErrorCode.NameIsRequired); + } + + [Fact] + public async Task Update_ShouldReturnFailure_WhenNameTooLong() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + await resources.Repository.AddAsync(person, autoSave: true); + + var input = new PersonInput { Name = new string('A', 101) }; + + // Act + var result = await service.Update(person.Id, input, autoSave: true); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(PersonCreateOrUpdateErrorCode.NameTooLong); + } + + [Fact] + public async Task Update_ShouldReturnFailure_WhenNameAlreadyInUseByOther() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1 = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + var person2 = new Person(new PersonInput { Name = TestUtils.Strings[1] }); + await resources.Repository.AddAsync(person1, autoSave: true); + await resources.Repository.AddAsync(person2, autoSave: true); + + var input = new PersonInput { Name = TestUtils.Strings[0] }; + + // Act + var result = await service.Update(person2.Id, input, autoSave: true); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(PersonCreateOrUpdateErrorCode.NameAlreadyInUse); + } + + [Fact] + public async Task Update_ShouldReturnSuccess_WhenNameUnchanged() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + await resources.Repository.AddAsync(person, autoSave: true); + + var input = new PersonInput { Name = TestUtils.Strings[0] }; + + // Act + var result = await service.Update(person.Id, input, autoSave: true); + + // Assert + result.Success.Should().BeTrue(); + } + + #endregion + + #region Delete + + [Fact] + public async Task Delete_ShouldReturnSuccess_WhenPersonExists() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + await resources.Repository.AddAsync(person, autoSave: true); + + // Act + var result = await service.Delete(person.Id, autoSave: true); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + result.Data.Should().BeTrue(); + + var dbPerson = await resources.Repository.AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == person.Id); + + dbPerson.Should().BeNull(); + } + + [Fact] + public async Task Delete_ShouldReturnFailure_WhenPersonNotFound() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + // Act + var result = await service.Delete(TestUtils.Guids[0], autoSave: true); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(PersonDeleteErrorCode.PersonNotFound); + } + + [Fact] + public async Task Delete_ShouldReturnFailure_WhenPersonInUse() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var wallet = new Wallet(new WalletInput + { + Name = TestUtils.Strings[4], + Color = TestUtils.Strings[1], + Icon = TestUtils.Strings[1], + }); + var title = new Title + { + Id = TestUtils.Guids[1], + Description = TestUtils.Strings[3], + Wallet = wallet, + Value = 10.0m, + }; + await GetRepository<Wallet>().AddAsync(wallet, autoSave: true); + await GetRepository<Title>().AddAsync(title, autoSave: true); + + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + await resources.Repository.AddAsync(person, autoSave: true); + + // Simulate TitlePerson relationship + var titlePerson = new TitlePerson(title.Id, new TitlePersonInput{ PersonId = person.Id, Percentage = 100m }); + person.TitlePeople.Add(titlePerson); + await resources.Repository.UpdateAsync(person, autoSave: true); + + // Act + var result = await service.Delete(person.Id, autoSave: true); + + // Assert + result.Success.Should().BeFalse(); + result.ErrorCode.Should().Be(PersonDeleteErrorCode.PersonInUse); + + var dbPerson = await resources.Repository.AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == person.Id); + + dbPerson.Should().NotBeNull(); + } + + #endregion + + #region ToggleInactive + + [Fact] + public async Task ToggleInactive_ShouldReturnTrue_WhenPersonExists() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + await resources.Repository.AddAsync(person, autoSave: true); + person.Inactivated.Should().BeFalse(); + + // Act + var result = await service.ToggleInactive(person.Id, autoSave: true); + + // Assert + result.Should().BeTrue(); + + var dbPerson = await resources.Repository.AsNoTracking() + .FirstAsync(p => p.Id == person.Id); + + dbPerson.Inactivated.Should().BeTrue(); + } + + [Fact] + public async Task ToggleInactive_ShouldToggleBackToFalse() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person = new Person(new PersonInput { Name = TestUtils.Strings[0] }); + await resources.Repository.AddAsync(person, autoSave: true); + + await service.ToggleInactive(person.Id, autoSave: true); + var dbPerson1 = await resources.Repository.AsNoTracking() + .FirstAsync(p => p.Id == person.Id); + dbPerson1.Inactivated.Should().BeTrue(); + + // Act + var result = await service.ToggleInactive(person.Id, autoSave: true); + + // Assert + result.Should().BeTrue(); + + var dbPerson2 = await resources.Repository.AsNoTracking() + .FirstAsync(p => p.Id == person.Id); + + dbPerson2.Inactivated.Should().BeFalse(); + } + + [Fact] + public async Task ToggleInactive_ShouldReturnFalse_WhenPersonNotFound() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + // Act + var result = await service.ToggleInactive(TestUtils.Guids[0], autoSave: true); + + // Assert + result.Should().BeFalse(); + } + + #endregion + + private PersonService GetService(Resources resources) + { + return new PersonService(resources.Repository); + } + + private Resources GetResources() + { + return new Resources + { + Repository = GetRepository<Person>() + }; + } + + private class Resources + { + public IRepository<Person> Repository { get; set; } + } +} \ No newline at end of file From 9b11f1a38172b288e041082cd3a354730709900b Mon Sep 17 00:00:00 2001 From: RafaelKC <rafaelkaua97@gmail.com> Date: Mon, 24 Nov 2025 22:43:51 -0300 Subject: [PATCH 13/15] FIN-31 added tests toperson on title --- .../TitleInputPeopleValidation.cs | 4 +- Fin.Domain/Titles/Entities/Title.cs | 1 + Fin.Test/Titles/TitleTest.cs | 227 ++++++++ Fin.Test/Titles/TitleUpdateHelpServiceTest.cs | 542 +++++++++++++++++- .../TitleInputPeopleValidationTest.cs | 492 ++++++++++++++++ 5 files changed, 1256 insertions(+), 10 deletions(-) create mode 100644 Fin.Test/Titles/Validations/TitleInputPeopleValidationTest.cs diff --git a/Fin.Application/Titles/Validations/UpdateOrCrestes/TitleInputPeopleValidation.cs b/Fin.Application/Titles/Validations/UpdateOrCrestes/TitleInputPeopleValidation.cs index bf44fe2..2e4e196 100644 --- a/Fin.Application/Titles/Validations/UpdateOrCrestes/TitleInputPeopleValidation.cs +++ b/Fin.Application/Titles/Validations/UpdateOrCrestes/TitleInputPeopleValidation.cs @@ -19,6 +19,8 @@ public async Task<ValidationPipelineOutput<TitleCreateOrUpdateErrorCode, List<Gu { var validation = new ValidationPipelineOutput<TitleCreateOrUpdateErrorCode, List<Guid>>(); + if (!input.TitlePeople.Any()) return validation; + var people = await personRepository .Where(person => input.TitlePeople.Select(tp => tp.PersonId).Contains(person.Id)) .ToListAsync(cancellationToken); @@ -76,7 +78,7 @@ private void ValidatePeopleSplitRange( ValidationPipelineOutput<TitleCreateOrUpdateErrorCode, List<Guid>> validation) { var splitSum = people.Sum(p => p.Percentage); - if (splitSum > 100 && splitSum < 0) + if (splitSum is > 100 or < 0.01m) validation.AddError(TitleCreateOrUpdateErrorCode.PeopleSplitRange); } } \ No newline at end of file diff --git a/Fin.Domain/Titles/Entities/Title.cs b/Fin.Domain/Titles/Entities/Title.cs index c955723..63c17a7 100644 --- a/Fin.Domain/Titles/Entities/Title.cs +++ b/Fin.Domain/Titles/Entities/Title.cs @@ -124,6 +124,7 @@ public List<TitlePerson> SyncPeopleAndReturnToRemove(List<TitlePersonInput> tit if (index != -1) { currentPerson.Update(updatedPeople[index].Percentage); + continue; } titlePeopleToDelete.Add(currentPerson); } diff --git a/Fin.Test/Titles/TitleTest.cs b/Fin.Test/Titles/TitleTest.cs index bd22844..3f8e739 100644 --- a/Fin.Test/Titles/TitleTest.cs +++ b/Fin.Test/Titles/TitleTest.cs @@ -1,3 +1,4 @@ +using Fin.Domain.People.Dtos; using Fin.Domain.Titles.Dtos; using Fin.Domain.Titles.Entities; using Fin.Domain.Titles.Enums; @@ -418,6 +419,232 @@ public void Update_ShouldRemoveAllCategories() } #endregion + + #region SyncPeopleAndReturnToRemove + +[Fact] +public void SyncPeopleAndReturnToRemove_ShouldAddNewPeople() +{ + // Arrange + var initialInput = new TitleInput + { + Value = 100m, + Type = TitleType.Income, + Description = "Test", + Date = DateTime.Now, + WalletId = TestUtils.Guids[0], + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = TestUtils.Guids[1], Percentage = 50m } + } + }; + var title = new Title(initialInput, 0); + + var updatePeople = new List<TitlePersonInput> + { + new() { PersonId = TestUtils.Guids[1], Percentage = 50m }, + new() { PersonId = TestUtils.Guids[2], Percentage = 30m }, + new() { PersonId = TestUtils.Guids[3], Percentage = 20m } + }; + + // Act + var result = title.SyncPeopleAndReturnToRemove(updatePeople); + + // Assert + title.TitlePeople.Should().HaveCount(3); + title.TitlePeople.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[1]); + title.TitlePeople.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[2]); + title.TitlePeople.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[3]); + + result.Should().HaveCount(1); + result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[1]); +} + +[Fact] +public void SyncPeopleAndReturnToRemove_ShouldRemovePeople() +{ + // Arrange + var initialInput = new TitleInput + { + Value = 100m, + Type = TitleType.Income, + Description = "Test", + Date = DateTime.Now, + WalletId = TestUtils.Guids[0], + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = TestUtils.Guids[1], Percentage = 40m }, + new() { PersonId = TestUtils.Guids[2], Percentage = 30m }, + new() { PersonId = TestUtils.Guids[3], Percentage = 30m } + } + }; + var title = new Title(initialInput, 0); + + var updatePeople = new List<TitlePersonInput> + { + new() { PersonId = TestUtils.Guids[1], Percentage = 100m } + }; + + // Act + var result = title.SyncPeopleAndReturnToRemove(updatePeople); + + // Assert + title.TitlePeople.Should().HaveCount(1); + title.TitlePeople.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[1]); + + result.Should().HaveCount(3); + result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[1]); + result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[2]); + result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[3]); +} + +[Fact] +public void SyncPeopleAndReturnToRemove_ShouldUpdateExistingPersonPercentage() +{ + // Arrange + var initialInput = new TitleInput + { + Value = 100m, + Type = TitleType.Income, + Description = "Test", + Date = DateTime.Now, + WalletId = TestUtils.Guids[0], + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = TestUtils.Guids[1], Percentage = 50m }, + new() { PersonId = TestUtils.Guids[2], Percentage = 50m } + } + }; + var title = new Title(initialInput, 0); + + var updatePeople = new List<TitlePersonInput> + { + new() { PersonId = TestUtils.Guids[1], Percentage = 70m }, + new() { PersonId = TestUtils.Guids[2], Percentage = 30m } + }; + + // Act + var result = title.SyncPeopleAndReturnToRemove(updatePeople); + + // Assert + title.TitlePeople.Should().HaveCount(2); + var person1 = title.TitlePeople.First(x => x.PersonId == TestUtils.Guids[1]); + var person2 = title.TitlePeople.First(x => x.PersonId == TestUtils.Guids[2]); + person1.Percentage.Should().Be(70m); + person2.Percentage.Should().Be(30m); + + result.Should().HaveCount(2); +} + +[Fact] +public void SyncPeopleAndReturnToRemove_ShouldRemoveAllPeople() +{ + // Arrange + var initialInput = new TitleInput + { + Value = 100m, + Type = TitleType.Income, + Description = "Test", + Date = DateTime.Now, + WalletId = TestUtils.Guids[0], + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = TestUtils.Guids[1], Percentage = 50m }, + new() { PersonId = TestUtils.Guids[2], Percentage = 50m } + } + }; + var title = new Title(initialInput, 0); + + var updatePeople = new List<TitlePersonInput>(); + + // Act + var result = title.SyncPeopleAndReturnToRemove(updatePeople); + + // Assert + title.TitlePeople.Should().BeEmpty(); + result.Should().HaveCount(2); + result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[1]); + result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[2]); +} + +[Fact] +public void SyncPeopleAndReturnToRemove_ShouldAddAndRemoveSimultaneously() +{ + // Arrange + var initialInput = new TitleInput + { + Value = 100m, + Type = TitleType.Income, + Description = "Test", + Date = DateTime.Now, + WalletId = TestUtils.Guids[0], + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = TestUtils.Guids[1], Percentage = 50m }, + new() { PersonId = TestUtils.Guids[2], Percentage = 50m } + } + }; + var title = new Title(initialInput, 0); + + var updatePeople = new List<TitlePersonInput> + { + new() { PersonId = TestUtils.Guids[1], Percentage = 60m }, // Keep and update + new() { PersonId = TestUtils.Guids[3], Percentage = 40m } // Add new + // Remove TestUtils.Guids[2] + }; + + // Act + var result = title.SyncPeopleAndReturnToRemove(updatePeople); + + // Assert + title.TitlePeople.Should().HaveCount(2); + title.TitlePeople.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[1]); + title.TitlePeople.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[3]); + title.TitlePeople.First(x => x.PersonId == TestUtils.Guids[1]).Percentage.Should().Be(60m); + title.TitlePeople.First(x => x.PersonId == TestUtils.Guids[3]).Percentage.Should().Be(40m); + + result.Should().HaveCount(2); + result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[1]); + result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[2]); +} + +[Fact] +public void SyncPeopleAndReturnToRemove_ShouldHandleEmptyInitialList() +{ + // Arrange + var initialInput = new TitleInput + { + Value = 100m, + Type = TitleType.Income, + Description = "Test", + Date = DateTime.Now, + WalletId = TestUtils.Guids[0], + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput>() + }; + var title = new Title(initialInput, 0); + + var updatePeople = new List<TitlePersonInput> + { + new() { PersonId = TestUtils.Guids[1], Percentage = 100m } + }; + + // Act + var result = title.SyncPeopleAndReturnToRemove(updatePeople); + + // Assert + title.TitlePeople.Should().HaveCount(1); + title.TitlePeople.First().PersonId.Should().Be(TestUtils.Guids[1]); + title.TitlePeople.First().Percentage.Should().Be(100m); + result.Should().BeEmpty(); +} + +#endregion #region MustReprocess diff --git a/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs b/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs index b1966a1..e8cd005 100644 --- a/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs +++ b/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs @@ -1,5 +1,6 @@ using Fin.Application.Titles.Services; using Fin.Application.Wallets.Services; +using Fin.Domain.People.Dtos; using Fin.Domain.People.Entities; using Fin.Domain.TitleCategories.Entities; using Fin.Domain.Titles.Dtos; @@ -77,15 +78,15 @@ public async Task UpdateTitleAndCategories_ShouldUpdateTitleAndRemoveCategories( title.SyncCategoriesAndReturnToRemove(updateInput.TitleCategoriesIds); var udpateContext = new UpdateTitleContext( - wallet.Id, + wallet.Id, TestUtils.UtcDateTimes[0], previousBalance, categoriesToRemove, new List<TitlePerson>() - ); - + ); + // Act - await service.PerformUpdateTitle(title, udpateContext, CancellationToken.None); + await service.PerformUpdateTitle(title, udpateContext, CancellationToken.None); await Context.SaveChangesAsync(); // Assert @@ -202,7 +203,7 @@ public async Task PrepareUpdateContext_ShouldNotRecalculateBalance_WhenMustNotRe // Assert context.Should().NotBeNull(); context.PreviousBalance.Should().Be(title.PreviousBalance); - + // Verify GetBalanceAt was NOT called _balanceServiceMock.Verify( b => b.GetBalanceAt(It.IsAny<Guid>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>()), @@ -550,7 +551,7 @@ public async Task GetTitlesForReprocessing_ShouldFilterByWallet() wallet1.Titles.Add(title1); wallet1.Titles.Add(title2); wallet2.Titles.Add(title3); - + await resources.WalletRepository.AddRangeAsync([wallet1, wallet2], autoSave: true); // Act @@ -611,7 +612,7 @@ public async Task ReprocessAffectedWallets_ShouldReprocessBothWallets_WhenWallet PreviousDate: TestUtils.UtcDateTimes[0], PreviousBalance: 1000m, CategoriesToRemove: new List<TitleTitleCategory>(), - PeopleToRemove: new List<TitlePerson>() + PeopleToRemove: new List<TitlePerson>() ); _balanceServiceMock @@ -669,7 +670,7 @@ public async Task ReprocessAffectedWallets_ShouldReprocessOnlyCurrentWallet_When PreviousDate: TestUtils.UtcDateTimes[0], PreviousBalance: 1000m, CategoriesToRemove: new List<TitleTitleCategory>(), - PeopleToRemove: new List<TitlePerson>() + PeopleToRemove: new List<TitlePerson>() ); _balanceServiceMock @@ -696,6 +697,527 @@ public async Task ReprocessAffectedWallets_ShouldReprocessOnlyCurrentWallet_When #endregion + #region UpdateTitleAndPeople + + [Fact] + public async Task UpdateTitleAndPeople_ShouldUpdateTitleAndRemovePeople() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var wallet = new Wallet(new WalletInput + { + Name = TestUtils.Strings[0], + Color = TestUtils.Strings[1], + Icon = TestUtils.Strings[2], + InitialBalance = 1000m + }); + await resources.WalletRepository.AddAsync(wallet, autoSave: true); + + var person1 = new Person(new PersonInput { Name = TestUtils.Strings[3] }); + var person2 = new Person(new PersonInput { Name = TestUtils.Strings[4] }); + await resources.PersonRepository.AddAsync(person1, autoSave: true); + await resources.PersonRepository.AddAsync(person2, autoSave: true); + + var title = new Title(new TitleInput + { + Description = TestUtils.Strings[5], + Value = 500m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[0], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 50m }, + new() { PersonId = person2.Id, Percentage = 50m } + } + }, 1000m); + await resources.TitleRepository.AddAsync(title, autoSave: true); + var previousBalance = title.PreviousBalance; + + // Create people to remove + var peopleToRemove = title.TitlePeople.Take(1).ToList(); + + var updateInput = new TitleInput + { + Description = "Updated Description", + Value = 600m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[1], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person2.Id, Percentage = 100m } + } + }; + + title.Update(updateInput, 1000m); + title.SyncPeopleAndReturnToRemove(updateInput.TitlePeople); + + var updateContext = new UpdateTitleContext( + wallet.Id, + TestUtils.UtcDateTimes[0], + previousBalance, + new List<TitleTitleCategory>(), + peopleToRemove + ); + + // Act + await service.PerformUpdateTitle(title, updateContext, CancellationToken.None); + await Context.SaveChangesAsync(); + + // Assert + var updatedTitle = await resources.TitleRepository.AsNoTracking() + .Include(t => t.TitlePeople) + .FirstAsync(t => t.Id == title.Id); + + updatedTitle.Description.Should().Be("Updated Description"); + updatedTitle.Value.Should().Be(600m); + updatedTitle.TitlePeople.Should().HaveCount(1); + updatedTitle.TitlePeople.First().PersonId.Should().Be(person2.Id); + updatedTitle.TitlePeople.First().Percentage.Should().Be(100m); + } + + [Fact] + public async Task UpdateTitleAndPeople_ShouldUpdateTitleAndAddPeople() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var wallet = new Wallet(new WalletInput + { + Name = TestUtils.Strings[0], + Color = TestUtils.Strings[1], + Icon = TestUtils.Strings[2], + InitialBalance = 1000m + }); + await resources.WalletRepository.AddAsync(wallet, autoSave: true); + + var person1 = new Person(new PersonInput { Name = TestUtils.Strings[3] }); + var person2 = new Person(new PersonInput { Name = TestUtils.Strings[4] }); + await resources.PersonRepository.AddAsync(person1, autoSave: true); + await resources.PersonRepository.AddAsync(person2, autoSave: true); + + var title = new Title(new TitleInput + { + Description = TestUtils.Strings[5], + Value = 500m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[0], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 100m } + } + }, 1000m); + await resources.TitleRepository.AddAsync(title, autoSave: true); + var previousBalance = title.PreviousBalance; + + var updateInput = new TitleInput + { + Description = "Updated Description", + Value = 600m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[1], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 50m }, + new() { PersonId = person2.Id, Percentage = 50m } + } + }; + + title.Update(updateInput, 1000m); + var peopleToRemove = title.SyncPeopleAndReturnToRemove(updateInput.TitlePeople); + + var updateContext = new UpdateTitleContext( + wallet.Id, + TestUtils.UtcDateTimes[0], + previousBalance, + new List<TitleTitleCategory>(), + peopleToRemove + ); + + // Act + await service.PerformUpdateTitle(title, updateContext, CancellationToken.None); + await Context.SaveChangesAsync(); + + // Assert + var updatedTitle = await resources.TitleRepository.AsNoTracking() + .Include(t => t.TitlePeople) + .FirstAsync(t => t.Id == title.Id); + + updatedTitle.TitlePeople.Should().HaveCount(2); + updatedTitle.TitlePeople.Select(tp => tp.PersonId).Should().Contain(person1.Id); + updatedTitle.TitlePeople.Select(tp => tp.PersonId).Should().Contain(person2.Id); + } + + [Fact] + public async Task UpdateTitleAndPeople_ShouldUpdatePersonPercentage() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var wallet = new Wallet(new WalletInput + { + Name = TestUtils.Strings[0], + Color = TestUtils.Strings[1], + Icon = TestUtils.Strings[2], + InitialBalance = 1000m + }); + await resources.WalletRepository.AddAsync(wallet, autoSave: true); + + var person1 = new Person(new PersonInput { Name = TestUtils.Strings[3] }); + await resources.PersonRepository.AddAsync(person1, autoSave: true); + + var title = new Title(new TitleInput + { + Description = TestUtils.Strings[5], + Value = 500m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[0], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 50m } + } + }, 1000m); + await resources.TitleRepository.AddAsync(title, autoSave: true); + + var updateInput = new TitleInput + { + Description = TestUtils.Strings[5], + Value = 500m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[0], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 75m } + } + }; + + title.Update(updateInput, 1000m); + var peopleToRemove = title.SyncPeopleAndReturnToRemove(updateInput.TitlePeople); + + var updateContext = new UpdateTitleContext( + wallet.Id, + TestUtils.UtcDateTimes[0], + 1000m, + new List<TitleTitleCategory>(), + peopleToRemove + ); + + // Act + await service.PerformUpdateTitle(title, updateContext, CancellationToken.None); + await Context.SaveChangesAsync(); + + // Assert + var updatedTitle = await resources.TitleRepository.AsNoTracking() + .Include(t => t.TitlePeople) + .FirstAsync(t => t.Id == title.Id); + + updatedTitle.TitlePeople.Should().HaveCount(1); + updatedTitle.TitlePeople.First().PersonId.Should().Be(person1.Id); + updatedTitle.TitlePeople.First().Percentage.Should().Be(75m); + } + + [Fact] + public async Task UpdateTitleAndPeople_ShouldRemoveAllPeople() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var wallet = new Wallet(new WalletInput + { + Name = TestUtils.Strings[0], + Color = TestUtils.Strings[1], + Icon = TestUtils.Strings[2], + InitialBalance = 1000m + }); + await resources.WalletRepository.AddAsync(wallet, autoSave: true); + + var person1 = new Person(new PersonInput { Name = TestUtils.Strings[3] }); + var person2 = new Person(new PersonInput { Name = TestUtils.Strings[4] }); + await resources.PersonRepository.AddAsync(person1, autoSave: true); + await resources.PersonRepository.AddAsync(person2, autoSave: true); + + var title = new Title(new TitleInput + { + Description = TestUtils.Strings[5], + Value = 500m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[0], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 50m }, + new() { PersonId = person2.Id, Percentage = 50m } + } + }, 1000m); + await resources.TitleRepository.AddAsync(title, autoSave: true); + + var updateInput = new TitleInput + { + Description = TestUtils.Strings[5], + Value = 500m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[0], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput>() + }; + + title.Update(updateInput, 1000m); + var peopleToRemove = title.SyncPeopleAndReturnToRemove(updateInput.TitlePeople); + + var updateContext = new UpdateTitleContext( + wallet.Id, + TestUtils.UtcDateTimes[0], + 1000m, + new List<TitleTitleCategory>(), + peopleToRemove + ); + + // Act + await service.PerformUpdateTitle(title, updateContext, CancellationToken.None); + await Context.SaveChangesAsync(); + + // Assert + var updatedTitle = await resources.TitleRepository.AsNoTracking() + .Include(t => t.TitlePeople) + .FirstAsync(t => t.Id == title.Id); + + updatedTitle.TitlePeople.Should().BeEmpty(); + } + + [Fact] + public async Task UpdateTitleAndPeople_ShouldHandleCategoriesAndPeopleSimultaneously() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var wallet = new Wallet(new WalletInput + { + Name = TestUtils.Strings[0], + Color = TestUtils.Strings[1], + Icon = TestUtils.Strings[2], + InitialBalance = 1000m + }); + await resources.WalletRepository.AddAsync(wallet, autoSave: true); + + var category1 = TestUtils.TitleCategories[0]; + var category2 = TestUtils.TitleCategories[1]; + await resources.TitleCategoryRepository.AddAsync(category1, autoSave: true); + await resources.TitleCategoryRepository.AddAsync(category2, autoSave: true); + + var person1 = new Person(new PersonInput { Name = TestUtils.Strings[3] }); + var person2 = new Person(new PersonInput { Name = TestUtils.Strings[4] }); + await resources.PersonRepository.AddAsync(person1, autoSave: true); + await resources.PersonRepository.AddAsync(person2, autoSave: true); + + var title = new Title(new TitleInput + { + Description = TestUtils.Strings[5], + Value = 500m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[0], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid> { category1.Id, category2.Id }, + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 50m }, + new() { PersonId = person2.Id, Percentage = 50m } + } + }, 1000m); + await resources.TitleRepository.AddAsync(title, autoSave: true); + + var updateInput = new TitleInput + { + Description = "Updated", + Value = 600m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[1], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid> { category2.Id }, + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 100m } + } + }; + + title.Update(updateInput, 1000m); + var categoriesToRemove = title.SyncCategoriesAndReturnToRemove(updateInput.TitleCategoriesIds); + var peopleToRemove = title.SyncPeopleAndReturnToRemove(updateInput.TitlePeople); + + var updateContext = new UpdateTitleContext( + wallet.Id, + TestUtils.UtcDateTimes[0], + 1000m, + categoriesToRemove, + peopleToRemove + ); + + // Act + await service.PerformUpdateTitle(title, updateContext, CancellationToken.None); + await Context.SaveChangesAsync(); + + // Assert + var updatedTitle = await resources.TitleRepository.AsNoTracking() + .Include(t => t.TitleTitleCategories) + .Include(t => t.TitlePeople) + .FirstAsync(t => t.Id == title.Id); + + updatedTitle.Description.Should().Be("Updated"); + updatedTitle.Value.Should().Be(600m); + updatedTitle.TitleTitleCategories.Should().HaveCount(1); + updatedTitle.TitleTitleCategories.First().TitleCategoryId.Should().Be(category2.Id); + updatedTitle.TitlePeople.Should().HaveCount(1); + updatedTitle.TitlePeople.First().PersonId.Should().Be(person1.Id); + updatedTitle.TitlePeople.First().Percentage.Should().Be(100m); + } + + #endregion + + #region PrepareUpdateContext - People + + [Fact] + public async Task PrepareUpdateContext_ShouldIncludePeopleToRemove() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var wallet = new Wallet(new WalletInput + { + Name = TestUtils.Strings[0], + Color = TestUtils.Strings[1], + Icon = TestUtils.Strings[2], + InitialBalance = 1000m + }); + await resources.WalletRepository.AddAsync(wallet, autoSave: true); + + var person1 = new Person(new PersonInput { Name = TestUtils.Strings[3] }); + var person2 = new Person(new PersonInput { Name = TestUtils.Strings[4] }); + await resources.PersonRepository.AddAsync(person1, autoSave: true); + await resources.PersonRepository.AddAsync(person2, autoSave: true); + + var title = new Title(new TitleInput + { + Description = TestUtils.Strings[5], + Value = 500m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[0], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 50m }, + new() { PersonId = person2.Id, Percentage = 50m } + } + }, 1000m); + await resources.TitleRepository.AddAsync(title, autoSave: true); + + var input = new TitleInput + { + Description = "Updated", + Value = 500m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[0], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 100m } + } + }; + + // Act + var context = await service.PrepareUpdateContext(title, input, mustReprocess: false, CancellationToken.None); + + // Assert + context.Should().NotBeNull(); + context.PeopleToRemove.Should().HaveCount(1); + context.CategoriesToRemove.Should().BeEmpty(); + } + + [Fact] + public async Task PrepareUpdateContext_ShouldReturnEmptyPeopleList_WhenNoPeopleRemoved() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var wallet = new Wallet(new WalletInput + { + Name = TestUtils.Strings[0], + Color = TestUtils.Strings[1], + Icon = TestUtils.Strings[2], + InitialBalance = 1000m + }); + await resources.WalletRepository.AddAsync(wallet, autoSave: true); + + var person1 = new Person(new PersonInput { Name = TestUtils.Strings[3] }); + await resources.PersonRepository.AddAsync(person1, autoSave: true); + + var title = new Title(new TitleInput + { + Description = TestUtils.Strings[5], + Value = 500m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[0], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 50m } + } + }, 1000m); + await resources.TitleRepository.AddAsync(title, autoSave: true); + + var input = new TitleInput + { + Description = "Updated", + Value = 500m, + Type = TitleType.Income, + Date = TestUtils.UtcDateTimes[0], + WalletId = wallet.Id, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput> + { + new() { PersonId = person1.Id, Percentage = 75m } // Same person, different percentage + } + }; + + // Act + var context = await service.PrepareUpdateContext(title, input, mustReprocess: false, CancellationToken.None); + + // Assert + context.Should().NotBeNull(); + context.PeopleToRemove.Should().HaveCount(1); + } + + #endregion + private TitleUpdateHelpService GetService(Resources resources) { return new TitleUpdateHelpService( @@ -714,7 +1236,8 @@ private Resources GetResources() TitleTitleCategoryRepository = GetRepository<TitleTitleCategory>(), TitleCategoryRepository = GetRepository<TitleCategory>(), WalletRepository = GetRepository<Wallet>(), - TitlePersonsRepository = GetRepository<TitlePerson>() + TitlePersonsRepository = GetRepository<TitlePerson>(), + PersonRepository = GetRepository<Person>() }; } @@ -725,5 +1248,6 @@ private class Resources public IRepository<TitleCategory> TitleCategoryRepository { get; set; } public IRepository<TitlePerson> TitlePersonsRepository { get; set; } public IRepository<Wallet> WalletRepository { get; set; } + public IRepository<Person> PersonRepository { get; set; } } } \ No newline at end of file diff --git a/Fin.Test/Titles/Validations/TitleInputPeopleValidationTest.cs b/Fin.Test/Titles/Validations/TitleInputPeopleValidationTest.cs new file mode 100644 index 0000000..9871b18 --- /dev/null +++ b/Fin.Test/Titles/Validations/TitleInputPeopleValidationTest.cs @@ -0,0 +1,492 @@ +using Fin.Application.Titles.Enums; +using Fin.Application.Titles.Validations.UpdateOrCrestes; +using Fin.Domain.People.Dtos; +using Fin.Domain.People.Entities; +using Fin.Domain.Titles.Dtos; +using Fin.Domain.Titles.Entities; +using Fin.Domain.Titles.Enums; +using Fin.Infrastructure.Database.Repositories; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; + +namespace Fin.Test.Titles.Validations; + +public class TitleInputPeopleValidationTest : TestUtils.BaseTestWithContext +{ + private TitleInput GetValidInput() => new() + { + Description = TestUtils.Strings[0], + Value = TestUtils.Decimals[0], + Date = TestUtils.UtcDateTimes[0], + WalletId = TestUtils.Guids[0], + Type = TitleType.Income, + TitleCategoriesIds = new List<Guid>(), + TitlePeople = new List<TitlePersonInput>() + }; + + private async Task<Person> CreatePersonInDatabase( + Resources resources, + Guid id, + string name, + bool inactivated = false) + { + var person = new Person(new PersonInput + { + Name = name + }); + + person.Id = id; + + if (inactivated != person.Inactivated) + person.ToggleInactivated(); + + await resources.PersonRepository.AddAsync(person, autoSave: true); + return person; + } + + private async Task<Title> CreateTitleInDatabase( + Resources resources, + Guid id, + List<Person> people) + { + var input = new TitleInput + { + Value = TestUtils.Decimals[0], + Date = TestUtils.UtcDateTimes[0], + Type = TitleType.Income, + Description = TestUtils.Strings[0], + TitleCategoriesIds = new List<Guid>(), + TitlePeople = people.Select(p => new TitlePersonInput + { + PersonId = p.Id, + Percentage = 50m + }).ToList() + }; + + var title = new Title(input, 0m); + title.Wallet = TestUtils.Wallets[0]; + title.Id = id; + await resources.TitleRepository.AddAsync(title, autoSave: true); + + // Eager load TitlePeople for verification + return await resources.TitleRepository + .Include(t => t.TitlePeople) + .FirstAsync(t => t.Id == id); + } + + #region ValidateAsync + + [Fact] + public async Task ValidateAsync_ShouldReturnSuccess_WhenPeopleAreValid() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1 = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1]); + var person2 = await CreatePersonInDatabase(resources, TestUtils.Guids[2], TestUtils.Strings[2]); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1.Id, Percentage = 60m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person2.Id, Percentage = 40m }); + + // Act + var result = await service.ValidateAsync(input); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + result.Code.Should().BeNull(); + result.Data.Should().BeNull(); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnSuccess_WhenNoPeople() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var input = GetValidInput(); + // TitlePeople is empty + + // Act + var result = await service.ValidateAsync(input); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnSuccess_WhenSplitIs100Percent() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1 = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1]); + var person2 = await CreatePersonInDatabase(resources, TestUtils.Guids[2], TestUtils.Strings[2]); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1.Id, Percentage = 50m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person2.Id, Percentage = 50m }); + + // Act + var result = await service.ValidateAsync(input); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnFailure_WhenSomePeopleNotFound() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1 = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1]); + var notFoundId = TestUtils.Guids[9]; + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1.Id, Percentage = 50m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = notFoundId, Percentage = 50m }); + + // Act + var result = await service.ValidateAsync(input); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.Code.Should().Be(TitleCreateOrUpdateErrorCode.SomePeopleNotFound); + result.Data.Should().HaveCount(1); + result.Data.Should().BeEquivalentTo(new List<Guid> { notFoundId }); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnFailure_WhenMultiplePeopleNotFound() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var notFoundId1 = TestUtils.Guids[8]; + var notFoundId2 = TestUtils.Guids[9]; + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = notFoundId1, Percentage = 50m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = notFoundId2, Percentage = 50m }); + + // Act + var result = await service.ValidateAsync(input); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.Code.Should().Be(TitleCreateOrUpdateErrorCode.SomePeopleNotFound); + result.Data.Should().HaveCount(2); + result.Data.Should().BeEquivalentTo(new List<Guid> { notFoundId1, notFoundId2 }); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnFailure_WhenSomePeopleInactiveOnCreate() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1 = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1]); + var person2_Inactive = await CreatePersonInDatabase(resources, TestUtils.Guids[2], TestUtils.Strings[2], inactivated: true); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1.Id, Percentage = 50m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person2_Inactive.Id, Percentage = 50m }); + + // Act + var result = await service.ValidateAsync(input, editingId: null); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.Code.Should().Be(TitleCreateOrUpdateErrorCode.SomePeopleInactive); + result.Data.Should().HaveCount(1); + result.Data.Should().BeEquivalentTo(new List<Guid> { person2_Inactive.Id }); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnSuccess_WhenPersonIsInactiveButAlreadyOnTitle() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1_Inactive = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1], inactivated: true); + var title = await CreateTitleInDatabase(resources, TestUtils.Guids[5], new List<Person> { person1_Inactive }); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1_Inactive.Id, Percentage = 100m }); + + // Act + var result = await service.ValidateAsync(input, editingId: title.Id); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnFailure_WhenAddingNewInactivePersonOnUpdate() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1_Active = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1]); + var person2_Inactive = await CreatePersonInDatabase(resources, TestUtils.Guids[2], TestUtils.Strings[2], inactivated: true); + var title = await CreateTitleInDatabase(resources, TestUtils.Guids[5], new List<Person> { person1_Active }); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1_Active.Id, Percentage = 50m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person2_Inactive.Id, Percentage = 50m }); // Adding a new inactive person + + // Act + var result = await service.ValidateAsync(input, editingId: title.Id); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.Code.Should().Be(TitleCreateOrUpdateErrorCode.SomePeopleInactive); + result.Data.Should().HaveCount(1); + result.Data.Should().BeEquivalentTo(new List<Guid> { person2_Inactive.Id }); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnFailure_WhenSplitExceeds100Percent() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1 = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1]); + var person2 = await CreatePersonInDatabase(resources, TestUtils.Guids[2], TestUtils.Strings[2]); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1.Id, Percentage = 60m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person2.Id, Percentage = 50m }); // Total: 110% + + // Act + var result = await service.ValidateAsync(input); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.Code.Should().Be(TitleCreateOrUpdateErrorCode.PeopleSplitRange); + result.Data.Should().BeNull(); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnFailure_WhenSplitIsNegative() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1 = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1]); + var person2 = await CreatePersonInDatabase(resources, TestUtils.Guids[2], TestUtils.Strings[2]); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1.Id, Percentage = -60m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person2.Id, Percentage = 50m }); // Total: 10% (but has negative) + + // Act + var result = await service.ValidateAsync(input); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.Code.Should().Be(TitleCreateOrUpdateErrorCode.PeopleSplitRange); + result.Data.Should().BeNull(); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnFailure_WhenSplitIsLessThan0Dot01() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1 = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1]); + var person2 = await CreatePersonInDatabase(resources, TestUtils.Guids[2], TestUtils.Strings[2]); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1.Id, Percentage = 0m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person2.Id, Percentage = 0.009m }); // Total: 10% (but has negative) + + // Act + var result = await service.ValidateAsync(input); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.Code.Should().Be(TitleCreateOrUpdateErrorCode.PeopleSplitRange); + result.Data.Should().BeNull(); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnSuccess_WhenSplitIs0Dot01() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1 = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1]); + var person2 = await CreatePersonInDatabase(resources, TestUtils.Guids[2], TestUtils.Strings[2]); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1.Id, Percentage = 0m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person2.Id, Percentage = 0.01m }); // Total: 10% (but has negative) + + // Act + var result = await service.ValidateAsync(input); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + result.Code.Should().BeNull(); + result.Data.Should().BeNull(); + } + + + [Fact] + public async Task ValidateAsync_ShouldReturnSuccess_WhenSplitIsLessThan100Percent() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1 = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1]); + var person2 = await CreatePersonInDatabase(resources, TestUtils.Guids[2], TestUtils.Strings[2]); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1.Id, Percentage = 30m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person2.Id, Percentage = 40m }); // Total: 70% + + // Act + var result = await service.ValidateAsync(input); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnSuccess_WhenOnlyOnePersonWith100Percent() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1 = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1]); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1.Id, Percentage = 100m }); + + // Act + var result = await service.ValidateAsync(input); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnFailure_WhenMultipleInactivePeople() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1_Inactive = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1], inactivated: true); + var person2_Inactive = await CreatePersonInDatabase(resources, TestUtils.Guids[2], TestUtils.Strings[2], inactivated: true); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1_Inactive.Id, Percentage = 50m }); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person2_Inactive.Id, Percentage = 50m }); + + // Act + var result = await service.ValidateAsync(input, editingId: null); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeFalse(); + result.Code.Should().Be(TitleCreateOrUpdateErrorCode.SomePeopleInactive); + result.Data.Should().HaveCount(2); + result.Data.Should().BeEquivalentTo(new List<Guid> { person1_Inactive.Id, person2_Inactive.Id }); + } + + [Fact] + public async Task ValidateAsync_ShouldReturnSuccess_WhenKeepingInactivePeopleAndAddingActiveOnes() + { + // Arrange + await ConfigureLoggedAmbientAsync(); + var resources = GetResources(); + var service = GetService(resources); + + var person1_Inactive = await CreatePersonInDatabase(resources, TestUtils.Guids[1], TestUtils.Strings[1], inactivated: true); + var person2_Active = await CreatePersonInDatabase(resources, TestUtils.Guids[2], TestUtils.Strings[2]); + var title = await CreateTitleInDatabase(resources, TestUtils.Guids[5], new List<Person> { person1_Inactive }); + + var input = GetValidInput(); + input.TitlePeople.Add(new TitlePersonInput { PersonId = person1_Inactive.Id, Percentage = 50m }); // Already on title + input.TitlePeople.Add(new TitlePersonInput { PersonId = person2_Active.Id, Percentage = 50m }); // New active person + + // Act + var result = await service.ValidateAsync(input, editingId: title.Id); + + // Assert + result.Should().NotBeNull(); + result.Success.Should().BeTrue(); + } + + #endregion + + private TitleInputPeopleValidation GetService(Resources resources) + { + return new TitleInputPeopleValidation( + resources.TitleRepository, + resources.PersonRepository + ); + } + + private Resources GetResources() + { + return new Resources + { + TitleRepository = GetRepository<Title>(), + PersonRepository = GetRepository<Person>() + }; + } + + private class Resources + { + public IRepository<Title> TitleRepository { get; set; } + public IRepository<Person> PersonRepository { get; set; } + } +} \ No newline at end of file From 99b05d87146b8ebff6df00fddddf8722bf90ee2c Mon Sep 17 00:00:00 2001 From: RafaelKC <rafaelkaua97@gmail.com> Date: Mon, 24 Nov 2025 22:46:50 -0300 Subject: [PATCH 14/15] FIN-31 adjusted --- Fin.Test/Titles/TitleUpdateHelpServiceTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs b/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs index e8cd005..ea99dad 100644 --- a/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs +++ b/Fin.Test/Titles/TitleUpdateHelpServiceTest.cs @@ -1213,7 +1213,7 @@ public async Task PrepareUpdateContext_ShouldReturnEmptyPeopleList_WhenNoPeopleR // Assert context.Should().NotBeNull(); - context.PeopleToRemove.Should().HaveCount(1); + context.PeopleToRemove.Should().HaveCount(0); } #endregion From b9ceec999f16751f2c03e1db0ce956b2853c8494 Mon Sep 17 00:00:00 2001 From: RafaelKC <rafaelkaua97@gmail.com> Date: Mon, 24 Nov 2025 23:04:44 -0300 Subject: [PATCH 15/15] FIN-31 adjusted --- Fin.Test/Titles/TitleTest.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Fin.Test/Titles/TitleTest.cs b/Fin.Test/Titles/TitleTest.cs index 3f8e739..f096172 100644 --- a/Fin.Test/Titles/TitleTest.cs +++ b/Fin.Test/Titles/TitleTest.cs @@ -457,8 +457,7 @@ public void SyncPeopleAndReturnToRemove_ShouldAddNewPeople() title.TitlePeople.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[2]); title.TitlePeople.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[3]); - result.Should().HaveCount(1); - result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[1]); + result.Should().HaveCount(0); } [Fact] @@ -494,8 +493,7 @@ public void SyncPeopleAndReturnToRemove_ShouldRemovePeople() title.TitlePeople.Should().HaveCount(1); title.TitlePeople.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[1]); - result.Should().HaveCount(3); - result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[1]); + result.Should().HaveCount(2); result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[2]); result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[3]); } @@ -536,7 +534,7 @@ public void SyncPeopleAndReturnToRemove_ShouldUpdateExistingPersonPercentage() person1.Percentage.Should().Be(70m); person2.Percentage.Should().Be(30m); - result.Should().HaveCount(2); + result.Should().HaveCount(0); } [Fact] @@ -608,8 +606,7 @@ public void SyncPeopleAndReturnToRemove_ShouldAddAndRemoveSimultaneously() title.TitlePeople.First(x => x.PersonId == TestUtils.Guids[1]).Percentage.Should().Be(60m); title.TitlePeople.First(x => x.PersonId == TestUtils.Guids[3]).Percentage.Should().Be(40m); - result.Should().HaveCount(2); - result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[1]); + result.Should().HaveCount(1); result.Select(x => x.PersonId).Should().Contain(TestUtils.Guids[2]); }