food-market/src/food-market.infrastructure/Persistence/Configurations/SalesConfigurations.cs
nns ea29ab63c7 feat(returns): возврат от покупателя (CustomerReturn) (P1-6)
Расширение RetailSale: IsReturn (bool) + ReferenceSaleId (Guid?, self-FK) +
RetailSaleLine.QtyReturned (агрегация для защиты от over-return).
Миграция Phase6e_RetailSaleReturns (IF NOT EXISTS, можно гонять на стейдже).

Контроллер api/sales/retail:
• Create принимает isReturn/referenceSaleId; для возврата с reference —
  валидация что reference существует, не сам возврат, и проведён.
• POST /{id}/create-return — создаёт Draft-возврат от проведённого чека,
  копируя строки с qty = (Quantity - QtyReturned).
• Post с IsReturn=true идёт через PostReturnAsync: проверяет лимит
  возврата по reference (Quantity - QtyReturned), создаёт StockMovement
  тип CustomerReturn с +Quantity, инкрементит QtyReturned на источнике.
• Unpost для возврата зеркально откатывает.
• Запрещён unpost исходного чека пока есть проведённые возвраты на него.

Web: кнопка «Создать возврат» на странице проведённой продажи. DTO
расширены полями isReturn/referenceSale*/qtyReturned.

Тесты: 3 интеграционных (полный цикл sale→create-return→post,
standalone-return без reference, блокировка unpost при активных возвратах).
Полный прогон: 27 зелёных интеграционных, 23 зелёных unit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:51:04 +05:00

52 lines
2.5 KiB
C#

using foodmarket.Domain.Sales;
using Microsoft.EntityFrameworkCore;
namespace foodmarket.Infrastructure.Persistence.Configurations;
public static class SalesConfigurations
{
public static void ConfigureSales(this ModelBuilder b)
{
b.Entity<RetailSale>(e =>
{
e.ToTable("retail_sales");
e.Property(x => x.Number).HasMaxLength(50).IsRequired();
e.Property(x => x.Notes).HasMaxLength(1000);
e.Property(x => x.Subtotal).HasPrecision(18, 4);
e.Property(x => x.DiscountTotal).HasPrecision(18, 4);
e.Property(x => x.Total).HasPrecision(18, 4);
e.Property(x => x.PaidCash).HasPrecision(18, 4);
e.Property(x => x.PaidCard).HasPrecision(18, 4);
e.HasOne(x => x.Store).WithMany().HasForeignKey(x => x.StoreId).OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.RetailPoint).WithMany().HasForeignKey(x => x.RetailPointId).OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.Customer).WithMany().HasForeignKey(x => x.CustomerId).OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.Currency).WithMany().HasForeignKey(x => x.CurrencyId).OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.ReferenceSale).WithMany().HasForeignKey(x => x.ReferenceSaleId).OnDelete(DeleteBehavior.Restrict);
e.HasMany(x => x.Lines).WithOne(l => l.RetailSale).HasForeignKey(l => l.RetailSaleId).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.Status });
e.HasIndex(x => new { x.OrganizationId, x.CashierUserId });
e.HasIndex(x => new { x.OrganizationId, x.IsReturn });
e.HasIndex(x => x.ReferenceSaleId);
});
b.Entity<RetailSaleLine>(e =>
{
e.ToTable("retail_sale_lines");
e.Property(x => x.Quantity).HasPrecision(18, 4);
e.Property(x => x.UnitPrice).HasPrecision(18, 4);
e.Property(x => x.Discount).HasPrecision(18, 4);
e.Property(x => x.LineTotal).HasPrecision(18, 4);
e.Property(x => x.VatPercent).HasPrecision(5, 2);
e.Property(x => x.QtyReturned).HasPrecision(18, 4);
e.HasOne(x => x.Product).WithMany().HasForeignKey(x => x.ProductId).OnDelete(DeleteBehavior.Restrict);
e.HasIndex(x => new { x.OrganizationId, x.ProductId });
});
}
}