payment-ppp-pos
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePPP Applied to POS (VTEX Sales App)
将 PPP 应用于 POS(VTEX Sales App)
When this skill applies
此 skill 的适用场景
Use this skill when the task is to create, extend, or debug a payment connector that charges a physical card on a POS terminal from VTEX Sales App.
- The user mentions PPP + POS, POS connector, in-store payments, Sales App card payments, payment terminal, or /
Venda Direta CreditoVenda Direta Debito - The source of truth is Payment Provider Protocol for Point of Sale (POS)
- The goal is to remove the "implement from scratch" barrier: clone the official PPF example from Payment Provider Framework Getting started, or overlay POS on existing PPP — not generate a generic ecommerce connector
Load companions when the step needs them. Do not dump all three into the first reply.
| Companion | Load when |
|---|---|
| generation.md | The user asked to create a POS connector. Ask the four questions, then implement in the listed order. |
| ppf-scaffold.md | Hosting is VTEX IO / PPF. Copy overlay files onto the official PPF example app. |
| examples.md | You need expected JSON, the state machine table, or IO retry vs standalone notification. |
The hard constraints below override ecommerce PPP and PCI habits. If a generated file violates one, STOP and fix it before writing more files.
Do not use this skill as the default choice for:
- Ecommerce PPP endpoints, manifests, and homologation shapes with no POS methods — use
payment-provider-protocol - IO connector wiring (,
PaymentProvider,configuration.json) — usethis.retrytogether with this skillpayment-provider-framework - Pix, Boleto, or browser-redirect async methods — use
payment-async-flow - Ecommerce card data and Secure Proxy — use
payment-pci-security - Sales App cart, PDP, or menu UI extensions — use
sales-app-extensibility
当任务是从 VTEX Sales App 创建、扩展或调试一个通过 POS 终端对实体卡进行扣款的支付连接器时,使用此 skill。
- 用户提到 PPP + POS、POS connector、门店支付、Sales App 卡支付、payment terminal,或 /
Venda Direta CreditoVenda Direta Debito - 事实来源是面向 POS 的 Payment Provider Protocol
- 目标是消除“从零实现”的障碍:从 Payment Provider Framework 入门指南克隆官方 PPF 示例,或将 POS 叠加到现有 PPP 上——而不是生成通用电商连接器
按具体步骤需要时加载配套 skill。不要在首次回复中一次性倾倒全部三个内容。
| 配套 skill | 何时加载 |
|---|---|
| generation.md | 用户要求创建 POS 连接器。先询问四个问题,再按列出的顺序实现。 |
| ppf-scaffold.md | 托管方式是 VTEX IO / PPF。将叠加文件复制到官方 PPF 示例应用。 |
| examples.md | 需要预期 JSON、状态机表,或 IO 重试与独立通知的对比时。 |
以下硬性约束优先于电商 PPP 和 PCI 惯例。如果生成的文件违反其中任何一项,请立即停止并先修复,然后再编写更多文件。
不要将此 skill 作为以下场景的默认选择:
- 不含 POS 方法的电商 PPP 端点、manifest 和 homologation 数据结构——使用
payment-provider-protocol - IO 连接器配置(、
PaymentProvider、configuration.json)——将this.retry与此 skill 配合使用payment-provider-framework - Pix、Boleto 或浏览器重定向异步方式——使用
payment-async-flow - 电商卡数据和 Secure Proxy——使用
payment-pci-security - Sales App 购物车、PDP 或菜单 UI 扩展——使用
sales-app-extensibility
Decision rules
决策规则
- Prefer VTEX IO / PPF unless the partner already hosts PPP middleware. Clone the official example app from the Payment Provider Framework guide and overlay POS; do not start from an empty Express app.
- POS payments are still PPP. Implement the six payment-flow endpoints, then overlay the POS rules in this skill.
- is a state machine. It never waits on the POS. It only returns the current phase. The connector answers Create Payment; it does not push the Gateway between phases. After the terminal app closes, VTEX Sales App triggers a Gateway callback that issues Create Payment (POS guide step 8). Wait for confirmation then polls the Gateway, which issues more Create Payment (step 9). Do not POST
authorize()until the processor reports a final status — retry-on-pending is unnecessary and adds about 1 second per authorize.callbackUrl
| Stored phase | Who drives the next Create Payment | Create Payment returns |
|---|---|---|
| (none yet) | Sales App checkout → Gateway (step 3) | Persist |
| Gateway / Sales App | Same terminal app until |
| Sales App after the terminal app closes (step 8) | Start the processor charge once; |
| Wait for confirmation polling (step 9) | Same wait app; no second charge; no retry POST |
| Wait app / Sales App after IO retry | Final status + |
- and
Venda Direta Creditoare asynchronous card-present methods. ReturnVenda Direta Debitoand resolve later viastatus: "undefined". Do not treat them as sync Visa/Mastercard authorizations.callbackUrl - Branch on . On PPF, use
paymentMethodfromisDirectSaleAuthorization(canonical@vtex/payment-providernames, and the narrowed type has noDirectSale/card). On standalone HTTP, compare to those same ASCII strings. A connector may support ecommerce and POS.secureProxyUrl - The Gateway does not receive the shopper's card for POS. All fields arrive as
card. Do not call Secure Proxy on this path.null - Use VTEX's ready-made Payment Apps unless the partner has a documented reason to build a custom one:
- Identify the terminal:
vtex.terminal-connector-app - Wait for the POS result:
vtex.challenge-wait-for-confirmation
- Identify the terminal:
- Do not use . That name appears as a flow-diagram example in the POS guide; the implementation contract and the published app are
vtex.challenge-terminal-connector-app.vtex.terminal-connector-app - is a serialized JSON string, not a nested object. On PPF, build pending responses with
paymentAppData.payloadso that is a compile error. Do not close those objects withAuthorizations.pending— that cast hides the mistake.as AuthorizationResponse,cardBrand, andfirstDigitsare required by the POS guide but are not on the SDK response types (checked onlastDigits1.4.0 and 1.8.0); the final approved/denied response is the one place a cast is justified.@vtex/payment-provider - Communication between the payment processor and the physical POS is outside VTEX. The connector only identifies the terminal, starts the processor transaction, and reports or
approvedto the Gateway.denied - Without VTEX IO, is a notification endpoint: POST the final status (include POS card fields). With VTEX IO,
callbackUrlis a retry endpoint: persist state, then POST it with an empty body. Inside acallbackUrlmethod usePaymentProvider. From athis.retry(request)extra route useservice.json— same client asnew Payments(ctx.vtex).retry(callbackUrl)(gateway-retry metric, IO tracing, configured timeout). Do not POSTthis.retry/statuson IO. Return the final status plus POS card fields on the next Create Payment /cardBrand.authorize() - POS serial collection and the processor webhook are extra public routes, not PPP inbound-request. The official example's is Test Suite only.
this.callback(req, resp) - must be greater than or equal to
delayToCancel.secondsWaiting - Every connector endpoint must respond in under 20 seconds in production (under 5 seconds during the Payment Provider Test Suite).
- This skill can generate connector code and a test checklist. It cannot replace a real POS, a Sales App device, a configured test account, or homologation.
- 除非合作方已经自行托管 PPP middleware,否则优先使用 VTEX IO / PPF。从 Payment Provider Framework 指南克隆官方示例应用并叠加 POS;不要从空白 Express app 开始。
- POS 支付仍属于 PPP。先实现六个支付流程端点,然后叠加此 skill 中的 POS 规则。
- 是一个状态机。它绝不等待 POS 完成,只返回当前阶段。连接器负责响应 Create Payment;不会在两个阶段之间主动推动 Gateway。终端 app 关闭后,VTEX Sales App 会触发 Gateway callback,进而发起 Create Payment(POS guide 第 8 步)。随后,Wait for confirmation 轮询 Gateway,Gateway 会发出更多 Create Payment(第 9 步)。在 processor 报告最终状态前,不要 POST
authorize();对 pending 状态执行重试没有必要,而且每次 authorize 会增加约 1 秒。callbackUrl
| 已存储阶段 | 谁驱动下一次 Create Payment | Create Payment 返回 |
|---|---|---|
| (尚无) | Sales App 结账 → Gateway(第 3 步) | 持久化 |
| Gateway / Sales App | 继续使用同一个终端 app,直到 |
| 终端 app 关闭后的 Sales App(第 8 步) | 仅启动一次 processor 扣款; |
| Wait for confirmation 轮询(第 9 步) | 同一个等待 app;不进行第二次扣款;不重试 POST |
| IO 重试后的 Wait app / Sales App | 最终状态 + |
- 和
Venda Direta Credito是异步 card-present 方式。返回Venda Direta Debito,并稍后通过status: "undefined"完成。不要将它们视为同步 Visa/Mastercard 授权。callbackUrl - 根据 分支。在 PPF 上,使用
paymentMethod中的@vtex/payment-provider(它使用规范的isDirectSaleAuthorization名称,且收窄后的类型没有DirectSale/card)。在独立 HTTP 实现中,将secureProxyUrl与相同的两个 ASCII 字符串比较。连接器可以同时支持电商和 POS。paymentMethod - Gateway 不会收到 POS 购物者的卡数据。所有 字段均为
card。不要在此路径上调用 Secure Proxy。null - 除非合作方有记录在案的定制理由,否则使用 VTEX 官方现成的 Payment Apps:
- 识别终端:
vtex.terminal-connector-app - 等待 POS 结果:
vtex.challenge-wait-for-confirmation
- 识别终端:
- 不要使用 。该名称仅作为 POS guide 中流程图的示例出现;实际实现约定和已发布的 app 是
vtex.challenge-terminal-connector-app。vtex.terminal-connector-app - 是序列化后的 JSON 字符串,不是嵌套对象。在 PPF 上,使用
paymentAppData.payload构建 pending 响应,使传入对象产生编译错误。不要使用Authorizations.pending关闭这些对象;这种强制转换会隐藏错误。POS guide 要求提供as AuthorizationResponse、cardBrand和firstDigits,但 SDK 响应类型中没有这些字段(已检查lastDigits1.4.0 和 1.8.0);只有在最终 approved/denied 响应中才适合使用强制转换。@vtex/payment-provider - 支付 processor 与实体 POS 之间的通信不在 VTEX 范围内。连接器只负责识别终端、启动 processor 交易,并向 Gateway 报告 或
approved。denied - 不使用 VTEX IO 时,是通知端点:POST 最终状态(包含 POS 卡字段)。使用 VTEX IO 时,
callbackUrl是重试端点:先持久化状态,然后使用空请求体 POST 它。在callbackUrl方法中使用PaymentProvider。从this.retry(request)的额外路由中使用service.json——它与new Payments(ctx.vtex).retry(callbackUrl)使用同一客户端(this.retry指标、IO tracing 和配置的 timeout)。在 IO 上不要 POSTgateway-retry/status。在下一次 Create Payment /cardBrand中返回最终状态和 POS 卡字段。authorize() - POS serial 收集和 processor webhook 是额外的公共路由,不是 PPP inbound-request。官方示例中的 仅供 Test Suite 使用。
this.callback(req, resp) - 必须大于或等于
delayToCancel。secondsWaiting - 生产环境中,每个连接器端点都必须在 20 秒内响应;Payment Provider Test Suite 中必须在 5 秒内响应。
- 此 skill 可以生成连接器代码和测试检查清单,但无法替代真实 POS、Sales App 设备、已配置的测试账户或 homologation。
Hard constraints
硬性约束
Code samples use example identifiers (, , , ) and example numbers (, / such as / ). Replace them with the partner's connector name, public hosts, timeouts, and processor card fragments. is not a VTEX-required default; the rule is . Example BIN/last-four values are truncated fragments, not a PAN.
PartnerPospartnernameconnector-partnerposprovider.example.comSECONDS_WAITING = 600firstDigitslastDigits4111111111600delayToCancel >= secondsWaiting代码示例使用示例标识符(、、、)和示例数字(,以及类似 / 的 / )。将它们替换为合作方的连接器名称、公共 host、timeout 和 processor 卡片段。 不是 VTEX 要求的默认值;规则是 。示例 BIN/last-four 值是截断片段,不是 PAN。
PartnerPospartnernameconnector-partnerposprovider.example.comSECONDS_WAITING = 6004111111111firstDigitslastDigits600delayToCancel >= secondsWaitingConstraint: Manifest must declare the POS payment methods by exact API name
约束:Manifest 必须使用精确 API 名称声明 POS 支付方式
GET (and PPF ) MUST include and/or with the exact ASCII names from the POS guide. Admin labels with accents (, ) are display names only.
/manifestpaymentProvider/configuration.jsonpaymentMethodsVenda Direta CreditoVenda Direta DebitoVenda Direta CréditoVenda Direta DébitoWhy this matters
The Gateway and Sales App payment conditions bind to these method names. If the manifest only lists / , merchants cannot configure POS. If the connector invents a name such as , Admin and Sales App will not attach the in-store flow.
VisaMastercard"POS Credit"Detection
If the task is POS and lacks or , STOP and add them before generating other code.
paymentMethodsVenda Direta CreditoVenda Direta DebitoCorrect
json
{
"paymentMethods": [
{ "name": "Venda Direta Debito", "allowsSplit": "onCapture" },
{ "name": "Venda Direta Credito", "allowsSplit": "onCapture" }
]
}Wrong
json
{
"paymentMethods": [
{ "name": "Visa", "allowsSplit": "onCapture" },
{ "name": "Mastercard", "allowsSplit": "onCapture" },
{ "name": "POS Credit", "allowsSplit": "onCapture" }
]
}GET (以及 PPF 的 )必须包含 POS guide 中精确的 ASCII 名称 和/或 。带重音符号的 Admin 标签(、)仅用于显示。
/manifestpaymentProvider/configuration.jsonpaymentMethodsVenda Direta CreditoVenda Direta DebitoVenda Direta CréditoVenda Direta Débito为什么这很重要
Gateway 和 Sales App 的支付条件绑定到这些方式名称。如果 manifest 只列出 / ,商家就无法配置 POS。如果连接器自创诸如 的名称,Admin 和 Sales App 将无法附加门店支付流程。
VisaMastercardPOS Credit检测方式
如果任务是 POS,但 中缺少 或 ,请立即停止,并在生成其他代码前补充它们。
paymentMethodsVenda Direta CreditoVenda Direta Debito正确
json
{
"paymentMethods": [
{ "name": "Venda Direta Debito", "allowsSplit": "onCapture" },
{ "name": "Venda Direta Credito", "allowsSplit": "onCapture" }
]
}错误
json
{
"paymentMethods": [
{ "name": "Visa", "allowsSplit": "onCapture" },
{ "name": "Mastercard", "allowsSplit": "onCapture" },
{ "name": "POS Credit", "allowsSplit": "onCapture" }
]
}Constraint: Detect POS from paymentMethod
and never read card PAN from the Gateway
paymentMethod约束:通过 paymentMethod 检测 POS,绝不能从 Gateway 读取卡 PAN
For and , Create Payment sends , , , , , and as . The connector MUST NOT require those fields, MUST NOT call Secure Proxy, and MUST NOT decline the payment because card data is missing.
Venda Direta CreditoVenda Direta Debitocard.holdercard.numbercard.csccard.expirationcard.documentcard.tokennullOn PPF, branch with from . That guard uses the SDK names (exact ASCII), and has no and no — so is a compile error after the narrow. Do not hand-roll or . On standalone HTTP, compare to those same two strings. and are already on ; do not cast them.
isDirectSaleAuthorization@vtex/payment-providerDirectSaleDirectSaleAuthorizationcardsecureProxyUrlauthorization.cardPOS_METHODS(authorization as any).paymentMethodpaymentMethodcallbackUrlvalueAuthorizationWhy this matters
The shopper pays on the physical terminal. Treating POS as an ecommerce card authorization fails the charge, or worse, invents Secure Proxy calls with empty tokens. A hand-rolled accented string fails at the Gateway instead of at compile time.
Detection
If POS authorize logic reads / or returns when is null, STOP and split POS from ecommerce. If PPF code uses to read , , or , STOP and use the typed fields / guard.
card.numbersecureProxyUrldeniedcardas anypaymentMethodcallbackUrlvalueCorrect
typescript
import {
isDirectSaleAuthorization,
AuthorizationRequest,
} from "@vtex/payment-provider";
async function authorize(request: AuthorizationRequest) {
if (isDirectSaleAuthorization(request)) {
return authorizePos(request);
}
return authorizeEcommerce(request);
}Wrong
typescript
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 会将 、、、、 和 作为 发送。连接器绝不能要求这些字段,绝不能调用 Secure Proxy,也绝不能因缺少卡数据而拒绝支付。
Venda Direta CreditoVenda Direta Debitocard.holdercard.numbercard.csccard.expirationcard.documentcard.tokennull在 PPF 上,使用 中的 进行分支。该守卫使用 SDK 的 名称(精确 ASCII),且 没有 和 ——因此收窄后访问 会产生编译错误。不要手写 ,也不要使用 。在独立 HTTP 实现中,将 与相同的两个字符串比较。 和 已存在于 上;不要强制转换它们。
@vtex/payment-providerisDirectSaleAuthorizationDirectSaleDirectSaleAuthorizationcardsecureProxyUrlauthorization.cardPOS_METHODS(authorization as any).paymentMethodpaymentMethodcallbackUrlvalueAuthorization为什么这很重要
购物者是在实体终端上付款。将 POS 当作电商卡授权会导致扣款失败,甚至在 token 为空时错误地发起 Secure Proxy 调用。手写带重音符号的字符串会直接导致 Gateway 失败,而不是在编译期失败。
检测方式
如果 POS authorize 逻辑读取 / ,或者在 为 null 时返回 ,请立即停止并将 POS 与电商流程分离。如果 PPF 代码使用 读取 、 或 ,请立即停止并改用有类型的字段或守卫。
card.numbersecureProxyUrlcarddeniedas anypaymentMethodcallbackUrlvalue正确
typescript
import {
isDirectSaleAuthorization,
AuthorizationRequest,
} from "@vtex/payment-provider";
async function authorize(request: AuthorizationRequest) {
if (isDirectSaleAuthorization(request)) {
return authorizePos(request);
}
return authorizeEcommerce(request);
}错误
typescript
async function authorize(request: CreatePaymentRequest) {
if (!request.card?.number) {
return { status: "denied", message: "Missing card number" };
}
return secureProxy.charge(request.card, request.secureProxyUrl);
}Constraint: First Create Payment response must open the terminal Payment App
约束:首次 Create Payment 响应必须打开终端 Payment App
When the connector has not yet received the POS serial number for this , Create Payment MUST return and for . MUST be a JSON string with , the HTTPS endpoint that receives .
paymentIdstatus: "undefined"paymentAppDatavtex.terminal-connector-apppayloadsubmitUrl{"serialNumber": "12345"}Replace the host in examples with the partner's real public URL. Persist and before responding.
callbackUrlpaymentIdOn PPF, return . is , so an object payload is a compile error. Do not wrap that object in .
Authorizations.pending(authorization, { delayToCancel, paymentAppData })AppData.payloadMaybe<string>as AuthorizationResponseWhy this matters
Without this response, Sales App never opens the camera/barcode challenge, so the processor never learns which terminal to use. A nested object is not injected into the Payment App. A whole-response cast turns that compile error off.
payloadDetection
If the first POS response is /, omits , uses , sets as an object, or uses on a pending body, STOP and fix.
approveddeniedpaymentAppDatavtex.challenge-terminal-connector-apppayloadas AuthorizationResponseCorrect
typescript
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);
}Wrong
typescript
return {
paymentId: request.paymentId,
status: "approved",
paymentAppData: {
appName: "vtex.challenge-terminal-connector-app",
payload: {
submitUrl: "https://provider.example.com/pos/serial",
},
},
};当连接器尚未收到某个 对应的 POS serial number 时,Create Payment 必须返回 状态,以及用于 的 。 必须是 JSON 字符串,其中包含用于接收 的 HTTPS 端点 。
paymentIdundefinedvtex.terminal-connector-apppaymentAppDatapayload{"serialNumber": "12345"}submitUrl将示例中的 host 替换为合作方真实的公共 URL。在响应前持久化 和 。
callbackUrlpaymentId在 PPF 上,返回 。 的类型是 ,因此传入对象 payload 会产生编译错误。不要使用 包装该对象。
Authorizations.pending(authorization, { delayToCancel, paymentAppData })AppData.payloadMaybe<string>as AuthorizationResponse为什么这很重要
如果没有此响应,Sales App 永远不会打开摄像头/条形码 challenge,processor 因而无法得知应使用哪个终端。嵌套的 对象不会被注入 Payment App。对整个响应进行强制转换会关闭此编译错误。
payload检测方式
如果首次 POS 响应是 /、缺少 、使用 、将 设为对象,或者对 pending 响应体使用 ,请立即停止并修复。
approveddeniedpaymentAppDatavtex.challenge-terminal-connector-apppayloadas AuthorizationResponse正确
typescript
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);
}错误
typescript
return {
paymentId: request.paymentId,
status: "approved",
paymentAppData: {
appName: "vtex.challenge-terminal-connector-app",
payload: {
submitUrl: "https://provider.example.com/pos/serial",
},
},
};Constraint: After the serial number, wait with vtex.challenge-wait-for-confirmation
vtex.challenge-wait-for-confirmation约束:获取 serial number 后,使用 vtex.challenge-wait-for-confirmation 等待
When receives , the connector MUST associate that serial with (POS guide step 7). Do not start the POS charge in that handler.
submitUrl{"serialNumber": "..."}paymentIdThe next Create Payment — driven by Sales App after the terminal app closes (step 8) — MUST start the processor charge once, then return plus with payload (steps 8.b–8.d). Later polls (step 9) MUST NOT start another charge and MUST NOT POST .
status: "undefined"vtex.challenge-wait-for-confirmation{"secondsWaiting": <int>}callbackUrlDo not start the POS charge before the serial number exists, and do not start it from the first (no serial yet).
authorize()secondsWaitingdelayToCancelundefineddelayToCancel >= secondsWaitingWhy this matters
Starting the terminal before it is identified charges the wrong device or none. Starting the charge in diverges from the published sequence (8.b starts the payment, 8.c confirms it started, 8.d returns the wait app). If is shorter than , the Gateway cancels while Sales App is still waiting. If Create Payment starts a new processor charge on every poll, the shopper is charged twice.
submitUrldelayToCancelsecondsWaitingDetection
If Wait for confirmation is returned before a serial is stored, if runs in the serial handler or on every poll, if is missing, or if , STOP.
startPosChargesecondsWaitingdelayToCancel < secondsWaitingCorrect
typescript
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 }),
},
});
}Wrong
typescript
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 }),
},
};
}当 收到 时,连接器必须将该 serial 与 关联(POS guide 第 7 步)。不要在该处理器中启动 POS 扣款。
submitUrl{"serialNumber": "..."}paymentId由 Sales App 在终端 app 关闭后驱动的下一次 Create Payment(第 8 步)必须仅启动一次 processor 扣款,然后返回 状态和 ,其 payload 为 (第 8.b–8.d 步)。后续轮询(第 9 步)绝不能启动第二次扣款,也绝不能 POST 。
undefinedvtex.challenge-wait-for-confirmation{"secondsWaiting": <int>}callbackUrl在 serial number 存在之前不要启动 POS 扣款,也不要在首次 中启动它。
authorize()secondsWaitingdelayToCancelundefineddelayToCancel >= secondsWaiting为什么这很重要
在识别终端之前启动终端操作,可能对错误的设备扣款,或根本没有设备可扣款。在 中启动扣款会偏离已发布的流程顺序(第 8.b 步启动支付,第 8.c 步确认已启动,第 8.d 步返回等待 app)。如果 小于 ,Gateway 会在 Sales App 仍在等待时取消支付。如果每次 Create Payment 轮询都创建新的 processor 交易,购物者就会被重复扣款。
submitUrldelayToCancelsecondsWaiting检测方式
如果在存储 serial 之前返回 Wait for confirmation、在 serial 处理器或每次轮询中运行 、缺少 ,或者 ,请立即停止。
startPosChargesecondsWaitingdelayToCancel < secondsWaiting正确
typescript
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 }),
},
});
}错误
typescript
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 }),
},
};
}Constraint: Approve or deny only through the mandatory callback, including POS card fields
约束:只能通过必需的 callback 执行批准或拒绝,并包含 POS 卡字段
The processor, not VTEX, reads the physical card. When it reports a final result, the connector MUST notify the Gateway. POS cannot be approved or denied without this callback. Include:
- :
statusor"approved""denied" cardBrand- (first six digits / BIN)
firstDigits - (last four digits)
lastDigits
Those three card fields are required for merchant reconciliation. They come from the processor after the terminal reads the card — not from the original Create Payment object.
cardDo not log or store the full PAN. + are the allowed fragments.
firstDigitslastDigitsWhy this matters
Without notifying the Gateway, Sales App stays on Wait for confirmation until timeout, then cancels. Missing BIN/last-four breaks financial reports even when the charge succeeded on the terminal. On IO, posting a standalone notification body to does not complete the flow — the Gateway expects a bodiless retry POST, then a new .
callbackUrlauthorize()Detection
If POS code returns on the first Create Payment, STOP. If PPF code es with / in the body, STOP and use / . If a Service route calls (there is no ), STOP and use . If pending POSTs to "nudge" the Gateway, STOP. If standalone code omits the three card fields on the notification POST, STOP.
approvedfetchcallbackUrlstatuscardBrandPayments.retrythis.retrythis.retrythisnew Payments(ctx.vtex).retry(stored.callbackUrl)authorize()callbackUrlCorrect (PPF extra route — typical processor webhook)
typescript
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);Correct (inside a method only)
PaymentProvidertypescript
return this.retry(authorization);The next MUST return , , , and from the persisted record. See examples.md for the standalone notification payload.
authorize()statuscardBrandfirstDigitslastDigitsWrong
typescript
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",
}),
});读取实体卡的是 processor,而不是 VTEX。当 processor 报告最终结果时,连接器必须通知 Gateway。没有此 callback,POS 就不能被批准或拒绝。必须包含:
- :
status或"approved""denied" cardBrand- (前六位 / BIN)
firstDigits - (后四位)
lastDigits
这三个卡字段是商家对账所必需的。它们来自终端读取卡片后的 processor 结果,而不是最初 Create Payment 中的 对象。
card不要记录或存储完整 PAN。允许存储的片段是 + 。
firstDigitslastDigits为什么这很重要
如果不通知 Gateway,Sales App 会一直停留在 Wait for confirmation,直到超时后被取消。缺少 BIN/last-four 会导致财务报告出错,即使终端扣款已经成功。在 IO 上,向 POST 独立通知请求体无法完成流程——Gateway 期望收到一个无请求体的重试 POST,随后发起新的 。
callbackUrlauthorize()检测方式
如果 POS 代码在首次 Create Payment 时返回 ,请立即停止。如果 PPF 代码使用带有 / 请求体的 调用 ,请立即停止并改用 / 。如果 Service 路由调用 (此处不存在 ),请立即停止并改用 。如果 pending POST 来“推动”Gateway,请立即停止。如果独立部署代码的通知 POST 缺少三个卡字段,请立即停止。
approvedstatuscardBrandfetchcallbackUrlPayments.retrythis.retrythis.retrythisnew Payments(ctx.vtex).retry(stored.callbackUrl)authorize()callbackUrl正确(PPF 额外路由——典型 processor webhook)
typescript
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 方法内部)
typescript
return this.retry(authorization);下一次 必须从持久化记录返回 、、 和 。有关独立通知 payload,请参阅 examples.md。
authorize()statuscardBrandfirstDigitslastDigits错误
typescript
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",
}),
});Constraint: Keep Create Payment idempotent and answers under 20 seconds
约束:保持 Create Payment 幂等,并在 20 秒内响应
Create Payment for a given MUST return the current stored phase and status. It MUST NOT create a second processor transaction. Polling from Wait for confirmation will call Create Payment many times while status is still . Those calls can overlap (different , same ) — a sequential get-then-save can create two records or drop a phase update.
paymentIdundefinedrequestIdpaymentIdOn PPF, persist with (capture ) then . On 412, re-read and continue from the stored document. Pair with . Do not put unique per-request fields (random , ) on the first identify-terminal write.
getRawJSONetagsaveJSON(..., ifMatch)payment-idempotencytidrequestIdVBase composes the metadata bucket as and rejects names longer than 50 characters (). Use a short bucket such as . The skill placeholders () fit ; a longer real does not.
{vendor}.{appName}.{bucket}400 Metadata bucket name too largepospartnername.connector-partnerpospos-paymentsvendor.appNameDo not wait for the shopper to finish on the POS inside the HTTP handler. Compare-and-set → , start the processor charge only as the winner of that write, and return. Password retries and insufficient-funds handling stay between the processor and the POS.
identify-terminalawait-posWhy this matters
The Gateway treats a >20s response as failure. Duplicate POS charges happen when each poll hits the acquirer again. Overlapping first writes for one return inconsistent s and break PPP idempotency. A long VBase bucket fails on the first Create Payment with an opaque 400.
paymentIdtidDetection
If the handler s the terminal result, calls when is already or final, uses + with no , or uses a VBase bucket that makes longer than 50 characters, STOP.
awaitprocessor.startPosChargephaseawait-posgetJSONsaveJSONetag{vendor}.{appName}.{bucket}Correct
typescript
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}`,
}),
},
});
}Wrong
typescript
async function authorizePos(request: CreatePaymentRequest) {
const result = await processor.startPosCharge(request);
await processor.waitUntilCardFinished(result, { timeoutMs: 120000 });
return { paymentId: request.paymentId, status: result.status };
}对于给定的 ,Create Payment 必须返回当前存储的阶段和状态。它绝不能创建第二个 processor 交易。Wait for confirmation 会在状态仍为 时多次调用 Create Payment。这些调用可能重叠( 不同,但 相同)——顺序执行的先获取再保存可能创建两条记录或丢失阶段更新。
paymentIdundefinedrequestIdpaymentId在 PPF 上,使用 读取并持久化(获取 ),然后通过 保存。遇到 412 时,重新读取并从已存储文档继续执行。与 配合使用。不要在首次 identify-terminal 写入中放入按请求唯一的字段(随机 、)。
getRawJSONetagsaveJSON(..., ifMatch)payment-idempotencytidrequestIdVBase 会将元数据 bucket 组合为 ,并拒绝超过 50 个字符的名称()。使用短 bucket,例如 。此 skill 中的占位符()可以用于 ;更长的真实 则不可以。
{vendor}.{appName}.{bucket}400 Metadata bucket name too largepospartnername.connector-partnerpospos-paymentsvendor.appName不要在 HTTP 处理器中等待购物者在 POS 上完成操作。通过 compare-and-set 将 转换为 ,只有写入成功的请求才能启动 processor 扣款,然后立即返回。密码重试和余额不足处理应留在 processor 与 POS 之间完成。
identify-terminalawait-pos为什么这很重要
Gateway 会将超过 20 秒的响应视为失败。当每次轮询都访问 acquirer 时,会发生重复 POS 扣款。对于同一个 ,重叠的首次写入会返回不一致的 ,从而破坏 PPP 幂等性。过长的 VBase bucket 会导致首次 Create Payment 出现不透明的 400 错误。
paymentIdtid检测方式
如果处理器 终端结果、在 已经是 或最终状态时调用 、使用没有 的 + ,或者使用会导致 超过 50 个字符的 VBase bucket,请立即停止。
awaitphaseawait-posprocessor.startPosChargeetaggetJSONsaveJSON{vendor}.{appName}.{bucket}正确
typescript
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}`,
}),
},
});
}错误
typescript
async function authorizePos(request: CreatePaymentRequest) {
const result = await processor.startPosCharge(request);
await processor.waitUntilCardFinished(result, { timeoutMs: 120000 });
return { paymentId: request.paymentId, status: result.status };
}Constraint: Collect the POS serial on a public custom route, not inbound-request
约束:通过公共自定义路由收集 POS serial,而不是 inbound-request
vtex.terminal-connector-app{"serialNumber":"..."}submitUrlservice.jsonroutesPaymentProviderServicePOST /payments/{paymentId}/inbound-request/{action}Why this matters
Inbound-request is Gateway-initiated. The terminal app posts from the Sales App device. If is an inbound path, the serial never arrives and the POS charge never starts.
submitUrlDetection
If contains , STOP. If PPF has no extra route for serial collection, STOP.
submitUrl/inboundservice.jsonCorrect
json
{
"routes": {
"posSerial": {
"path": "/_v/partnerpos/pos/serial/:paymentId",
"public": true
}
}
}Wrong
json
{
"submitUrl": "https://account.myvtex.com/_v/api/my-connector/payments/PAY1/inbound/serial"
}vtex.terminal-connector-appsubmitUrl{"serialNumber":"..."}service.jsonroutesPaymentProviderServicePOST /payments/{paymentId}/inbound-request/{action}为什么这很重要
Inbound-request 由 Gateway 发起,而终端 app 是从 Sales App 设备发起 POST。如果 使用 inbound 路径,serial 将永远不会到达,POS 扣款也永远不会启动。
submitUrl检测方式
如果 中包含 ,请立即停止。如果 PPF 没有用于收集 serial 的额外 路由,请立即停止。
submitUrl/inboundservice.json正确
json
{
"routes": {
"posSerial": {
"path": "/_v/partnerpos/pos/serial/:paymentId",
"public": true
}
}
}错误
json
{
"submitUrl": "https://account.myvtex.com/_v/api/my-connector/payments/PAY1/inbound/serial"
}Preferred pattern
推荐模式
Follow generation.md in order. For PPF, overlay POS on the official example app from the Payment Provider Framework guide using ppf-scaffold.md. Compare IO vs standalone payloads in examples.md.
text
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按照 generation.md 中列出的顺序执行。对于 PPF,按照 ppf-scaffold.md,将 POS 叠加到 Payment Provider Framework 指南中的官方 PPF 示例应用上。使用 examples.md 比较 IO 与独立部署的 payload。
text
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 orderCommon failure modes
常见失败模式
- Generating an ecommerce PPP connector (Visa/Mastercard + Secure Proxy) when the user asked for PPP + POS.
- Omitting /
Venda Direta Creditofrom the manifest, or using accented Admin labels as API names.Venda Direta Debito - Returning before the terminal finishes, or waiting inside Create Payment for the card to be inserted.
approved - Using instead of
vtex.challenge-terminal-connector-app.vtex.terminal-connector-app - Sending as an object instead of
paymentAppData.payload, or hiding that withJSON.stringify(...)on a pending body.as AuthorizationResponse - Setting below
delayToCancel, so the Gateway cancels a payment the Wait app is still polling.secondsWaiting - Starting the POS charge in the handler (POS guide step 8.b starts it on the following Create Payment).
submitUrl - Starting a new POS charge on every Create Payment poll (broken idempotency).
- POSTing on pending responses to "nudge" the Gateway. Sales App and Wait for confirmation already drive Create Payment.
callbackUrl - Using +
getJSONwith nosaveJSONwhile Create Payment calls overlap for oneetag.paymentId - Using a VBase bucket that makes longer than 50 characters (default to
{vendor}.{appName}.{bucket}).pos - Skipping the Gateway callback, or omitting ,
cardBrand, andfirstDigits.lastDigits - On VTEX IO: POSTing a standalone notification payload (+ card fields) to
statusinstead of a bodilesscallbackUrl/Payments.retry.this.retry - Copying the example's Test Suite as the POS processor webhook.
this.callback(req, resp) - Calling from a
this.retryextra route (noservice.jsonPaymentProvider). Usethisthere.new Payments(ctx.vtex).retry(callbackUrl) - Putting on PPP inbound-request instead of a public custom route.
submitUrl - Applying Secure Proxy to POS authorize because "it is a card payment".
payment-pci-security - Building a Sales App cart/PDP extension instead of a payment connector.
- Starting from an empty Express app when the partner can use PPF / the official example app.
- 用户要求 PPP + POS,却生成电商 PPP 连接器(Visa/Mastercard + Secure Proxy)。
- Manifest 中缺少 /
Venda Direta Credito,或者将带重音符号的 Admin 标签用作 API 名称。Venda Direta Debito - 在终端完成前返回 ,或者在 Create Payment 中等待插入卡片。
approved - 使用 ,而不是
vtex.challenge-terminal-connector-app。vtex.terminal-connector-app - 将 作为对象而不是
paymentAppData.payload发送,或者用JSON.stringify(...)隐藏 pending 响应体中的错误。as AuthorizationResponse - 将 设为低于
delayToCancel,导致 Gateway 在 Wait app 仍在轮询时取消支付。secondsWaiting - 在 处理器中启动 POS 扣款(POS guide 第 8.b 步要求在下一次 Create Payment 中启动)。
submitUrl - 每次 Create Payment 轮询都启动新的 POS 扣款(幂等性被破坏)。
- 在 pending 响应中 POST 来“推动”Gateway。Sales App 和 Wait for confirmation 本身会驱动 Create Payment。
callbackUrl - 在 Create Payment 调用可能重叠时,使用没有 的
etag+getJSON。saveJSON - 使用会导致 超过 50 个字符的 VBase bucket(默认使用
{vendor}.{appName}.{bucket})。pos - 跳过 Gateway callback,或者缺少 、
cardBrand和firstDigits。lastDigits - **在 VTEX IO 上:**向 POST 独立通知请求体(
callbackUrl+ 卡字段),而不是执行无请求体的status/Payments.retry。this.retry - 将示例中的 Test Suite 误用作 POS processor webhook。
this.callback(req, resp) - 从 额外路由调用
service.json(此处没有this.retry的PaymentProvider)。应在那里使用this。new Payments(ctx.vtex).retry(callbackUrl) - 将 放在 PPP inbound-request 上,而不是公共自定义路由上。
submitUrl - 因为“这是卡支付”,就在 POS authorize 中应用 的 Secure Proxy。
payment-pci-security - 构建 Sales App 购物车/PDP 扩展,而不是支付连接器。
- 合作方可以使用 PPF / 官方示例应用时,仍从空白 Express app 开始。
Review checklist
审查检查清单
- For PPF, was the app overlaid on the official PPF example (or an existing PPF connector), not an empty HTTP server?
- Does (and IO
/manifest) declareconfiguration.jsonand/orVenda Direta Creditowith those exact names?Venda Direta Debito - Does authorize branch with (PPF) or the exact ASCII method names (standalone), and skip Gateway card PAN / Secure Proxy for POS?
isDirectSaleAuthorization - Is the first POS response +
Authorizations.pendingwith stringvtex.terminal-connector-app(no whole-response cast)?payload.submitUrl - Is a public custom route (
submitUrl) that acceptsservice.jsonand is not inbound-request?{"serialNumber": "..."} - Does only persist the serial (step 7), and does the next Create Payment start the processor charge once (step 8.b)?
submitUrl - Is the waiting response +
undefinedwithvtex.challenge-wait-for-confirmation?secondsWaiting - Is , and does every endpoint respond in under 20 seconds?
delayToCancel >= secondsWaiting - Are overlapping Create Payment calls for the same safe (
paymentId+getRawJSON, no second POS charge, no retry-on-pending)?ifMatch - Is the PPF VBase bucket short enough that is ≤ 50 characters (use
{vendor}.{appName}.{bucket})?pos - PPF: processor webhook persists card fragments then (or
Payments.retry(callbackUrl)insidethis.retry)? NextPaymentProviderreturns the card fragments (cast only that final response)?authorize() - Standalone: notification POST includes ,
status,cardBrand,firstDigits?lastDigits - Is Cancel implemented so Gateway timeout can abort an POS payment?
undefined - Has the partner been told they still need a Sales App device, POS terminal, test account, payment conditions, and homologation?
- 对于 PPF,应用是否叠加在官方 PPF 示例(或现有 PPF 连接器)上,而不是空白 HTTP server?
- (以及 IO
/manifest)是否使用精确名称声明了configuration.json和/或Venda Direta Credito?Venda Direta Debito - authorize 是否使用 (PPF)或精确的 ASCII 方式名称(独立部署)进行分支,并且对 POS 跳过 Gateway 卡 PAN / Secure Proxy?
isDirectSaleAuthorization - 首次 POS 响应是否为 +
Authorizations.pending,并包含字符串形式的vtex.terminal-connector-app(没有对整个响应强制转换)?payload.submitUrl - 是否为接受
submitUrl的公共自定义路由({"serialNumber": "..."}),而不是 inbound-request?service.json - 是否只持久化 serial(第 7 步),并由下一次 Create Payment 只启动一次 processor 扣款(第 8.b 步)?
submitUrl - 等待响应是否为 +
undefined,并包含vtex.challenge-wait-for-confirmation?secondsWaiting - 是否满足 ,且每个端点都在 20 秒内响应?
delayToCancel >= secondsWaiting - 同一个 的重叠 Create Payment 调用是否安全(
paymentId+getRawJSON、不进行第二次 POS 扣款、不在 pending 状态重试)?ifMatch - PPF VBase bucket 是否足够短,使 不超过 50 个字符(使用
{vendor}.{appName}.{bucket})?pos - **PPF:**processor webhook 是否先持久化卡片段,再执行 (或在
Payments.retry(callbackUrl)内执行PaymentProvider)?下一次this.retry是否返回卡片段(仅对最终响应强制转换)?authorize() - **独立部署:**通知 POST 是否包含 、
status、cardBrand和firstDigits?lastDigits - 是否实现了 Cancel,使 Gateway timeout 能够中止 状态的 POS 支付?
undefined - 是否已告知合作方仍需准备 Sales App 设备、POS 终端、测试账户、支付条件和 homologation?
Testing prerequisites (cannot be automated by the skill)
测试前置条件(此 skill 无法自动化)
VTEX often provides a preconfigured store for connector testing. To configure a store manually:
- Install the connector (PPF: follow the Payment Provider Framework install steps).
- Register a Gateway affiliation for the connector.
- Create payment conditions Venda Direta Crédito and/or Venda Direta Débito on that affiliation.
- Show those methods on Sales App (). Standard condition IDs are
checkout-instore-custom.js(debit) and44(credit); confirm on the Admin Payment Conditions page.45 - Install VTEX Sales App on a device and complete a purchase on a real or certified test POS.
- Run the Payment Provider Test Suite, including async approved/denied cases, then open a homologation ticket when required.
VTEX 通常会提供一个预配置的商店用于测试连接器。要手动配置商店:
- 安装连接器(PPF:遵循 Payment Provider Framework 安装步骤)。
- 为连接器注册 Gateway affiliation。
- 在该 affiliation 上创建支付条件 Venda Direta Crédito 和/或 Venda Direta Débito。
- 在 Sales App 中显示这些方式()。标准条件 ID 为
checkout-instore-custom.js(借记卡)和44(信用卡);请在 Admin Payment Conditions 页面中确认。45 - 在设备上安装 VTEX Sales App,并使用真实或经过认证的测试 POS 完成一次购买。
- 运行 Payment Provider Test Suite,包括异步 approved/denied 用例;如有需要,再创建 homologation ticket。
Related skills
相关 skills
Companions for this skill:
- generation.md — Questions to ask, clone-the-example workflow, implementation order
- ppf-scaffold.md — Overlay files for the official PPF example app
- examples.md — Partner prompts and IO vs standalone payloads
Other payment / Sales App skills:
- — Mandatory PPP endpoints and response shapes; use together with this skill
payment-provider-protocol - — Use when the POS connector is a VTEX IO app
payment-provider-framework - — Generic
payment-async-flow+undefinedrules; POS adds Payment Apps and card-fragment callbacks on topcallbackUrl - —
payment-idempotencymust not create a second POS charge; Create Payment for one id can overlap (etag /paymentId)ifMatch - — Ecommerce card-not-present only; do not apply Secure Proxy to POS authorize
payment-pci-security - — Sales App UI extensions; not the POS payment connector
sales-app-extensibility
此 skill 的配套内容:
- generation.md——需要询问的问题、克隆示例工作流以及实现顺序
- ppf-scaffold.md——官方 PPF 示例应用的叠加文件
- examples.md——合作方提示以及 IO 与独立部署的 payload
其他支付 / Sales App skills:
- ——必需的 PPP 端点和响应结构;与此 skill 配合使用
payment-provider-protocol - ——当 POS 连接器是 VTEX IO app 时使用
payment-provider-framework - ——通用的
payment-async-flow+undefined规则;POS 在此基础上增加了 Payment Apps 和卡片段 callbackcallbackUrl - ——
payment-idempotency绝不能创建第二次 POS 扣款;同一个 id 的 Create Payment 调用可能重叠(etag /paymentId)ifMatch - ——仅用于电商 card-not-present;不要在 POS authorize 中应用 Secure Proxy
payment-pci-security - ——Sales App UI 扩展;不是 POS 支付连接器
sales-app-extensibility
Reference
参考资料
- Payment Provider Protocol for Point of Sale (POS) - VTEX Sales App — Manifest methods, two-phase Payment Apps, timeouts, callback card fields, testing
- Payment App — and
paymentAppData.appNamecontractpayload - Payment Provider Framework — Clone the official example app (Getting started), IO connector implementation, , extra routes
this.retry - Payment Provider Protocol (Help Center) — Callback URL, notification payload including /
cardBrand/firstDigitslastDigits - Define payment methods displayed on VTEX Sales App — Sales App payment condition filters (debit,
44credit)45 - Payment provider homologation — Test Suite and publishing
- Registering gateway affiliations — Affiliation setup for tests
- Configuring payment conditions — Admin conditions for Venda Direta
- 面向 POS 的 Payment Provider Protocol - VTEX Sales App——Manifest 方式、两阶段 Payment Apps、timeout、callback 卡字段、测试
- Payment App——和
paymentAppData.appName约定payload - Payment Provider Framework——克隆官方示例应用(入门指南)、IO 连接器实现、、额外路由
this.retry - Payment Provider Protocol(帮助中心)——Callback URL、包含 /
cardBrand/firstDigits的通知 payloadlastDigits - 定义 VTEX Sales App 中显示的支付方式——Sales App 支付条件筛选(借记卡、
44信用卡)45 - Payment provider homologation——Test Suite 和发布
- 注册 Gateway affiliations——Affiliation 设置以进行测试
- 配置支付条件——为 Venda Direta 配置 Admin 条件