mcpField
P
payment-provider-framework

payment-provider-framework

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

关于 payment-provider-framework

面向在 VTEX IO 中开发或维护支付连接器的技能,涵盖框架接线、配置、卡数据请求及测试发布流程,并强调 TypeScript 与配置模式约束。

主要功能

  1. 01配置 paymentProvider builder 与 configuration.json
  2. 02实现 PaymentProvider 路由与服务
  3. 03处理 Gateway 重试及安全代理卡流程
  4. 04检查 TypeScript 3.9.7 与依赖兼容性
  5. 05规划测试、发布和 VTEX homologation

适用场景

  1. 01搭建新的 VTEX IO 支付连接器
  2. 02排查 builder 或类型声明导致的构建失败
  3. 03配置支付方式、自定义字段及功能标志
  4. 04测试并发布支付连接器

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/payment-provider-framework确认已加载。

详细介绍

Payment Provider Framework(VTEX IO)

适用场景

使用此技能的场景:

  • 创建或维护以 VTEX IO 应用实现的支付连接器(而非自行托管的独立 HTTP 服务)
  • 在 node/index.ts 中接入 @vtex/payment-provider、PaymentProvider 和 PaymentProviderService
  • 配置 paymentProvider 构建器、configuration.json(支付方式、customFields、功能标志)
  • 在 IO 上实现 this.retry(request),以满足 Gateway 重试语义
  • 扩展 SecureExternalClient,并在 IO 上的银行卡流程请求中传递 secureProxy
  • 通过支付关联、工作区、beta/stable 版本发布、VTEX App Store 和 VTEX 认证进行测试

请勿将此技能用于:

  • PPP HTTP 契约、逐字段响应要求,以及概述中的九个端点——请使用 payment-provider-protocol
  • 幂等性和重复 paymentId 处理——请使用 payment-idempotency
  • 异步 undefined 状态、callbackUrl 通知与重试(IO 与非 IO)——请使用 payment-async-flow
  • IO 接入以外的 PCI 规则、日志记录和令牌语义——请使用 payment-pci-security
  • POS / Sales App 刷卡流程覆盖层(Venda Direta 方式、Payment Apps、终端序列号)——请将此技能与 payment-ppp-pos 配合使用

决策规则

  • IO 上的 PPF:「Payment Provider Framework 是基于 VTEX IO 构建支付连接器的方式。」该应用使用 IO 基础设施;API 路由、请求/响应类型和 Secure Proxy 均按照 VTEX 指南集成。请从官方 Payment Provider Framework 文档中介绍的示例应用开始。
  • 前提条件:遵循 Payment Provider Protocol 文章以及在 VTEX 上集成新支付服务商指南中的实现前提条件。
  • 依赖项:在应用的 node 文件夹中添加 @vtex/payment-provider(例如,在 package.json 中使用 1.x)。将 @vtex/api 保留在 devDependencies 中(例如使用 6.x);链接操作可能会将其升级到高于 6.x 的版本,这可以接受。如果类型出错,请删除项目根目录和 node 文件夹中的 node_modules 和 yarn.lock,然后分别在两处运行 yarn install -f。
  • paymentProvider 构建器:在 manifest.json 中,将 "paymentProvider": "1.x" 与 node 并列,以应用 Payment Gateway 回调和 PPP 路由的策略。
  • configuration.json:声明 paymentMethods,使构建器能够实现这些支付方式,而无需在 /manifest 上重新声明所有内容。使用与 List Payment Provider Manifest API 参考相符的名称;只有在支付方式确实是全新的时候才创建名称。在 Admin 中添加新支付方式可能需要提交支持工单。
  • PaymentProvider:每个 PPP 路由对应一个类方法;TypeScript 会强制校验结构——请参阅 API 参考中的 Payment Flow 端点。
  • PaymentProviderService:注册默认路由 /manifest、/payments、/settlements、/refunds、/cancellations、/inbound;需要时传入额外的 routes/clients。
  • 覆盖 /manifest:仅限已批准的用例——请提交工单。路由覆盖结构示例请参阅 Preferred pattern 部分。
  • 可配置选项:使用 configuration.json/构建器选项设置 implementsOAuth、implementsSplit、usesProviderHeadersName、usesBankInvoiceEnglishName、usesSecureProxy、requiresDocument、acceptSplitPartialRefund、usesAutoSettleOptions 等标志。在 IO 上设置 name,并依赖自动生成的 serviceUrl,除非文档另有说明。不要臆造字段——未知键(例如 usesTestSuite)会导致构建器验证错误。规范字段列表和 customFields 格式请参阅下文的「configuration.json 架构」限制。
  • 网关重试:在 PPF 中,协议要求重试时调用 this.retry(request)——请参阅 PPP 文章中的 Payment authorization 部分。
  • IO 上的卡数据:「优先使用 SecureExternalClient,并将 Create Payment 中的 secureProxy: secureProxyUrl 作为参数;目标地址必须在允许列表中。」Secure Proxy 支持的 Content-Type 仅有 application/json 和 application/x-www-form-urlencoded。**重要:**只有 Create Payment(授权)请求携带 secureProxyUrl。授权后的操作(取消、捕获、退款)不会传输卡数据,必须通过 ExternalClient 携带凭据并使用 outbound-access 策略直接调用 PSP API。
  • Checkout 测试:账号必须获准使用 IO 连接器(提交注明应用名称和账号的工单)。发布 beta 版本,在 master 上安装,等待约 1 小时,打开 affiliation URL,启用测试模式和工作区,配置支付条件(约 10 分钟),然后下测试订单;之后发布 stable 版本并进行 homologation。
  • 发布:按照 Billing Options 指南配置 billingOptions;按照 Submitting your app 提交应用。根据 Integrating a new payment provider on VTEX 指南准备 homologation 材料(连接器应用名称、合作伙伴联系人、生产端点、允许使用的账号、新支付方式/流程)(SLA 通常约为 30 天)。
  • 更新:在新的 beta 版本中发布更改,重新测试 affiliations,然后发布 stable 版本;如有要求,重新进行 homologation。

硬性限制

约束:Builder-Hub 使用 TypeScript 3.9.7——代码和依赖项必须兼容

vtex.builder-hub 使用 TypeScript 3.9.7 编译 IO 应用。它还会 忽略 tsconfig.json 中的 skipLibCheck: true——node_modules 中的每个 .d.ts 文件都会进行类型检查。这意味着,即使你自己的代码有效,只要某个传递依赖带有较新的 .d.ts 语法,就会导致构建失败,并产生数百条与你的代码无关的错误。

为何重要

Agent 和开发者经常编写使用 TS 4.x 及更高版本语法的代码,或安装最新的 @types/* 包。构建会因开发者从未碰过的文件中出现难以理解的错误而失败,导致大量无谓的迭代。

禁止使用的语法(与 TS 3.9.7 不兼容)

语法最低 TS 版本示例
模板字面量类型4.1type X = `${string}/${string}`
带类型的 catch 子句4.0catch (error: any)
override 关键字4.3类中的 override method()
import type ... = require()4.5import type X = require("pkg")
satisfies 运算符4.9obj satisfies Type

正确的 catch 块写法

// CORRECT — TS 3.9.7 compatible
try {
  // ...
} catch (error) {
  const err = error as any
  console.log(err.message)
}

// WRONG — TS 4.0+ only
try {
  // ...
} catch (error: any) {
  console.log(error.message)
}

未使用的变量会导致错误,而非警告。 builder-hub 会将已声明但未使用的变量视为编译错误。避免解构出未使用的字段:

// WRONG — if callbackUrl is not used, build fails
const { paymentId, callbackUrl, value } = authorization

// CORRECT
const { paymentId, value } = authorization

安全的依赖项版本(兼容 TS 3.9.7)

在 node/package.json 中使用 resolutions,将传递依赖固定到不包含较新 .d.ts 语法的版本。**/<package> 模式也会固定嵌套副本。

{
  "dependencies": {
    "@vtex/payment-provider": "1.x"
  },
  "devDependencies": {
    "@vtex/api": "6.50.1",
    "@types/node": "12.20.55",
    "@types/express-serve-static-core": "4.17.2",
    "@types/express": "4.17.10",
    "@types/serve-static": "1.15.0",
    "@opentelemetry/api": "1.0.4",
    "typescript": "3.9.7"
  },
  "resolutions": {
    "@types/node": "12.20.55",
    "@types/express-serve-static-core": "4.17.2",
    "@types/express": "4.17.10",
    "@types/serve-static": "1.15.0",
    "@opentelemetry/api": "1.0.4",
    "**/@types/express-serve-static-core": "4.17.2",
    "**/@types/koa": "2.15.0"
  }
}
包安全版本首个不兼容版本原因
@types/node12.20.5513.x+(部分 API).d.ts 中使用了较新的语法
@types/express-serve-static-core4.17.24.17.13+模板字面量类型
@types/express4.17.104.17.11+依赖 @types/express-serve-static-core@^4.17.18
@types/koa2.15.03.ximport type ... = require()
@opentelemetry/api1.0.4更新的版本TS 4.x 语法
@types/serve-static1.15.0更新的版本传递依赖问题

**诊断新出现的不兼容包:**如果构建因 node_modules 中 .d.ts 文件的错误而失败,请根据错误路径确定所属的包,测试较旧版本,直到找到不含较新语法的版本,并将其同时添加到 devDependencies 和 resolutions(使用 **/<package> 模式)。

检测

如果生成的代码使用了上表中的任何语法,或者 package.json 缺少针对类型包的 resolutions,请停止操作并先修复,再尝试运行 vtex link。

约束:configuration.json 只能使用有效的架构字段,且 customFields 格式必须正确

paymentProvider 构建器会根据严格的架构验证 configuration.json。未知字段会导致构建错误。select 类型字段的 customFields[].options 数组必须使用 text 和 value 键,不能使用 label。

为何重要

如果 usesTestSuite 或 useAntifraud 等字段不在当前架构中,就会导致 vtex link 立即失败。在 select 选项中使用 label 而非 text,会导致 Admin UI 无法正常工作,或验证失败。

标准字段(请对照当前 VTEX 文档确认):

name(必填)、serviceUrl(IO 上自动设置)、implementsOAuth、implementsSplit、usesProviderHeadersName、usesBankInvoiceEnglishName、usesSecureProxy、requiresDocument、acceptSplitPartialRefund、usesAutoSettleOptions、paymentMethods、customFields。

已知会导致构建失败的字段:usesTestSuite(架构中不存在)。

select 类型的正确 customFields

{
  "name": "AcmePayConnector",
  "usesAutoSettleOptions": true,
  "paymentMethods": [
    { "name": "Visa", "allowsSplit": "onCapture" }
  ],
  "customFields": [
    {
      "name": "Environment",
      "type": "select",
      "options": [
        { "text": "Sandbox", "value": "sandbox" },
        { "text": "Production", "value": "production" }
      ]
    }
  ]
}

错误的 customFields

{
  "customFields": [
    {
      "name": "Environment",
      "type": "select",
      "options": [
        { "label": "Sandbox", "value": "sandbox" }
      ]
    }
  ]
}

检测

如果 configuration.json 包含不在标准字段列表中的键,或 select 选项使用了 label 而非 text,请停止操作并先修复,再进行构建。

约束:在 configuration.json 中声明 paymentProvider 构建器和真实的连接器标识

IO 连接器必须在 manifest.json 中包含 paymentProvider 构建器,并提供 paymentProvider/configuration.json,其中包含非占位符的 name 和准确的 paymentMethods。不要在生产配置中使用字面占位符 "MyConnector"(或类似名称)。

重要性

缺少该构建器,PPP 路由和 Gateway 策略就不会接入。占位符名称会导致 Admin、关联关系和认证流程出问题。

检测

如果 manifest.json 缺少 paymentProvider,或者 configuration.json 仍使用示例占位符名称,请停止发布并修复。

正确

{
  "name": "PartnerAcmeCard",
  "paymentMethods": [
    { "name": "Visa", "allowsSplit": "onCapture" },
    { "name": "BankInvoice", "allowsSplit": "onAuthorize" }
  ]
}

错误

{
  "name": "MyConnector",
  "paymentMethods": []
}
约束:仅通过 PaymentProviderService 和 PaymentProvider 实现注册 PPP 路由

该服务必须封装一个继承自 @vtex/payment-provider 中 PaymentProvider 的类,以注册标准 PPP 路径。除非 VTEX 明确规定了替代方案,否则不要在不使用该包的情况下手动实现相同的路由。

重要性

路由缺失或不匹配会导致 Gateway 调用和认证流程出错;该包能确保处理程序与协议保持一致。

检测

如果 node/index.ts 手动暴露 PPP 路径,却没有使用连接器类实例化 PaymentProviderService,请按照文档中的模式进行调整。

正确

import { PaymentProviderService } from "@vtex/payment-provider";
import { YourPaymentConnector } from "./connector";

export default new PaymentProviderService({
  connector: YourPaymentConnector,
});

错误

// Ad-hoc router only — no PaymentProviderService / PaymentProvider base
export default someCustomRouterWithoutPPPPackage;
约束:PaymentProviderService 的 clients 字段需要 { implementation, options },不能直接传入类

向 PaymentProviderService 传入自定义 IOClients 时,clients 字段需要一个包含 implementation(类)和 options(重试/超时配置)的对象,遵循 @vtex/api 中的 ServiceConfig 接口。直接传入类会导致运行时错误。

重要性

这是一个常见错误,会产生令人困惑的运行时错误,而不是明确的类型错误,因为 PPF 类型可能不会严格强制执行此要求。

正确

import { PaymentProviderService } from '@vtex/payment-provider'
import MyConnector from './connector'
import { Clients } from './clients'

export default new PaymentProviderService({
  connector: MyConnector,
  clients: {
    implementation: Clients,
    options: {
      default: {
        retries: 2,
        timeout: 15000,
      },
    },
  },
})

错误

export default new PaymentProviderService({
  connector: MyConnector,
  clients: Clients,  // WRONG — expects { implementation, options }
})
约束:IO 上的 Gateway 重试应使用 this.retry(request)

如果 PPP 流程要求在 IO 上执行重试,处理程序必须按照协议规定调用 this.retry(request),而不是使用绕过框架的自定义重试辅助函数。

重要性

“Gateway 需要由框架驱动的重试行为;省略该行为会导致授权和结算行为不一致。”

检测

搜索支付处理程序中的协议重试场景;如果重试未使用 this.retry 实现,请在发布前修复。

正确

// Inside a PaymentProvider subclass method, when the protocol requires retry:
return this.retry(request);

错误

// Re-implementing gateway retry with setTimeout/fetch instead of this.retry
await fetch(callbackUrl, { method: "POST", body: JSON.stringify(payload) });
约束:在 IO 上通过 Secure Proxy 转发卡授权调用,并使用允许列表中的目标地址

对于采用 usesSecureProxy 行为的 IO 卡支付流程,代理 HTTP 调用必须通过 SecureExternalClient(或等效的 VTEX 模式)进行,必须将 secureProxy 设置为支付请求中的 secureProxyUrl,并且目标地址必须是 VTEX 允许列表中的 PCI 端点。仅支持 application/json 或 application/x-www-form-urlencoded 请求体。如果 usesSecureProxy 为 false,支付服务商必须通过 PCI 认证,并按照 VTEX 要求为 serviceUrl 提供 AOC。

重要性

“跳过 Secure Proxy 或使用错误的内容类型会导致 PCI 范围、代理验证或收单机构集成出问题,进而阻碍认证流程,或导致卡数据以不安全的方式暴露。”

检测

检查客户端代码中是否存在包含卡令牌的 POST 请求,而请求配置中没有 secureProxy,或请求目标未在 VTEX 注册。

正确

import { SecureExternalClient, CardAuthorization } from "@vtex/payment-provider";
import type { InstanceOptions, IOContext, RequestConfig } from "@vtex/api";

export class MyPCICertifiedClient extends SecureExternalClient {
  constructor(protected context: IOContext, options?: InstanceOptions) {
    super("https://pci-certified.example.com", context, options);
  }

  public authorize = (cardRequest: CardAuthorization) =>
    this.http.post(
      "authorize",
      {
        holder: cardRequest.holderToken,
        number: cardRequest.numberToken,
        expiration: cardRequest.expiration,
        csc: cardRequest.cscToken,
      },
      {
        headers: { Authorization: "Bearer ..." },
        secureProxy: cardRequest.secureProxyUrl,
      } as RequestConfig
    );
}

错误

// Direct outbound call with raw card fields and no secureProxy
await http.post("https://acquirer.example/pay", { pan, cvv, expiry });
约束:只有 Create Payment 会接收 secureProxyUrl——身份验证后的操作会直接调用 PSP

secureProxyUrl 字段仅存在于 Create Payment(授权)请求中。取消、捕获和退款操作不携带卡数据,也不会接收 secureProxyUrl。这些操作必须使用 ExternalClient(来自 @vtex/api)和 API 凭据直接调用 PSP API,并通过 manifest.json 中的 outbound-access 策略进行保护。

为何重要

在取消、捕获或退款处理程序中尝试使用 SecureExternalClient 或 secureProxyUrl 会失败,因为这些请求中的该字段为 undefined。这并非 PCI 合规问题——这些操作只会引用交易 ID,不会涉及卡数据。

检测

如果取消、捕获或退款处理程序引用了 secureProxyUrl 或使用了 SecureExternalClient,请停止。这些操作必须使用 ExternalClient 直接通过 HTTP 调用 PSP。

正确——双客户端模式

// clients/pspSecure.ts — for authorization only (via Secure Proxy)
import { SecureExternalClient } from "@vtex/payment-provider";
import type { InstanceOptions, IOContext } from "@vtex/api";

export class PspSecureClient extends SecureExternalClient {
  constructor(ctx: IOContext, opts?: InstanceOptions) {
    super("https://api.psp.com", ctx, opts);
  }

  public async authorize(data: object, secureProxyUrl: string) {
    return this.http.post("/v1/payments", data, {
      secureProxy: secureProxyUrl,
    } as any)
  }
}

// clients/psp.ts — for cancel, capture, refund (direct calls)
import { ExternalClient } from "@vtex/api";
import type { InstanceOptions, IOContext } from "@vtex/api";

export class PspClient extends ExternalClient {
  constructor(ctx: IOContext, opts?: InstanceOptions) {
    super("https://api.psp.com", ctx, opts);
  }

  public async capture(transactionId: string, amount: number) {
    return this.http.post(`/v1/payments/${transactionId}/capture`, { amount })
  }

  public async cancel(transactionId: string) {
    return this.http.post(`/v1/payments/${transactionId}/cancel`, {})
  }

  public async refund(transactionId: string, amount: number) {
    return this.http.post(`/v1/payments/${transactionId}/refund`, { amount })
  }
}

错误

// WRONG — trying to use SecureExternalClient for capture
async settle(settlement: SettlementRequest) {
  const client = this.context.clients.pspSecure as PspSecureClient
  // secureProxyUrl is undefined here — this will fail
  await client.capture(settlement.tid, settlement.value, settlement.secureProxyUrl)
}
约束:不要在客户端类之外访问客户端实例的 .http

ExternalClient 和 SecureExternalClient 上的 http 属性受 protected 保护。从 PaymentProvider 子类调用 client.http.post(...) 会导致 builder-hub 中的 TypeScript 编译错误。

为何重要

开发者尝试从连接器类而不是通过客户端的公共方法发起 HTTP 调用时,经常会犯这个错误。builder-hub 会强制执行 protected 访问限制,导致构建失败。

检测

如果连接器代码访问了客户端实例上的 .http(例如,this.context.clients.myClient.http.post(...)),请停止。应在客户端子类中公开一个公共方法。

正确

// Inside the client class — this.http is accessible (protected = same class)
export class PspClient extends ExternalClient {
  public async capturePayment(tid: string, amount: number) {
    return this.http.post(`/v1/payments/${tid}/capture`, { amount })
  }
}

// Inside the connector — call the public method
async settle(settlement: SettlementRequest) {
  const clients = this.context.clients as any as { psp: PspClient }
  return clients.psp.capturePayment(settlement.tid, settlement.value)
}

错误

// Inside the connector — .http is protected, build fails
async settle(settlement: SettlementRequest) {
  const clients = this.context.clients as any as { psp: PspClient }
  return clients.psp.http.post(`/v1/capture`, { amount: settlement.value })
  //                  ^^^^ TS error: Property 'http' is protected
}

首选模式

项目文件结构
/
├── manifest.json                          # App identity, builders, policies
├── paymentProvider/
│   └── configuration.json                 # Payment methods, connector options
├── service.json                           # Runtime config (memory, timeout, replicas, custom routes)
└── node/
    ├── package.json                       # Dependencies WITH resolutions for TS 3.9.7
    ├── tsconfig.json                      # TypeScript config (skipLibCheck ignored by builder)
    ├── yarn.lock                          # REQUIRED — builder rejects build without it
    ├── index.ts                           # Entry point: exports PaymentProviderService
    ├── connector.ts                       # Class extending PaymentProvider
    ├── clients/
    │   ├── index.ts                       # IOClients with getOrSet
    │   ├── psp.ts                         # ExternalClient for direct PSP calls (cancel/capture/refund)
    │   └── pspSecure.ts                   # SecureExternalClient for card authorization via Secure Proxy
    └── typings/
        └── psp.ts                         # TypeScript interfaces

关键文件说明:

  • node/ 中的 yarn.lock 为必需文件——没有它,构建器会拒绝构建。不要将其添加到 .vtexignore。
  • service.json 放在项目根目录,不要放在 node/ 中。
  • configuration.json 放在 paymentProvider/ 中,不要放在根目录。
manifest.json 中的 builders 和 policies
{
  "builders": {
    "node": "7.x",
    "paymentProvider": "1.x"
  },
  "policies": [
    { "name": "vbase-read-write" },
    { "name": "outbound-access", "attrs": { "host": "api.psp.com", "path": "/*" } },
    { "name": "outbound-access", "attrs": { "host": "api.sandbox.psp.com", "path": "/*" } }
  ]
}

注意:如果使用 VBase 存储状态,则必须配置 vbase-read-write 策略。否则,vbase.saveJSON() 会返回 403。

tsconfig.json
{
  "compilerOptions": {
    "target": "es2019",
    "module": "commonjs",
    "lib": ["es2019"],
    "outDir": "./dist",
    "rootDir": ".",
    "strict": false,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["**/*.ts"],
  "exclude": ["node_modules", "**/node_modules", "dist"]
}

注意:builder-hub 会使用自己的值覆盖 outDir、rootDir 和 paths。skipLibCheck: true 会被忽略——所有 .d.ts 文件都会被检查。应改用 package.json 中的 resolutions 控制依赖的类型版本。

注册 IOClients
import { IOClients } from '@vtex/api'
import { PspClient } from './psp'
import { PspSecureClient } from './pspSecure'

export class Clients extends IOClients {
  public get psp(): PspClient {
    return this.getOrSet('psp', PspClient)
  }

  public get pspSecure(): PspSecureClient {
    return this.getOrSet('pspSecure', PspSecureClient)
  }
}
在连接器中访问自定义客户端和 IOContext
import { PaymentProvider } from '@vtex/payment-provider'
import type { Clients } from './clients'

export default class MyConnector extends PaymentProvider {
  // IOContext is at this.context.vtex, NOT this.context directly
  private get account(): string {
    const vtexCtx = (this.context as any).vtex as { account: string; workspace: string }
    return vtexCtx?.account || ''
  }

  private get workspace(): string {
    const vtexCtx = (this.context as any).vtex as { account: string; workspace: string }
    return vtexCtx?.workspace || 'master'
  }

  // Custom clients require a cast
  private get clients() {
    return this.context.clients as any as {
      psp: Clients['psp']
      pspSecure: Clients['pspSecure']
      vbase: import('@vtex/api').VBase
    }
  }

  // VBase is available directly
  private get vbase() {
    return this.context.clients.vbase
  }
}
使用客户端、自定义路由和 service.json 的 PaymentProviderService
// node/index.ts
import { PaymentProviderService } from '@vtex/payment-provider'
import MyConnector from './connector'
import { Clients } from './clients'

export default new PaymentProviderService({
  connector: MyConnector,
  clients: {
    implementation: Clients,
    options: {
      default: {
        retries: 2,
        timeout: 15000,
      },
    },
  },
  // Optional: custom routes (e.g., for redirect callbacks)
  // routes: {
  //   myCallback: myCallbackHandler,
  // },
})

使用自定义路由时,请在项目根目录的 service.json 中注册:

{
  "memory": 256,
  "ttl": 10,
  "timeout": 10,
  "minReplicas": 2,
  "maxReplicas": 10,
  "routes": {
    "myCallback": {
      "path": "/_v/my-connector/callback",
      "public": true
    }
  }
}

PPF 构建器会将自身的路由(PPP 端点)与你的路由合并——无需重新声明标准 PPP 路由。

通过 affiliation 获取凭据

PPF 会自动将 affiliation 标头映射到连接器属性:

VTEX 标头连接器属性典型用途
X-PROVIDER-API-AppKeythis.apiKeyPSP 客户端 ID / API 密钥
X-PROVIDER-API-AppTokenthis.appTokenPSP 客户端密钥 / API 令牌

this.isTestSuite 表示该交易是否为测试交易(sandbox)。

configuration.json 中 customFields 的自定义设置可通过每个请求中的 authorization 对象获取(具体访问方式取决于 PPF 版本——请查看 @vtex/payment-provider 中的类型定义)。

PPF 响应辅助函数——各辅助函数会自动填充的内容

使用来自 @vtex/payment-provider 的 Authorizations、Settlements 和 Refunds 辅助函数时:

辅助函数从请求中自动填充不要作为第二个参数传入
Authorizations.approve(auth, { ... })paymentId—
Authorizations.deny(auth, { ... })paymentIdnsu(不属于 deny 类型)
Settlements.approve(settle, { settleId })paymentId、valuevalue(已从请求中合并)
Refunds.approve(refund, { refundId })paymentId、valuevalue(已从请求中合并)

如果严格类型中未包含 delayToCancel,但要在 approveCard 中添加它,请使用带类型断言的展开语法:

// TS 3.9.7 compatible
const baseResponse = Authorizations.approve(authorization, {
  authorizationId: result.authorizationId,
  nsu: result.nsu,
  tid: result.tid,
  acquirer: 'MyPSP',
  code: '200',
  message: 'Approved',
})
const response = {
  ...baseResponse,
  delayToCancel: 21600,
  delayToAutoSettle: 21600,
  delayToAutoSettleAfterAntifraud: 1800,
} as any
return response
PSP 集成检查清单(与提供商无关)

在接入 PSP 客户端之前:

  1. 打开 PSP 的 API 浏览器 / OpenAPI 规范,确定各环境(测试/正式)的基础 URL。
  2. 请勿在 baseURL 和操作路径中重复版本号或路径段(例如,如果基础 URL 为 https://checkout-test.psp.com/v71,则支付路径应为 /payments,而不是 /v71/payments)。
  3. 在完成连接器实现之前,先通过测试调用(例如创建支付)进行验证。
  4. 如果 PSP 需要 OAuth 令牌,请实现访问令牌的内存缓存——VTEX Gateway 在某些流程中有 2 秒超时,依次调用令牌接口和 API 会超时。
// Simple in-memory token cache (TS 3.9.7 compatible)
const tokenCache: Record<string, { token: string; expiresAt: number }> = {}

function getCachedToken(clientId: string): string | null {
  const entry = tokenCache[clientId]
  if (entry && Date.now() < entry.expiresAt) return entry.token
  return null
}

function setCachedToken(clientId: string, token: string, expiresIn: number): void {
  tokenCache[clientId] = {
    token,
    expiresAt: Date.now() + (expiresIn - 300) * 1000,
  }
}
测试用关联 URL 格式
https://{account}.myvtex.com/admin/affiliations/connector/Vtex.PaymentGateway.Connectors.PaymentProvider.PaymentProviderConnector_{connector-name}/

将 {connector-name} 替换为 ${vendor}-${appName}-${appMajor}(例如:vtex-payment-provider-example-v1)。

测试流程概要:发布 beta 版本(例如 [email protected]——参见 Making your app publicly available 文档),在 master 上安装,等待约 1 小时,打开关联,在 Payment Control 下启用 Enable test mode 并设置 Workspace(通常为 master),添加支付条件,等待约 10 分钟,然后下单;接着部署稳定版本并完成认证。

在生产环境使用前,请将所有示例供应商名称、端点和凭据替换为真实应用的对应信息。

常见故障模式

  • 缺少 paymentProvider builder,或 paymentMethods 为空/配置错误,导致 /manifest 和 Admin 无法正确列出支付方式。
  • 在 node_modules/.d.ts 文件中出现数百条 TS 错误,导致构建失败——package.json 中缺少 resolutions,无法将类型包固定到与 TS 3.9.7 兼容的版本。
  • skipLibCheck: true 不起作用——builder-hub 会忽略此设置。请改用 resolutions。
  • 在 connector 代码中使用 catch (error: any) 或其他 TS 4.x+ 语法——请使用与 TS 3.9.7 兼容的写法。
  • configuration.json 中包含无效字段(usesTestSuite、自创的键)——会导致 builder 验证错误。
  • customFields 的下拉选项使用 label 而非 text——会导致 Admin UI 异常或验证失败。
  • node/ 目录中缺少 yarn.lock——builder 会拒绝构建。
  • 将 service.json 放在 node/ 目录中,而不是项目根目录。
  • 将 clients: Clients 直接传给 PaymentProviderService,而非传入 { implementation: Clients, options: {...} }。
  • 在 connector 中访问 client.http.post(...)——http 是 protected;请在 client 类中公开方法。
  • 使用 SecureExternalClient 处理 cancel/capture/refund——只有 Create Payment 会携带 secureProxyUrl;授权后的操作使用 ExternalClient。
  • 访问 this.context.account,而不是 this.context.vtex.account——IOContext 嵌套在 .vtex 下。
  • 类型或安装版本不一致(@vtex/api / @vtex/payment-provider),且没有在根目录和 node 中执行干净的重新安装流程。
  • 跳过 this.retry(request),并使用临时 HTTP 逻辑重复实现重试——Gateway 行为会与 PPP 不一致。
  • 卡片请求未使用 secureProxy、Content-Type 错误,或目标地址不在允许列表中——Secure Proxy 或 PCI 审核会失败。
  • 测试时未将账户加入允许列表、没有可售商品,或未等待主安装/支付条件传播完成。
  • 未经 VTEX 批准就覆盖 /manifest,或在主版本升级后保留过期的 x-provider-app。
  • Homologation 工单缺少生产端点、允许的账户或购买流程详情。
  • vtex link --no-watch 会隐藏编译错误——建议使用 watch 模式查看完整错误输出。
  • 在 master workspace 上运行 vtex link 会失败——请使用 dev workspace(vtex use dev-workspace)。
  • manifest.json 中缺少 vbase-read-write 策略——vbase.saveJSON() 会返回 403。
  • PSP 基础 URL 中路径段重复——例如版本已在基础 URL 中时出现 /checkout/v71/v71/payments。

审查清单

  • 该连接器是否为使用 PaymentProvider + PaymentProviderService 的 IO 应用(而非仅提供独立中间件指南)?
  • manifest.json 和 paymentProvider/configuration.json 是否与实际连接器名称及支持的方法一致?
  • configuration.json 是否只使用有效的架构字段?customFields 的选择选项是否使用 text(而非 label)?
  • package.json 是否包含 resolutions,将 @types/* 和其他软件包固定到兼容 TS 3.9.7 的版本?
  • 所有代码是否兼容 TS 3.9.7?(不使用 catch (e: any)、模板字面量类型、override 或 satisfies)
  • yarn.lock 是否存在于 node/ 中,且未列入 .vtexignore?
  • service.json 是否位于项目根目录(而非 node/ 中)?
  • PaymentProviderService 是否通过 clients: { implementation, options } 实例化(而非直接传入类)?
  • 客户端类是否公开方法,而非依赖从外部访问 this.http?
  • 是否仅在 Create Payment(授权)时使用 SecureExternalClient,而在 cancel/capture/refund 时使用 ExternalClient?
  • 可选的 manifest 覆盖项是否已获工单批准,且 handler / headers / x-provider-app 是否正确?
  • 每个路由实现是否都与 @vtex/payment-provider 中的类型及 payment-provider-protocol 中的响应形状一致?
  • 是否在需要时使用 this.retry(request) 实现 Gateway 重试?
  • 卡支付流程是否使用 SecureExternalClient(或等效方案),并设置 secureProxy: secureProxyUrl 和允许列表中的目标地址?
  • Beta/预发布环境测试是否在发布稳定版本前遵循 affiliation、测试模式、工作区和支付条件相关步骤?
  • 计费、App Store 提交和认证所需的前置条件是否记录在内部发布清单中?
  • manifest.json 是否包含针对 PSP 主机的 vbase-read-write 和 outbound-access 策略?

相关技能

参考资料

评论

支持 Markdown