mcpField
P
payment-provider-protocol

payment-provider-protocol

github.com/vtex开发471.0k 次安装2 周前更新
代码仓库

关于 payment-provider-protocol

payment-provider-protocol 是帮助开发者构建 VTEX 支付连接器中间件的技能,涵盖 PPP 端点实现与测试套件认证要求。它整理了必需的支付流程端点、响应字段及清单配置规则。

主要功能

  1. 01实现六个必需的支付流程端点
  2. 02遵循 PPP 响应字段与状态码要求
  3. 03在清单中声明支持的支付方式
  4. 04提供可选的商户配置流程端点
  5. 05核对传输安全与响应时限要求

适用场景

  1. 01从头构建 PSP 与 VTEX 的连接器
  2. 02调试或扩展 PPP 端点
  3. 03准备 VTEX 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)为可选,但建议用于商户入驻。
  • 所有端点都必须通过 HTTPS 在 443 端口上提供服务,并使用 TLS 1.2。
  • 在认证测试期间,连接器必须在 5 秒内响应;在生产环境中,必须在 20 秒内响应。
  • 支付服务提供商必须通过 PCI-DSS 认证,或在处理卡片支付时使用 Secure Proxy。
  • 所有调用均由 Gateway 发起。除通过 callbackUrl(异步通知)和 Secure Proxy(转发卡片数据)外,中间件不得调用 Gateway。

硬性约束

约束:实现所有必需的支付流程端点

连接器必须实现全部六个支付流程端点: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 值不正确会导致支付在错误的时间自动取消或自动扣款。

检测 如果响应对象缺少其端点所需的任何字段,请停止并添加缺失字段。

正确

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 端点必须返回一个 paymentMethods 数组,其中列出连接器支持的所有支付方式,并为每种方式配置正确的 name 和 allowsSplit。

为何重要 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;

常见故障模式

  • 端点实现不完整 — 只实现创建支付和捕获,跳过清单、取消、退款和入站请求。测试套件会测试所有端点,因此验证测试会失败。运行时,Gateway 无法取消或退款。
  • HTTP 方法错误 — 对清单端点使用 POST,或对创建支付使用 GET。Gateway 会发送特定的 HTTP 方法;处理程序不匹配时会返回 404 或 405。
  • 缺少延迟值或延迟值为零 — 在创建支付响应中省略 delayToAutoSettle、delayToAutoSettleAfterAntifraud 或 delayToCancel,或将其设为零。这会导致立即自动捕获或自动取消,造成提前结算或支付丢失。
  • 响应结构不完整 — 只返回 paymentId 和 status,却未提供 authorizationId、tid、nsu、acquirer 等字段。Gateway 会反序列化所有字段,并将缺失字段视为失败。

审查清单

  • 是否已实现全部 6 个支付流程端点(清单、创建支付、取消、捕获、退款、入站请求)?
  • 每个端点是否都返回包含所有必需字段的完整响应结构?
  • 清单是否声明了服务提供方实际支持的所有支付方式?
  • 是否使用了正确的 HTTP 方法(清单使用 GET,其余端点使用 POST)?
  • delayToAutoSettle、delayToAutoSettleAfterAntifraud 和 delayToCancel 是否设为合理的非零值?
  • 连接器是否通过 HTTPS 在端口 443 上提供服务,并使用 TLS 1.2?
  • 连接器是否能在测试套件中于 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 — 按客户端调整超时和重试、VBase 正确性约束,以及基于 IO 的支付连接器的结构化日志

参考资料

评论

支持 Markdown