关于 payment-ppp-pos
payment-ppp-pos 是用于创建、扩展或调试 VTEX Sales App 实体 POS 终端支付连接器的技能。它聚焦 POS 专属的 PPP 状态流程及 VTEX IO / PPF 约束,而非通用电商支付连接器。
主要功能
- 01实现 POS 支付流程端点
- 02指导终端识别与结果轮询
- 03校验 POS 支付方式名称
- 04区分 POS 与电商银行卡处理
- 05生成连接器代码和测试清单
适用场景
- 01创建 VTEX IO POS 连接器
- 02为现有 PPP 中间件添加 POS 支持
- 03排查终端识别和支付轮询问题
- 04准备 POS 连接器验证测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vtex/ skills --skill payment-ppp-pos安装后在 Claude Code 中输入/payment-ppp-pos确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/examples.mdgeneration.mdppf-scaffold.md详细介绍
PPP 应用于 POS(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)
- 目标是消除“从零开始实现”的障碍:从 Payment Provider Framework 入门指南克隆官方 PPF 示例,或在现有 PPP 上叠加 POS 支持,而不是生成通用电商连接器
在需要时加载配套文档。首次回复时不要一次性加载全部三份。
| 配套文档 | 加载时机 |
|---|---|
| generation.md | 用户要求创建 POS 连接器时。先询问四个问题,然后按列出的顺序实现。 |
| ppf-scaffold.md | 托管环境为 VTEX IO / PPF 时。将覆盖文件复制到官方 PPF 示例应用上。 |
| examples.md | 需要预期 JSON、状态机表格,或 IO 重试与独立通知的相关内容时。 |
以下硬性约束优先于电商 PPP 和 PCI 惯例。如果生成的文件违反其中任何一条,请停止并先修正,再继续编写文件。
以下情况不要默认使用此技能:
- 不涉及 POS 方法的电商 PPP 端点、清单和认证格式——使用
payment-provider-protocol - IO 连接器接线(
PaymentProvider、configuration.json、this.retry)——将此技能与payment-provider-framework配合使用 - Pix、Boleto 或浏览器重定向异步方式——使用
payment-async-flow - 电商银行卡数据和 Secure Proxy——使用
payment-pci-security - Sales App 购物车、PDP 或菜单 UI 扩展——使用
sales-app-extensibility
决策规则
- 除非合作伙伴已经托管 PPP 中间件,否则优先选择 VTEX IO / PPF。从 Payment Provider Framework 指南中克隆官方示例应用,并叠加 POS 支持;不要从空的 Express 应用开始。
- POS 支付仍属于 PPP。先实现六个支付流程端点,再叠加此技能中的 POS 规则。
authorize()是一个状态机。它不会等待 POS,只返回当前阶段。连接器会响应 Create Payment;它不会推动 Gateway 进入下一阶段。终端应用关闭后,VTEX Sales App 会触发 Gateway 回调,由 Gateway 发出 Create Payment(POS 指南第 8 步)。随后,Wait for confirmation 会轮询 Gateway,由 Gateway 发出更多 Create Payment(第 9 步)。在处理器报告最终状态之前,不要向callbackUrl发送 POST——在待处理状态下重试没有必要,每次 authorize 还会额外增加约 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) | 同一个等待应用;不进行第二次扣款;不重试 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 字符串。连接器可以同时支持电商和 POS。 - POS 流程中,Gateway 不会收到购物者的银行卡信息。所有
card字段都会是null。此流程中不要调用 Secure Proxy。 - 除非合作伙伴有明确记录的理由要构建自定义应用,否则请使用 VTEX 提供的现成支付应用:
- 识别终端:
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断言这些对象,因为这种转换会掩盖错误。POS 指南要求的cardBrand、firstDigits和lastDigits不在 SDK 响应类型中(已在@vtex/payment-provider1.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 中不要 POSTstatus/cardBrand。在下一次 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、firstDigits / lastDigits,例如 411111 / 1111)。请将它们替换为合作伙伴的连接器名称、公共主机、超时时间和处理方卡号片段。600 不是 VTEX 要求的默认值;规则是 delayToCancel >= secondsWaiting。示例 BIN / 卡号后四位的值是截断后的片段,不是 PAN。
约束:清单必须按精确的 API 名称声明 POS 支付方式
GET /manifest(以及 PPF paymentProvider/configuration.json 中的 paymentMethods)必须包含 Venda Direta Credito 和/或 Venda Direta Debito,且名称必须与 POS 指南中的 ASCII 名称完全一致。带重音符号的 Admin 标签(Venda Direta Crédito、Venda Direta Débito)仅用作显示名称。
原因
Gateway 和 Sales App 的支付条件会绑定到这些支付方式名称。如果清单仅列出 Visa / Mastercard,商家就无法配置 POS。如果连接器自行编造名称,例如 "POS Credit",Admin 和 Sales App 就不会关联店内支付流程。
检测
如果任务涉及 POS,且 paymentMethods 中缺少 Venda Direta Credito 或 Venda Direta Debito,请停止并先添加它们,再生成其他代码。
正确
{
"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 与相同的两个字符串进行比较。callbackUrl 和 value 已经在 Authorization 上;不要进行类型转换。
原因
购物者在实体终端上付款。将 POS 当作电商银行卡授权处理,会导致扣款失败;更糟的是,还可能使用空令牌调用 Secure Proxy。手动编写带重音符号的字符串,会导致问题在 Gateway 而不是编译时暴露。
检测
如果 POS 授权逻辑读取 card.number / secureProxyUrl,或者在 card 为 null 时返回 denied,请停止并将 POS 逻辑与电商逻辑分开。如果 PPF 代码使用 as any 读取 paymentMethod、callbackUrl 或 value,请停止并改用类型化字段/守卫。
正确
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 MUST 返回 status: "undefined" 和 vtex.terminal-connector-app 的 paymentAppData。payload MUST 是一个 JSON 字符串,其中包含 submitUrl,即接收 {"serialNumber": "12345"} 的 HTTPS 端点。
将示例中的主机替换为合作伙伴实际的公网 URL。响应前持久化 callbackUrl 和 paymentId。
在 PPF 中,返回 Authorizations.pending(authorization, { delayToCancel, paymentAppData })。AppData.payload 的类型为 Maybe<string>,因此对象类型的 payload 会导致编译错误。不要将该对象包装在 as AuthorizationResponse 中。
为何重要
没有此响应,Sales App 就不会打开摄像头/条码挑战,因此处理器无法获知要使用哪个终端。嵌套的 payload 对象不会注入 Payment App。对整个响应进行类型转换会掩盖此编译错误。
检测
如果首次 POS 响应为 approved/denied、未包含 paymentAppData、使用 vtex.challenge-terminal-connector-app、将 payload 设为对象,或在 pending 响应体上使用 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": "..."} 时,连接器 MUST 将该序列号与 paymentId 关联(POS 指南步骤 7)。不要在该处理器中启动 POS 扣款。
Sales App 在终端应用关闭后触发的下一次 Create Payment(步骤 8)MUST 启动一次处理器扣款,然后返回 status: "undefined",以及带有 payload {"secondsWaiting": <int>} 的 vtex.challenge-wait-for-confirmation(步骤 8.b–8.d)。后续轮询(步骤 9)MUST NOT 启动另一次扣款,也 MUST NOT POST callbackUrl。
序列号尚不存在时不要启动 POS 扣款,也不要在首次 authorize() 时启动(此时还没有序列号)。
secondsWaiting 是 Wait for confirmation 应用轮询的时长。delayToCancel(Create Payment 响应中的秒数)是 Gateway 在调用 Cancel 前让支付保持 undefined 的时长。设置 delayToCancel >= secondsWaiting。
为何重要
在终端尚未识别时启动扣款,会向错误的设备扣款,或根本无法扣款。在 submitUrl 中启动扣款会偏离已发布的流程(8.b 启动支付,8.c 确认支付已启动,8.d 返回等待应用)。如果 delayToCancel 短于 secondsWaiting,Gateway 会在 Sales App 仍在等待时取消支付。如果 Create Payment 在每次轮询时都启动新的处理器扣款,购物者就会被扣款两次。
检测
如果尚未保存序列号就返回 Wait for confirmation、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"cardBrandfirstDigits(前六位数字 / BIN)lastDigits(后四位数字)
这三个卡片字段是商户对账所必需的。它们由终端读取卡片后从处理器获取,而不是来自原始 Create Payment card 对象。
不要记录或存储完整 PAN。firstDigits + lastDigits 是允许保留的片段。
为什么这很重要
如果不通知 Gateway,Sales App 会一直停留在 Wait for confirmation,直到超时后取消。即使终端上的扣款成功,缺少 BIN / 后四位也会导致财务报告出错。在 IO 上,向 callbackUrl 发送独立的通知正文并不能完成流程——Gateway 需要先收到无正文的重试 POST,然后再调用一次新的 authorize()。
检测
如果 POS 代码在首次 Create Payment 时返回 approved,请停止。如果 PPF 代码在 fetch 中向 callbackUrl 发送包含 status / cardBrand 的正文,请停止并改用 Payments.retry / this.retry。如果 Service 路由调用 this.retry(这里没有 this),请停止并改用 new Payments(ctx.vtex).retry(stored.callbackUrl)。如果待处理的 authorize() POST 请求向 callbackUrl 发送内容以“提示”Gateway,请停止。如果独立代码在通知 POST 中遗漏了这三个卡片字段,请停止。
正确(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 必须返回当前存储的阶段和状态。不得创建第二笔处理器交易。只要状态仍为 undefined,Wait for confirmation 就会多次轮询 Create Payment。这些调用可能会重叠(requestId 不同,paymentId 相同)——先读取再保存的顺序操作可能会创建两条记录或丢弃阶段更新。
在 PPF 上,使用 getRawJSON 持久化(捕获 etag),然后使用 saveJSON(..., ifMatch)。遇到 412 时,重新读取并根据存储的文档继续操作。请结合 payment-idempotency 使用。首次写入 identify-terminal 时,不要写入每个请求各不相同的字段(随机 tid、requestId)。
VBase 会将元数据存储桶组成为 {vendor}.{appName}.{bucket},并拒绝超过 50 个字符的名称(400 Metadata bucket name too large)。请使用较短的存储桶名称,例如 pos。技能占位符(partnername.connector-partnerpos)适用于 pos-payments;实际的 vendor.appName 更长时则不适用。
不要在 HTTP 处理器中等待顾客完成 POS 操作。使用 compare-and-set 将 identify-terminal 变为 await-pos,仅在该写入获胜时启动处理器扣款,然后返回。密码重试和余额不足处理应在处理器与 POS 之间完成。
为什么这很重要
Gateway 会将超过 20 秒的响应视为失败。每次轮询都再次调用收单机构时,就会发生重复 POS 扣款。同一 paymentId 的首次写入发生重叠时,会返回不一致的 tid,破坏 PPP 幂等性。过长的 VBase 存储桶会在首次 Create Payment 时失败,并返回含糊的 400 错误。
检测
如果处理器 await 终端结果、在 phase 已是 await-pos 或最终状态时调用 processor.startPosCharge、使用不带 etag 的 getJSON + saveJSON,或者使用的 VBase 存储桶会导致 {vendor}.{appName}.{bucket} 超过 50 个字符,请停止。
正确
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 会向 submitUrl POST {"serialNumber":"..."}。该 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。请在 examples.md 中比较 IO 与独立版的负载。
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,却生成了电商 PPP 连接器(Visa/Mastercard + Secure Proxy)。
- 清单中遗漏
Venda Direta Credito/Venda Direta Debito,或将带重音符号的 Admin 标签用作 API 名称。 - 在终端完成操作前就返回
approved,或在 Create Payment 中等待插入银行卡。 - 使用
vtex.challenge-terminal-connector-app,而不是vtex.terminal-connector-app。 - 将
paymentAppData.payload作为对象发送,而不是使用JSON.stringify(...);或者在待处理的请求体上使用as AuthorizationResponse来掩盖这一问题。 - 将
delayToCancel设置得低于secondsWaiting,导致 Gateway 在 Wait 应用仍在轮询时取消付款。 - 在
submitUrl处理程序中启动 POS 扣款(POS 指南第 8.b 步是在随后的 Create Payment 中启动扣款)。 - 每次 Create Payment 轮询时都启动新的 POS 扣款(幂等性失效)。
- 在待处理响应中 POST
callbackUrl来“提醒”Gateway。Sales App 和 Wait for confirmation 已会驱动 Create Payment。 - 对同一个
paymentId的并发 Create Payment 调用使用getJSON+saveJSON,却不使用etag。 - 使用的 VBase bucket 会导致
{vendor}.{appName}.{bucket}超过 50 个字符(默认使用pos)。 - 跳过 Gateway 回调,或遗漏
cardBrand、firstDigits和lastDigits。 - **在 VTEX IO 上:**将独立通知负载(
status+ 卡片字段)POST 到callbackUrl,而不是发送无请求体的Payments.retry/this.retry。 - 将示例中的 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 authorize 应用
payment-pci-securitySecure Proxy。 - 构建 Sales App 购物车/PDP 扩展,而不是支付连接器。
- 合作伙伴可以使用 PPF / 官方示例应用时,却从空 Express 应用开始构建。
审查清单
- 对于 PPF,是否基于官方 PPF 示例(或现有 PPF 连接器)叠加构建应用,而不是从空白 HTTP 服务开始?
-
/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,不产生第二笔 POS 扣款,不在 pending 时重试)? - PPF VBase 存储桶名称是否足够短,使
{vendor}.{appName}.{bucket}不超过 50 个字符(使用pos)? - PPF: 处理器 webhook 是否先持久化卡片片段,再调用
Payments.retry(callbackUrl)(或在PaymentProvider内调用this.retry)?下一次authorize()是否返回卡片片段(仅对最终响应进行强制转换)? - 独立实现: 通知 POST 是否包含
status、cardBrand、firstDigits、lastDigits? - 是否已实现 Cancel,以便 Gateway 超时可以中止状态为
undefined的 POS 付款? - 是否已告知合作伙伴仍需要 Sales App 设备、POS 终端、测试账户、付款条件和认证?
测试前提条件(无法通过技能自动化)
VTEX 通常会提供预配置的商店用于连接器测试。如需手动配置商店:
- 安装连接器(PPF:按照 Payment Provider Framework 安装步骤操作)。
- 为连接器注册 Gateway 关联。
- 在该关联上创建付款条件 Venda Direta Crédito 和/或 Venda Direta Débito。
- 在 Sales App 中显示这些付款方式(
checkout-instore-custom.js)。标准条件 ID 为44(借记卡)和45(信用卡);请在 Admin 的 Payment Conditions 页面确认。 - 在设备上安装 VTEX Sales App,并在真实 POS 或认证测试 POS 上完成购买。
- 运行 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 扣款;同一 ID 的 Create Payment 可能并发执行(etag /ifMatch)payment-pci-security— 仅适用于电子商务非面对面刷卡;不要将 Secure Proxy 用于 POS authorizesales-app-extensibility— Sales App UI 扩展;不适用于 POS 付款连接器
参考资料
- 销售点(POS)支付服务商协议 - VTEX 销售应用 — Manifest 方法、两阶段 Payment Apps、超时、回调卡片字段、测试
- Payment App —
paymentAppData.appName和payload契约 - Payment Provider Framework — 克隆官方示例应用(入门指南)、IO 连接器实现、
this.retry、额外路由 - Payment Provider Protocol(帮助中心) — 回调 URL、包含
cardBrand/firstDigits/lastDigits的通知载荷 - 定义 VTEX 销售应用中显示的支付方式 — 销售应用支付条件筛选器(
44借记卡、45信用卡) - 支付服务商认证 — 测试套件和发布
- 注册网关关联 — 测试用关联设置
- 配置支付条件 — Venda Direta 的管理条件