Commit graph

11 commits

Author SHA1 Message Date
nurdotnet 50e3676d71 phase2a: stock foundation (Stock + StockMovement) + MoySklad counterparty import
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>
2026-04-22 00:51:07 +05:00
nurdotnet c47826e015 fix(catalog): widen Article + Barcode.Code to 500 chars for real-world catalogs
Import against a live MoySklad account crashed with PostgreSQL 22001 after
loading 21500/~N products: Article column was varchar(100), but some MoySklad
items have longer internal codes, and Barcode.Code needed to grow for future
GS1 DataMatrix / Честный ЗНАК tracking codes (up to ~300 chars).

- EF config: Product.Article 100 → 500, ProductBarcode.Code 100 → 500.
- Migration Phase1e_WidenArticleBarcode (applied to dev DB).
- Defensive Trim() in the MoySklad importer for Name/Article/Barcode so even
  future schema drift won't take the whole import down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:15:00 +05:00
nurdotnet 22502c11fd fix(moysklad): accept fractional prices (decimal, not long) in DTOs
MoySklad returns minPrice.value and salePrices[*].value as fractional numbers
for some accounts/products (not always pure kopecks). Deserializing as long
failed with "out of bounds for an Int64" → 500 on import. Switch MsMoney.Value
and MsSalePrice.Value to decimal, which accepts both integer and decimal
representations. Division by 100m already happens when mapping to local
Product, so semantics are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:59:44 +05:00
nurdotnet cdf26d8719 fix(moysklad): add User-Agent header + enable HTTP auto-decompression
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>
2026-04-21 23:49:58 +05:00
nurdotnet 1ef337a0f6 fix(moysklad): drop Accept-Encoding: gzip to avoid JSON parse failure
HttpClient in DI isn't configured with AutomaticDecompression, so MoySklad
returned a gzip-compressed body that ReadFromJsonAsync choked on (0x1F is
the gzip magic byte). Cheapest correct fix is to not advertise gzip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:46:12 +05:00
nurdotnet 5d308a0538 fix(moysklad): set Accept header as raw string to bypass .NET normalization
The typed MediaTypeWithQualityHeaderValue API adds a space after the
semicolon ("application/json; charset=utf-8"), and MoySklad rejects anything
other than the exact literal "application/json;charset=utf-8" with error 1062.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:34:44 +05:00
nurdotnet 067f52cf43 fix(moysklad): exact Accept header value per MoySklad requirement (code 1062)
MoySklad rejects application/json without charset=utf-8 with error 1062
"Неверное значение заголовка 'Accept'". They require the exact value
"application/json;charset=utf-8".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:32:30 +05:00
nurdotnet 05553bdc3d fix(moysklad): trailing slash on BaseUrl so HttpClient keeps /1.2/ in path
Logs showed every outbound MoySklad call was hitting
  https://api.moysklad.ru/api/remap/entity/organization
instead of the intended
  https://api.moysklad.ru/api/remap/1.2/entity/organization

Cause: per RFC 3986 §5.3, when HttpClient resolves a relative URI against
a base URI whose path does not end with '/', the last segment of the base
path is discarded. So BaseAddress "…/api/remap/1.2" + relative "entity/…"
produced "…/api/remap/entity/…". MoySklad returned 503 and we translated
it into a useless "401 сессия истекла" for the user.

Fixes:
- Append trailing slash to BaseUrl.
- Surface the real upstream status + body: MoySkladApiResult<T> wrapper,
  and the controller now maps 401/403 → "invalid token", 502/503 →
  "MoySklad unavailable", anything else → "MoySklad returned {code}: {body}".
  No more lying-as-401.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 23:26:32 +05:00
nurdotnet 25f25f9171 phase1e: MoySklad import integration (admin-only, per-request token, no persistence)
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>
2026-04-21 21:07:58 +05:00
nurdotnet cb66684134 phase1a: catalog domain (countries, currencies, vat, units, counterparties, stores, retail points, products)
Domain (foodmarket.Domain.Catalog):
- Global references: Country (ISO-2), Currency (ISO-3 + symbol + minor unit)
- Tenant references: VatRate (Percent + IncludedInPrice + IsDefault), UnitOfMeasure (ОКЕИ code + DecimalPlaces)
- Counterparty: kind (Supplier/Customer/Both), type (Legal/Individual), BIN/IIN/TaxNumber, bank details
- Store + RetailPoint with fiscal placeholders
- ProductGroup: hierarchy via ParentId + denormalized Path
- PriceType (Розничная/Оптовая), Product (article, VAT, group, supplier, flags IsService/IsWeighed/IsAlcohol/IsMarked, min/max stock)
- ProductPrice (composite unique product+priceType), ProductBarcode (EAN13/EAN8/CODE128/UPC), ProductImage

Infrastructure:
- CatalogConfigurations with fluent API (indexes, precision 18/4 for money, FK with Restrict)
- 13 new DbSets on AppDbContext + builder.ConfigureCatalog()
- Migration Phase1Catalog — adds countries, currencies, vat_rates, units_of_measure, counterparties, stores, retail_points, product_groups, price_types, products, product_prices, product_barcodes, product_images

Seeders:
- SystemReferenceSeeder (always): 12 countries (KZ, RU, CN, TR, …), 5 currencies (KZT primary, RUB, USD, EUR, CNY)
- DevDataSeeder extended: for Demo Market seeds VAT (0%, 12% default+included), units (шт/кг/л/м/уп), price types (Розничная default, Оптовая), main store, POS-1

Total DB schema: 26 tables.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:16:10 +05:00
nns fd2f5ae4f3 Phase 0: project scaffolding and end-to-end auth
- .NET 8 LTS solution with 7 projects (domain/application/infrastructure/api/shared/pos.core/pos[WPF])
- Central package management (Directory.Packages.props), .editorconfig, global.json pin to 8.0.417
- PostgreSQL 14 dev DB via existing brew service; food_market database created
- ASP.NET Identity + OpenIddict 5 (password + refresh token flows) with ephemeral dev keys
- EF Core 8 + Npgsql; multi-tenant query filter via reflection over ITenantEntity
- Initial migration: 13 tables (Identity + OpenIddict + organizations)
- AuthorizationController implements /connect/token; seeders create demo org + admin
- Protected /api/me endpoint returns current user + org claims
- React 19 + Vite 8 + Tailwind v4 SPA with TanStack Query, React Router 7
- Login flow with dev-admin placeholder, bearer interceptor + refresh token fallback
- docs/architecture.md, CLAUDE.md, README.md

Verified end-to-end: health check, password grant issues JWT with org_id,
web app builds successfully (310 kB gzipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:59:13 +05:00