food-market/docs/audit-moysklad.md
nurdotnet 495f0aabee
Some checks failed
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Failing after 1s
CI / Web (React + Vite) (push) Failing after 1m13s
docs: audit of our domain entities vs. live MoySklad API
Cross-checked every entity (Product, Counterparty, Supply, RetailSale,
Stock, Store, RetailPoint, Organization, ProductGroup, Barcode, Price,
PriceType, Country, Currency, VatRate, UoM) against real responses from
MoySklad's API — a flat list of:
 - fields we have and MS doesn't (to justify or drop)
 - fields MS has and we don't (to add)
 - semantic mismatches (e.g. MS holds prices in kopecks, our decimal)

Report only, no code changes — to be discussed with the user before
touching models/migrations. Priorities are split into P1 (import
parity: ExternalCode, Code, TrackingType enum, PaymentItemType, KZ
entrepreneur type), P2 (semantic fixes: RetailSale payment sums,
Overhead on supply, legal fields on Organization), P3 (nice-to-have),
and a list of deliberate divergences (why our VatRate/StockMovement
exist even though MS doesn't model them that way).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:57:06 +05:00

465 lines
33 KiB
Markdown
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.

# Аудит наших доменных сущностей vs. MoySklad API
Источник правды — живой MoySklad API `/api/remap/1.2/`. Проверялись ключи на реальных ответах (`?limit=2` на нашем аккаунте). Цель: каждая наша сущность должна либо повторять MoySklad, либо иметь явно оправданное отличие. Никаких «выдуманных» полей.
Условные обозначения:
- **⛔** — у нас есть поле, которого нет у MoySklad → либо оправдать комментарием, либо удалить.
- **** — у MoySklad есть поле, которого нет у нас → потенциально добавить.
- **⚠️** — важный нюанс (тип, семантика, обязательность).
---
## Counterparty → `entity/counterparty`
Ключи MoySklad (из ответа API, верхний уровень): `accountId, accounts, archived, bonusPoints, bonusProgram, companyType, created, externalCode, files, group, id, meta, name, notes, owner, salesAmount, shared, state, tags, updated` + расширяемые: `legalTitle, legalAddress, inn, kpp, ogrn, ogrnip, certificateNumber, certificateDate, phone, email, actualAddress, description, discountCardNumber, priceType, sex, salesChannel`.
| Наше поле | MoySklad | Комментарий |
|---|---|---|
| `Name` | `name` | ОК |
| `LegalName` | `legalTitle` | rename? или доп. комментарий-алиас |
| `Kind` (CounterpartyKind) | **нет** | ⛔ уже исправили enum (`Unspecified/Supplier/Customer/Both`), но MoySklad не имеет этого поля вообще — он различает контрагентов через `tags` или через `state` (статус в пайплайне продаж/закупок). **TODO:** либо оставить Kind только как UI-фильтр (не импортировать из MoySklad), либо перейти на теги |
| `Type` (LegalEntity/Individual) | `companyType` | ⚠️ у MoySklad 3 значения: `legal`, `individual`, `entrepreneur` (ИП!). У нас ИП отсутствует — **добавить `IndividualEntrepreneur` в enum** (для РК актуально) |
| `Bin` (БИН, РК) | `inn` (12-значный БИН пишется туда) | ⚠️ MoySklad для всех рынков использует `inn` — 12 цифр это ИИН РФ, 12 цифр РК — БИН. Мы вынесли `Bin` отдельно, при импорте MoySklad кладёт в `inn`. **TODO:** документировать маппинг Bin ↔ inn |
| `Iin` (ИИН, РК) | `inn` (тот же) | ⚠️ same — MoySklad не различает |
| `TaxNumber` | `inn` | дубль |
| `CountryId` | `country` (extended, по `meta`) | ⚠️ MoySklad не на верхнем уровне — тянется при `?expand=country` |
| `Address` | `actualAddress` | ОК |
| `Phone` | `phone` | ОК |
| `Email` | `email` | ОК |
| `BankName, BankAccount, Bik` | `accounts` (массив объектов) | ⚠️ у MoySklad это **коллекция счетов** (до нескольких банков). У нас одиночные поля — **либо сделать коллекцию Accounts, либо документировать "берём первый"** |
| `ContactPerson` | `contactpersons` (sub-endpoint) | ⚠️ у MoySklad это отдельный endpoint `counterparty/{id}/contactpersons` — массив. У нас скалярное поле |
| `Notes` | `description` (или `notes` разные в разных версиях API?) | ⚠️ в ответе API было `notes`ОК |
| `IsActive` | `archived` (inverse) | ОК |
| — | `tags` (массив) | **добавить** — удобно для классификации (в том числе заменой Kind) |
| — | `state` (ссылка на состояние в пайплайне) | отложить до Phase N (CRM) |
| — | `bonusPoints, bonusProgram, discountCardNumber` | отложить до дисконтных карт |
| — | `salesAmount` (вычисляемое) | не храним |
| — | `priceType` (персональный тип цены) | полезно для опта; добавить `Guid? DefaultPriceTypeId` |
**TODO:**
1. Enum `CounterpartyType`: добавить `IndividualEntrepreneur = 3`.
2. Коллекция `CounterpartyAccount` (BankName/BankAccount/Bik/IsDefault) — или явный комментарий «храним только основной».
3. Коллекция `CounterpartyTag` (string) — для классификации при импорте из MoySklad.
4. Поле `DefaultPriceTypeId``PriceType` (для опта/персональной цены).
5. Комментарий на `Bin/Iin/TaxNumber`: при импорте из MoySklad все три могут прилететь из одного поля `inn` — логика различения по длине (12 цифр РК-формат) / по companyType.
---
## Organization → `entity/organization`
Ключи MS: `accountId, accounts, archived, bonusPoints, bonusProgram, companyType, companyVat__ru, created, email, externalCode, group, id, isEgaisEnable, meta, name, owner, payerVat, shared, updated` + extended: `legalTitle, legalAddress, actualAddress, inn, kpp, ogrn, ogrnip, okpo, director, chiefAccountant, phone, fax, utmUrl`.
| Наше | MS | Комментарий |
|---|---|---|
| `Name` | `name` | ОК |
| `CountryCode` | **нет** | ⛔ у MoySklad нет — у них multi-tenant через account. У нас — multi-tenant через Organization, но CountryCode неочевиден. Оставить как есть, документировать почему (нам нужно для налоговых/локальных настроек) |
| `Bin` | `inn` | то же что и Counterparty |
| `Address` | `actualAddress` | ОК |
| `Phone` | `phone` | ОК |
| `Email` | `email` | ОК |
| `IsActive` | `archived` inverse | ОК |
| — | `legalTitle, legalAddress` | для офиц. документов |
| — | `kpp, ogrn, ogrnip, okpo` | РФ-специфично, пропускаем для РК |
| — | `payerVat` (bool, плательщик НДС) | полезно — есть ли НДС у нашей организации |
| — | `director, chiefAccountant` | для подписей на накладных |
| — | `accounts` (банковские) | аналогично Counterparty |
| — | `isEgaisEnable` | РФ, пропускаем |
**TODO:**
1. `LegalName`, `LegalAddress`, `PayerVat` (bool), `DirectorName`, `ChiefAccountantName` — для накладных/счетов.
2. `CountryCode` оставить + `<see langword="…"/>` комментарий почему у нас есть, а у MS нет.
---
## Product → `entity/product`
Ключи MS: `accountId, archived, barcodes, buyPrice, code, discountProhibited, externalCode, files, group, id, images, isSerialTrackable, meta, minPrice, name, owner, pathName, paymentItemType, productFolder, salePrices, shared, supplier, trackingType, uom, updated, useParentVat, variantsCount, volume, weight` + optional: `article, country, description, effectiveVat, minPrice.currency, taxSystem, vat, tnved, syncId, modifications`.
| Наше | MS | Комментарий |
|---|---|---|
| `Name` | `name` | ОК |
| `Article` | `article` | ОК |
| `Description` | `description` | ОК |
| `UnitOfMeasureId` | `uom.meta` | ОК |
| `VatRateId` | `vat` (число) + `useParentVat` | ⚠️ у MS НДС хранится как число (20, 10, 12, 0) прямо на товаре. **Мы отдельная сущность VatRate**. Обоснование: нам нужно хранить локализованные названия ("НДС 12%", "Без НДС"), is-default, и позволять разным организациям иметь разные ставки. НО — при импорте надо резолвить число в VatRate по organization_id |
| `ProductGroupId` | `productFolder.meta` | ОК |
| `DefaultSupplierId` | `supplier.meta` | ОК (у MS тоже одиночная ссылка) |
| `CountryOfOriginId` | `country.meta` | ОК |
| `IsService` | `paymentItemType` (одно из значений = "SERVICE") | ⚠️ у MS это enum с ~10 значений; у нас bool. **TODO:** либо enum, либо документировать что мы учитываем только IsService |
| `IsWeighed` | **нет** | ⛔ у MS этого нет; характеристика ритейла, нам нужно для касс с весами. **Оставить, документировать.** |
| `IsAlcohol` | `tnved` (класс товара) или через group | ⚠️ у MS через tnved-код или through type классификаторы. Наше bool — упрощение. **Оставить с комментарием.** |
| `IsMarked` | `trackingType` (enum: NOT_TRACKED, BEER_ALCOHOL, …) | ⚠️ У MS это enum из 10+ вариантов маркировки. Наш `IsMarked: bool` — потеря информации. **TODO:** заменить на enum `TrackingType` (NOT_TRACKED/TOBACCO/ALCOHOL/SHOES/MEDICINE/…) |
| `MinStock, MaxStock` | `minimumBalance` (число), `stock` (runtime) | ⚠️ у MS есть только `minimumBalance` (нижняя граница). MaxStock — наш |
| `PurchasePrice, PurchaseCurrencyId` | `buyPrice.value, buyPrice.currency.meta` | ОК (MS упаковывает в объект, мы разнесли — **одно и то же**) |
| `ImageUrl` | `images` (массив через sub-endpoint) | ⚠️ у MS images коллекция, у нас одна + отдельная ProductImage. ОК, двойная запись для UX |
| `IsActive` | `archived` inverse | ОК |
| `Prices` (collection) | `salePrices` (массив inline в MS) | ⚠️ у MS цены — **массив внутри товара**, у нас — отдельная таблица. Оба норм; просто маппинг при sync |
| `Barcodes` (collection) | `barcodes` (массив inline) | ОК |
| `Images` (collection) | `images` (sub-endpoint) | ОК |
| — | `code` | внутренний код (отличается от `article`). **Добавить `Code`** |
| — | `externalCode` | используется при импорте/ERP-интеграциях. **Добавить `ExternalCode`** (актуально для импорта из MoySklad, 1C) |
| — | `discountProhibited` | «запрет скидок» — полезно на кассе |
| — | `minPrice.value/currency` | минимальная отпускная цена. **Добавить `MinPrice` + `MinPriceCurrencyId`** |
| — | `paymentItemType` | для фискализации: «товар/услуга/работа/подарочная карта/…». **Добавить enum `PaymentItemType`** (нужно для 54-ФЗ / КZ fiscal receipts) |
| — | `tnved` | код ТН ВЭД для трансграничной торговли |
| — | `volume, weight` | для логистики (доставка) |
| — | `variantsCount` | runtime агрегат, не храним |
| — | `files` | вложения (паспорта качества, фото упаковки) — отложить |
**TODO:**
1. Добавить `Code`, `ExternalCode` на Product.
2. Заменить `IsMarked` на enum `TrackingType`.
3. Добавить `MinPrice`, `MinPriceCurrencyId`.
4. Добавить enum `PaymentItemType` + поле.
5. Поля `Volume`, `Weight`, `DiscountProhibited`.
6. Запомнить маппинг: `useParentVat` → наследовать НДС от ProductGroup (у нас сейчас не реализовано, надо подумать).
---
## ProductGroup → `entity/productfolder`
Ключи MS: `accountId, archived, externalCode, group, id, meta, name, owner, pathName, shared, updated, useParentVat` + `vat, effectiveVat, productFolder` (родитель).
| Наше | MS | Комментарий |
|---|---|---|
| `Name` | `name` | ОК |
| `ParentId` | `productFolder.meta` | ОК (MS использует то же имя для родителя что и для самой сущности) |
| `Path` | `pathName` | ОК |
| `SortOrder` | **нет** | ⛔ у MS нет сортировки групп. Оставить, это UX |
| `IsActive` | `archived` inverse | ОК |
| — | `externalCode` | для импорта |
| — | `vat, useParentVat` | ставка НДС по умолчанию для товаров группы |
**TODO:**
1. Добавить `ExternalCode`.
2. Добавить `VatRateId?` + `UseParentVat: bool` (для наследования).
---
## ProductBarcode → `product.barcodes[]`
У MS barcode — объект внутри product: `{type: 'ean13'|'ean8'|'code128'|'upc'|'gtin', value: '...'}`. Отдельной сущности нет.
| Наше | MS | Комментарий |
|---|---|---|
| `Code` | `value` | ОК |
| `Type` | `type` | ⚠️ MS использует строки ('ean13', 'gtin', …) — мы уже enum |
| `IsPrimary` | **нет** | ⛔ у MS нет — первый считается основным. **Оставить с комментарием — у нас явная пометка.** |
OK, расхождений существенных нет.
---
## ProductPrice → `product.salePrices[]`
У MS цены — массив объектов в product: `{value, currency: {meta}, priceType: {meta}}`. Отдельной сущности нет.
Наше — отдельная таблица. Это **нормализованный вариант** — оправдано если цен много и есть выборки по PriceType. **TODO:** маппинг при импорте — проитерировать salePrices и создать ProductPrice per PriceType.
---
## PriceType → `entity/pricetype`
Ключи MS (из context): `id, name, externalCode`.
| Наше | MS | Комментарий |
|---|---|---|
| `Name` | `name` | ОК |
| `IsDefault` | **нет** | ⛔ у MS — default определяется порядком или отдельно в настройках аккаунта. **Оставить** |
| `IsRetail` | **нет** | ⛔ наш флаг «используется на кассе». **Оставить** |
| `SortOrder` | **нет** | ⛔ UX. **Оставить** |
| — | `externalCode` | для импорта |
**TODO:**
1. `ExternalCode`.
---
## Country → `entity/country`
Ключи MS: `code, description, externalCode, id, meta, name, updated`.
| Наше | MS | Комментарий |
|---|---|---|
| `Code` | `code` | ⚠️ у MS формат ISO3166-1 **alpha-2 или числовой**у нас alpha-2 |
| `Name` | `name` | ОК |
| `SortOrder` | **нет** | ⛔ UX |
| — | `description` | |
| — | `externalCode` | |
OK, мелочь.
---
## Currency → `entity/currency`
Ключи MS: `archived, code, default, fullName, id, indirect, isoCode, majorUnit, meta, minorUnit, multiplicity, name, rate, rateUpdateType, system`.
| Наше | MS | Комментарий |
|---|---|---|
| `Code` | `isoCode` или `code` | ⚠️ у MS `isoCode` (строка "KZT") и `code` (цифровой "398") — у нас `Code` = строка ISO |
| `Name` | `name` | ОК |
| `Symbol` | **нет** | ⛔ у MS нет символа "₸" — но это UX. **Оставить** |
| `MinorUnit` | `minorUnit` | ОК |
| `IsActive` | `archived` inverse | ОК |
| — | `default` (валюта аккаунта) | |
| — | `rate, rateUpdateType` | курс к базовой валюте (при мульти-валютности) |
| — | `multiplicity, indirect` | конвертация; если не мульти-валютные — не надо |
| — | `fullName` | «Тенге Казахстана» vs «KZT» |
**TODO:**
1. Добавить `IsDefault: bool` (ровно одна валюта = true per tenant, или глобально).
2. `Rate, RateUpdateType` + `FullName` — отложить до мульти-валютности.
---
## VatRate — у MoySklad нет `entity/vatrate`
⚠️ У MS **ставки НДС хранятся как числовое поле на товаре** (`vat`). Отдельной таблицы нет — набор значений {0, 5, 7, 10, 12, 18, 20} и «без НДС» встроен в систему.
Наше `VatRate` — отдельная сущность. **Обоснование сохранить:**
1. Локализованное название ("НДС 12%", "Без НДС").
2. IsDefault per organization.
3. Разные организации в разных налоговых режимах (с НДС / УСН).
4. При добавлении новой ставки (например, на случай гипотетического увеличения в РК) — не править перечисление в коде.
Но: **следите**, чтобы у товара хранился `VatRateId`, а не отдельно `vat: decimal`. При импорте из MS мапим число в запись VatRate.
**Комментарий в коде нужен** — явно сказать, что мы отклонились от MoySklad сознательно.
---
## UnitOfMeasure → `entity/uom`
Ключи MS: `code, description, externalCode, id, meta, name, updated`.
| Наше | MS | Комментарий |
|---|---|---|
| `Code` (ОКЕИ) | `code` | ОК, MS использует ОКЕИ-коды (796, 166, 112) |
| `Symbol` | **нет** | ⛔ у MS только `name` ("штука"). Мы вынесли "шт" отдельно для коротких надписей на ценниках/кассовых чеках. **Оставить.** |
| `Name` | `name` | ОК |
| `DecimalPlaces` | **нет** | ⛔ у MS на уровне продукта (`variantsCount`?), а не UoM. Наш `DecimalPlaces` определяет можно ли дробные количества (0=штучный, 3=весовой). **Оставить — важно для UX касс.** |
| `IsBase` | **нет** | ⛔ наше «базовая единица организации». Мелочь, оставить |
| `IsActive` | `archived` inverse (у MS есть `archived` в uom? перепроверить) | ⚠️ в нашем ответе API archived не было — у MS uom этого поля может не быть, потому что единицы системные |
| — | `description` | |
| — | `externalCode` | |
**TODO:**
1. `ExternalCode`.
---
## Store → `entity/store`
Ключи MS: `accountId, address, archived, externalCode, group, id, meta, name, owner, pathName, shared, slots, updated, zones`.
| Наше | MS | Комментарий |
|---|---|---|
| `Name` | `name` | ОК |
| `Code` | `externalCode`? или отдельно? | ⚠️ у MS только `externalCode`. **Добавить ExternalCode или rename Code→ExternalCode** |
| `Kind` (Warehouse/RetailFloor) | **нет** | ⛔ у MS такого деления нет. Обоснование: нам нужно отличать «склад» от «торгового зала» для UI и настроек касс. **Оставить с комментарием** |
| `Address` | `address` | ОК |
| `Phone` | **нет** | ⛔ у MS нет. Оставить |
| `ManagerName` | **нет** | ⛔ у MS нет. Оставить |
| `IsMain` | **нет** (но можно проставить через default) | ⛔ Оставить |
| `IsActive` | `archived` inverse | ОК |
| — | `pathName` | (если будут иерархические склады) |
| — | `slots` (ячейки склада) | отложить |
| — | `zones` (зоны склада) | отложить |
**TODO:**
1. `ExternalCode` (или переименовать Code → ExternalCode).
---
## RetailPoint → `entity/retailstore`
У MS это **«Точка продаж» / кассовое место**. Огромное количество полей (~60) — в основном фискальные настройки.
| Наше | MS | Комментарий |
|---|---|---|
| `Name` | `name` | ОК |
| `Code` | `externalCode` | rename or add |
| `StoreId` | `store.meta` | ОК |
| `Address` | **нет** (возможно `organization.actualAddress`) | ⛔ адрес не у точки, а у организации/склада. Пересмотреть, куда класть |
| `Phone` | **нет** | ⛔ |
| `FiscalSerial` | **нет такого поля**; есть `fiscalType`, `fiscalMemoryNumber`?, `ofdEnabled` | ⚠️ у MS фискальные настройки множественные. У нас один скаляр — упрощение. **TODO:** уточнить по мере подключения ККМ |
| `FiscalRegNumber` | `ofdEnabled` + `ofdSettings` | same |
| `IsActive` | `active, archived` | MS различает active и archived — у нас только IsActive |
| — | `priceType.meta` | тип цены для этой точки — **важно** |
| — | `allowCustomPrice` | разрешить ручную цену на кассе |
| — | `allowCreateProducts` | создать товар прямо на кассе |
| — | `discountEnable, discountMaxPercent` | скидки на кассе |
| — | `cashiers` (коллекция) | кто может работать за кассой |
| — | `sellReserves` | продавать резерв |
| — | `receiptTemplate` | шаблон чека |
| — | `returnFromClosedShiftEnabled` | возврат из закрытой смены |
| — | `requiredBirthdate/Email/Phone/Fio/Sex/DiscountCardNumber` | обязательные поля при продаже |
| — | `markingSellingMode, marksCheckMode, sendMarksForCheck` | маркировка товаров |
**TODO:**
1. Обязательно: `DefaultPriceTypeId` (ссылка на `PriceType`).
2. Настройки кассы (скоп Phase 3 — касса): `AllowCustomPrice`, `AllowCreateProducts`, `SellReserves`, `DiscountMaxPercent`, `RequireCustomer...` — добавлять по мере реализации POS.
3. Коллекция `RetailPointCashier` (user_id, может ли работать).
---
## Supply → `entity/supply` + `supply/{id}/positions`
Document keys: `accountId, agent, applicable, created, externalCode, files, group, id, meta, moment, name, organization, owner, payedSum, positions, printed, published, rate, shared, store, sum, updated, vatEnabled, vatIncluded, vatSum`.
| Наше | MS | Комментарий |
|---|---|---|
| `Number` | `name` | ⚠️ у MS «номер документа» = `name`. У нас `Number` — семантически то же |
| `Date` | `moment` | ОК |
| `Status` (Draft/Posted) | `applicable` (bool) | ⚠️ у MS это bool «проведён или нет». У нас enum Draft/Posted → эквивалентно |
| `SupplierId` | `agent.meta` | ⚠️ у MS вместо `supplier` общее слово `agent` (контрагент) |
| `StoreId` | `store.meta` | ОК |
| `CurrencyId` | `rate.currency.meta` | ⚠️ MS упаковывает в rate объект с курсом |
| `SupplierInvoiceNumber` | **нет на верхнем уровне**; есть в `attributes` | ⛔ у MS через custom attributes. Оставить |
| `SupplierInvoiceDate` | same | same |
| `Notes` | `description` | rename или комментарий |
| `Total` | `sum` | ОК |
| `PostedAt` | `updated` (когда applicable ставится true) | ⚠️ у MS нет выделенного поля; мы отдельно фиксируем |
| `PostedByUserId` | `owner.meta` | условно |
| — | `vatEnabled` | |
| — | `vatIncluded` | НДС включён в цену |
| — | `vatSum` | суммарный НДС документа |
| — | `payedSum` | сколько оплачено |
| — | `organization.meta` | ⚠️ у MS документ привязан к организации. **У нас TenantEntity несёт OrganizationId — уже есть** |
| — | `printed, published` | распечатан/опубликован |
| — | `overhead` (доп.расходы) | доставка/таможня — **важно для фактической себестоимости** |
**Supply.Positions (SupplyLine) → supply/{id}/positions:**
Ключи MS: `accountId, assortment, discount, id, meta, overhead, price, quantity, vat, vatEnabled`.
| Наше (SupplyLine) | MS position | Комментарий |
|---|---|---|
| `ProductId` | `assortment.meta` | ⚠️ у MS `assortment` = может быть product ИЛИ variant ИЛИ service ИЛИ bundle. Мы только продукт |
| `Quantity` | `quantity` | ОК |
| `UnitPrice` | `price` | ⚠️ у MS `price` — в копейках (integer `100 = 1.00`). У нас decimal. **Маппинг при импорте: делить на 100** |
| `LineTotal` | **нет** (вычисляется) | ⛔ у MS не хранится |
| `SortOrder` | **нет** | ⛔ наш UX |
| — | `discount` | строковая скидка |
| — | `vat` | ставка НДС на позицию |
| — | `vatEnabled` | |
| — | `overhead` | доля накладных (для себестоимости) |
**TODO Supply:**
1. Поля: `VatEnabled`, `VatIncluded`, `VatSum`, `PayedSum`, `Overhead`.
2. Lines: `Discount` (decimal), `VatPercent` (snapshot, уже подобное есть в RetailSaleLine), `VatEnabled`.
3. Комментарий: MS `price` в копейках — при импорте делить.
---
## RetailSale → `entity/retaildemand` + `retaildemand/{id}/positions`
Document keys: огромный список, ключевое: `agent, applicable, cashSum, noCashSum, qrSum, prepaymentCashSum, prepaymentNoCashSum, prepaymentQrSum, advancePaymentSum, fiscal, retailShift, retailStore, store, positions, rate, sum, vatEnabled, vatIncluded, vatSum, name, moment, organization, syncId`.
| Наше | MS | Комментарий |
|---|---|---|
| `Number` | `name` | ОК |
| `Date` | `moment` | ОК |
| `Status` | `applicable` | ⚠️ bool vs enum |
| `StoreId` | `store.meta` | ОК |
| `RetailPointId` | `retailStore.meta` | ОК |
| `CustomerId` | `agent.meta` | ОК (nullable если не знаем покупателя) |
| `CashierUserId` | **нет напрямую**; `retailShift` → cashier | ⚠️ |
| `CurrencyId` | `rate.currency.meta` | ОК |
| `Subtotal, DiscountTotal, Total` | `sum` (= Total) | ⚠️ MS **не хранит subtotal и discount total отдельно** — только total. Но цена в позиции уже после скидки? Нет — `positions[].discount` хранится, total = sum(price*qty - discount) |
| `Payment` (PaymentMethod enum) | **cashSum + noCashSum + qrSum** | ⚠️ MS — **не enum, а суммы по видам оплаты**. Т.е. при mixed-оплате можно часть наличными + часть картой. **Наш enum Payment + PaidCash + PaidCard — неполный.** TODO: добавить `PaidQr` + убрать enum в пользу «сколько чем заплачено» |
| `PaidCash` | `cashSum` | ⚠️ у MS в копейках |
| `PaidCard` | `noCashSum` | ⚠️ в копейках |
| `Notes` | `description` | ОК |
| `PostedAt` | — | наш |
| `PostedByUserId` | `owner.meta` | условно |
| — | `qrSum` | **добавить `PaidQr`** (QR-оплата актуальна для КZ) |
| — | `retailShift.meta` | кассовая смена (отложить) |
| — | `fiscal` | пробит ли фискально |
| — | `syncId` | идентификатор для офлайн-касс (при резинхроне) |
| — | `prepaymentCashSum/NoCashSum/QrSum, advancePaymentSum` | предоплаты |
**RetailSale.Positions (RetailSaleLine) → retaildemand/{id}/positions:**
Ключи: `accountId, assortment, discount, id, meta, price, quantity, vat, vatEnabled`.
| Наше (RetailSaleLine) | MS | Комментарий |
|---|---|---|
| `ProductId` | `assortment.meta` | ОК |
| `Quantity` | `quantity` | ОК |
| `UnitPrice` | `price` | ⚠️ копейки |
| `Discount` | `discount` | ОК |
| `LineTotal` | вычисляется | наш |
| `VatPercent` | `vat` | ОК (snapshot) |
| `SortOrder` | — | наш UX |
| — | `vatEnabled` | |
**TODO RetailSale:**
1. Добавить `PaidQr: decimal`.
2. **Убрать `PaymentMethod` enum** в пользу денормализованных `PaidCash, PaidCard, PaidQr, PaidBonus` + computed `Method` (если все кроме одного = 0 → Cash/Card/QR, иначе Mixed). Либо оставить enum + PayHint, но быть готовым к частичной оплате.
3. `VatEnabled, VatIncluded, VatSum` (сумма НДС на документ — вычисляется).
4. Комментарий: MS `price/cashSum/noCashSum` в копейках при импорте.
---
## Stock → `report/stock/bystore`
У MS **нет отдельной сущности "Stock"** — это **отчёт**. Ответ `report/stock/bystore` содержит:
```json
{ "meta": {...}, "stockByStore": [ { "name": "Склад №1", "meta": {...}, "stock": 10.0, "reserve": 2.0, "inTransit": 0.0, "quantity": 12.0 } ] }
```
Т.е. по каждому (product, store) — stock (сколько есть), reserve (резерв), inTransit (в пути), quantity = stock+inTransit.
У нас `Stock`**материализованный агрегат** (Quantity, ReservedQuantity, computed Available). Это **технически наше решение**, не требование бизнеса.
**TODO:**
1. Комментарий в Stock.cs: объяснить, что это материализация (у MS динамический репорт).
2. **Добавить `InTransit: decimal`** — товар в пути (между складами при перемещении).
---
## StockMovement — у MoySklad такой сущности нет
⚠️ MS **не хранит journal движений** в явном виде — остатки рассчитываются в реальном времени из документов (supply, retaildemand, loss, enter, move и т.д.). Поэтому на вопрос «почему на складе минус 5» MS возвращает историю документов, а не journal.
Наше `StockMovement`**явный immutable journal**. Обоснование:
1. Мгновенный ответ на «почему такой остаток» без пробегания по всем документам.
2. Атомарные корректировки при баг-фиксах миграций.
3. Упрощённая репликация в офлайн-кассы.
Это **сознательное отклонение** от MS — должно быть задокументировано в коде и в `docs/`. **TODO:** комментарий в StockMovement.cs + упоминание в `CLAUDE.md`.
---
## Свод по приоритетам
### Приоритет 1 — базовая совместимость импорта (на этой неделе):
- Product: `Code`, `ExternalCode`, `TrackingType` (enum) вместо `IsMarked`, `MinPrice`/`MinPriceCurrencyId`, `PaymentItemType` (enum)
- Counterparty: `CounterpartyType.IndividualEntrepreneur`, `ExternalCode`, tags (коллекция)
- ProductGroup: `ExternalCode`
- PriceType: `ExternalCode`
- Country, Currency, UnitOfMeasure, Store: `ExternalCode`
- RetailPoint: `DefaultPriceTypeId`
### Приоритет 2 — смысловые (следующая итерация):
- RetailSale: `PaidQr`, убрать enum PaymentMethod в пользу суммовых полей
- Supply: `Overhead`, `VatSum`, `VatEnabled`, `VatIncluded`
- Organization: `LegalName`, `LegalAddress`, `PayerVat`, `DirectorName`
- Product: `Volume, Weight, DiscountProhibited`
- Stock: `InTransit`
### Приоритет 3 — при необходимости:
- Counterparty: коллекция `Account`, `DefaultPriceType`
- ProductGroup: `VatRateId?` + `UseParentVat`
- RetailPoint: кассовые настройки (allowCustomPrice, discountMaxPercent, cashiers...)
- Store: slots, zones
### Сознательно не копируем MS:
- `CounterpartyKind` (Supplier/Customer/Both) — у нас enum, у MS теги. Оставляем для UX/фильтрации.
- `Store.Kind` (Warehouse vs RetailFloor) — у MS нет, нам нужно.
- `VatRate` как отдельная сущность — у MS число на товаре. У нас справочник ради локализации.
- `StockMovement` journal — у MS нет. Выбор архитектуры.
- `Product.IsWeighed` / `IsAlcohol` — упрощения под ритейл.
- `UnitOfMeasure.Symbol`, `DecimalPlaces` — UX.