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>
12 KiB
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:
- Логин как
demo-admin@food-market.local(sentinel). - Открыть
/catalog/products→ в шапке «30 894 записей» ≥ 30000 ✓. - Поиск по частичному названию «шоколад» → ≥ 1 строка ✓.
- Поиск по штрихкоду
4820039807373→ ≥ 1 строка ✓. - Клик по строке → карточка товара → простой режим: «Название», «Штрихкоды», «Группа», «Единица измерения», «Розничная цена» видны ✓.
- В 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→ GETdownloadHrefс токеном →IObjectStorage.SaveAsync→ProductImage. - Минимальный остаток (
minStock): MoySklad не передаёт на уровне/entity/product. Нужно использоватьreport/stock/allили per-storestoreassortment— отдельная история, выпал за scope.
Итог
- Каталог «Демо-магазин» на
https://test.admin.food-market.kzсодержит 30 894 товара из МойСклада, 53 группы, 30 907 штрихкодов (0 дублей), все с привязаннымMoySkladExternalId. Токен у организации не хранится. - Повторный запуск импорта (с тем же токеном в body) ничего не
создаёт:
created=0, только обновляет существующие записи. 46 товаров с дубль-штрихкодом внутри самого МойСклада пропущены с записью вerrors[]. - Прод,
global.json,deploy/*/nginxне трогались.