mcpField
P
payment-provider-protocol

payment-provider-protocol

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

payment-provider-protocol について

payment-provider-protocolは、VTEX決済コネクターのミドルウェアを構築する開発者向けスキルです。必須のPPPエンドポイントやレスポンス契約、マニフェスト、認定テストの要件を説明します。

主な機能

  1. 01必須の決済フロー用6エンドポイントを実装
  2. 02PPPのレスポンス項目とステータスコードに準拠
  3. 03マニフェストに対応決済方法を宣言
  4. 04任意の加盟店設定エンドポイントを追加
  5. 05通信セキュリティと応答時間の要件を確認

活用シーン

  1. 01PSPとVTEXをつなぐコネクターの構築
  2. 02PPPエンドポイントのデバッグや拡張
  3. 03VTEX Payment Provider Test Suiteの認定準備
  4. 04決済エンドポイントのレスポンス形式の検証

インストール

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

$npx skills add https://github.com/vtex/skills --skill payment-provider-protocol

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

詳細

PPP エンドポイントの実装

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

以下の場合にこのスキルを使用します。

  • PSP を VTEX Payment Gateway と統合する新しい決済コネクターミドルウェアを構築する場合
  • 9 つの PPP エンドポイントのいずれかを実装、デバッグ、または拡張する場合
  • VTEX Payment Provider Test Suite の認定に向けてコネクターを準備する場合

以下の場合はこのスキルを使用しないでください。

  • 冪等性と重複防止のロジック — payment-idempotency を使用してください
  • 非同期決済フローとコールバック URL — payment-async-flow を使用してください
  • PCI コンプライアンスと Secure Proxy によるカード処理 — payment-pci-security を使用してください
  • POS/VTEX Sales App のカード提示型決済(Venda Direta Credito、Venda Direta Debito、端末 Payment Apps)— エンドポイントの形式については、このスキルと併せて payment-ppp-pos を使用してください

判断ルール

  • コネクターは、6 つすべての決済フローエンドポイント(Manifest、Create Payment、Cancel、Capture/Settle、Refund、Inbound Request)を実装する必要があります。
  • 設定フロー(3 つのエンドポイント:Create Auth Token、Provider Auth Redirect、Get Credentials)は任意ですが、加盟店のオンボーディングには推奨されます。
  • すべてのエンドポイントは、TLS 1.2 を使用し、ポート 443 の HTTPS 経由で提供する必要があります。
  • コネクターは、認定テスト中は 5 秒未満、本番環境では 20 秒未満で応答する必要があります。
  • プロバイダーは PCI-DSS 認定を受けているか、カード決済に Secure Proxy を使用する必要があります。
  • すべての呼び出しは Gateway が開始します。ミドルウェアが Gateway を呼び出すのは、callbackUrl(非同期通知)と Secure Proxy(カードデータの転送)を介する場合に限られます。

厳守すべき制約

制約:必須の決済フローエンドポイントをすべて実装する

コネクターは、次の 6 つすべての決済フローエンドポイントを実装する必要があります。GET /manifest、POST /payments、POST /payments/{paymentId}/cancellations、POST /payments/{paymentId}/settlements、POST /payments/{paymentId}/refunds、POST /payments/{paymentId}/inbound-request/{action}。

重要な理由 VTEX Payment Provider Test Suite は、認定中にすべてのエンドポイントを検証します。エンドポイントが不足しているとテストに失敗し、コネクターは承認されません。実行時、Gateway はすべてのエンドポイントを必要とします。キャンセルエンドポイントがないと、決済を取り消せません。

検出方法 コネクターのルーター/ハンドラーファイルに 6 つすべての決済フローパスのハンドラーが定義されていない場合は、そこで作業を止め、先に不足しているエンドポイントを追加してください。

正しい例

import { Router } from "express";

const router = Router();

// All 6 payment-flow endpoints implemented
router.get("/manifest", manifestHandler);
router.post("/payments", createPaymentHandler);
router.post("/payments/:paymentId/cancellations", cancelPaymentHandler);
router.post("/payments/:paymentId/settlements", capturePaymentHandler);
router.post("/payments/:paymentId/refunds", refundPaymentHandler);
router.post("/payments/:paymentId/inbound-request/:action", inboundRequestHandler);

export default router;

誤った例

import { Router } from "express";

const router = Router();

// Missing manifest, inbound-request, and refund endpoints
// This will fail homologation and break runtime operations
router.post("/payments", createPaymentHandler);
router.post("/payments/:paymentId/cancellations", cancelPaymentHandler);
router.post("/payments/:paymentId/settlements", capturePaymentHandler);

export default router;
制約:正しい HTTP ステータスコードとレスポンス形式を返す

各エンドポイントは、PPP API に記載された正確なレスポンス形式を返す必要があります。Create Payment は paymentId、status、authorizationId、tid、nsu、acquirer、code、message、delayToAutoSettle、delayToAutoSettleAfterAntifraud、delayToCancel を返す必要があります。Cancel は paymentId、cancellationId、code、message、requestId を返す必要があります。Capture は paymentId、settleId、value、code、message、requestId を返す必要があります。Refund は paymentId、refundId、value、code、message、requestId を返す必要があります。

重要な理由 Gateway はこれらのフィールドをプログラムで解析します。フィールドが不足しているとデシリアライズエラーが発生し、Gateway は決済を失敗として扱います。delayToAutoSettle の値が正しくないと、決済が不適切なタイミングで自動キャンセルまたは自動キャプチャされます。

検出方法 レスポンスオブジェクトに、そのエンドポイントで必須のフィールドが 1 つでも欠けている場合は、そこで作業を止め、不足しているフィールドを追加してください。

正しい例

interface CreatePaymentResponse {
  paymentId: string;
  status: "approved" | "denied" | "undefined";
  authorizationId: string | null;
  nsu: string | null;
  tid: string | null;
  acquirer: string | null;
  code: string | null;
  message: string | null;
  delayToAutoSettle: number;
  delayToAutoSettleAfterAntifraud: number;
  delayToCancel: number;
  paymentUrl?: string;
}

async function createPaymentHandler(req: Request, res: Response): Promise<void> {
  const { paymentId, value, currency, paymentMethod, card, callbackUrl } = req.body;

  const result = await processPaymentWithAcquirer(req.body);

  const response: CreatePaymentResponse = {
    paymentId,
    status: result.status,
    authorizationId: result.authorizationId ?? null,
    nsu: result.nsu ?? null,
    tid: result.tid ?? null,
    acquirer: "MyAcquirer",
    code: result.code ?? null,
    message: result.message ?? null,
    delayToAutoSettle: 21600,    // 6 hours in seconds
    delayToAutoSettleAfterAntifraud: 1800, // 30 minutes in seconds
    delayToCancel: 21600,         // 6 hours in seconds
  };

  res.status(200).json(response);
}

誤った例

// Missing required fields — Gateway will reject this response
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
  const result = await processPaymentWithAcquirer(req.body);

  // Missing: authorizationId, nsu, tid, acquirer, code, message,
  // delayToAutoSettle, delayToAutoSettleAfterAntifraud, delayToCancel
  res.status(200).json({
    paymentId: req.body.paymentId,
    status: result.status,
  });
}
制約:マニフェストですべての対応決済方法を宣言する必要があります

GET /manifest エンドポイントは、コネクターが対応するすべての決済方法を、各メソッドの正しい name と allowsSplit の設定とともに列挙した paymentMethods 配列を返さなければなりません。

重要な理由 Gateway はマニフェストを読み込み、利用可能な決済方法を判定します。決済方法が欠けていると、マーチャントは VTEX Admin でその方法を設定できません。allowsSplit の値が間違っていると、分割決済に失敗します。

検出方法 マニフェストハンドラーが空の paymentMethods 配列を返す場合、またはプロバイダーが実際には対応していない決済方法をハードコードしている場合は、作業を停止してマニフェストを修正してください。

正しい例

async function manifestHandler(_req: Request, res: Response): Promise<void> {
  const manifest = {
    paymentMethods: [
      { name: "Visa", allowsSplit: "onCapture" },
      { name: "Mastercard", allowsSplit: "onCapture" },
      { name: "American Express", allowsSplit: "onCapture" },
      { name: "BankInvoice", allowsSplit: "onAuthorize" },
      { name: "Pix", allowsSplit: "disabled" },
    ],
  };

  res.status(200).json(manifest);
}

誤った例

// Empty manifest — no payment methods will appear in the Admin
async function manifestHandler(_req: Request, res: Response): Promise<void> {
  res.status(200).json({ paymentMethods: [] });
}

推奨パターン

アーキテクチャの概要:

Shopper → VTEX Checkout → VTEX Payment Gateway → [Your Connector Middleware] → Acquirer/PSP
                                ↕
                    Configuration Flow (Admin)

すべてのエンドポイント契約に推奨される TypeScript インターフェース:

// --- Manifest ---
interface ManifestResponse {
  paymentMethods: Array<{
    name: string;
    allowsSplit: "onCapture" | "onAuthorize" | "disabled";
  }>;
}

// --- Create Payment ---
interface CreatePaymentRequest {
  reference: string;
  orderId: string;
  transactionId: string;
  paymentId: string;
  paymentMethod: string;
  value: number;
  currency: string;
  installments: number;
  card?: {
    holder: string;
    number: string;
    csc: string;
    expiration: { month: string; year: string };
  };
  miniCart: Record<string, unknown>;
  callbackUrl: string;
  returnUrl?: string;
}

interface CreatePaymentResponse {
  paymentId: string;
  status: "approved" | "denied" | "undefined";
  authorizationId: string | null;
  nsu: string | null;
  tid: string | null;
  acquirer: string | null;
  code: string | null;
  message: string | null;
  delayToAutoSettle: number;
  delayToAutoSettleAfterAntifraud: number;
  delayToCancel: number;
  paymentUrl?: string;
}

// --- Cancel Payment ---
interface CancelPaymentResponse {
  paymentId: string;
  cancellationId: string | null;
  code: string | null;
  message: string | null;
  requestId: string;
}

// --- Capture/Settle Payment ---
interface CapturePaymentResponse {
  paymentId: string;
  settleId: string | null;
  value: number;
  code: string | null;
  message: string | null;
  requestId: string;
}

// --- Refund Payment ---
interface RefundPaymentResponse {
  paymentId: string;
  refundId: string | null;
  value: number;
  code: string | null;
  message: string | null;
  requestId: string;
}

// --- Inbound Request ---
interface InboundResponse {
  requestId: string;
  paymentId: string;
  responseData: {
    statusCode: number;
    contentType: string;
    content: string;
  };
}

6 つのエンドポイントをすべて備えた決済フロールーター:

import { Router, Request, Response } from "express";

const router = Router();

router.get("/manifest", async (_req: Request, res: Response) => {
  res.status(200).json({
    paymentMethods: [
      { name: "Visa", allowsSplit: "onCapture" },
      { name: "Mastercard", allowsSplit: "onCapture" },
      { name: "Pix", allowsSplit: "disabled" },
    ],
  });
});

router.post("/payments", async (req: Request, res: Response) => {
  const body: CreatePaymentRequest = req.body;
  const result = await processWithAcquirer(body);

  const response: CreatePaymentResponse = {
    paymentId: body.paymentId,
    status: result.status,
    authorizationId: result.authorizationId ?? null,
    nsu: result.nsu ?? null,
    tid: result.tid ?? null,
    acquirer: "MyProvider",
    code: result.code ?? null,
    message: result.message ?? null,
    delayToAutoSettle: 21600,
    delayToAutoSettleAfterAntifraud: 1800,
    delayToCancel: 21600,
  };
  res.status(200).json(response);
});

router.post("/payments/:paymentId/cancellations", async (req: Request, res: Response) => {
  const { paymentId } = req.params;
  const { requestId } = req.body;
  const result = await cancelWithAcquirer(paymentId);

  res.status(200).json({
    paymentId,
    cancellationId: result.cancellationId ?? null,
    code: result.code ?? null,
    message: result.message ?? "Successfully cancelled",
    requestId,
  });
});

router.post("/payments/:paymentId/settlements", async (req: Request, res: Response) => {
  const body = req.body;
  const result = await captureWithAcquirer(body.paymentId, body.value);

  res.status(200).json({
    paymentId: body.paymentId,
    settleId: result.settleId ?? null,
    value: result.capturedValue ?? body.value,
    code: result.code ?? null,
    message: result.message ?? null,
    requestId: body.requestId,
  });
});

router.post("/payments/:paymentId/refunds", async (req: Request, res: Response) => {
  const body = req.body;
  const result = await refundWithAcquirer(body.paymentId, body.value);

  res.status(200).json({
    paymentId: body.paymentId,
    refundId: result.refundId ?? null,
    value: result.refundedValue ?? body.value,
    code: result.code ?? null,
    message: result.message ?? null,
    requestId: body.requestId,
  });
});

router.post("/payments/:paymentId/inbound-request/:action", async (req: Request, res: Response) => {
  const body = req.body;
  const result = await handleInbound(body);

  res.status(200).json({
    requestId: body.requestId,
    paymentId: body.paymentId,
    responseData: {
      statusCode: 200,
      contentType: "application/json",
      content: JSON.stringify(result),
    },
  });
});

export default router;

設定フローのエンドポイント(マーチャントのオンボーディング用、任意):

import { Router, Request, Response } from "express";

const configRouter = Router();

// 1. POST /authorization/token
configRouter.post("/authorization/token", async (req: Request, res: Response) => {
  const { applicationId, returnUrl } = req.body;
  const token = await generateAuthorizationToken(applicationId, returnUrl);
  res.status(200).json({ applicationId, token });
});

// 2. GET /authorization/redirect
configRouter.get("/authorization/redirect", async (req: Request, res: Response) => {
  const { token } = req.query;
  const providerLoginUrl = buildProviderLoginUrl(token as string);
  res.redirect(302, providerLoginUrl);
});

// 3. GET /authorization/credentials
configRouter.get("/authorization/credentials", async (req: Request, res: Response) => {
  const { authorizationCode } = req.query;
  const credentials = await exchangeCodeForCredentials(authorizationCode as string);
  res.status(200).json({
    applicationId: "vtex",
    appKey: credentials.appKey,
    appToken: credentials.appToken,
  });
});

export default configRouter;

よくある失敗

  • エンドポイントの部分的な実装 — Create Payment と Capture だけを実装し、Manifest、Cancel、Refund、Inbound Request を省略しています。テストスイートはすべてのエンドポイントをテストするため、ホモロゲーションに失敗します。実行時に Gateway は決済をキャンセルまたは返金できません。
  • HTTP メソッドの誤り — Manifest エンドポイントに POST を使用したり、Create Payment に GET を使用したりしています。Gateway は特定の HTTP メソッドを送信するため、ハンドラーが一致しない場合は 404 または 405 が返されます。
  • 遅延値がない、または 0 — Create Payment のレスポンスから delayToAutoSettle、delayToAutoSettleAfterAntifraud、または delayToCancel を省略するか、0 に設定しています。その結果、自動キャプチャまたは自動キャンセルが即座に実行され、決済が早期に確定したり、決済が失われたりします。
  • 不完全なレスポンス形式 — authorizationId、tid、nsu、acquirer などを含めず、paymentId と status だけを返しています。Gateway はすべてのフィールドをデシリアライズするため、欠けているフィールドがあると失敗として扱われます。

レビューチェックリスト

  • 決済フローのエンドポイント 6 つ(Manifest、Create Payment、Cancel、Capture、Refund、Inbound Request)をすべて実装していますか?
  • 各エンドポイントは、必須フィールドをすべて含む完全なレスポンス形式を返しますか?
  • Manifest は、プロバイダーが実際に対応するすべての決済方法を宣言していますか?
  • 正しい HTTP メソッド(Manifest は GET、それ以外はすべて POST)を使用していますか?
  • delayToAutoSettle、delayToAutoSettleAfterAntifraud、delayToCancel は、適切な 0 以外の値に設定されていますか?
  • コネクターは、TLS 1.2 を使用してポート 443 の HTTPS 経由で提供されていますか?
  • コネクターは、テストスイートでは 5 秒以内、本番環境では 20 秒以内に応答しますか?
  • マーチャントによるセルフオンボーディングが必要な場合、設定フローのエンドポイントを実装していますか?

関連スキル

  • payment-idempotency — 重複防止のための冪等性キー(paymentId、requestId)と状態マシン
  • payment-async-flow — 非同期決済方法、callbackUrl、および 7 日間の再試行期間
  • payment-pci-security — PCI 準拠、Secure Proxy、カードデータの取り扱い
  • payment-ppp-pos — POS/Sales App の対面カード決済オーバーレイ。エンドポイントの形式にはこのスキルと併用してください
  • vtex-io-application-performance — IO ベースの決済コネクターにおける、クライアントごとのタイムアウト/再試行の調整、VBase の正確性に関する制約、構造化ログ

参考資料

コメント