Клиент на 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— организация продавца; без неё позиции заводятся от организации ключа, у личного ключа — от его владельца; заказы организации — с еёorganizationIdpublish: 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 ярлыка либо накладной отправления, как у клиента на PHPdispatchиcancelповторяют сбой тем же ключом идемпотентности — действие не выполняется дважды- Смену статуса заказа сайт узнаёт уведомлением «Смена статуса заказа» через клиент на PHP либо опросом
fulfillments
Не нашли ответ на свой вопрос?
Создать обращение