food-market/docs/catalog-import-progress.md
nns 929d0b6a38
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
test(s32): stage-ui-catalog-import spec + финал в progress-doc
Playwright 1/1 passed против test.admin.food-market.kz после полной
заливки МойСклада в Демо-магазин:

- /catalog/products под demo-admin — 30 894 записей в шапке списка.
- Поиск по частичному названию «шоколад» → хотя бы одна строка.
- Поиск по штрихкоду 4820039807373 → хотя бы одна строка; клик →
  карточка товара в простом режиме (Название, Штрихкоды, Группа,
  Единица измерения, Розничная цена видны; «Типы цен» в sidebar нет).

Второй прогон импорта с тем же токеном в теле запроса → created=0,
updated=30894 (все существующие refresh'нуты). Идемпотентность по
MoySkladExternalId подтверждена.

Выборочная сверка 10 случайных товаров с МойСкладом (имя, штрихкоды,
цена продажи, закупочная) — 10/10 совпали ровно.

Token checks:
- Organizations.MoySkladToken IS NULL для «Демо-магазин» (токен
  никогда не сохранялся в БД, передавался только в теле запроса).
- /tmp/ms-token после импорта shred -u — локально ничего не осталось.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-04 23:46:11 +05:00

12 KiB
Raw Blame History

Catalog import МойСклад → «Демо-магазин» (stage)

Решение владельца. Стенд закрыт basic-auth'ом на gateway от интернета (внутренняя сеть 192.168.1.x — без пароля), поэтому можно залить полный каталог включая закупочные цены.

Токен МойСклада. /var/www/sklad/appsettings.Production.json, ключ MoySklad:Token, забирается через ssh + sudo на prod-vm. В логи / коммиты / docs / отчёты токен никогда не попадает. Передаётся только через env-переменную или stdin.

Границы. Не трогаем проект sklad, прод admin.food-market.kz, global.json, deploy/*/nginx.

Чек-лист

  • 1. Аудит текущего MoySklad-импорта + добор недостающих полей.
  • 2. Импорт в «Демо-магазин» на стейдже без дублей.
  • 3. Токен у организации стенда не сохранён.
  • 4. Отчёт: группы/товары + выборочная сверка 10 карточек.
  • 5. Playwright-проверка каталога изнутри сети + повторный запуск импорта → 0 новых.

Протокол выполнения

1. Аудит + добор

Было в MoySkladImportService:

  • ✅ товары, группы, штрихкоды (ean13/ean8/code128/gtin/upca/upce), цены продажи (первая «Розничная» или первая), закупочные (buyPrice → ReferencePrice), весовой признак (weighed), артикул (article ?? code).
  • ❌ upsert по внешнему id МойСклада — был по article, что ломает идемпотентность для товаров без article.
  • ❌ merge по штрихкоду с локальным дублем — просто пропускал с ошибкой «штрихкод занят».
  • ❌ skip архивных — импортировал их как IsActive=false.
  • ❌ UOM-mapping — всегда ставил «штука» (code=796).
  • ❌ auth-scheme — только Bearer; sklad на prod-vm использует Basic.
  • ⚠️ фото — у реального каталога (30 941 товар) images.meta.size=0 во всех выборках, фото в МойСкладе не загружены. Пропустили как «нечего импортировать» (см. gaps ниже).
  • ⚠️ мин. остаток — проверил MoySklad API /entity/product (keys: 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): поля minimumBalance или аналога у product нет. Остатки живут в report/stock, не в карточке товара → импортировать нечего.

Добавлено (Phase32a_MoySkladExternalId):

  • products.MoySkladExternalId varchar(64) + partial-unique index (OrgId, MoySkladExternalId) WHERE IS NOT NULL.
  • product_groups.MoySkladExternalId varchar(64) + аналогичный индекс.
  • MoySkladClient: auto-detect Basic vs Bearer по форме токена; StreamProductsAsync(includeArchived=false) при SkipArchived=true; GetAllUomsAsync для построения карты MS uom.id → MS code → наш code.
  • MoySkladImportService.ImportProductsAsync(options):
    • приоритет resolution: MS id → штрихкод → article;
    • SkipArchived=true → архивные товары в MoySklad не попадают в поток;
    • MergeByBarcode=true → локальный товар с таким же штрихкодом апдейтится, не плодится вторым;
    • дедуп штрихкодов внутри входа: первый выигрывает, остальные — в Errors + счётчик SkippedDuplicateBarcodeInSource;
    • UOM: MS uom.id → MS code → наш code; weighed=true → всегда «кг».
  • MoySkladImportResult расширен: MergedByBarcode, SkippedArchivedInSource, SkippedDuplicateBarcodeInSource, GroupsUpdated.
  • MoySkladImportController.ImportRequest — SkipArchived и MergeByBarcode (оба default true), токен всё ещё принимается в body и НЕ сохраняется в Organizations.MoySkladToken.

2. Импорт в «Демо-магазин»

Токен МойСклада получен безопасно:

ssh nns@192.168.1.190 "sudo cat /var/www/sklad/appsettings.Production.json" \
  | python3 -c '…MoySklad.Token…'  > /tmp/ms-token (0600)

В аутентификации — Basic (sklad хранит base64(login:password)), что auto-detect на нашей стороне подхватил.

Деплой: ~/deploy-stage.sh (api + web → test.admin.food-market.kz).

Запуск:

POST /api/admin/moysklad/import-products
Authorization: Bearer <demo-admin JWT>
{ "Token": "***", "OverwriteExisting": true,
  "SkipArchived": true, "MergeByBarcode": true }
→ { "jobId": "8bb5b13e-c675-402d-a30e-edf26b8848f7" }

Первый прогон — 376 секунд (6 мин 16 сек):

Метрика Значение
total (все активные в MoySklad) 30 941
created 30 894
merge-barcode (в локальный существующий) 1
skipped (без merge) 0
архивных в источнике пропущено 0 (35 архивных отфильтрованы на уровне API-stream)
дубль-штрихкод внутри MoySklad 46 (имена → errors[])
группы created 51
группы updated 1 (default «Продукты питания» → привязан MS id после совпадения по имени)

3. Токен не сохранён

Проверка:

SELECT COALESCE("MoySkladToken" IS NOT NULL, false)
FROM organizations WHERE "Id" = 'b7c403fe-7fc8-48af-9490-4a9a2eafe2c2';
→ f

Organizations.MoySkladToken IS NULL для «Демо-магазин». Токен был только в теле POST /api/admin/moysklad/import-products, в БД не записан. Локальный /tmp/ms-token после импорта shred -u.

4. Отчёт

По БД после первого импорта:

products.count where OrgId=demo = 30 894
product_groups.count where OrgId=demo = 53 (51 из MS + 1 updated + 1 default system)
product_barcodes.count where OrgId=demo = 30 907
distinct barcode codes = 30 907
duplicate rows = 0 ✓

Второй прогон импорта (идемпотентность) — 362 секунды:

Метрика Значение
total 30 941
created 0 ✓
updated 30 894 (все существующие refresh'нуты)
merge-barcode 0
groupsCreated 0
groupsUpdated 52

Второй прогон не создал ни одной новой записи — upsert по MS id работает как положено.

Выборочная сверка 10 товаров (случайный sample по БД → MoySklad API по тому же MS id, все совпадают 1:1):

MS id local name MS name штрихкоды розничная (local → MS) закупочная (local → MS)
47c17be8… пошехонский 350гр пошехонский 350гр ✓ 4820039807373 ✓ 2540 → 2540 ✓ 2030 → 2030
509f4a29… САРДИНЕЛЛА С Д/М 240ГР САРДИНЕЛЛА С Д/М 240ГР ✓ 4620004410894 ✓ 640 → 640 ✓ 512 → 512
982113cc… бьюти-блендер фрукты груша бьюти-блендер фрукты груша ✓ 6978461215094 ✓ 470 → 470 ✓ 300 → 300
785f457f… Печенье овсяное протеиновое «флэпджек» ш Печенье овсяное протеиновое «флэпджек» ш ✓ 4607018913849 ✓ 455 → 455 ✓ 350 → 350
637dcfee… Паста для шугаринга плотная 300гр Паста для шугаринга плотная 300гр ✓ 4870219750029 ✓ 1050 → 1050 ✓ 700 → 700
ee17df53… шоколад Альпен Гольд черника-йогуртовая шоколад Альпен Гольд черника-йогуртовая ✓ 7622201426804 ✓ 595 → 595 ✓ 349 → 349
2a132fa3… Пастила детская клубника и яблоко Пастила детская клубника и яблоко ✓ 4607942730734 ✓ 225 → 225 ✓ 150 → 150
6f8b9c68… Монастырский чай №26 против паразитов Монастырский чай №26 против паразитов ✓ 4631142248812 ✓ 1850 → 1850 ✓ 1400 → 1400
36f5aa58… Лейс стакс паприка Лейс стакс паприка ✓ 4690388108946 ✓ 1565 → 1565 ✓ 1250 → 1250
eddfb85b… Айран домашний 0,5л Айран домашний 0,5л ✓ 7915178548083 ✓ 590 → 590 ✓ 490 → 490

Соответствие полное (10/10) по имени, штрихкоду, цене продажи, закупочной цене.

5. Playwright UI

tests/e2e/scenarios/stage-ui-catalog-import.spec.ts — 1 тест, chromium-desktop, против https://test.admin.food-market.kz:

  1. Логин как demo-admin@food-market.local (sentinel).
  2. Открыть /catalog/products → в шапке «30 894 записей» ≥ 30000 ✓.
  3. Поиск по частичному названию «шоколад» → ≥ 1 строка ✓.
  4. Поиск по штрихкоду 4820039807373 → ≥ 1 строка ✓.
  5. Клик по строке → карточка товара → простой режим: «Название», «Штрихкоды», «Группа», «Единица измерения», «Розничная цена» видны ✓.
  6. В sidebar под простым режимом нет «Типы цен» ✓.

Прогон: 1 passed (9.6s). Скриншоты в tests/e2e/reports/: catalog-import-list.png, catalog-import-search-name.png, catalog-import-search-barcode.png, catalog-import-product-card.png.

Gaps / known-not-implemented

  • Фото товаров: сам MoySklad в этом аккаунте images не хранит (все продукты, что проверяли, — images.meta.size = 0). Download +upload в наш storage не реализовали — нет источника. Если в будущем у клиентов будут картинки, нужно будет добавить: /entity/product/{id}/images → GET downloadHref с токеном → IObjectStorage.SaveAsync → ProductImage.
  • Минимальный остаток (minStock): MoySklad не передаёт на уровне /entity/product. Нужно использовать report/stock/all или per-store storeassortment — отдельная история, выпал за scope.

Итог

  • Каталог «Демо-магазин» на https://test.admin.food-market.kz содержит 30 894 товара из МойСклада, 53 группы, 30 907 штрихкодов (0 дублей), все с привязанным MoySkladExternalId. Токен у организации не хранится.
  • Повторный запуск импорта (с тем же токеном в body) ничего не создаёт: created=0, только обновляет существующие записи. 46 товаров с дубль-штрихкодом внутри самого МойСклада пропущены с записью в errors[].
  • Прод, global.json, deploy/*/nginx не трогались.