Stage deploy crashed in CrashLoopBackoff because the production container
landed on an empty fresh Postgres, then OpenIddictClientSeeder hit
"relation public.OpenIddictApplications does not exist". The Migrate()
call was guarded by IsDevelopment() so prod never bootstrapped.
Migrations are idempotent — running them every startup is the standard
pattern for SaaS containers (no separate migrate-then-app step needed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Domain:
- foodmarket.Domain.Inventory.Stock — materialized aggregate per (Product, Store)
with Quantity, ReservedQuantity, computed Available. Unique index on tenant+
product+store.
- foodmarket.Domain.Inventory.StockMovement — append-only journal with signed
quantity, optional UnitCost, MovementType enum (Initial, Supply, RetailSale,
WholesaleSale, CustomerReturn, SupplierReturn, TransferOut, TransferIn,
WriteOff, Enter, InventoryAdjustment), document linkage (type, id, number),
OccurredAt, CreatedBy, Notes.
Application:
- IStockService.ApplyMovementAsync draft — appends movement row + upserts
materialized Stock row in the same unit of work. Callers control SaveChanges
so a posting doc can bundle all lines atomically.
Infrastructure:
- StockService implementation over AppDbContext.
- InventoryConfigurations EF mapping (precision 18,4 on quantities/costs;
indexes for product+time, store+time, document lookup).
- Migration Phase2a_Stock applied to dev DB (tables stocks, stock_movements).
API (GET, read-only for now):
- /api/inventory/stock — filter by store, product, includeZero; joins product +
unit + store names; server-side pagination.
- /api/inventory/movements — journal filtered by store/product/date range;
movement type as string enum for UI labels.
- Both [Authorize] (any authenticated user).
MoySklad:
- MsCounterparty DTO (name, legalTitle, inn, kpp, companyType, tags...).
- MoySkladClient.StreamCounterpartiesAsync — paginated like products.
- MoySkladImportService.ImportCounterpartiesAsync — maps tags → Kind (supplier /
customer / both), companyType → LegalEntity/Individual; dedup by Name;
defensive trim on all string fields; per-item try/catch; batches of 500.
- /api/admin/moysklad/import-counterparties endpoint (Admin policy).
Web:
- /inventory/stock list page (store filter, include-zero toggle, search; shows
quantity/reserved/available with red-on-negative, grey-on-zero accents).
- /inventory/movements list page (store filter; colored quantity +/-, Russian
labels for each movement type).
- MoySklad import page restructured: single token test + two import buttons
(Товары, Контрагенты) + reusable ImportResult panel that handles both.
- Sidebar: new "Остатки" group with Остатки + Движения; icons Boxes + History.
Uses the ListPageShell pattern introduced in d3aa13d — sticky top bar, sticky
table header, only the body scrolls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User is importing the real catalog from MoySklad — the placeholder KZ-market
demo products I seeded would just pollute the results. Nuked via:
TRUNCATE product_prices, product_barcodes, products, product_groups,
counterparties CASCADE;
DemoCatalogSeeder stays in the source tree, commented out in Program.cs —
anyone running without MoySklad access can re-enable it by uncommenting
one line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues surfaced after the previous gzip-removal:
1. MoySklad's nginx edge returned 415 on some requests without a User-Agent.
Send a friendly UA string (food-market/0.1 + repo URL).
2. Previous fix dropped gzip support entirely; re-enable it properly by
configuring AutomaticDecompression on the typed HttpClient's primary
handler via AddHttpClient.ConfigurePrimaryHttpMessageHandler. Now the
response body is transparently decompressed before the JSON deserializer
sees it — no more 0x1F errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ASP.NET Core's [Authorize(Roles=...)] relies on ClaimsIdentity.RoleClaimType to
match, which may not be wired to "role" in the OpenIddict validation handler's
identity (depending on middleware order with AddIdentity). Tokens clearly carry
"role": "Admin" but IsInRole("Admin") returns false.
- Register AddAuthorization policy "AdminAccess" that checks the `role` claim
explicitly (c.Type == Claims.Role && Value in {Admin, SuperAdmin}). Works
regardless of how ClaimsIdentity was constructed.
- MoySkladImportController now uses [Authorize(Policy = "AdminAccess")].
- Add /api/_debug/whoami that echoes authType, roleClaimType, claims, and
IsInRole result — makes next auth issue trivial to diagnose.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of the 404 on /api/admin/moysklad/test (and /api/me):
- AddIdentity<> sets DefaultChallengeScheme = IdentityConstants.ApplicationScheme
(cookies), so unauthorized API calls got 302 → /Account/Login → 404 instead of 401.
- Ephemeral OpenIddict keys (AddEphemeralSigningKey) regenerated on every API
restart, silently invalidating any JWT already stored in the browser.
Fixes:
- Explicitly set DefaultScheme / DefaultAuthenticateScheme / DefaultChallengeScheme
to OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme so [Authorize]
challenges now return 401 (axios interceptor can react + retry or redirect).
- Replace ephemeral RSA keys with a persistent dev RSA key stored in
src/food-market.api/App_Data/openiddict-dev-key.xml (gitignored). Generated on
first run, reused on subsequent starts. Dev tokens now survive API restarts.
Production must register proper X509 certificates via configuration.
- .gitignore: add App_Data/, *.pem, openiddict-dev-key.xml patterns.
- Web axios: on hard 401 with failed refresh, redirect to /login rather than
leaving the user stuck on a protected screen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Infrastructure (foodmarket.Infrastructure.Integrations.MoySklad):
- MoySkladDtos: minimal shapes of products, folders, uom, prices, barcodes from JSON-API 1.2
- MoySkladClient: HttpClient wrapper with Bearer auth per call
- WhoAmIAsync (GET entity/organization) for connection test
- StreamProductsAsync (paginated 1000/page, IAsyncEnumerable)
- GetAllFoldersAsync (all product folders in one go)
- MoySkladImportService: orchestrates the full import
- Creates missing product folders with Path preserved
- Maps MoySklad VAT percent → local VatRate (fallback to default)
- Maps barcodes: ean13/ean8/code128/gtin/upca/upce → our BarcodeType enum
- Extracts retail price from salePrices (prefers "Розничная"), divides kopeck→major
- Extracts buyPrice → PurchasePrice
- Skips existing products by article OR primary barcode (unless overwrite flag set)
- Batch SaveChanges every 500 items to keep EF tracker light
- Returns counts + per-item error list
API: POST /api/admin/moysklad/test — returns org name if token valid
API: POST /api/admin/moysklad/import-products { token, overwriteExisting }
— Authorize(Roles = "Admin,SuperAdmin")
Web: /admin/import/moysklad page
- Amber notice: token is not persisted (request-scope only), how to create
a service token in moysklad.ru with read-only rights
- Test connection button + result banner
- Import button with "overwrite existing" checkbox
- Result panel with 4 counters + collapsible error list
Sidebar adds "Импорт" section with MoySklad link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Starter experience so the system is usable immediately after git clone → migrate → run.
DemoCatalogSeeder (Development only, runs once — skips if tenant has any products):
- 8 product groups: Напитки (Безалкогольные, Алкогольные), Молочка, Хлеб, Кондитерские,
Бакалея, Снеки — hierarchical Path computed
- 2 demo suppliers: ТОО «Продтрейд» (legal entity, KZ BIN, bank details), ИП Иванов (individual)
- 35 realistic KZ-market products with:
- Demo barcodes in 2xxx internal range (won't collide with real products)
- Retail price + purchase price at 72% of retail
- Country of origin (KZ / RU)
- Хлеб marked as 0% VAT (socially important goods in KZ)
- Сыр «Российский» marked as весовой
- Articles in kebab-case: DR-SOD-001, DAI-MLK-002, SW-CHO-001 etc.
Product form (full page /catalog/products/new and /:id, not modal):
- 5 sections: Основное / Классификация / Остатки и закупка / Цены / Штрихкоды
- Dropdowns for unit, VAT, group, country, supplier, currency via useLookups hooks
- Defaults pre-filled for new product (default VAT, base unit, KZT)
- Prices table: add/remove rows, pick price type + amount + currency
- Barcodes table: EAN-13/8/CODE128/UPC options, "primary" enforces single
- Server-side atomic save (existing Prices+Barcodes replaced on PUT)
Products page: row click → edit page, Add button → new page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>