food-market/src/food-market.infrastructure/Persistence/Migrations/20261009010000_Phase39_NegativeStockAlerts.cs
nns 9ba1edb236
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
feat(stock): разрешение продажи в минус + предупреждения об отрицательных остатках
Решение владельца 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>
2026-10-09 02:18:29 +05:00

59 lines
3.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using foodmarket.Infrastructure.Persistence;
#nullable disable
namespace foodmarket.Infrastructure.Persistence.Migrations
{
/// <summary>Phase39 — предупреждения об отрицательных остатках
/// (<c>negative_stock_alerts</c>). Создаётся/обновляется автоматически при уходе
/// остатка в минус, закрывается при возврате ≥ 0. Частичный уникальный индекс
/// гарантирует максимум один ОТКРЫТЫЙ алерт на (org, store, product). Идемпотентно.</summary>
[DbContext(typeof(AppDbContext))]
[Migration("20261009010000_Phase39_NegativeStockAlerts")]
public partial class Phase39_NegativeStockAlerts : Migration
{
protected override void Up(MigrationBuilder b)
{
b.Sql(@"
CREATE TABLE IF NOT EXISTS public.negative_stock_alerts (
""Id"" uuid PRIMARY KEY,
""OrganizationId"" uuid NOT NULL,
""StoreId"" uuid NOT NULL,
""ProductId"" uuid NOT NULL,
""Quantity"" numeric(18,4) NOT NULL,
""CauseDocType"" varchar(50),
""CauseDocId"" uuid,
""CauseDocNumber"" varchar(50),
""CauseUserId"" uuid,
""CauseAt"" timestamp with time zone,
""FirstDetectedAt"" timestamp with time zone NOT NULL,
""LastUpdatedAt"" timestamp with time zone NOT NULL,
""ResolvedAt"" timestamp with time zone,
""NotifiedAt"" timestamp with time zone,
""EscalatedAt"" timestamp with time zone,
""CreatedAt"" timestamp with time zone NOT NULL,
""UpdatedAt"" timestamp with time zone,
CONSTRAINT ""FK_negative_stock_alerts_stores_StoreId"" FOREIGN KEY (""StoreId"") REFERENCES public.stores(""Id"") ON DELETE CASCADE,
CONSTRAINT ""FK_negative_stock_alerts_products_ProductId"" FOREIGN KEY (""ProductId"") REFERENCES public.products(""Id"") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS ""IX_negative_stock_alerts_OrganizationId_ResolvedAt""
ON public.negative_stock_alerts (""OrganizationId"", ""ResolvedAt"");
CREATE INDEX IF NOT EXISTS ""IX_negative_stock_alerts_OrganizationId_StoreId_ProductId""
ON public.negative_stock_alerts (""OrganizationId"", ""StoreId"", ""ProductId"");
-- Максимум один ОТКРЫТЫЙ алерт на (org, store, product).
CREATE UNIQUE INDEX IF NOT EXISTS ""UX_negative_stock_alerts_open""
ON public.negative_stock_alerts (""OrganizationId"", ""StoreId"", ""ProductId"")
WHERE ""ResolvedAt"" IS NULL;
");
}
protected override void Down(MigrationBuilder b)
{
b.Sql(@"DROP TABLE IF EXISTS public.negative_stock_alerts;");
}
}
}