Commit graph

32 commits

Author SHA1 Message Date
nns 1ee4b84e53 feat(barcode-uniqueness): pre-check на Create/Update + warnings импорта + admin endpoint
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 28s
CI / Web (React + Vite) (push) Successful in 25s
Docker Images / API image (push) Successful in 39s
Docker Images / Web image (push) Successful in 5s
Docker Images / Deploy stage (push) Successful in 18s
Pre-check:
- ProductsController.FindBarcodeConflictAsync ищет штрихкоды,
  принадлежащие другим товарам организации; на Create/Update при
  конфликте возвращается 400 «Штрихкод 1234 уже используется
  товаром «Кока-кола 0.5л».» вместо 500 от unique index.

MoySklad-импорт:
- При попытке привязать уже занятый штрихкод — пишется warning
  «{товар}: штрихкод {код} уже занят, пропущен.» в errors[],
  товар остаётся, дубль не сохраняется.
- В конце импорта проходит финальный SELECT по дубликатам в БД
  (если есть исторические) — warnings типа «Внимание: штрихкод X
  привязан к нескольким товарам — почисти вручную.».

Admin-endpoint:
- GET /api/catalog/products/barcode-duplicates (Admin/Manager)
  возвращает массив { code, products: [{productId, productName,
  article}, ...] } для будущей UI-чистки.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:26:20 +05:00
nns a49db1c90d feat(org-settings): AllowFractionalPrices — переключатель дробных цен
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 30s
CI / Web (React + Vite) (push) Successful in 24s
Docker Images / API image (push) Successful in 40s
Docker Images / Web image (push) Successful in 26s
Docker Images / Deploy stage (push) Successful in 17s
Новая галка в настройках магазина «Разрешить дробные цены (с копейками)»
(default false). Когда выключено — все денежные поля принимают и
сохраняют только целые числа.

- Organization.AllowFractionalPrices + миграция Phase5h.
- OrgSettings DTO/Input + UI настроек (галка с подсказкой).
- MoneyInput получил prop allowFractional: при false запрещает ввод
  точки/запятой и форматирует целым числом, при true — две цифры
  после запятой как раньше.
- ProductEditPage / SupplyEditPage / RetailSaleEditPage передают
  org.allowFractionalPrices во все MoneyInput.
- Списки Products / Supplies / RetailSales форматируют суммы по
  настройке (с .00 или без).
- Сервер защищён от обхода UI: ProductsController / SuppliesController /
  RetailSalesController при сохранении округляют purchasePrice /
  price.amount / unitPrice / discount / paidCash / paidCard до целого
  если флаг выключен.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:21:04 +05:00
nns c2fa47c341 feat(product): группа обязательна, ≥1 штрихкод, умные дефолты на новом
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 26s
CI / Web (React + Vite) (push) Successful in 25s
Docker Images / API image (push) Successful in 38s
Docker Images / Web image (push) Successful in 26s
Docker Images / Deploy stage (push) Successful in 18s
- Product.ProductGroupId теперь NOT NULL (Guid вместо Guid?). Миграция
  Phase5g_RequiredProductGroup делает backfill: создаёт «Продукты
  питания» в каждой организации, у которой есть товары без группы,
  переносит туда null-значения, потом ALTER COLUMN NOT NULL.
- ProductDto/ProductInput: ProductGroupId/Name без `?`.
- ProductsController.Create/Update: 400 если barcodes пустой.
- MoySklad-импорт: при отсутствии productFolder у товара ставится
  defaultGroupId — id «Продукты питания» (создаётся при необходимости).
- DemoCatalogSeeder: «Продукты питания» добавлена в seed-набор групп.
- ProductEditPage:
  • новый товар сразу получает 1 EAN-13 в barcodes,
  • Single-select Единица измерения и Группа лишились опции «—»,
  • дефолт unitOfMeasureId — id единицы code='796' (штука),
  • дефолт productGroupId — «Продукты питания» (или первая),
  • Save disabled пока имя/единица/группа/≥1 штрихкод не заполнены,
  • если штрихкоды удалены — красная подсказка вместо нейтральной.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:24:10 +05:00
nns a94c38d074 feat(org-settings): настройка ShowMinMaxStock для мин/макс остатков
Добавлена Organization.ShowMinMaxStock (bool, default false) — флаг
видимости полей «Минимальный / Максимальный остаток» на карточке
товара. В UI настроек магазина появилась соответствующая галка
с подсказкой. По умолчанию выключено — большинству магазинов
эти поля не нужны.

Миграция Phase5f_ShowMinMaxStock добавляет колонку.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:02:53 +05:00
nns d6dcc75aa0 refactor(currencies): убрать IsActive и MinorUnit из UI/API
- Currency.IsActive удалён полностью (domain/DTO/API/web/миграция).
  Валюты — глобальный справочник; «архивировать» USD глобально
  бессмысленно, а per-tenant видимости у валют нет.
- MinorUnit остаётся в БД (нужен для форматирования цен), но скрыт
  из UI: убран CurrencyDto.MinorUnit, CurrencyInput.MinorUnit,
  колонка «Знаки» из списка.
- Форма валюты — 3 поля: Код / Название / Символ.
- Миграция Phase5e_DropCurrencyIsActive дропает колонку.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:01:13 +05:00
nns 3ed6fe25be refactor(vat): Product.Vat как decimal(5,2), поле видно только при VatEnabled
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 29s
CI / Web (React + Vite) (push) Successful in 22s
Docker Images / API image (push) Successful in 38s
Docker Images / Web image (push) Successful in 23s
Docker Images / Deploy stage (push) Successful in 17s
Единственная роль галки «В том числе НДС» на товаре — показать/скрыть
поле «Ставка НДС %». Никакой семантики «в том числе/сверху» на товаре
не живёт — это логика документа (продажи/поставки).

- Product.Vat: int → decimal (миграция Phase5d_ProductVatDecimal меняет
  тип колонки на numeric(5,2)).
- ProductDto/ProductInput: decimal? Vat.
- ResolveDefaultVatAsync, seeders, MoySklad import — decimal.
- MoySklad import: если vatEnabled пришёл — уважаем, иначе прежний
  fallback «vat=0 → без НДС».
- UI: вместо жёсткого Select [0,10,12,16,20] — TextInput number step=0.01;
  поле рендерится только когда form.vatEnabled=true; дефолт для нового
  товара подставляется из Country.VatRate организации.
- В таблице товаров ставка печатается с 2 знаками (16.00%).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:51:11 +05:00
nns 143f9d5330 feat(org-settings): галки «Услуга»/«Маркируемый» скрываются по умолчанию
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 24s
CI / Web (React + Vite) (push) Successful in 25s
Docker Images / API image (push) Successful in 38s
Docker Images / Web image (push) Successful in 25s
Docker Images / Deploy stage (push) Successful in 19s
Добавлены organizations.ShowServiceOnProduct и ShowMarkedOnProduct
(оба default false). В UI карточки товара чекбоксы «Услуга» и
«Маркируемый» рендерятся только если соответствующий флаг включен;
в фильтрах списка товаров Tri-фильтры тоже прячутся. В БД поля
IsService/IsMarked у Product сохраняются как обычно — просто UI их
не показывает.

Это параллель к ShowVatEnabledOnProduct: по умолчанию UI максимально
простой, а нишевые фичи включаются через настройки магазина.

Миграция Phase5c_ShowServiceMarkedOnProduct добавляет обе колонки.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:39:06 +05:00
nns 4f4df4a715 refactor(countries): drop SortOrder, sort by Name, auto-width columns
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 27s
CI / Web (React + Vite) (push) Successful in 24s
Docker Images / API image (push) Successful in 40s
Docker Images / Web image (push) Successful in 25s
Docker Images / Deploy stage (push) Successful in 18s
- Country.SortOrder удалено из домена/DTO/API/seeder/web/UI.
- Миграция Phase5b_DropCountrySortOrder дропает колонку.
- Список стран сортируется по Name ASC.
- В форме: поле «Порядок» убрано.
- В таблице: убрана колонка «Порядок», ширины колонок сжаты по
  содержимому (Код 80px, Валюта 120px, НДС 100px, Название flex).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:14:01 +05:00
nurdotnet 9d8c386def feat(vat): ставка в стране + опц. переопределение на товаре
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 27s
CI / Web (React + Vite) (push) Successful in 23s
Docker Images / API image (push) Successful in 38s
Docker Images / Web image (push) Successful in 25s
Docker Images / Deploy stage (push) Successful in 18s
Phase5_VatAsCountryProperty:
- countries.VatRate (numeric(5,2)) — ставка страны, источник правды.
  Seed: KZ=16, RU=20, BY=20, DE=19, CN=13, TR=18, UZ=12, KG=12, KR=10,
  IT=22, PL=23, US=0.
- organizations.ShowVatEnabledOnProduct (bool, default false) — флаг
  отображения на карточке товара.
- organizations.DefaultVat удалён (заменён страной).
- products.Vat ОСТАЁТСЯ: для KZ есть льготные категории (хлеб/молоко =
  0%) и фискальный чек требует ставку на каждой позиции.

Country domain: + DefaultCurrency / VatRate (уже было DefaultCurrencyId
из Phase4, сейчас дополнено).

Organization domain: DefaultVat убран, ShowVatEnabledOnProduct добавлен.

Backend:
- ProductInput.Vat теперь int? — если UI скрывает поле и прислал null,
  ProductsController берёт дефолт из страны организации (Country.VatRate
  при создании; при update сохраняет прежнее значение).
- CountriesController.List/Get/Create/Update возвращает/принимает
  DefaultCurrency и VatRate.
- MoySklad импорт: дефолт Vat загружается из страны организации.
- SystemReferenceSeeder: новые валюты BYN/UZS/KGS/TRY/KRW/PLN, seed
  country-currency-vat для всех 12 стран.
- OrganizationSettingsController: VatRate read-only из страны,
  ShowVatEnabledOnProduct редактируется.

Web:
- Country type + CountriesPage форма редактирования (валюта, ставка НДС).
- OrganizationSettingsPage: "Ставка НДС" read-only
  (берётся из страны, ссылка на /catalog/countries), галочка
  "Указывать ставку НДС на товаре".
- ProductEditPage: блок Ставка НДС % + галка "В том числе НДС" теперь
  показываются только если showVatEnabledOnProduct=true. В payload
  при save.mutate отправляется vat=null если скрыто.
- ProductsPage: колонка НДС показывается только при включённом флаге.

Galleries/products/settings других этапов — не задеты.
2026-04-24 11:56:28 +05:00
nurdotnet 414d185765 feat(product): enum Packaging (штучный/весовой/разливной) вместо IsWeighed
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 30s
CI / Web (React + Vite) (push) Successful in 21s
Docker Images / API image (push) Successful in 46s
Docker Images / Web image (push) Successful in 23s
Docker Images / Deploy stage (push) Successful in 17s
Миграция Phase4b_ProductPackaging:
  products.IsWeighed (bool) → products.Packaging (int enum)
  1=Piece (default), 2=Weight, 3=Liquid
Backfill: прежние весовые товары → Weight.

Domain/DTO/Input/Controller/Seeder/MoySkladImport — всё обновлено.

Web:
- Packaging enum в types.ts.
- ProductEditPage: select "Фасовка" вместо checkbox "Весовой".
- Подпись чекбокса НДС уточнена: "НДС применяется (ставка выше)" —
  ссылается на поле Vat на товаре.
- Удалён IsMarked checkbox текст → "Маркируемый (Честный знак / Datamatrix)".
- ProductsPage фильтр: select Packaging вместо Tri(IsWeighed).
2026-04-24 11:08:43 +05:00
nurdotnet 773ecde6ba feat(org-settings): Country↔Currency, Organization.DefaultCurrency/MultiCurrency/DefaultVat + UI настроек
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 27s
CI / Web (React + Vite) (push) Successful in 24s
Docker Images / API image (push) Successful in 38s
Docker Images / Web image (push) Successful in 24s
Docker Images / Deploy stage (push) Successful in 18s
Миграция Phase4_CountryCurrencyOrgDefaults:
- countries.DefaultCurrencyId (FK → currencies)
- organizations.DefaultCurrencyId, MultiCurrencyEnabled, DefaultVat
- Seed: KZ→KZT, RU→RUB, BY→BYN, US→USD, DE→EUR, CN→CNY, TR→TRY
- Default для org: KZT, vat=16

Backend:
- Organization сущность получила DefaultCurrency/MultiCurrencyEnabled/DefaultVat.
- OrganizationSettingsController: GET/PUT /api/organization/settings.
- DevDataSeeder при создании/backfill орга выставляет KZT + vat=16.

Web:
- /settings/organization: форма с выбором страны (авто-подтягивает валюту),
  чекбоксом multi-currency, ставкой НДС по умолчанию.
- useOrgSettings() хук.
- SupplyEditPage / RetailSaleEditPage / ProductEditPage: select валюты
  показывается только если multiCurrencyEnabled=true, иначе
  подтягивается DefaultCurrency организации и рисуется символ валюты
  справа от цены.
- ProductEditPage при создании нового товара берёт VAT из org.DefaultVat.
- В sidebar добавлен раздел 'Настройки → Организация', убран
  Ставки НДС (сущность удалена раньше).
2026-04-24 11:03:25 +05:00
nurdotnet 2fc6d207f3 feat(moysklad-import): async jobs с прогрессом + токен в настройках
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 36s
CI / Web (React + Vite) (push) Successful in 23s
Docker Images / API image (push) Successful in 42s
Docker Images / Web image (push) Successful in 25s
Docker Images / Deploy stage (push) Successful in 18s
Pain points:
1. Импорт на ~30k товарах проходит 15-30 мин, nginx рвал на 60s → 504.
2. При импорте/очистке ничего не видно — ни счётчика, ни прогресса.
3. Токен приходилось вводить каждый раз вручную.

Фиксы:
- Async-job pattern: POST /api/admin/moysklad/import-products и
  /api/admin/cleanup/all/async возвращают jobId, реальная работа
  в Task.Run. GET /api/admin/jobs/{id} — статус +
  Total/Created/Updated/Skipped/Deleted/Stage/Message.
- ImportJobRegistry (singleton, in-memory) — хранит job-progress.
- MoySkladImportService обновляет progress по мере пейджинга
  (в т.ч. счётчик Created/Updated/Skipped).
- Cleanup разбит на именованные шаги, Stage меняется по мере
  "Товары…" → "Группы…" → "Контрагенты…".
- Токен per-organization: Organization.MoySkladToken + миграция
  Phase3_OrganizationMoySkladToken. Endpoints:
  GET/PUT /api/admin/moysklad/settings.
- Импорт-endpoints больше не требуют token в теле — берут из org.
- HttpContextTenantContext.UseOverride(orgId) — AsyncLocal-scope
  для background tasks (HttpContext там нет, а query-filter'у нужен
  orgId — ставим через override).

Nginx (host + web-container) получил 60-минутный timeout на
/api/admin/import/ чтобы старый sync-путь тоже не ронять (на
случай если кто-то вернёт sync call).

Web:
- MoySkladImportPage переработан: блок "Токен API" (save/test
  mask), блок импорта с кнопками без поля токена.
- JobCard с polling каждые 1.5s отображает живые счётчики и stage.
- DangerZone тоже теперь async с live-прогрессом.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:49:11 +05:00
nurdotnet bd15854b42 fix(moysklad/import): per-page retry + чаще SaveChanges
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 27s
CI / Web (React + Vite) (push) Successful in 23s
Docker Images / API image (push) Successful in 35s
Docker Images / Web image (push) Successful in 5s
Docker Images / Deploy stage (push) Successful in 17s
Почему импорт раньше обрывался на ~9500/29500 товаров:
- StreamPagedAsync бросал исключение при любом сетевом глюке или
  таймауте HttpClient (90s) на одной из страниц и весь цикл сыпался.
- Флаш делался раз в 500 товаров, так что при обрыве на 9500-м можно
  было потерять последние 499.

Фиксы:
- Per-page retry до 5 раз с exp-backoff (2,4,8,16с) — обрабатываем
  только сетевые ошибки (HttpRequestException / TaskCanceledException /
  IOException). API-ошибки типа 4xx проходят наверх как есть.
- SaveChangesAsync теперь каждые 100 товаров вместо 500 — меньше
  вероятность потерять при внезапном обрыве на границе.
- При исчерпании retries — бросаем осмысленное исключение с offset'ом.

Пользователь сейчас имеет 9500 из 29509 товаров (группа "Алкоголь" — 20
из 518). Нужно перезапустить импорт в UI с overwriteExisting=true —
существующие товары обновит, недостающие подтянет.
2026-04-23 23:30:37 +05:00
nurdotnet beae0ad604 feat(moysklad): import archived entities too (as IsActive=false)
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 50s
CI / Web (React + Vite) (push) Successful in 25s
Docker Images / API image (push) Successful in 36s
Docker Images / Web image (push) Successful in 6s
Docker Images / Deploy stage (push) Successful in 18s
Раньше архивных контрагентов/товаров MoySklad API по умолчанию не
возвращает (default filter = active only). Для полной синхронизации
MoySklad → food-market теперь делаем 2 прохода: сначала активных
(default), затем filter=archived=true — отдаём всё одним потоком.

В service убран skip-if-archived; archived записи импортируются
c IsActive=false (уже было в ApplyCounterparty / ApplyProduct;
продублировал для product folders: IsActive = !f.Archived).

Клиент: рефакторинг — один generic StreamPagedAsync<T>(path, archivedOnly)
вместо трёх копий постраничного цикла.

Теперь пользовательский MoySklad-каталог мапится в food-market 1:1
включая архив. Счётчик "Пропущено" отныне значит только "уже существует
и галка Перезаписать не стоит".
2026-04-23 21:35:44 +05:00
nurdotnet 5f0692587a fix(db): reconcile stage schema — drop TrackingType, add IsMarked
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 39s
CI / Web (React + Vite) (push) Successful in 22s
Docker Images / API image (push) Successful in 36s
Docker Images / Web image (push) Successful in 4s
Docker Images / Deploy stage (push) Successful in 18s
Phase2c2_MoySkladAlignment и Phase2c3_MsStrict остались в
__EFMigrationsHistory на стейдже, но .cs-файлы были удалены при откате
кода (8fc9ef1). В результате:
- снапшот не соответствовал актуальной БД
- колонка TrackingType висела в БД, а код ждал IsMarked
- /api/admin/moysklad/import-products валился с 42703

Эта миграция:
1. Добавляет IsMarked bool NOT NULL DEFAULT false
2. Если TrackingType есть — бэкфиллит IsMarked = (TrackingType <> 0)
   и удаляет колонку (idempotent через information_schema check)
3. Auto-scaffold также синхронизировал snapshot (был устаревшим —
   содержал VatRate/IsAlcohol/Kind/Symbol и пр., которых в коде давно нет).

Локально применилось без ошибок.
2026-04-23 21:23:45 +05:00
nurdotnet 8346c9a72e feat(admin): temp cleanup buttons + fix MoySklad import duplicates
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 35s
CI / Web (React + Vite) (push) Successful in 23s
Docker Images / API image (push) Successful in 1m10s
Docker Images / Web image (push) Successful in 41s
Docker Images / Deploy stage (push) Successful in 18s
Проблема: при импорте контрагентов/товаров с галкой «перезаписать» код
ставил Add() новой сущности вместо Update() существующей, порождая
дубликаты. Исправил оба потока — теперь по ключу (Name для контрагентов,
Article для товаров) ищем существующую запись и обновляем её на месте.
Коллекции (цены/штрихкоды товара) при апдейте не трогаем, чтобы не
затереть ручные правки пользователя.

Временные админские кнопки для разбора последствий прошлых импортов:
- DELETE /api/admin/cleanup/counterparties — сносит контрагентов + зависимые поставки + их stock-movements (RetailSale.CustomerId обнуляется, Product.DefaultSupplierId обнуляется)
- DELETE /api/admin/cleanup/all — сносит всё tenant-scoped (товары/группы/контрагенты/поставки/чеки/остатки/движения). Организация, пользователи, справочники (единицы, страны, валюты, типы цен, склады, точки продаж) остаются.
- GET /api/admin/cleanup/stats — превью с количеством записей.

UI: секция «Опасная зона» внизу страницы /admin/import/moysklad с двумя
красными кнопками + подтверждение словом «УДАЛИТЬ». Показываются счётчики
до и что удалилось после.
2026-04-23 20:58:59 +05:00
nurdotnet 8fc9ef1a2e feat: strict MoySklad schema — реплика потерянного f7087e9
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 27s
CI / Web (React + Vite) (push) Successful in 23s
Docker Images / API image (push) Successful in 34s
Docker Images / Web image (push) Successful in 27s
Docker Images / Deploy stage (push) Successful in 15s
Main расходился с БД стейджа (Phase2c3_MsStrict в history, но код ещё ссылался на VatRate etc.) — деплой ломался. Реплицирую удаление сущностей вручную, чтобы код совпадал с таблицами.

Убрано (нет в MoySklad — не выдумываем):
- Domain: VatRate сущность целиком; Counterparty.Kind + enum CounterpartyKind; Store.Kind + enum StoreKind; Product.IsAlcohol; UnitOfMeasure.Symbol/DecimalPlaces/IsBase.
- EF: DbSet<VatRate>, ConfigureVatRate, Product.VatRate navigation, индекс Counterparty.Kind.
- DTO/Input: соответствующие поля и VatRateDto/Input.
- API: VatRatesController удалён; references в Products/Counterparties/Stores/UoM/Supplies/Retail/Stock.

Добавлено как в MoySklad:
- Product.Vat (int) + Product.VatEnabled — MoySklad держит НДС числом на товаре.
- KZ default VAT 16% — applied в сидерах и в MoySkladImportService когда товар не принёс свой vat.

MoySkladImportService:
- ResolveKind убран; CompanyType=entrepreneur→Individual (как и было).
- VatRates lookup → прямой p.Vat ?? 16 + p.Vat > 0 для VatEnabled.
- baseUnit ищется по code="796" вместо IsBase.

Web:
- types.ts: убраны CounterpartyKind/StoreKind/VatRate/Product.vatRateId/vatPercent/isAlcohol/UoM.symbol/decimalPlaces/isBase; добавлено Product.vat/vatEnabled; унифицировано unitSymbol→unitName.
- VatRatesPage удалён, роут из App.tsx тоже.
- CounterpartiesPage/StoresPage/UnitsOfMeasurePage: убраны соответствующие поля в формах.
- ProductEditPage: select "Ставка НДС" теперь с фиксированными 0/10/12/16/20 + чекбокс VatEnabled.
- Stock/RetailSale/Supply pages: unitSymbol → unitName.

deploy-stage unguarded — теперь код соответствует DB, авто-deploy безопасен.
2026-04-23 17:32:02 +05:00
nurdotnet e9a82dd528 fix(moysklad): убираем выдумку Kind полностью — у MoySklad этого поля нет
Some checks are pending
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 Images / API image (push) Waiting to run
Docker Images / Web image (push) Waiting to run
Проверил через API под реальным токеном (entity/counterparty?expand=group,tags):
у MoySklad **нет** поля «Поставщик/Покупатель» у контрагентов вообще. Есть только:
- group (группа доступа сотрудников, у всех "Основной")
- tags (произвольные ярлыки, у большинства пусто)
- state (пользовательская цепочка статусов)
- companyType (legal/individual/entrepreneur — это наш Type)

Один и тот же контрагент может быть поставщиком в одной приёмке и покупателем
в другом чеке — классификация контекстная, не атрибут сущности.

Изменения:
- ImportCounterpartiesAsync.ResolveKind теперь ВСЕГДА возвращает Unspecified.
  Никаких эвристик по тегам — просто null для Kind.
- useSuppliers хук теперь useCounterparties — возвращает ВСЕХ контрагентов,
  не фильтрует по Kind. Селекторы поставщика в Supply/RetailSale показывают
  всех. Пользователь сам выбирает кто поставщик в этом конкретном документе.
- Создание контрагента в UI: дефолт Kind = Unspecified, не Supplier.

Поле Kind в нашей модели остаётся для пользователей которые сами хотят
классифицировать. Но импорт его не трогает.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:01:10 +05:00
nurdotnet 50f12ef7f0 fix(moysklad): не выдумывать Kind=Both для импортированных контрагентов
У MoySklad НЕТ встроенного поля «Поставщик/Покупатель» у контрагентов —
эта классификация целиком пользовательская через теги или группы. Импорт
ставил Kind=Both дефолтом когда тегов не было, что искажало данные:
все 586 контрагентов на stage стали «Оба», хотя в MoySklad ничего такого
не было.

- CounterpartyKind: добавлен Unspecified=0 как дефолт
- ImportCounterpartiesAsync.ResolveKind: возвращает Unspecified когда
  тегов нет; Both только если в тегах ОБА маркера ("постав" + "покуп");
  иначе один из конкретных
- UI: dropdown получил опцию «Не указано», лейбл «Оба» переименован в
  «Поставщик + Покупатель» (точнее)
- Существующие данные: SQL UPDATE Kind=3 → Kind=0 на stage (586 строк)
  и dev (0 строк, локально пусто)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:01:10 +05:00
nurdotnet 1c108b88a4 phase2c: RetailSale document — посты в stock как минусовые движения
Domain (foodmarket.Domain.Sales):
- RetailSale: Number "ПР-{yyyy}-{NNNNNN}", Date, Status (Draft/Posted),
  Store/RetailPoint/Customer/Currency, Subtotal/DiscountTotal/Total,
  Payment (Cash/Card/BankTransfer/Bonus/Mixed) + PaidCash/PaidCard split,
  CashierUserId, Notes, Lines.
- RetailSaleLine: ProductId, Quantity, UnitPrice, Discount, LineTotal,
  VatPercent (snapshot), SortOrder.
- PaymentMethod enum.

EF: retail_sales + retail_sale_lines, unique index (tenant,Number),
indexes by date/status/cashier. Migration Phase2c_RetailSale.

API /api/sales/retail (Authorize):
- GET list with filters status/store/from/to/search.
- GET {id} with lines joined to products + units, customer/retail-point
  names resolved.
- POST create draft (lines optional, totals computed server-side).
- PUT update — replaces lines wholesale; rejected if Posted.
- DELETE — drafts only.
- POST {id}/post — creates -qty StockMovements via IStockService for each
  line (decreasing stock), Type=RetailSale; flips to Posted, stamps PostedAt.
- POST {id}/unpost — reverses with +qty movements tagged "retail-sale-reversal".
- Auto-numbering scoped per tenant + year.

Web:
- types: RetailSaleStatus, PaymentMethod, RetailSaleListRow, RetailSaleLineDto,
  RetailSaleDto.
- /sales/retail list (number, date+time, status badge, store, cashier point,
  customer (or "аноним"), payment method, line count, total).
- /sales/retail/new + /:id edit page mirrors Supply edit page UX:
  sticky top bar (Back / Save / Post / Unpost / Delete), reqs grid with
  date/store/customer/currency/payment/paid-cash/paid-card, lines table
  with inline qty/price/discount + Subtotal/Discount/К оплате footer.
- ProductPicker reused. On line add, picks retail price from product's
  prices list (matches "розн" in priceTypeName) or first.
- Sidebar new group "Продажи" → "Розничные чеки" (ShoppingCart).

Posting cycle ready: Supply (+stock) → ... → RetailSale (-stock).
В Stock и Движения видно текущее состояние и историю.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:07:37 +05:00
nurdotnet 61f2c21016 phase2b: Supply document (приёмка) — posts to stock atomically
Domain (foodmarket.Domain.Purchases):
- Supply: Number (auto "П-{yyyy}-{000001}" per tenant), Date, Status
  (Draft/Posted), Supplier (Counterparty), Store, Currency, invoice refs,
  Notes, Total, PostedAt/PostedByUserId, Lines.
- SupplyLine: ProductId, Quantity, UnitPrice, LineTotal, SortOrder.

EF: supplies + supply_lines tables, unique index (tenant,Number), indexes
by date/status/supplier/product. Migration Phase2b_Supply applied.

API (/api/purchases/supplies, roles Admin/Manager/Storekeeper for mutations):
- GET list with filters (status, storeId, supplierId, search by number/name),
  projected columns.
- GET {id} with full line list joined to products + units.
- POST create draft (lines optional at creation, grand total computed).
- PUT update — replaces all lines; rejected if already Posted.
- DELETE — drafts only.
- POST {id}/post — creates +qty StockMovements via IStockService.ApplyMovementAsync
  for each line, flips to Posted, stamps PostedAt. Atomic (one SaveChanges).
- POST {id}/unpost — reverses with -qty movements tagged "supply-reversal",
  returns to Draft so edits can resume.
- Auto-numbering scans existing numbers matching prefix per year+tenant.

Web:
- types: SupplyStatus, SupplyListRow, SupplyLineDto, SupplyDto.
- /purchases/supplies list (number, date, status badge, supplier, store,
  line count, total in currency).
- /purchases/supplies/new + /:id edit page (sticky top bar with
  Back / Save / Post / Unpost / Delete; reqisites grid; lines table with
  inline qty/price and running total + grand total in bottom row).
- ProductPicker modal: full-text search over products (name/article/barcode),
  shows purchase price for quick reference, click to add line.
- Sidebar new group "Закупки" → "Приёмки" (TruckIcon).

Flow: create draft → add lines via picker → edit qty/price → Save → Post.
Posting writes StockMovement rows (visible on Движения) and updates Stock
aggregate (visible on Остатки). Unpost reverses in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 01:06:08 +05:00
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