Расширение 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>
52 lines
2.5 KiB
C#
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 });
|
|
});
|
|
}
|
|
}
|