Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Сверка с МойСкладом (docs/money-moysklad-reference.md) + домен/инфра/API: - Счета и кассы (MoneyAccount: Cash/Safe/Bank/Acquiring, начальный остаток, привязка к магазину, IsDefault/IsActive, инвариант CurrentBalance). - Денежные документы (MoneyTransaction: PaymentIn/Out, CashIn/Out, переводы парой TransferOut↔TransferIn; статьи расходов ExpenseItem; проведение/отмена в Serializable-транзакции через MoneyService). - Оплата поставщикам: Supply/SupplierReturn.PayedSum + MoneyTransactionLink, SuppliesController отдаёт PaymentStatus + фильтр paymentStatus. - Взаиморасчёты: CounterpartyAdjustment + MoneyReportsController.SupplierDebts. - Смены кассы: CashShift + CashDrawerOperation (open/close, expected↔actual, Z-отчёт), запрет розничной продажи без открытой смены в RetailSalesController. - Отчёт «Деньги»: balances/movement с проверкой инварианта остатков. Права: RolePermissions +MoneyView/MoneyEdit/CashShiftOperate; сидер раздаёт кассиру CashShiftOperate+MoneyView, кладовщику — нет; 5 системных статей расходов на новую орг. Миграция Phase33a (ручная, с [Migration]+[DbContext]). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
177 lines
8.6 KiB
C#
177 lines
8.6 KiB
C#
using foodmarket.Domain.Money;
|
||
using Microsoft.EntityFrameworkCore;
|
||
|
||
namespace foodmarket.Infrastructure.Persistence.Configurations;
|
||
|
||
/// <summary>Схема таблиц модуля «Деньги». Названия колонок/таблиц
|
||
/// подчинены общему стилю (snake_case через ToTable + EF-camel для свойств);
|
||
/// суммовые — <c>decimal(18,4)</c>; концуррентный токен — Postgres <c>xmin</c>.</summary>
|
||
public static class MoneyConfigurations
|
||
{
|
||
public static void ConfigureMoney(this ModelBuilder b)
|
||
{
|
||
b.Entity<MoneyAccount>(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<ExpenseItem>(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<MoneyTransaction>(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<MoneyTransactionLink>(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<CashShift>(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<CashDrawerOperation>(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<CounterpartyAdjustment>(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<foodmarket.Domain.Purchases.Supply>(e =>
|
||
{
|
||
e.Property(x => x.PayedSum).HasPrecision(18, 4);
|
||
e.HasIndex(x => new { x.OrganizationId, x.SupplierId, x.Status, x.PayedSum });
|
||
});
|
||
|
||
b.Entity<foodmarket.Domain.Purchases.SupplierReturn>(e =>
|
||
{
|
||
e.Property(x => x.PayedSum).HasPrecision(18, 4);
|
||
});
|
||
|
||
b.Entity<foodmarket.Domain.Sales.RetailSale>(e =>
|
||
{
|
||
e.HasIndex(x => new { x.OrganizationId, x.CashShiftId })
|
||
.HasFilter("\"CashShiftId\" IS NOT NULL");
|
||
});
|
||
}
|
||
}
|