Клиент на TypeScript

Что делает

Пакет @opotopo/seller-api — клиент Seller API на TypeScript для сайта на Node.js: выгружает каталог на площадку, снимает позиции, выпавшие из каталога, и ведёт заказы. Зависимостей нет, запросы идут через fetch. Файлы позиций передаются адресами http(s); загрузка файлов, приём уведомлений и выдача данных — у клиента на PHP.

Пакет открытый, лицензия MIT: код и выпуски — github.com/AdzerKI/seller-api-typescript, поддерживаемые версии Node.js — страница пакета в npm.

Установка

npm install @opotopo/seller-api

Клиент

import { SellerApiClient } from '@opotopo/seller-api';

const client = new SellerApiClient({
  url: 'https://api.opotopo.com',
  key: process.env.OPOTOPO_API_KEY!,
  organizationId: process.env.OPOTOPO_ORGANIZATION_ID,
});
  • key — ключ раздела «API» с разрешениями записи и снятия объявлений, для заказов — и чтения и исполнения заказов
  • organizationId — организация продавца; без неё позиции заводятся от организации ключа, у личного ключа — от его владельца; заказы организации — с её organizationId
  • publish: false оставляет новые позиции черновиками
  • retryTimes и retrySleepMs — число попыток запроса и пауза между ними, если площадка не прислала Retry-After; timeoutMs — предел ожидания ответа
  • fetch — своя реализация fetch, например через прокси; без неё — встроенная в Node.js

Каталог

Каталог сайта — объект с интерфейсом CatalogSource: mapping() отдаёт сопоставление позиции, records() — записи каталога с интерфейсом ItemRecord перечнем либо асинхронным обходом.

import type { CatalogMapping, CatalogSource, ItemRecord } from '@opotopo/seller-api';

class DrawingCatalog implements CatalogSource {
  mapping(): CatalogMapping {
    return {
      category: 'opotopoCategoryId',
      fields: { sku: 'article', title: 'name', description: 'description', price: 'price' },
      attributes: { 'Грузоподъёмность, т': 'capacity' },
      images: { cover: 'coverUrl', second: 'secondPhotoUrl', gallery: 'photoUrls' },
      model3d: { url: 'modelUrl', orientation: 'modelOrientation' },
      variants: [
        { name: 'Чертежи', price: 'price', files: 'drawingUrls' },
        { name: 'Чертежи + 3D', price: 'priceWithModel', files: 'drawingUrls', includes3dModel: true },
      ],
    };
  }

  async *records(): AsyncIterable<ItemRecord> {
    for (const row of await db.drawings()) {
      yield { key: () => String(row.id), value: (path) => row[path] };
    }
  }
}
  • key() — постоянный ключ записи на сайте
  • value() — значение по пути из сопоставления
  • Ключи сопоставления — те же, что у клиента на PHP, путь категории своего магазина — shopCategory, ключи варианта — includes3dModel: true для варианта с 3D-моделью, путь срока обновлений updateMonths и путь фото image
  • Изображения и файлы варианта — адрес http(s) либо перечень адресов; иное значение — отказ позиции до отправки

Реестр позиций

Реестр — объект с интерфейсом ItemStore: состояние позиций каталога между прогонами на стороне сайта, например таблицей его базы.

import type { ItemState, ItemStore } from '@opotopo/seller-api';

const store: ItemStore = {
  async load(catalog) {
    const rows: ItemState[] = await db.opotopoItems.findMany({ where: { catalog } });
    return new Map(rows.map((row) => [row.key, row]));
  },
  async save(catalog, state) {
    await db.opotopoItems.upsert({
      where: { catalog_key: { catalog, key: state.key } },
      create: { catalog, ...state },
      update: state,
    });
  },
  async delete(catalog, keys) {
    await db.opotopoItems.deleteMany({ where: { catalog, key: { in: [...keys] } } });
  },
};
  • load() — записи каталога по ключу записи
  • save() — запись позиции: ключ записи, артикул, объявление, отпечаток позиции, отказ последней отправки с деталями и время последней успешной; уникальный ключ — каталог и ключ записи
  • delete() — снятие записей позиций, выпавших из каталога

Синхронизация

import { Synchronizer } from '@opotopo/seller-api';

const synchronizer = new Synchronizer(client, store, 100);
const catalogs = { drawings: new DrawingCatalog() };

let report = await synchronizer.run(catalogs);
report = await synchronizer.run(catalogs, { only: 'drawings' });
report = await synchronizer.run(catalogs, { full: true });
  • третий аргумент — размер пачки
  • Отправка изменённых позиций, пачки, повторы и снятие выпавших позиций — как у клиента на PHP; full: true отправляет все
  • gitSources итога — источник git отправленных позиций пакета: ключ развёртывания, адрес webhook и отказы забора версий, как у клиента на PHP
  • Неизвестный каталог в only — исключение RangeError
  • Ответ с ошибкой после всех попыток — исключение ApiError с кодом ответа status и телом ошибки problem, прогон прерывается
  • Прогон по расписанию — планировщиком сервера сайта; прогоны не должны накладываться

Отказы

SyncReport прогона несёт число заведённых created, обновлённых updated, неизменных skipped и снятых archived позиций и отказы failures: каталог, артикул, код и детали. Позиция с отказом повторяется следующим прогоном.

  • VALIDATION_ERROR с деталью URL_REQUIRED по пути значения — изображение либо файл варианта заданы не адресом
  • FILES_UNAVAILABLE — файлы позиции площадке недоступны
  • FILES_SCAN_REJECTED — файлы позиции не прошли проверку площадки, деталь scanRejections — причина по каждому файлу: FILE_INFECTED — найден вирус, FILE_SCAN_LIMITS_EXCEEDED — файл либо архив сверх пределов проверки; замените файл
  • прочие коды — «Справочник API» раздела «API»

Заказы

const week = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const page = await client.fulfillments({ statuses: ['PENDING'], from: week });
for (const order of page.content) {
  await client.dispatch(order.id, { carrierName: 'Курьер магазина', trackingNumber: 'TRK-1' });
}
await client.cancel(fulfillmentId, 'Нет в наличии');
const label = await client.shipmentDocument(shipmentId, 'LABEL');
  • fulfillments — заказы статусами и периодом оформления, новые первыми, страница — page и size; fulfillment — один заказ
  • dropOffPoints, dispatch, cancel и shipmentDocument — пункты приёма, передача, отмена и PDF ярлыка либо накладной отправления, как у клиента на PHP
  • dispatch и cancel повторяют сбой тем же ключом идемпотентности — действие не выполняется дважды
  • Смену статуса заказа сайт узнаёт уведомлением «Смена статуса заказа» через клиент на PHP либо опросом fulfillments
Статья была полезна?

Не нашли ответ на свой вопрос?

Создать обращение