using foodmarket.Domain.Money; using Microsoft.EntityFrameworkCore; namespace foodmarket.Infrastructure.Persistence.Configurations; /// Схема таблиц модуля «Деньги». Названия колонок/таблиц /// подчинены общему стилю (snake_case через ToTable + EF-camel для свойств); /// суммовые — decimal(18,4); концуррентный токен — Postgres xmin. public static class MoneyConfigurations { public static void ConfigureMoney(this ModelBuilder b) { b.Entity(e => { e.ToTable("money_accounts"); e.UseXminAsConcurrencyToken(); e.Ignore(x => x.Xmin); e.Property(x => x.Name).HasMaxLength(200).IsRequired(); e.Property(x => x.BankName).HasMaxLength(200); e.Property(x => x.AccountNumber).HasMaxLength(100); e.Property(x => x.OpeningBalance).HasPrecision(18, 4); e.HasOne(x => x.Store).WithMany().HasForeignKey(x => x.StoreId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.Currency).WithMany().HasForeignKey(x => x.CurrencyId).OnDelete(DeleteBehavior.Restrict); e.HasIndex(x => new { x.OrganizationId, x.Name }); e.HasIndex(x => new { x.OrganizationId, x.Kind, x.IsActive }); // IsDefault — на уровне контроллера проверяем «только один per Kind»: // составной уникальный индекс с фильтром IsDefault=true. e.HasIndex(x => new { x.OrganizationId, x.Kind }) .IsUnique() .HasFilter("\"IsDefault\" = true"); }); b.Entity(e => { e.ToTable("expense_items"); e.UseXminAsConcurrencyToken(); e.Ignore(x => x.Xmin); e.Property(x => x.Name).HasMaxLength(200).IsRequired(); e.Property(x => x.Code).HasMaxLength(50); e.Property(x => x.Notes).HasMaxLength(1000); e.HasIndex(x => new { x.OrganizationId, x.Name }).IsUnique(); e.HasIndex(x => new { x.OrganizationId, x.Code }) .IsUnique() .HasFilter("\"Code\" IS NOT NULL"); }); b.Entity(e => { e.ToTable("money_transactions"); e.UseXminAsConcurrencyToken(); e.Ignore(x => x.Xmin); e.Property(x => x.Number).HasMaxLength(50).IsRequired(); e.Property(x => x.Amount).HasPrecision(18, 4); e.Property(x => x.Purpose).HasMaxLength(500); e.Property(x => x.Notes).HasMaxLength(1000); e.HasOne(x => x.Account).WithMany().HasForeignKey(x => x.AccountId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.Counterparty).WithMany().HasForeignKey(x => x.CounterpartyId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.ExpenseItem).WithMany().HasForeignKey(x => x.ExpenseItemId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.Currency).WithMany().HasForeignKey(x => x.CurrencyId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.LinkedTransaction).WithMany() .HasForeignKey(x => x.LinkedTransactionId).OnDelete(DeleteBehavior.Restrict); e.HasMany(x => x.Links).WithOne(l => l.Transaction) .HasForeignKey(l => l.TransactionId).OnDelete(DeleteBehavior.Cascade); e.HasIndex(x => new { x.OrganizationId, x.Number }).IsUnique(); e.HasIndex(x => new { x.OrganizationId, x.Date }); e.HasIndex(x => new { x.OrganizationId, x.Kind, x.Status }); e.HasIndex(x => new { x.OrganizationId, x.AccountId, x.Date }); e.HasIndex(x => new { x.OrganizationId, x.CounterpartyId }) .HasFilter("\"CounterpartyId\" IS NOT NULL"); }); b.Entity(e => { e.ToTable("money_transaction_links"); e.Property(x => x.LinkedSum).HasPrecision(18, 4); e.HasOne(x => x.Supply).WithMany().HasForeignKey(x => x.SupplyId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.SupplierReturn).WithMany().HasForeignKey(x => x.SupplierReturnId).OnDelete(DeleteBehavior.Restrict); // CHECK: ровно одна целевая ссылка должна быть заполнена. e.ToTable(t => t.HasCheckConstraint( "ck_money_link_single_target", "((\"SupplyId\" IS NOT NULL)::int + (\"SupplierReturnId\" IS NOT NULL)::int) = 1")); e.HasIndex(x => new { x.OrganizationId, x.SupplyId }) .HasFilter("\"SupplyId\" IS NOT NULL"); e.HasIndex(x => new { x.OrganizationId, x.SupplierReturnId }) .HasFilter("\"SupplierReturnId\" IS NOT NULL"); e.HasIndex(x => x.TransactionId); }); b.Entity(e => { e.ToTable("cash_shifts"); e.UseXminAsConcurrencyToken(); e.Ignore(x => x.Xmin); e.Property(x => x.Number).HasMaxLength(50).IsRequired(); e.Property(x => x.OpeningFloat).HasPrecision(18, 4); e.Property(x => x.SalesCash).HasPrecision(18, 4); e.Property(x => x.SalesCard).HasPrecision(18, 4); e.Property(x => x.ReturnsCash).HasPrecision(18, 4); e.Property(x => x.ReturnsCard).HasPrecision(18, 4); e.Property(x => x.DrawerIn).HasPrecision(18, 4); e.Property(x => x.DrawerOut).HasPrecision(18, 4); e.Property(x => x.ExpectedCash).HasPrecision(18, 4); e.Property(x => x.ActualCash).HasPrecision(18, 4); e.Property(x => x.CashDifference).HasPrecision(18, 4); e.Property(x => x.Notes).HasMaxLength(1000); e.HasOne(x => x.RetailPoint).WithMany().HasForeignKey(x => x.RetailPointId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.CashAccount).WithMany().HasForeignKey(x => x.CashAccountId).OnDelete(DeleteBehavior.Restrict); e.HasOne(x => x.AcquiringAccount).WithMany().HasForeignKey(x => x.AcquiringAccountId).OnDelete(DeleteBehavior.Restrict); e.HasMany(x => x.DrawerOperations).WithOne(d => d.CashShift) .HasForeignKey(d => d.CashShiftId).OnDelete(DeleteBehavior.Cascade); e.HasIndex(x => new { x.OrganizationId, x.Number }).IsUnique(); e.HasIndex(x => new { x.OrganizationId, x.OpenedAt }); e.HasIndex(x => new { x.OrganizationId, x.RetailPointId, x.Status }); // Только одна открытая смена per RetailPoint. e.HasIndex(x => new { x.OrganizationId, x.RetailPointId }) .IsUnique() .HasFilter("\"Status\" = 1"); }); b.Entity(e => { e.ToTable("cash_drawer_operations"); e.UseXminAsConcurrencyToken(); e.Ignore(x => x.Xmin); e.Property(x => x.Amount).HasPrecision(18, 4); e.Property(x => x.Reason).HasMaxLength(500).IsRequired(); e.HasIndex(x => new { x.OrganizationId, x.CashShiftId }); e.HasIndex(x => new { x.OrganizationId, x.At }); }); b.Entity(e => { e.ToTable("counterparty_adjustments"); e.UseXminAsConcurrencyToken(); e.Ignore(x => x.Xmin); e.Property(x => x.Number).HasMaxLength(50).IsRequired(); e.Property(x => x.Amount).HasPrecision(18, 4); e.Property(x => x.Notes).HasMaxLength(1000); e.HasOne(x => x.Counterparty).WithMany().HasForeignKey(x => x.CounterpartyId).OnDelete(DeleteBehavior.Restrict); e.HasIndex(x => new { x.OrganizationId, x.Number }).IsUnique(); e.HasIndex(x => new { x.OrganizationId, x.Date }); e.HasIndex(x => new { x.OrganizationId, x.CounterpartyId, x.Status }); }); // Расширения существующих таблиц: b.Entity(e => { e.Property(x => x.PayedSum).HasPrecision(18, 4); e.HasIndex(x => new { x.OrganizationId, x.SupplierId, x.Status, x.PayedSum }); }); b.Entity(e => { e.Property(x => x.PayedSum).HasPrecision(18, 4); }); b.Entity(e => { e.HasIndex(x => new { x.OrganizationId, x.CashShiftId }) .HasFilter("\"CashShiftId\" IS NOT NULL"); }); } }