mcpField
P
payment-ppp-pos

payment-ppp-pos

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

payment-ppp-pos について

payment-ppp-posは、VTEX Sales Appから物理POS端末で決済するコネクターの作成、拡張、デバッグを支援するスキルです。汎用EC決済ではなく、POS固有のPPP状態遷移とVTEX IO / PPFの制約を扱います。

主な機能

  1. 01POS決済フローのエンドポイント実装
  2. 02端末識別と結果ポーリングのガイド
  3. 03POS決済方法名の検証
  4. 04POSとECのカード処理の分離
  5. 05コネクターコードとテスト項目の提示

活用シーン

  1. 01VTEX IO向けPOSコネクターの作成
  2. 02既存PPPミドルウェアへのPOS対応追加
  3. 03端末識別や決済ポーリングのデバッグ
  4. 04POSコネクターの検証テスト準備

インストール

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

$npx skills add https://github.com/vtex/skills --skill payment-ppp-pos

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
examples.md
generation.md
ppf-scaffold.md

詳細

POS に適用する PPP(VTEX Sales App)

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

VTEX Sales App から、POS 端末で物理カードに請求する決済コネクターを作成、拡張、またはデバッグする場合に、このスキルを使用してください。

  • ユーザーが PPP + POS、POS コネクター、店舗内決済、Sales App のカード決済、決済端末、または Venda Direta Credito / Venda Direta Debito に言及している場合
  • 信頼できる情報源が Payment Provider Protocol for Point of Sale (POS) である場合
  • 目的が「一から実装する」という障壁を取り除くことである場合。一般的な EC コネクターを生成するのではなく、Payment Provider Framework の「Getting started」にある公式 PPF のサンプルをクローンするか、既存の PPP に POS をオーバーレイします。

手順で必要になったときに、関連ドキュメントを読み込んでください。最初の回答ですべてを読み込まないでください。

関連ドキュメント読み込むタイミング
generation.mdユーザーが POS コネクターの作成を依頼した場合。4 つの質問をしてから、記載された順序で実装します。
ppf-scaffold.mdホスティング先がVTEX IO / PPFの場合。公式 PPF サンプルアプリにオーバーレイファイルをコピーします。
examples.md期待される JSON、状態遷移表、または IO の再試行とスタンドアロンの通知が必要な場合。

以下の厳格な制約は、EC 向け PPP の慣習や PCI の慣習よりも優先されます。生成したファイルが制約に違反している場合は、ファイルの作成を続けず、先に修正してください。

以下の場合は、このスキルを既定の選択肢として使用しないでください。

  • POS メソッドを含まない EC 向け PPP エンドポイント、マニフェスト、ホモロゲーション形式 — payment-provider-protocol を使用してください
  • IO コネクターの接続設定(PaymentProvider、configuration.json、this.retry)— このスキルと併せて payment-provider-framework を使用してください
  • Pix、Boleto、またはブラウザーリダイレクト型の非同期メソッド — payment-async-flow を使用してください
  • EC 向けのカードデータと Secure Proxy — payment-pci-security を使用してください
  • Sales App のカート、PDP、またはメニュー UI 拡張機能 — sales-app-extensibility を使用してください

判断ルール

  • パートナーがすでに PPP ミドルウェアをホストしている場合を除き、VTEX IO / PPF を優先してください。Payment Provider Framework ガイドにある公式サンプルアプリをクローンして POS をオーバーレイし、空の Express アプリから始めないでください。
  • POS 決済も PPP です。6 つの決済フローエンドポイントを実装してから、このスキルの POS ルールをオーバーレイします。
  • authorize() は状態遷移です。POS の完了を待つことはなく、現在のフェーズのみを返します。コネクターは Create Payment に応答します。Gateway を次のフェーズに進める指示は送りません。端末アプリが終了した後、VTEX Sales App が Gateway コールバックを実行し、Create Payment を発行します(POS ガイドの手順 8)。その後、Wait for confirmation が Gateway をポーリングし、Gateway がさらに Create Payment を発行します(手順 9)。プロセッサーが最終ステータスを報告するまで callbackUrl に POST しないでください。保留中の再試行は不要であり、authorize 1 回につき約 1 秒余計にかかります。 | 保存されているフェーズ | 次の Create Payment を実行する主体 | Create Payment の戻り値 | |---|---|---| | (まだなし) | Sales App のチェックアウト → Gateway(ステップ 3) | identify-terminal を保存し、undefined + vtex.terminal-connector-app | | identify-terminal(シリアル番号なし) | Gateway / Sales App | submitUrl にシリアル番号が保存されるまで、同じターミナルアプリ | | identify-terminal(シリアル番号を保存済み) | ターミナルアプリ終了後の Sales App(ステップ 8) | プロセッサの決済を一度だけ開始し、undefined + vtex.challenge-wait-for-confirmation | | await-pos | 確認のポーリングを待機(ステップ 9) | 同じ待機アプリ。2 回目の決済は開始せず、再試行の POST も行わない | | approved / denied | 待機アプリ / IO の再試行後の Sales App | 最終ステータス + cardBrand + firstDigits + lastDigits |
  • Venda Direta Credito と Venda Direta Debito は非同期の対面カード決済方式です。status: "undefined" を返し、後から callbackUrl 経由で結果を確定します。同期型の Visa/Mastercard 承認として扱わないでください。
  • paymentMethod に基づいて分岐してください。PPF では、@vtex/payment-provider の isDirectSaleAuthorization を使用します(正規の DirectSale 名を使用し、型を絞り込むと card / secureProxyUrl はありません)。スタンドアロン HTTP では、同じ ASCII 文字列と比較してください。コネクターは e コマースと POS の両方に対応できます。
  • Gateway は POS 決済で購入者のカード情報を受け取りません。すべての card フィールドは null です。この処理経路では Secure Proxy を呼び出さないでください。
  • パートナーがカスタムアプリを構築する、文書化された理由がない限り、VTEX の既製の Payment App を使用してください。
    • ターミナルを識別する:vtex.terminal-connector-app
    • POS の結果を待つ:vtex.challenge-wait-for-confirmation
  • vtex.challenge-terminal-connector-app は使用しないでください。この名前は POS ガイドのフロー図の例として記載されていますが、実装契約と公開済みアプリは vtex.terminal-connector-app です。
  • paymentAppData.payload はネストされたオブジェクトではなく、シリアライズされた JSON 文字列です。PPF では、Authorizations.pending を使って保留中の応答を作成すれば、コンパイルエラーで検出できます。as AuthorizationResponse でこれらのオブジェクトを型変換しないでください。その型変換で誤りが隠れてしまいます。cardBrand、firstDigits、lastDigits は POS ガイドで必須ですが、SDK の応答型には含まれていません(@vtex/payment-provider 1.4.0 と 1.8.0 で確認済み)。型変換が妥当なのは、最終的な承認/拒否応答のみです。
  • 決済プロセッサと実 POS 間の通信は VTEX の対象外です。コネクターはターミナルを識別し、プロセッサのトランザクションを開始して、Gateway に approved または denied を報告するだけです。
  • VTEX IO を使用しない場合、callbackUrl は通知エンドポイントです。最終ステータスを POST してください(POS のカードフィールドを含めます)。VTEX IO を使用する場合、callbackUrl は再試行エンドポイントです。状態を保存してから、空の本文で POST してください。PaymentProvider のメソッド内では this.retry(request) を使用します。service.json の追加ルートでは new Payments(ctx.vtex).retry(callbackUrl) を使用します。これは this.retry と同じクライアントです(gateway-retry メトリクス、IO トレース、設定済みタイムアウトを使用します)。IO では status / cardBrand を POST しないでください。次の Create Payment / authorize() で最終ステータスと POS のカードフィールドを返します。
  • POS のシリアル番号の収集とプロセッサの webhook は、PPP のインバウンドリクエストではなく、追加の公開ルートです。公式サンプルの this.callback(req, resp) は Test Suite 専用です。
  • delayToCancel は secondsWaiting 以上にする必要があります。
  • 本番環境では、すべてのコネクターエンドポイントが 20 秒未満で応答する必要があります(Payment Provider Test Suite では 5 秒未満)。
  • このスキルでは、コネクターコードとテストチェックリストを生成できます。実際の POS、Sales App デバイス、設定済みのテストアカウント、またはホモロゲーションの代わりにはなりません。

厳守事項

コードサンプルでは、識別子の例(PartnerPos、partnername、connector-partnerpos、provider.example.com)と数値の例(SECONDS_WAITING = 600、411111/1111などのfirstDigits/lastDigits)を使用しています。これらは、パートナーのコネクター名、公開ホスト、タイムアウト、プロセッサーのカード情報の一部に置き換えてください。600はVTEXで必須のデフォルト値ではありません。ルールはdelayToCancel >= secondsWaitingです。BIN/下4桁の例は切り詰められた一部の値であり、PANではありません。

制約:マニフェストでは、POSの決済方法を正確なAPI名で宣言する必要があります

GET /manifest(およびPPFのpaymentProvider/configuration.jsonのpaymentMethods)には、POSガイドに記載された正確なASCII名であるVenda Direta Creditoおよび/またはVenda Direta Debitoを含める必要があります。アクセント記号付きの管理画面ラベル(Venda Direta Crédito、Venda Direta Débito)は表示名にすぎません。

重要な理由 GatewayとSales Appの決済条件は、これらの決済方法名に紐づいています。マニフェストにVisa/Mastercardしかない場合、加盟店はPOSを設定できません。コネクターが"POS Credit"などの名前を独自に作成すると、管理画面とSales Appは店頭での決済フローを紐づけられません。

検出方法 タスクがPOSであり、paymentMethodsにVenda Direta CreditoまたはVenda Direta Debitoがない場合は、STOPして、他のコードを生成する前にこれらを追加してください。

正しい例

{
  "paymentMethods": [
    { "name": "Venda Direta Debito", "allowsSplit": "onCapture" },
    { "name": "Venda Direta Credito", "allowsSplit": "onCapture" }
  ]
}

誤った例

{
  "paymentMethods": [
    { "name": "Visa", "allowsSplit": "onCapture" },
    { "name": "Mastercard", "allowsSplit": "onCapture" },
    { "name": "POS Credit", "allowsSplit": "onCapture" }
  ]
}
制約:paymentMethodでPOSを判定し、GatewayからカードのPANを読み取らないでください

Venda Direta CreditoおよびVenda Direta Debitoの場合、Create Paymentはcard.holder、card.number、card.csc、card.expiration、card.document、card.tokenをnullとして送信します。コネクターは、これらのフィールドを必須としてはならず、Secure Proxyを呼び出してはならず、カードデータがないことを理由に決済を拒否してはなりません。

PPFでは、@vtex/payment-providerのisDirectSaleAuthorizationを使って分岐してください。このガードはSDKのDirectSale名(正確なASCII名)を使います。また、DirectSaleAuthorizationにはcardもsecureProxyUrlもありません。そのため、絞り込み後にauthorization.cardを参照するとコンパイルエラーになります。独自にPOS_METHODSや(authorization as any).paymentMethodを作成しないでください。スタンドアロンHTTPでは、paymentMethodを同じ2つの文字列と比較してください。callbackUrlとvalueはすでにAuthorizationに含まれているため、キャストしないでください。

重要な理由 購入者は実店舗の端末で支払います。POSをECのカード決済承認として処理すると、決済に失敗したり、さらに悪い場合は空のトークンでSecure Proxyの呼び出しを生成したりします。アクセント記号付きの文字列を独自に作成すると、コンパイル時ではなくGatewayで失敗します。

検出方法 POSの決済承認ロジックがcard.number/secureProxyUrlを読み取るか、cardがnullのときにdeniedを返す場合は、STOPして、POSとECを分岐してください。PPFコードでas anyを使ってpaymentMethod、callbackUrl、valueを読み取っている場合は、STOPして、型付きフィールド/ガードを使用してください。

正しい例

import {
  isDirectSaleAuthorization,
  AuthorizationRequest,
} from "@vtex/payment-provider";

async function authorize(request: AuthorizationRequest) {
  if (isDirectSaleAuthorization(request)) {
    return authorizePos(request);
  }

  return authorizeEcommerce(request);
}

誤った例

async function authorize(request: CreatePaymentRequest) {
  if (!request.card?.number) {
    return { status: "denied", message: "Missing card number" };
  }

  return secureProxy.charge(request.card, request.secureProxyUrl);
}
制約:最初の Create Payment 応答でターミナル Payment App を開く必要があります

コネクターがこの paymentId の POS シリアル番号をまだ受け取っていない場合、Create Payment は status: "undefined" と vtex.terminal-connector-app の paymentAppData を返す必要があります。payload は、{"serialNumber": "12345"} を受け取る HTTPS エンドポイントである submitUrl を含む JSON 文字列でなければなりません。

例のホストは、パートナーの実際の公開 URL に置き換えてください。応答を返す前に、callbackUrl と paymentId を永続化してください。

PPF では、Authorizations.pending(authorization, { delayToCancel, paymentAppData }) を返してください。AppData.payload は Maybe<string> であるため、オブジェクトのペイロードはコンパイルエラーになります。そのオブジェクトを as AuthorizationResponse でラップしないでください。

これが重要な理由 この応答がないと、Sales App でカメラ/バーコード認証が開かず、プロセッサーは使用するターミナルを特定できません。ネストされた payload オブジェクトは Payment App に挿入されません。応答全体へのキャストは、そのコンパイルエラーを無効にします。

検出方法 最初の POS 応答が approved/denied になっている、paymentAppData がない、vtex.challenge-terminal-connector-app を使用している、payload がオブジェクトになっている、または保留中の本文に as AuthorizationResponse を使用している場合は、処理を停止して修正してください。

正しい例

import { Authorizations } from "@vtex/payment-provider";

const SECONDS_WAITING = 600;

async function authorizePos(request: CreatePaymentRequest) {
  const existing = await store.get(request.paymentId);

  if (!existing) {
    await store.save(request.paymentId, {
      phase: "identify-terminal",
      status: "undefined",
      callbackUrl: request.callbackUrl,
      paymentMethod: request.paymentMethod,
      value: request.value,
    });

    return Authorizations.pending(request, {
      acquirer: "PartnerPos",
      code: "POS_IDENTIFY_TERMINAL",
      message: "Identify the POS terminal",
      delayToAutoSettle: 21600,
      delayToAutoSettleAfterAntifraud: 1800,
      delayToCancel: SECONDS_WAITING,
      paymentAppData: {
        appName: "vtex.terminal-connector-app",
        payload: JSON.stringify({
          submitUrl: `https://provider.example.com/pos/serial/${request.paymentId}`,
        }),
      },
    });
  }

  return continuePos(request, existing);
}

誤った例

return {
  paymentId: request.paymentId,
  status: "approved",
  paymentAppData: {
    appName: "vtex.challenge-terminal-connector-app",
    payload: {
      submitUrl: "https://provider.example.com/pos/serial",
    },
  },
};
制約:シリアル番号を受け取った後は vtex.challenge-wait-for-confirmation で待機する

submitUrl が {"serialNumber": "..."} を受け取ったら、コネクターはそのシリアル番号を paymentId に関連付ける必要があります(POS ガイドのステップ 7)。このハンドラーで POS の決済を開始しないでください。

Sales App はターミナルアプリが閉じた後に次の Create Payment を実行します(ステップ 8)。この Create Payment で、プロセッサーの決済を一度だけ開始し、その後、ペイロード {"secondsWaiting": <int>} を指定した vtex.challenge-wait-for-confirmation と status: "undefined" を返す必要があります(ステップ 8.b~8.d)。後続のポーリング(ステップ 9)では、別の決済を開始したり、callbackUrl に POST したりしてはいけません。

シリアル番号が存在する前に POS の決済を開始しないでください。また、最初の authorize() からも開始しないでください(この時点ではシリアル番号がありません)。

secondsWaiting は、「確認待ち」アプリがポーリングする時間です。delayToCancel(秒単位、Create Payment 応答)は、Gateway が決済を undefined のままにしておく時間です。delayToCancel >= secondsWaiting に設定してください。

これが重要な理由 ターミナルが特定される前に決済を開始すると、誤ったデバイス、またはデバイスなしで決済されます。submitUrl で決済を開始すると、公開されている手順から逸脱します(8.b で決済を開始し、8.c で開始されたことを確認し、8.d で待機アプリを返します)。delayToCancel が secondsWaiting より短い場合、Sales App がまだ待機中なのに Gateway が決済をキャンセルします。Create Payment がポーリングのたびに新しいプロセッサー決済を開始すると、購入者に二重請求されます。

検出方法 シリアル番号が保存される前に「確認待ち」が返される、シリアル番号ハンドラーまたはポーリングのたびに startPosCharge が実行される、secondsWaiting がない、または delayToCancel < secondsWaiting の場合は、処理を停止してください。

正しい例

async function onSerialNumber(paymentId: string, serialNumber: string) {
  const payment = await store.get(paymentId);
  if (!payment || payment.phase !== "identify-terminal") {
    return;
  }

  await store.save(paymentId, {
    ...payment,
    serialNumber,
  });
}

function waitingResponse(request: CreatePaymentRequest) {
  return Authorizations.pending(request, {
    acquirer: "PartnerPos",
    code: "POS_WAITING",
    message: "Waiting for POS confirmation",
    delayToAutoSettle: 21600,
    delayToAutoSettleAfterAntifraud: 1800,
    delayToCancel: SECONDS_WAITING,
    paymentAppData: {
      appName: "vtex.challenge-wait-for-confirmation",
      payload: JSON.stringify({ secondsWaiting: SECONDS_WAITING }),
    },
  });
}

誤った例

async function authorizePos(request: CreatePaymentRequest) {
  await processor.startPosCharge({ paymentId: request.paymentId });

  return {
    paymentId: request.paymentId,
    status: "undefined",
    delayToCancel: 30,
    paymentAppData: {
      appName: "vtex.challenge-wait-for-confirmation",
      payload: JSON.stringify({ secondsWaiting: 600 }),
    },
  };
}
制約:POS のカード情報を含め、承認または拒否は必須のコールバック経由でのみ行う

物理カードを読み取るのは VTEX ではなく、プロセッサーです。プロセッサーから最終結果が通知されたら、コネクターは必ず Gateway に通知しなければなりません。このコールバックなしでは、POS を承認も拒否もできません。次の情報を含めてください。

  • status:"approved" または "denied"
  • cardBrand
  • firstDigits(先頭6桁/BIN)
  • lastDigits(末尾4桁)

この3つのカード情報は、加盟店の照合に必須です。カードを端末で読み取った後にプロセッサーから取得するものであり、Create Payment の元の card オブジェクトから取得するものではありません。

PAN 全体をログに記録したり、保存したりしないでください。使用が許可されているのは firstDigits と lastDigits の断片だけです。

重要な理由 Gateway に通知しないと、Sales App は確認待ちの状態でタイムアウトまで待機し、その後キャンセルします。BIN/末尾4桁が欠けると、端末で決済が成功していても財務レポートに支障が出ます。IO では、通知本文だけを callbackUrl に送信してもフローは完了しません。Gateway が必要とするのは、本文なしの再試行 POST と、それに続く新しい authorize() です。

検出方法 POS コードが最初の Create Payment で approved を返す場合は、処理を停止してください。PPF コードが本文に status/cardBrand を含めて callbackUrl を fetch する場合は、処理を停止し、Payments.retry/this.retry を使用してください。Service ルートが this.retry を呼び出す場合(this は存在しません)は、処理を停止し、new Payments(ctx.vtex).retry(stored.callbackUrl) を使用してください。保留中の authorize() が Gateway への「通知」として callbackUrl に POST する場合は、処理を停止してください。単独のコードで通知 POST に3つのカード情報を含めていない場合は、処理を停止してください。

正しい例(PPF の追加ルート — 一般的なプロセッサーの Webhook)

import { Payments } from "@vtex/payment-provider";

await vbase.saveJSON("pos", paymentId, {
  status: "approved",
  cardBrand: event.cardBrand,
  firstDigits: event.firstDigits,
  lastDigits: event.lastDigits,
  authorizationId: event.authorizationId,
  nsu: event.nsu,
  tid: event.tid,
});

await new Payments(ctx.vtex).retry(callbackUrl);

正しい例(PaymentProvider メソッド内のみ)

return this.retry(authorization);

次の authorize() は、永続化されたレコードから status、cardBrand、firstDigits、lastDigits を返さなければなりません。単独の通知ペイロードの例は、examples.md を参照してください。

誤った例

async function authorizePos(request: CreatePaymentRequest) {
  return {
    paymentId: request.paymentId,
    status: "approved",
    message: "Assume the terminal will succeed",
  };
}

// Also wrong on VTEX IO: notification payload to callbackUrl
await fetch(authorization.callbackUrl, {
  method: "POST",
  body: JSON.stringify({
    paymentId: request.paymentId,
    status: "approved",
    cardBrand: "Visa",
    firstDigits: "411111",
    lastDigits: "1111",
  }),
});
制約:Create Payment の冪等性を保ち、応答を20秒以内に返す

特定の paymentId に対する Create Payment は、保存されている現在のフェーズとステータスを返さなければなりません。プロセッサーの取引を2回目に作成してはなりません。確認待ち状態からのポーリングでは、ステータスがまだ undefined の間に Create Payment が何度も呼び出されます。これらの呼び出しは重複して実行される可能性があるため(異なる requestId、同じ paymentId)、順次 get してから save する方法では、レコードが2つ作成されたり、フェーズの更新が失われたりする可能性があります。

PPF では、getRawJSON(etag を取得)で永続化してから、saveJSON(..., ifMatch) を使用してください。412 が返された場合は、再読み込みして、保存済みのドキュメントを基に処理を続行してください。payment-idempotency と組み合わせてください。最初の端末識別時の書き込みに、リクエストごとに固有のフィールド(ランダムな tid、requestId)を含めないでください。

VBase はメタデータのバケット名を {vendor}.{appName}.{bucket} として構成し、50文字を超える名前を拒否します(400 Metadata bucket name too large)。pos のような短いバケット名を使用してください。スキルのプレースホルダー(partnername.connector-partnerpos)なら pos-payments に収まりますが、実際の長い vendor.appName では収まりません。

HTTP ハンドラー内で、購入者が POS での操作を終えるまで待たないでください。比較と設定で identify-terminal → await-pos に進め、書き込みの勝者となった場合に限ってプロセッサーの決済を開始し、応答を返してください。パスワードの再試行や残高不足への対応は、プロセッサーと POS の間で処理してください。

重要な理由 Gateway は20秒を超える応答を失敗とみなします。各ポーリングでアクワイアラーに再度アクセスすると、POS の二重請求が発生します。1つの paymentId に対する最初の書き込みが重複して実行されると、一貫しない tid が返され、PPP の冪等性が損なわれます。VBase のバケット名が長すぎると、最初の Create Payment で詳細不明の 400 エラーが発生します。

検出方法 ハンドラーが端末の結果を await する場合、phase がすでに await-pos または最終状態なのに processor.startPosCharge を呼び出す場合、etag を使わずに getJSON と saveJSON を使う場合、または {vendor}.{appName}.{bucket} が50文字を超える VBase バケットを使用する場合は、処理を停止してください。

正しい例

const POS_BUCKET = "pos";

async function savePos(vbase: VBase, paymentId: string, record: PosRecord, etag?: string) {
  try {
    await vbase.saveJSON(POS_BUCKET, paymentId, record, undefined, etag);
    return true;
  } catch (err) {
    const status = (err as any).response && (err as any).response.status;
    if (status === 412) {
      return false;
    }
    throw err;
  }
}

async function continuePos(
  vbase: VBase,
  request: CreatePaymentRequest,
  existing: PosRecord,
  etag?: string
) {
  if (existing.status === "denied") {
    return {
      ...Authorizations.deny(request, {
        acquirer: "PartnerPos",
        code: existing.code,
        message: existing.message,
        tid: existing.tid,
        delayToCancel: SECONDS_WAITING,
      }),
      cardBrand: existing.cardBrand,
      firstDigits: existing.firstDigits,
      lastDigits: existing.lastDigits,
    } as AuthorizationResponse;
  }

  if (existing.status === "approved") {
    return {
      ...Authorizations.approve(request, {
        authorizationId: existing.authorizationId as string,
        nsu: existing.nsu,
        tid: existing.tid as string,
        acquirer: "PartnerPos",
        code: existing.code,
        message: existing.message,
      }),
      delayToCancel: SECONDS_WAITING,
      cardBrand: existing.cardBrand,
      firstDigits: existing.firstDigits,
      lastDigits: existing.lastDigits,
    } as AuthorizationResponse;
  }

  if (existing.phase === "await-pos") {
    return waitingResponse(request);
  }

  if (existing.serialNumber && existing.phase === "identify-terminal") {
    const won = await savePos(
      vbase,
      request.paymentId,
      { ...existing, phase: "await-pos" },
      etag
    );
    if (won) {
      await processor.startPosCharge({
        paymentId: request.paymentId,
        serialNumber: existing.serialNumber,
        amount: existing.value,
      });
    }
    return waitingResponse(request);
  }

  return Authorizations.pending(request, {
    acquirer: "PartnerPos",
    code: "POS_IDENTIFY_TERMINAL",
    message: "Identify the POS terminal",
    delayToCancel: SECONDS_WAITING,
    paymentAppData: {
      appName: "vtex.terminal-connector-app",
      payload: JSON.stringify({
        submitUrl: `https://provider.example.com/pos/serial/${request.paymentId}`,
      }),
    },
  });
}

誤った例

async function authorizePos(request: CreatePaymentRequest) {
  const result = await processor.startPosCharge(request);
  await processor.waitUntilCardFinished(result, { timeoutMs: 120000 });
  return { paymentId: request.paymentId, status: result.status };
}
制約:POS のシリアル番号は inbound-request ではなく、公開カスタムルートで収集します

vtex.terminal-connector-app は {"serialNumber":"..."} を submitUrl に POST します。この URL は、Payment App が呼び出せる公開 HTTPS エンドポイントでなければなりません。PPF では、service.json の routes にこのエンドポイントを宣言し、PaymentProviderService に渡してください。この手順では、POST /payments/{paymentId}/inbound-request/{action} を使用しないでください。

重要な理由 inbound-request は Gateway によって開始されます。ターミナルアプリは Sales App のデバイスから POST します。submitUrl が inbound パスの場合、シリアル番号は届かず、POS の決済も開始されません。

検出方法 submitUrl に /inbound が含まれている場合は、作業を中止してください。PPF にシリアル番号収集用の追加 service.json ルートがない場合も、作業を中止してください。

正しい例

{
  "routes": {
    "posSerial": {
      "path": "/_v/partnerpos/pos/serial/:paymentId",
      "public": true
    }
  }
}

誤った例

{
  "submitUrl": "https://account.myvtex.com/_v/api/my-connector/payments/PAY1/inbound/serial"
}

推奨パターン

示されている順序で generation.md に従ってください。PPF では、ppf-scaffold.md を使用して、Payment Provider Framework ガイドの公式サンプルアプリに POS の機能を追加してください。IO とスタンドアロンのペイロードの比較については、examples.md を参照してください。

Sales App checkout
  → Gateway POST /payments (Venda Direta Credito|Debito, card = null)
  → authorize(): undefined + vtex.terminal-connector-app (submitUrl)
  → POST submitUrl {"serialNumber":"..."}   (public custom route; store serial only)
  → Sales App closes the terminal app and calls the Gateway (connector does not retry)
  → authorize(): start processor charge once, then undefined + vtex.challenge-wait-for-confirmation
  → Wait for confirmation polls authorize() (still undefined; no second charge; no retry POST)
  → Processor webhook
       PPF: persist + Payments.retry(callbackUrl) / this.retry inside PaymentProvider
       Standalone: POST notification payload (status + card fragments)
  → authorize() returns approved|denied + cardBrand/firstDigits/lastDigits
  → Sales App places the order

よくある失敗

  • ユーザーが PPP + POS を求めているのに、eコマース向けの PPP コネクター(Visa/Mastercard + Secure Proxy)を生成する。
  • Venda Direta Credito / Venda Direta Debito をマニフェストから省略する、またはアクセント記号付きの Admin ラベルを API 名として使用する。
  • ターミナルの処理が完了する前に approved を返す、またはカードの挿入を待つために Create Payment 内で待機する。
  • vtex.terminal-connector-app ではなく vtex.challenge-terminal-connector-app を使用する。
  • paymentAppData.payload を JSON.stringify(...) ではなくオブジェクトとして送信する、または保留中のボディに対して as AuthorizationResponse を使い、その問題を隠す。
  • delayToCancel を secondsWaiting より短く設定し、Wait アプリがポーリング中に Gateway が決済をキャンセルする。
  • submitUrl ハンドラーで POS 決済を開始する(POS ガイドの手順 8.b では、次の Create Payment で開始します)。
  • Create Payment のポーリングごとに新しい POS 決済を開始する(冪等性が壊れます)。
  • Gateway を「促す」ために、保留中のレスポンスで callbackUrl を POST する。Sales App と Wait for confirmation がすでに Create Payment を実行します。
  • 1 つの paymentId に対する Create Payment の呼び出しが重複しているのに、etag を使わず getJSON + saveJSON を使用する。
  • {vendor}.{appName}.{bucket} が 50 文字を超える VBase バケットを使用する(デフォルトは pos)。
  • Gateway コールバックを省略する、または cardBrand、firstDigits、lastDigits を省略する。
  • VTEX IO の場合: ボディなしの Payments.retry / this.retry ではなく、スタンドアロンの通知ペイロード(status + カードフィールド)を callbackUrl に POST する。
  • サンプルの Test Suite にある this.callback(req, resp) を POS プロセッサーの Webhook としてコピーする。
  • service.json の追加ルートから this.retry を呼び出す(そこには PaymentProvider の this がありません)。その場合は new Payments(ctx.vtex).retry(callbackUrl) を使用してください。
  • submitUrl を公開カスタムルートではなく、PPP の inbound-request に設定する。
  • 「カード決済だから」という理由で、POS のオーソリに payment-pci-security の Secure Proxy を適用する。
  • 決済コネクターではなく、Sales App のカート / PDP 拡張機能を構築する。
  • パートナーが PPF / 公式サンプルアプリを使用できるのに、空の Express アプリから始める。

レビューチェックリスト

  • PPF の場合、空の HTTP サーバーではなく、公式 PPF サンプル(または既存の PPF コネクター)にアプリを重ねていますか?
  • /manifest(および IO の configuration.json)で、Venda Direta Credito および/または Venda Direta Debito を正確な名前で宣言していますか?
  • authorize は isDirectSaleAuthorization(PPF)または正確な ASCII メソッド名(スタンドアロン)で分岐し、POS では Gateway のカード PAN/Secure Proxy をスキップしていますか?
  • POS の最初の応答は、Authorizations.pending と vtex.terminal-connector-app を返し、payload.submitUrl は文字列ですか(応答全体をキャストしていませんか)?
  • submitUrl は service.json で定義された公開カスタムルートであり、{"serialNumber": "..."} を受け付け、inbound-request ではありませんか?
  • submitUrl が行うのはシリアル番号の保存のみ(ステップ 7)で、プロセッサーへの課金を一度だけ開始するのは次の Create Payment(ステップ 8.b)ですか?
  • 待機中の応答は undefined と vtex.challenge-wait-for-confirmation で、secondsWaiting を含んでいますか?
  • delayToCancel >= secondsWaiting であり、すべてのエンドポイントが 20 秒未満で応答しますか?
  • 同じ paymentId に対する Create Payment の重複呼び出しは安全ですか(getRawJSON と ifMatch を使用し、2 回目の POS 課金や pending 時の再試行がありませんか)?
  • PPF の VBase バケット名は {vendor}.{appName}.{bucket} が 50 文字以下になるよう短くしていますか(pos を使用)?
  • **PPF:**プロセッサーの Webhook はカード情報の一部を保存してから Payments.retry(callbackUrl) を呼び出しますか(または PaymentProvider 内で this.retry を呼び出しますか)?次の authorize() はカード情報の一部を返しますか(最後の応答のみをキャストしていますか)?
  • **スタンドアロン:**通知 POST に status、cardBrand、firstDigits、lastDigits が含まれていますか?
  • Gateway のタイムアウト時に undefined の POS 決済を中止できるよう、Cancel を実装していますか?
  • Sales App 端末、POS 端末、テストアカウント、決済条件、ホモロゲーションが引き続き必要であることをパートナーに伝えていますか?
テストの前提条件(スキルでは自動化できません)

VTEX では、コネクターのテスト用に事前設定済みのストアが提供されることがよくあります。ストアを手動で設定するには、次の手順に従います。

  1. コネクターをインストールします(PPF の場合は、Payment Provider Framework のインストール手順に従います)。
  2. コネクターの Gateway アフィリエーションを登録します。
  3. そのアフィリエーションに決済条件 Venda Direta Crédito および/または Venda Direta Débito を作成します。
  4. Sales App(checkout-instore-custom.js)でこれらの決済方法を表示します。標準の条件 ID は 44(デビット)と 45(クレジット)です。Admin の Payment Conditions ページで確認してください。
  5. VTEX Sales App を端末にインストールし、実際の POS または認定済みのテスト POS で購入を完了します。
  6. 非同期の承認/拒否ケースを含めて Payment Provider Test Suite を実行し、必要に応じてホモロゲーションチケットを作成します。

関連スキル

このスキルと併用するスキル:

  • generation.md — 尋ねる質問、サンプルをクローンするワークフロー、実装順序
  • ppf-scaffold.md — 公式 PPF サンプルアプリに重ねるファイル
  • examples.md — パートナー向けプロンプトと IO/スタンドアロンのペイロード

その他の決済/Sales App スキル:

  • payment-provider-protocol — 必須の PPP エンドポイントと応答形式。このスキルと併用してください
  • payment-provider-framework — POS コネクターが VTEX IO アプリの場合に使用します
  • payment-async-flow — undefined と callbackUrl の一般的なルール。POS ではこれに Payment Apps とカード情報の一部を使ったコールバックが加わります
  • payment-idempotency — paymentId によって POS への課金が 2 回発生してはいけません。同じ ID の Create Payment は並行して実行される場合があります(etag/ifMatch)
  • payment-pci-security — EC のカード非対面決済専用です。POS の authorize に Secure Proxy を適用しないでください
  • sales-app-extensibility — Sales App の UI 拡張。POS 決済コネクターではありません

参考資料

コメント