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
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Решение владельца 08.10: продавать/списывать в минус РАЗРЕШЕНО, но с постоянным предупреждением и требованием провести инвентаризацию. - Снята блокировка «недостаточно остатка» (409 oversell): розница, касса (POS-синк), опт-отгрузка, списание, перемещение, возврат поставщику, отмена приёмки. Деньги (минус счёта) — по-прежнему запрещены (другое правило). - NegativeStockAlert (сущность + миграция Phase39): создаётся/обновляется при уходе остатка < 0 (единая точка — StockService), авто-закрытие при возврате ≥ 0. Частичный уникальный индекс: один открытый алерт на (org, store, product). Причина: тип/номер документа/автор/время. - API /api/negative-stock: список (с подсказкой «возможно, не проведена приёмка П-…»), /count (для баннеров), /recount (черновик инвентаризации по минусам склада). Отчёт «Остатки» — фильтр negativeOnly. - Веб: постоянный баннер «N товаров в минусе — нужна инвентаризация» (сворачивается, крестиком не закрыть) + «Пересчитать эти товары»; в остатках — красная пометка и фильтр «Только минусы». - Тесты: integration 2/2 (минус разрешён → алерт → recount → авто-закрытие; фильтр); full-chain шаг 9/13 переписаны (oversell/отмена приёмки → проведено + предупреждение). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
2.6 KiB
C#
76 lines
2.6 KiB
C#
using foodmarket.Application.Common.Tenancy;
|
||
using foodmarket.Application.Inventory;
|
||
using foodmarket.Domain.Inventory;
|
||
using foodmarket.Infrastructure.Persistence;
|
||
using Microsoft.EntityFrameworkCore;
|
||
|
||
namespace foodmarket.Infrastructure.Inventory;
|
||
|
||
public class StockService : IStockService
|
||
{
|
||
private readonly AppDbContext _db;
|
||
private readonly ITenantContext _tenant;
|
||
private readonly INegativeStockAlertService _negAlerts;
|
||
|
||
public StockService(AppDbContext db, ITenantContext tenant, INegativeStockAlertService negAlerts)
|
||
{
|
||
_db = db;
|
||
_tenant = tenant;
|
||
_negAlerts = negAlerts;
|
||
}
|
||
|
||
public async Task<decimal> ApplyMovementAsync(StockMovementDraft d, CancellationToken ct = default)
|
||
{
|
||
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("Tenant not set.");
|
||
|
||
_db.StockMovements.Add(new StockMovement
|
||
{
|
||
OrganizationId = orgId,
|
||
ProductId = d.ProductId,
|
||
StoreId = d.StoreId,
|
||
Quantity = d.Quantity,
|
||
UnitCost = d.UnitCost,
|
||
Type = d.Type,
|
||
DocumentType = d.DocumentType,
|
||
DocumentId = d.DocumentId,
|
||
DocumentNumber = d.DocumentNumber,
|
||
OccurredAt = d.OccurredAt ?? DateTime.UtcNow,
|
||
CreatedByUserId = d.CreatedByUserId,
|
||
Notes = d.Notes,
|
||
});
|
||
|
||
var stock = await _db.Stocks.FirstOrDefaultAsync(
|
||
s => s.ProductId == d.ProductId && s.StoreId == d.StoreId, ct);
|
||
|
||
if (stock is null)
|
||
{
|
||
stock = new Stock
|
||
{
|
||
OrganizationId = orgId,
|
||
ProductId = d.ProductId,
|
||
StoreId = d.StoreId,
|
||
Quantity = d.Quantity,
|
||
};
|
||
_db.Stocks.Add(stock);
|
||
}
|
||
else
|
||
{
|
||
stock.Quantity += d.Quantity;
|
||
}
|
||
|
||
// Предупреждение об отрицательном остатке: единая точка детекции для всех
|
||
// документов. Остаток < 0 → открыть/обновить алерт; ≥ 0 → закрыть.
|
||
await _negAlerts.OnStockLevelAsync(d.StoreId, d.ProductId, stock.Quantity,
|
||
d.DocumentType, d.DocumentId, d.DocumentNumber, d.CreatedByUserId, d.OccurredAt, ct);
|
||
|
||
return stock.Quantity;
|
||
}
|
||
|
||
public async Task<decimal> ApplyMovementsAsync(IEnumerable<StockMovementDraft> drafts, CancellationToken ct = default)
|
||
{
|
||
var last = 0m;
|
||
foreach (var d in drafts) last = await ApplyMovementAsync(d, ct);
|
||
return last;
|
||
}
|
||
}
|