mcpField
M
marketplace-fulfillment

marketplace-fulfillment

github.com/vtex開発471.0k 回インストール2 週間前に更新
リポジトリ

marketplace-fulfillment について

VTEX の外部セラー向けフルフィルメント実装スキルです。在庫・チェックアウトのシミュレーション、注文予約と出荷、OMS 経由の請求書・追跡情報更新を扱います。

主な機能

  1. 01フルフィルメントシミュレーションと配送方法に対応
  2. 02予約を作成してorderIdを返却
  3. 03注文の出荷承認を処理
  4. 04分割出荷を含む請求書を送信
  5. 05追跡情報を別途更新

活用シーン

  1. 01VTEXの商品インデックスとチェックアウトに接続
  2. 02市場注文をセラーの予約・注文に変換
  3. 03出荷の請求書と追跡情報を送信

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/vtex/skills --skill marketplace-fulfillment

インストール後、Claude Code で/marketplace-fulfillmentを入力して読み込まれたか確認します。

詳細

フルフィルメント、シミュレーション、注文、OMS のフォローアップ

このスキルを使用する場面

外部セラーの統合を構築するときに、このスキルを使用します。VTEX は在庫状況、配送、チェックアウトのシミュレーション、注文の作成を、あなたのフルフィルメントのベース URL に転送します。出荷後には、マーケットプレイスの OMS API を呼び出して、請求書と追跡情報を送信します。

  • POST /pvt/orderForms/simulation を実装する(インデックス作成および/またはチェックアウト。顧客コンテキストの有無を問わない)
  • POST /pvt/orders を実装する(注文作成。予約を作成し、orderId として自システムの予約 ID を返す)
  • POST /pvt/orders/{sellerOrderId}/fulfill を処理する(承認後の注文出荷。パスには注文作成時に orderId として返したものと同じ ID を使用)
  • POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice で請求書通知を送信する(パスにはマーケットプレイスの注文 ID を指定)
  • PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber} で追跡情報を更新する
  • 分割出荷の部分請求を実装する

次の場合、このスキルは使用しないでください。

  • カタログまたは SKU の同期(marketplace-catalog-sync を参照)
  • Feed/Hook による注文イベントの取得(marketplace-order-hook を参照)
  • 一般的な API レート制限(marketplace-rate-limiting を参照)

判断ルール

外部セラープロトコル(セラーホストで実装)
  • フルフィルメントのシミュレーション — POST /pvt/orderForms/simulation。VTEX は商品インデックス作成時とチェックアウト時にこれを呼び出します。リクエストには items のみ(およびオプションのクエリパラメーター)が含まれる場合も、チェックアウトの完全なコンテキスト(items、postalCode、country、clientProfileData、shippingData、selectedSla など)が含まれる場合もあります。郵便番号やプロファイル情報がない場合(インデックス作成時によくあるケース)でも、レスポンスで各商品の在庫状況を示す必要があります。完全なコンテキストがある場合は、items、logisticsInfo(リクエストされた商品ごとに 1 件)、postalCode、country を返し、契約で求められるとおり allowMultipleDeliveries を true に設定します。items[].id はセラー SKU ID、items[].seller はマーケットプレイスアカウント上のセラー IDです。
  • レスポンスの形式 — 各 logisticsInfo[] 行はリクエストされた商品(itemIndex)に対応します。すべての配送オプション(自宅配送、および該当する場合は店舗受け取り)を含む slas[]、チャネルごとの在庫数を含む deliveryChannels[]、shipsTo、stockBalance を含めます。SLA フィールドには、price(商品ごとの配送料、セント単位)、shippingEstimate / shippingEstimateDate(例:5bd、30m)が含まれます。店舗受け取りの SLA には pickupStoreInfo(住所、friendlyName など)を含める必要があります。
  • SLA — シミュレーションハンドラーは 2.5 秒以内に応答する必要があります。超過すると、オファーは在庫なしとして扱われます。
  • 注文作成 — 注文の JSON 配列を指定して POST /pvt/orders を呼び出します。各注文について、自システムに予約を作成します。レスポンスは各注文に orderId を追加した同じ構造にする必要があります。この値は自システムの予約 IDであり、後続のプロトコル呼び出し(例:フルフィルメント承認のパスパラメーター)で sellerOrderId になります。
  • 注文出荷(フルフィルメント承認) — マーケットプレイスの承認後、VTEX は POST /pvt/orders/{sellerOrderId}/fulfill を呼び出します。このとき、sellerOrderId は注文作成時に返した orderId と同じ値です。本文には marketplaceOrderId と marketplaceOrderGroup が含まれます。自システムの予約を確定注文に変換します。レスポンス本文には date、marketplaceOrderId、orderId(セラー側の参照 ID)、receipt を含めます。
OMS API(セラー → マーケットプレイス)
  • POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice を使用して請求書を送信します。必須フィールドは type、invoiceNumber、invoiceValue(セント単位)、issuanceDate、および items 配列です。パスの marketplaceOrderId は VTEX マーケットプレイスの注文 ID であり、予約 ID ではありません。
  • 販売請求書(出荷)には type: "Output"、返品請求書には type: "Input" を使用します。
  • 配送業者から追跡情報が提供された後、PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber} を使用して、追跡情報を別途送信します。
  • 分割出荷の場合、荷物ごとに請求書を 1 件送信し、その荷物に含まれる商品だけを記載します。各 invoiceValue には、その商品分のみを反映させる必要があります。
  • 注文の請求書を送信した後は、返品請求書(type: "Input")を先に送信しない限り、注文をキャンセルできません。
  • フルフィルメントシミュレーションのエンドポイントは 2.5 秒以内に応答する必要があります。そうでない場合、商品は在庫切れとみなされます。

アーキテクチャ/データフロー(概要):

VTEX Checkout / indexation          External Seller                    VTEX OMS (marketplace)
       │                                   │                                   │
       │── POST /pvt/orderForms/simulation ▶│  Price, stock, SLAs               │
       │◀── 200 + items + logisticsInfo ───│                                   │
       │                                   │                                   │
       │── POST /pvt/orders (array) ───────▶│  Create reservation               │
       │◀── same + orderId (reservation) ─│                                   │
       │                                   │                                   │
       │── POST /pvt/orders/{id}/fulfill ──▶│  Commit / pick pack               │
       │◀── date, marketplaceOrderId, ... ─│                                   │
       │                                   │── POST .../invoice ────────────────▶│
       │                                   │── PATCH .../invoice/{n} ─────────▶│

厳守すべき制約

制約:OMS パスのマーケットプレイス注文 ID

/api/oms/pvt/orders/{orderId}/... の {orderId} には、VTEX マーケットプレイスの注文 ID(OMS)を指定する必要があります。POST /pvt/orders で返した orderId(予約 ID)ではありません。請求書または追跡情報の API を呼び出す前に、プロトコル(注文確定ペイロード、フルフィルメントの本文、またはイベント)から marketplaceOrderId を対応付けてください。

重要な理由

OMS URL に予約 ID を使用すると、マーケットプレイスの注文と一致しません。請求書と追跡情報が顧客の注文に紐付けられません。

検出方法

POST /pvt/orders のレスポンスの orderId と POST .../oms/.../invoice の両方に同じ変数を、対応付けを行わずに使用している場合 → 停止してください。

正しい例

// reservationId from your POST /pvt/orders response; marketplaceOrderId from VTEX payload
await omsClient.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, payload);

誤った例

await omsClient.post(`/api/oms/pvt/orders/${reservationId}/invoice`, payload);

制約:フルフィルメントシミュレーションの契約と応答時間

セラーは、すべてのリクエストに対して有効な items 配列を返す POST /pvt/orderForms/simulation を実装する必要があります。リクエストにチェックアウトのコンテキスト(例:postalCode、clientProfileData、shippingData)が含まれる場合、レスポンスには、関連するすべてのモード(提供する場合は店舗受け取りも含む)に対応した logisticsInfo と slas、および必要な場合は allowMultipleDeliveries: true を含める必要があります。ハンドラーは 2.5 秒以内に処理を完了する必要があります。

重要な理由

logisticsInfo が不完全、または SLA がないと、チェックアウトの配送方法選択が機能しません。応答が遅いと、オファーが利用不可と判定され、コンバージョンに悪影響を及ぼします。

検出方法

シミュレーションのリクエストに shippingData が含まれていたのに、価格付きの items だけを返し、logisticsInfo を省略している場合 → 警告します。キャッシュなしで p95 レイテンシが 2 秒に近づいている場合 → 警告します。

正しい例

// Pseudocode: branch on whether checkout context is present
if (hasCheckoutContext(req.body)) {
  return res.json({
    country: req.body.country,
    items: pricedItems,
    logisticsInfo: buildLogisticsPerItem(pricedItems, req.body),
    postalCode: req.body.postalCode,
    allowMultipleDeliveries: true,
  });
}
return res.json({ items: availabilityOnlyItems /* + minimal logistics if required */ });

誤った例

// WRONG: Full checkout body but response omits logisticsInfo / SLAs
res.json({ items: pricedItemsOnly });

制約:注文確定時にセラーの orderId(予約 ID)を返す

POST /pvt/orders は、注文の JSON 配列を受け付けます。レスポンスには同じ注文を含め、各要素の orderId に、セラーシステムの 予約識別子を設定する必要があります。VTEX はこの値を POST /pvt/orders/{sellerOrderId}/fulfill の sellerOrderId として使用します。

重要な理由

orderId を省略したり、架空の orderId を再利用したりすると、マーケットプレイスの注文フローと予約の紐付けが壊れ、ディスパッチのルーティングが正しく行われなくなります。

検出方法

ハンドラーが orderId を追加せずに 200 を返す場合、または配列ではなく単一のオブジェクトを返す場合 → 警告します。

正しい例

app.post("/pvt/orders", (req, res) => {
  const orders = req.body as Array<Record<string, unknown>>;
  const out = orders.map((order) => {
    const reservationId = createReservation(order);
    return { ...order, orderId: reservationId, followUpEmail: "" };
  });
  res.json(out);
});

誤った例

app.post("/pvt/orders", (req, res) => {
  createReservation(req.body);
  res.status(200).send(); // WRONG: missing orderId echo
});

制約:必要なすべてのフィールドを含む正しい形式の請求書を送信する

請求書通知には、type、invoiceNumber、invoiceValue、issuanceDate、items 配列を必ず含めてください。invoiceValue はセント単位で指定してください。items 配列には注文の商品と一致する項目を含めてください。

重要な理由

必須フィールドが欠けていると、API によって請求書が 400 Bad Request で拒否され、注文が「handling」ステータスのままになります。invoiceValue が正しくない場合(ドル単位で指定するなど)、マーケットプレイスとの照合作業で金額の不一致が発生します。

検出方法

請求書通知のペイロードに invoiceNumber、invoiceValue、issuanceDate、または items が欠けている場合は、必須フィールドの不足について警告してください。invoiceValue がドル単位と思われる場合(例:9990 ではなく 99.90)は、セントへの換算について警告してください。

正しい例

import axios, { AxiosInstance } from "axios";

interface InvoiceItem {
  id: string;
  quantity: number;
  price: number; // in cents
}

interface InvoicePayload {
  type: "Output" | "Input";
  invoiceNumber: string;
  invoiceValue: number; // total in cents
  issuanceDate: string; // ISO 8601
  invoiceUrl?: string;
  invoiceKey?: string;
  courier?: string;
  trackingNumber?: string;
  trackingUrl?: string;
  items: InvoiceItem[];
}

async function sendInvoiceNotification(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoice: InvoicePayload
): Promise<void> {
  // Validate required fields before sending
  if (!invoice.invoiceNumber) {
    throw new Error("invoiceNumber is required");
  }
  if (!invoice.invoiceValue || invoice.invoiceValue <= 0) {
    throw new Error("invoiceValue must be a positive number in cents");
  }
  if (!invoice.issuanceDate) {
    throw new Error("issuanceDate is required");
  }
  if (!invoice.items || invoice.items.length === 0) {
    throw new Error("items array is required and must not be empty");
  }

  // Warn if invoiceValue looks like it's in dollars instead of cents
  if (invoice.invoiceValue < 100 && invoice.items.length > 0) {
    console.warn(
      `Warning: invoiceValue ${invoice.invoiceValue} seems very low. ` +
        `Ensure it's in cents (e.g., 9990 for $99.90).`
    );
  }

  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, invoice);
}

// Example usage:
async function invoiceOrder(
  client: AxiosInstance,
  marketplaceOrderId: string
): Promise<void> {
  await sendInvoiceNotification(client, marketplaceOrderId, {
    type: "Output",
    invoiceNumber: "NFE-2026-001234",
    invoiceValue: 15990, // $159.90 in cents
    issuanceDate: new Date().toISOString(),
    invoiceUrl: "https://invoices.example.com/NFE-2026-001234.pdf",
    invoiceKey: "35260614388220000199550010000012341000012348",
    items: [
      { id: "123", quantity: 1, price: 9990 },
      { id: "456", quantity: 2, price: 3000 },
    ],
  });
}

誤った例

// WRONG: Missing required fields, value in dollars instead of cents
async function sendBrokenInvoice(
  client: AxiosInstance,
  marketplaceOrderId: string
): Promise<void> {
  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
    // Missing 'type' field — API may reject or default incorrectly
    invoiceNumber: "001234",
    invoiceValue: 159.9, // WRONG: dollars, not cents — causes financial mismatch
    // Missing 'issuanceDate' — API will reject with 400
    // Missing 'items' — API cannot match invoice to order items
  });
}

制約:出荷後、速やかに追跡情報を更新する

追跡情報は、配送業者から提供され次第、必ず送信してください。既存の請求書に追跡情報を追加するには、PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber} を使用してください。

重要な理由

追跡情報の更新が遅れると、顧客はマーケットプレイスで配送状況を確認できません。注文は「invoiced」ステータスのままとなり、「delivering」、さらに「delivered」へと進みません。これにより、カスタマーサポートへの問い合わせが発生し、販売者の評判を損ないます。

検出方法

追跡情報をリアルタイムで送信せず、毎日の更新用にまとめている場合は、追跡情報の迅速な更新について警告してください。配送業者からまだ情報が提供されていないのに、初回の請求書呼び出しに追跡情報が含まれている場合(ハードコードされた値、または空の値)も警告してください。

正しい例

interface TrackingUpdate {
  courier: string;
  trackingNumber: string;
  trackingUrl?: string;
  isDelivered?: boolean;
}

async function updateOrderTracking(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string,
  tracking: TrackingUpdate
): Promise<void> {
  await client.patch(
    `/api/oms/pvt/orders/${marketplaceOrderId}/invoice/${invoiceNumber}`,
    tracking
  );
}

// Send tracking as soon as carrier provides it
async function onCarrierPickup(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string,
  carrierData: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
  await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
    courier: carrierData.name,
    trackingNumber: carrierData.trackingId,
    trackingUrl: carrierData.trackingUrl,
  });
  console.log(
    `Tracking updated for marketplace order ${marketplaceOrderId}: ${carrierData.trackingId}`
  );
}

// Update delivery status when confirmed
async function onDeliveryConfirmed(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string
): Promise<void> {
  await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
    courier: "",
    trackingNumber: "",
    isDelivered: true,
  });
  console.log(`Marketplace order ${marketplaceOrderId} marked as delivered`);
}

誤った例

// WRONG: Sending empty/fake tracking data with the invoice
async function invoiceWithFakeTracking(
  client: AxiosInstance,
  marketplaceOrderId: string
): Promise<void> {
  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
    type: "Output",
    invoiceNumber: "NFE-001",
    invoiceValue: 9990,
    issuanceDate: new Date().toISOString(),
    items: [{ id: "123", quantity: 1, price: 9990 }],
    // WRONG: Hardcoded tracking — carrier hasn't picked up yet
    courier: "TBD",
    trackingNumber: "PENDING",
    trackingUrl: "",
  });
  // Customer sees "PENDING" as tracking number — useless information
}

制約:分割出荷時の部分請求書発行に対応する

注文を複数の荷物に分けて出荷する場合、各出荷分には、その荷物に含まれる商品のみを記載した請求書を必ず発行してください。invoiceValue には、その出荷分の商品だけの金額を反映してください。

重要な理由

一部の商品しか出荷していないのに、注文全体の金額を記載した請求書を1通だけ送信すると、金額の不一致が発生します。マーケットプレイスで支払いを正しく照合できず、注文ステータスが適切に進まない可能性があります。

検出方法

部分出荷なのに、注文全体の金額を記載した請求書を1通だけ送信している場合は、部分請求書発行について警告してください。items 配列が実際に出荷される商品と一致しない場合も警告してください。

正しい例

interface OrderItem {
  id: string;
  name: string;
  quantity: number;
  price: number; // per unit in cents
}

interface Shipment {
  items: OrderItem[];
  invoiceNumber: string;
}

async function sendPartialInvoices(
  client: AxiosInstance,
  marketplaceOrderId: string,
  shipments: Shipment[]
): Promise<void> {
  for (const shipment of shipments) {
    // Calculate value for only the items in this shipment
    const shipmentValue = shipment.items.reduce(
      (total, item) => total + item.price * item.quantity,
      0
    );

    await sendInvoiceNotification(client, marketplaceOrderId, {
      type: "Output",
      invoiceNumber: shipment.invoiceNumber,
      invoiceValue: shipmentValue,
      issuanceDate: new Date().toISOString(),
      items: shipment.items.map((item) => ({
        id: item.id,
        quantity: item.quantity,
        price: item.price,
      })),
    });

    console.log(
      `Partial invoice ${shipment.invoiceNumber} sent for marketplace order ${marketplaceOrderId}: ` +
        `${shipment.items.length} items, value=${shipmentValue}`
    );
  }
}

// Example: Order with 3 items shipped in 2 packages
await sendPartialInvoices(client, "vtex-marketplace-order-id-12345", [
  {
    invoiceNumber: "NFE-001-A",
    items: [
      { id: "sku-1", name: "Laptop", quantity: 1, price: 250000 },
    ],
  },
  {
    invoiceNumber: "NFE-001-B",
    items: [
      { id: "sku-2", name: "Mouse", quantity: 1, price: 5000 },
      { id: "sku-3", name: "Keyboard", quantity: 1, price: 12000 },
    ],
  },
]);

誤った例

// WRONG: Sending full order value for partial shipment
async function wrongPartialInvoice(
  client: AxiosInstance,
  marketplaceOrderId: string,
  totalOrderValue: number,
  shippedItems: OrderItem[]
): Promise<void> {
  await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
    type: "Output",
    invoiceNumber: "NFE-001-A",
    invoiceValue: totalOrderValue, // WRONG: Full order value, not partial
    issuanceDate: new Date().toISOString(),
    items: shippedItems.map((item) => ({
      id: item.id,
      quantity: item.quantity,
      price: item.price,
    })),
    // invoiceValue doesn't match sum of items — financial mismatch
  });
}

推奨パターン

フルフィルメントのシミュレーションを実装する

POST /pvt/orderForms/simulation を登録してください。items[](id = 販売者の SKU、seller = マーケットプレイス上の販売者 ID)を解析します。リクエストボディに postalCode / clientProfileData がない場合は、インデックス作成として扱い、在庫状況(および契約上必要な場合は最小限の配送情報)を返してください。リクエストボディにチェックアウト用フィールドが含まれる場合は、itemIndex ごとに logisticsInfo を構築し、slas(配送と、該当する場合は pickupStoreInfo を含む店舗受け取り)、deliveryChannels、stockBalance を設定し、country / postalCode を指定して、allowMultipleDeliveries: true に設定してください。CPU と I/O の使用量を制限し、2.5 秒未満に収めてください。

import { RequestHandler } from "express";

const fulfillmentSimulation: RequestHandler = async (req, res) => {
  const body = req.body as Record<string, unknown>;
  const items = body.items as Array<{ id: string; quantity: number; seller: string }>;
  const hasCheckoutContext = Boolean(body.postalCode && body.shippingData);

  const pricedItems = await priceAndStockForItems(items);

  if (!hasCheckoutContext) {
    res.json({ items: pricedItems, logisticsInfo: minimalLogistics(pricedItems) });
    return;
  }

  res.json({
    country: body.country,
    postalCode: body.postalCode,
    items: pricedItems,
    logisticsInfo: buildFullLogistics(pricedItems, body),
    allowMultipleDeliveries: true,
  });
};

function minimalLogistics(
  pricedItems: Array<{ id: string; requestIndex: number }>
): unknown[] {
  return pricedItems.map((_, i) => ({
    itemIndex: i,
    quantity: 1,
    shipsTo: ["USA"],
    slas: [],
    stockBalance: "",
    deliveryChannels: [{ id: "delivery", stockBalance: "" }],
  }));
}

function buildFullLogistics(
  pricedItems: Array<{ id: string; requestIndex: number }>,
  _checkoutBody: unknown
): unknown[] {
  // Replace with SLA / carrier rules derived from checkoutBody
  return pricedItems.map((_, i) => ({
    itemIndex: i,
    quantity: 1,
    shipsTo: ["USA"],
    slas: [],
    stockBalance: 0,
    deliveryChannels: [],
  }));
}

async function priceAndStockForItems(
  items: Array<{ id: string; quantity: number; seller: string }>
): Promise<Array<Record<string, unknown>>> {
  return items.map((item, requestIndex) => ({
    id: item.id,
    quantity: item.quantity,
    seller: item.seller,
    measurementUnit: "un",
    merchantName: null,
    price: 0,
    priceTags: [],
    priceValidUntil: null,
    requestIndex,
    unitMultiplier: 1,
    attachmentOfferings: [],
  }));
}
注文の作成(予約)を実装する

POST /pvt/orders を登録してください。リクエストボディは配列です。各注文を予約として保存し、orderId(予約キー)と通常は followUpEmail を加えた同じオブジェクトを返してください。この orderId は、VTEX が POST /pvt/orders/{sellerOrderId}/fulfill の sellerOrderId として渡す値です。

import { RequestHandler } from "express";

function createReservation(_order: Record<string, unknown>): string {
  return `res-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}

const orderPlacement: RequestHandler = (req, res) => {
  const orders = req.body as Array<Record<string, unknown>>;
  const out = orders.map((order) => ({
    ...order,
    orderId: createReservation(order),
    followUpEmail: "",
  }));
  res.json(out);
};
フルフィルメント承認エンドポイントを実装する

このエンドポイントは、支払いが承認されたときにマーケットプレイスから呼び出されます。

import express, { RequestHandler } from "express";

interface FulfillOrderRequest {
  marketplaceOrderId: string;
  marketplaceOrderGroup: string;
}

interface OrderMapping {
  /** Same id returned as orderId from POST /pvt/orders (reservation) */
  sellerOrderId: string;
  marketplaceOrderId: string;
  items: OrderItem[];
  status: string;
}

// Store for order mappings — use a real database in production
const orderStore = new Map<string, OrderMapping>();

const authorizeFulfillmentHandler: RequestHandler = async (req, res) => {
  const sellerOrderId = req.params.sellerOrderId;
  const { marketplaceOrderId, marketplaceOrderGroup }: FulfillOrderRequest = req.body;

  console.log(
    `Fulfillment authorized: reservation=${sellerOrderId}, marketplaceOrder=${marketplaceOrderId}, group=${marketplaceOrderGroup}`
  );

  // Store the marketplace order ID mapping
  const order = orderStore.get(sellerOrderId);
  if (!order) {
    res.status(404).json({ error: "Order not found" });
    return;
  }

  order.marketplaceOrderId = marketplaceOrderId;
  order.status = "fulfillment_authorized";
  orderStore.set(sellerOrderId, order);

  // Trigger fulfillment process asynchronously
  enqueueFulfillment(sellerOrderId).catch(console.error);

  res.status(200).json({
    date: new Date().toISOString(),
    marketplaceOrderId,
    // Echo seller reservation id (same as path param / placement orderId)
    orderId: sellerOrderId,
    receipt: null,
  });
};

async function enqueueFulfillment(sellerOrderId: string): Promise<void> {
  console.log(`Enqueued fulfillment for ${sellerOrderId}`);
}

const app = express();
app.use(express.json());
app.post("/pvt/orders/:sellerOrderId/fulfill", authorizeFulfillmentHandler);
フルフィルメント後に請求書を送信する

注文の梱包が完了し、請求書が作成されたら、請求書通知を送信してください。

async function fulfillAndInvoice(
  client: AxiosInstance,
  order: OrderMapping
): Promise<void> {
  // Generate invoice from your invoicing system
  const invoice = await generateInvoice(order);

  // Send invoice notification to VTEX marketplace
  await sendInvoiceNotification(client, order.marketplaceOrderId, {
    type: "Output",
    invoiceNumber: invoice.number,
    invoiceValue: invoice.totalCents,
    issuanceDate: invoice.issuedAt.toISOString(),
    invoiceUrl: invoice.pdfUrl,
    invoiceKey: invoice.accessKey,
    items: order.items.map((item) => ({
      id: item.id,
      quantity: item.quantity,
      price: item.price,
    })),
  });

  console.log(
    `Invoice ${invoice.number} sent for order ${order.marketplaceOrderId}`
  );
}

async function generateInvoice(order: OrderMapping): Promise<{
  number: string;
  totalCents: number;
  issuedAt: Date;
  pdfUrl: string;
  accessKey: string;
}> {
  const totalCents = order.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
  return {
    number: `NFE-${Date.now()}`,
    totalCents,
    issuedAt: new Date(),
    pdfUrl: `https://invoices.example.com/NFE-${Date.now()}.pdf`,
    accessKey: "35260614388220000199550010000012341000012348",
  };
}
配送業者が集荷したら追跡情報を送信する
async function handleCarrierPickup(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string,
  carrier: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
  await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
    courier: carrier.name,
    trackingNumber: carrier.trackingId,
    trackingUrl: carrier.trackingUrl,
  });

  console.log(
    `Tracking ${carrier.trackingId} sent for marketplace order ${marketplaceOrderId}`
  );
}
配達を確認する
async function handleDeliveryConfirmation(
  client: AxiosInstance,
  marketplaceOrderId: string,
  invoiceNumber: string
): Promise<void> {
  await client.patch(
    `/api/oms/pvt/orders/${marketplaceOrderId}/invoice/${invoiceNumber}`,
    {
      isDelivered: true,
      courier: "",
      trackingNumber: "",
    }
  );

  console.log(`Marketplace order ${marketplaceOrderId} marked as delivered`);
}
完全な例
import axios, { AxiosInstance } from "axios";

function createMarketplaceClient(
  accountName: string,
  appKey: string,
  appToken: string
): AxiosInstance {
  return axios.create({
    baseURL: `https://${accountName}.vtexcommercestable.com.br`,
    headers: {
      "Content-Type": "application/json",
      "X-VTEX-API-AppKey": appKey,
      "X-VTEX-API-AppToken": appToken,
    },
    timeout: 10000,
  });
}

async function completeFulfillmentFlow(
  client: AxiosInstance,
  order: OrderMapping
): Promise<void> {
  // 1. Fulfill and invoice
  await fulfillAndInvoice(client, order);

  // 2. When carrier picks up, send tracking
  const carrierData = await waitForCarrierPickup(order.sellerOrderId);
  const invoice = await getLatestInvoice(order.sellerOrderId);

  await handleCarrierPickup(
    client,
    order.marketplaceOrderId,
    invoice.number,
    carrierData
  );

  // 3. When delivered, confirm
  await waitForDeliveryConfirmation(order.sellerOrderId);
  await handleDeliveryConfirmation(
    client,
    order.marketplaceOrderId,
    invoice.number
  );
}

async function waitForCarrierPickup(
  sellerOrderId: string
): Promise<{ name: string; trackingId: string; trackingUrl: string }> {
  // Replace with actual carrier integration
  return {
    name: "Correios",
    trackingId: "BR123456789",
    trackingUrl: "https://tracking.example.com/BR123456789",
  };
}

async function getLatestInvoice(
  sellerOrderId: string
): Promise<{ number: string }> {
  // Replace with actual invoice lookup
  return { number: `NFE-${sellerOrderId}` };
}

async function waitForDeliveryConfirmation(
  sellerOrderId: string
): Promise<void> {
  // Replace with actual delivery confirmation logic
  console.log(`Waiting for delivery confirmation: ${sellerOrderId}`);
}

よくある失敗

  • インデックス作成のシミュレーションをチェックアウトと同じように扱う。 同じエンドポイントに、インデックス作成時には(顧客の所在地を含まない)最小限の本文が、チェックアウト時には詳細な本文が送信されます。最小限のリクエストに対してチェックアウト相当の logisticsInfo を返すのは、不要な処理となる場合があります。チェックアウトのリクエストに対して、SLA と logisticsInfo を含めず、価格だけを返すと、配送方法を選択できなくなります。

  • 注文作成時に orderId を省略する。 VTEX は、各注文をエコーバックし、orderId に予約 ID を設定した配列形式のレスポンスを想定しています。空の 200 レスポンスや orderId の欠落は、注文処理を滞らせます。

  • OMS の請求書 URL で予約の orderId を使用する。 注文作成後、/api/oms/pvt/orders/... を呼び出す際は、プロトコルの marketplaceOrderId を使用する必要があります。2 つの ID を混同すると、404 エラーや、請求書/追跡情報の処理が失敗してもエラーが表示されない事態につながります。

  • フルフィルメントの承認前に請求書を送信する。 セラーが、マーケットプレイスからフルフィルメント承認コールバックを受け取る前に、注文作成直後に請求書通知を送信します。支払いがまだ保留中または審査中の可能性があります。承認前に請求書を発行すると、請求書が拒否されたり、注文が不整合な状態になったりすることがあります。POST /pvt/orders/{sellerOrderId}/fulfill を受信した後にのみ、請求書を送信してください。

  • キャンセル時の返品請求書を処理しない。 セラーが返品請求書を先に送信せず、キャンセル注文エンドポイントを直接呼び出して、請求書発行済みの注文をキャンセルしようとします。注文が「invoiced」ステータスになると、返品請求書(type: "Input")なしではキャンセルできません。キャンセル注文 API はリクエストを拒否します。

// Correct: Send return invoice before canceling an invoiced order
async function cancelInvoicedOrder(
  client: AxiosInstance,
  marketplaceOrderId: string,
  originalItems: InvoiceItem[],
  originalInvoiceValue: number
): Promise<void> {
  // Step 1: Send return invoice (type: "Input")
  await sendInvoiceNotification(client, marketplaceOrderId, {
    type: "Input", // Return invoice
    invoiceNumber: `RET-${Date.now()}`,
    invoiceValue: originalInvoiceValue,
    issuanceDate: new Date().toISOString(),
    items: originalItems,
  });

  // Step 2: Now cancel the order
  await client.post(
    `/api/marketplace/pvt/orders/${marketplaceOrderId}/cancel`,
    { reason: "Customer requested return" }
  );
}
  • フルフィルメントのシミュレーションが 2.5 秒のタイムアウトを超える。 セラーのフルフィルメントシミュレーションエンドポイントで、複雑なデータベースクエリや外部 API 呼び出しを実行した結果、応答時間の上限を超えます。VTEX マーケットプレイスがフルフィルメントシミュレーションのレスポンスを待つ時間は、最大 2.5 秒です。それを過ぎると、商品は在庫なし/非アクティブと見なされ、ストアフロントやチェックアウトに表示されなくなります。価格と在庫データを事前にキャッシュしてください。

レビューチェックリスト

  • POST /pvt/orderForms/simulation は、最小限のリクエスト(インデックス作成)とチェックアウト形式のリクエストの両方を処理し、コンテキストがある場合は logisticsInfo と slas を返しますか?
  • POST /pvt/orders は配列を受け取り、各注文に orderId(予約 ID)を含めて返しますか?
  • POST /pvt/orders/{sellerOrderId}/fulfill は marketplaceOrderId と marketplaceOrderGroup を読み取り、sellerOrderId が注文作成時の予約と一致するか確認しますか?
  • OMS の /invoice および PATCH .../invoice の呼び出しでは、予約 ID ではなく marketplaceOrderId を使用していますか?
  • セラーはフルフィルメント承認コールバックを受信した後にのみ、物理的なフルフィルメントを開始しますか?
  • IO/BFF コネクターの場合、キャッシュとルートの選択は vtex-io のスキル(シミュレーションの SLA、データのスコープ)に沿っていますか?
  • 請求書ペイロードには、必須フィールド(type、invoiceNumber、invoiceValue、issuanceDate、items)がすべて含まれていますか?
  • invoiceValue はドルではなくセント単位ですか?
  • 追跡情報は、運送業者から実データが提供された後に別途送信されますか(ハードコードされたプレースホルダーではありませんか)?
  • 分割配送の場合、各請求書にはその荷物の商品のみとその金額のみが含まれていますか?
  • 請求書発行済みの注文のキャンセルは、最初に返品請求書(type: "Input")を通じて処理されていますか?
  • フルフィルメントシミュレーションエンドポイントは 2.5 秒以内に応答しますか?

参考資料

VTEX は、GitHub リポジトリ vtex-apps/external-seller-example で、外部セラーサービスのオープンソースのリファレンス実装も管理しています(ひな形として役立ちます。動作は上記の公式プロトコルドキュメントに合わせてください)。

コメント