Loading...
Loading...
Compare original and translation side by side
callbackUrlundefinedpayment-provider-protocolpaymentIdrequestIdpayment-idempotencypayment-pci-securitycallbackUrlundefinedpayment-provider-protocolpaymentIdrequestIdpayment-idempotencypayment-pci-securitystatus: "undefined"BankInvoicecallbackUrlX-VTEX-API-AppKeyX-VTEX-API-AppTokencallbackUrl/paymentsX-VTEX-signaturecallbackUrldelayToCancelstatus: "undefined"BankInvoicecallbackUrlX-VTEX-API-AppKeyX-VTEX-API-AppTokencallbackUrl/paymentscallbackUrlX-VTEX-signaturedelayToCancelundefinedundefinedstatus: "undefined""approved""denied""approved""denied"status: "approved"status: "denied""undefined"async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
const asyncMethods = ["BankInvoice", "Pix"];
const isAsync = asyncMethods.includes(paymentMethod);
if (isAsync) {
const pending = await acquirer.initiateAsyncPayment(req.body);
// Store payment and callbackUrl for later notification
await store.save(paymentId, {
paymentId,
status: "undefined",
callbackUrl,
acquirerReference: pending.reference,
});
res.status(200).json({
paymentId,
status: "undefined", // Correct: payment is pending
authorizationId: pending.authorizationId ?? null,
nsu: pending.nsu ?? null,
tid: pending.tid ?? null,
acquirer: "MyProvider",
code: "PENDING",
message: "Awaiting customer action",
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: computeDelayToCancel(paymentMethod, pending),
paymentUrl: pending.qrCodeUrl ?? pending.boletoUrl ?? undefined,
});
return;
}
// Synchronous methods (credit card) can return final status
const result = await acquirer.authorizeSyncPayment(req.body);
res.status(200).json({
paymentId,
status: result.status, // "approved" or "denied" is OK for sync
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: result.code ?? null,
message: result.message ?? null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
});
}
const PIX_MIN_DELAY = 900; // 15 minutes
const PIX_MAX_DELAY = 3600; // 60 minutes
function computeDelayToCancel(paymentMethod: string, pending: any): number {
if (paymentMethod === "Pix") {
// Use provider QR TTL but clamp to 15–60 minutes
const providerTtlSeconds = pending.pixTtlSeconds ?? 1800; // default 30 min
return Math.min(Math.max(providerTtlSeconds, PIX_MIN_DELAY), PIX_MAX_DELAY);
}
if (paymentMethod === "BankInvoice") {
// Example: seconds until boleto due date
const now = Date.now();
const dueDate = new Date(pending.dueDate).getTime();
const diffSeconds = Math.max(Math.floor((dueDate - now) / 1000), 0);
return diffSeconds;
}
// Other async methods: follow provider SLA if provided
if (pending.expirySeconds) {
return pending.expirySeconds;
}
// Conservative fallback: 24h
return 86400;
}async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod } = req.body;
// WRONG: Creating a Pix charge and immediately returning "approved"
// The customer hasn't scanned the QR code yet — no money collected
const pixCharge = await acquirer.createPixCharge(req.body);
res.status(200).json({
paymentId,
status: "approved", // WRONG — Pix hasn't been paid yet!
authorizationId: pixCharge.id,
nsu: null,
tid: null,
acquirer: "MyProvider",
code: null,
message: null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
});
}status: "undefined""approved""denied""approved""denied"status: "approved"status: "denied""undefined"async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
const asyncMethods = ["BankInvoice", "Pix"];
const isAsync = asyncMethods.includes(paymentMethod);
if (isAsync) {
const pending = await acquirer.initiateAsyncPayment(req.body);
// 存储支付信息与callbackUrl,用于后续通知
await store.save(paymentId, {
paymentId,
status: "undefined",
callbackUrl,
acquirerReference: pending.reference,
});
res.status(200).json({
paymentId,
status: "undefined", // 正确:支付处于待处理状态
authorizationId: pending.authorizationId ?? null,
nsu: pending.nsu ?? null,
tid: pending.tid ?? null,
acquirer: "MyProvider",
code: "PENDING",
message: "等待用户操作",
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: computeDelayToCancel(paymentMethod, pending),
paymentUrl: pending.qrCodeUrl ?? pending.boletoUrl ?? undefined,
});
return;
}
// 同步支付方式(信用卡)可以返回最终状态
const result = await acquirer.authorizeSyncPayment(req.body);
res.status(200).json({
paymentId,
status: result.status, // 同步方式返回"approved"或"denied"是合法的
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: result.code ?? null,
message: result.message ?? null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
});
}
const PIX_MIN_DELAY = 900; // 15分钟
const PIX_MAX_DELAY = 3600; // 60分钟
function computeDelayToCancel(paymentMethod: string, pending: any): number {
if (paymentMethod === "Pix") {
// 使用供应商提供的二维码有效期,限制在15-60分钟区间内
const providerTtlSeconds = pending.pixTtlSeconds ?? 1800; // 默认30分钟
return Math.min(Math.max(providerTtlSeconds, PIX_MIN_DELAY), PIX_MAX_DELAY);
}
if (paymentMethod === "BankInvoice") {
// 示例:计算距离Boleto到期日的秒数
const now = Date.now();
const dueDate = new Date(pending.dueDate).getTime();
const diffSeconds = Math.max(Math.floor((dueDate - now) / 1000), 0);
return diffSeconds;
}
// 其他异步支付方式:如果供应商提供了SLA则遵循
if (pending.expirySeconds) {
return pending.expirySeconds;
}
// 保守兜底:24小时
return 86400;
}async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod } = req.body;
// 错误:创建Pix订单后立即返回"approved"
// 客户还未扫描二维码,资金并未到账
const pixCharge = await acquirer.createPixCharge(req.body);
res.status(200).json({
paymentId,
status: "approved", // 错误 — Pix还未完成支付!
authorizationId: pixCharge.id,
nsu: null,
tid: null,
acquirer: "MyProvider",
code: null,
message: null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
});
}callbackUrlX-VTEX-signaturecallbackUrlX-VTEX-signatureundefinedcallbackUrlcallbackUrlasync function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, callbackUrl } = req.body;
// Store the exact callbackUrl from the request
await store.save(paymentId, {
paymentId,
status: "undefined",
callbackUrl, // Stored exactly as received, including query params
});
// Return async "undefined" response (see previous constraint)
res.status(200).json({
paymentId,
status: "undefined",
authorizationId: null,
nsu: null,
tid: null,
acquirer: "MyProvider",
code: "PENDING",
message: "Awaiting customer action",
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 86400,
});
}
// When the acquirer webhook arrives, use the stored callbackUrl
async function handleAcquirerWebhook(req: Request, res: Response): Promise<void> {
const { paymentReference, status } = req.body;
const payment = await store.findByAcquirerRef(paymentReference);
if (!payment) {
res.status(404).send();
return;
}
const pppStatus = status === "paid" ? "approved" : "denied";
// Update local state first
await store.updateStatus(payment.paymentId, pppStatus);
// Use the EXACT stored callbackUrl — do not modify it
await fetch(payment.callbackUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-VTEX-API-AppKey": process.env.VTEX_APP_KEY!,
"X-VTEX-API-AppToken": process.env.VTEX_APP_TOKEN!,
},
body: JSON.stringify({
paymentId: payment.paymentId,
status: pppStatus,
}),
});
res.status(200).send();
}// WRONG: Hardcoding callback URL — ignores X-VTEX-signature and environment
async function handleAcquirerWebhook(req: Request, res: Response): Promise<void> {
const { paymentReference, status } = req.body;
const payment = await store.findByAcquirerRef(paymentReference);
// WRONG — hardcoded URL, missing X-VTEX-signature authentication
await fetch("https://mystore.vtexpayments.com.br/api/callback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
paymentId: payment.paymentId,
status: status === "paid" ? "approved" : "denied",
}),
});
res.status(200).send();
}callbackUrlX-VTEX-signaturecallbackUrlX-VTEX-signatureundefinedcallbackUrlcallbackUrlasync function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, callbackUrl } = req.body;
// 存储请求中携带的完整callbackUrl
await store.save(paymentId, {
paymentId,
status: "undefined",
callbackUrl, // 完全按接收内容存储,包括所有查询参数
});
// 返回异步"undefined"响应(参考上一个约束)
res.status(200).json({
paymentId,
status: "undefined",
authorizationId: null,
nsu: null,
tid: null,
acquirer: "MyProvider",
code: "PENDING",
message: "等待用户操作",
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 86400,
});
}
// 收到收单机构webhook时,使用存储的callbackUrl
async function handleAcquirerWebhook(req: Request, res: Response): Promise<void> {
const { paymentReference, status } = req.body;
const payment = await store.findByAcquirerRef(paymentReference);
if (!payment) {
res.status(404).send();
return;
}
const pppStatus = status === "paid" ? "approved" : "denied";
// 先更新本地状态
await store.updateStatus(payment.paymentId, pppStatus);
// 使用存储的完整callbackUrl — 不要修改
await fetch(payment.callbackUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-VTEX-API-AppKey": process.env.VTEX_APP_KEY!,
"X-VTEX-API-AppToken": process.env.VTEX_APP_TOKEN!,
},
body: JSON.stringify({
paymentId: payment.paymentId,
status: pppStatus,
}),
});
res.status(200).send();
}// 错误:硬编码回调地址 — 忽略了X-VTEX-signature与环境差异
async function handleAcquirerWebhook(req: Request, res: Response): Promise<void> {
const { paymentReference, status } = req.body;
const payment = await store.findByAcquirerRef(paymentReference);
// 错误 — 硬编码地址,缺少X-VTEX-signature鉴权信息
await fetch("https://mystore.vtexpayments.com.br/api/callback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
paymentId: payment.paymentId,
status: status === "paid" ? "approved" : "denied",
}),
});
res.status(200).send();
}/paymentspaymentIdpaymentId"undefined""approved""denied""undefined""approved""denied"POST /paymentsundefined"undefined"paymentIdasync function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
// Check for existing payment — may have been updated via callback
const existing = await store.findByPaymentId(paymentId);
if (existing) {
// Do NOT call the acquirer again.
// Return a response derived from the current stored state.
res.status(200).json({
...existing.response,
status: existing.status, // Reflect the latest state: "undefined" | "approved" | "denied"
});
return;
}
// First time — call the acquirer once
const asyncMethods = ["BankInvoice", "Pix"];
const isAsync = asyncMethods.includes(paymentMethod);
const acquirerResult = await acquirer.authorize(req.body);
const initialStatus = isAsync ? "undefined" : acquirerResult.status;
const response = {
paymentId,
status: initialStatus,
authorizationId: acquirerResult.authorizationId ?? null,
nsu: acquirerResult.nsu ?? null,
tid: acquirerResult.tid ?? null,
acquirer: "MyProvider",
code: acquirerResult.code ?? null,
message: acquirerResult.message ?? null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: isAsync
? computeDelayToCancel(paymentMethod, acquirerResult)
: 21600,
...(acquirerResult.paymentUrl
? { paymentUrl: acquirerResult.paymentUrl }
: {}),
};
await store.save(paymentId, {
paymentId,
status: initialStatus,
response,
callbackUrl,
acquirerReference: acquirerResult.reference,
});
res.status(200).json(response);
}async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
// WRONG: No idempotency — every retry hits the acquirer again
const result = await acquirer.authorize(req.body);
res.status(200).json({
paymentId,
status: result.status,
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: null,
message: null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
});
}paymentId/paymentspaymentId"undefined""approved""denied""undefined""approved""denied"undefinedPOST /payments"undefined"paymentIdasync function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
// 检查是否已存在该支付记录 — 状态可能已经被回调更新
const existing = await store.findByPaymentId(paymentId);
if (existing) {
// 不要再次调用收单机构接口
// 基于当前存储的状态返回响应
res.status(200).json({
...existing.response,
status: existing.status, // 反映最新状态:"undefined" | "approved" | "denied"
});
return;
}
// 首次调用 — 仅调用一次收单机构接口
const asyncMethods = ["BankInvoice", "Pix"];
const isAsync = asyncMethods.includes(paymentMethod);
const acquirerResult = await acquirer.authorize(req.body);
const initialStatus = isAsync ? "undefined" : acquirerResult.status;
const response = {
paymentId,
status: initialStatus,
authorizationId: acquirerResult.authorizationId ?? null,
nsu: acquirerResult.nsu ?? null,
tid: acquirerResult.tid ?? null,
acquirer: "MyProvider",
code: acquirerResult.code ?? null,
message: acquirerResult.message ?? null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: isAsync
? computeDelayToCancel(paymentMethod, acquirerResult)
: 21600,
...(acquirerResult.paymentUrl
? { paymentUrl: acquirerResult.paymentUrl }
: {}),
};
await store.save(paymentId, {
paymentId,
status: initialStatus,
response,
callbackUrl,
acquirerReference: acquirerResult.reference,
});
res.status(200).json(response);
}async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
// 错误:无幂等处理 — 每次重试都会再次调用收单机构接口
const result = await acquirer.authorize(req.body);
res.status(200).json({
paymentId,
status: result.status,
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: null,
message: null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
});
}delayToCanceldelayToCanceldelayToCanceldelayToCanceldelayToCancelundefineddelayToCancel = 604800delayToCanceldelayToCanceldelayToCancel = 604800delayToCanceldelayToCancelcomputeDelayToCancelasync function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
const isAsync = ["BankInvoice", "Pix"].includes(paymentMethod);
if (isAsync) {
const pending = await acquirer.initiateAsyncPayment(req.body);
await store.save(paymentId, {
paymentId,
status: "undefined",
callbackUrl,
acquirerReference: pending.reference,
});
res.status(200).json({
paymentId,
status: "undefined",
authorizationId: pending.authorizationId ?? null,
nsu: pending.nsu ?? null,
tid: pending.tid ?? null,
acquirer: "MyProvider",
code: "PENDING",
message: "Awaiting customer action",
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
// WRONG: hardcoded 7 days for every async method
delayToCancel: 604800,
paymentUrl: pending.qrCodeUrl ?? pending.boletoUrl ?? undefined,
});
return;
}
// ...
}delayToCanceldelayToCanceldelayToCanceldelayToCancelundefineddelayToCancel = 604800delayToCanceldelayToCanceldelayToCancel = 604800delayToCanceldelayToCancelcomputeDelayToCancelasync function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
const isAsync = ["BankInvoice", "Pix"].includes(paymentMethod);
if (isAsync) {
const pending = await acquirer.initiateAsyncPayment(req.body);
await store.save(paymentId, {
paymentId,
status: "undefined",
callbackUrl,
acquirerReference: pending.reference,
});
res.status(200).json({
paymentId,
status: "undefined",
authorizationId: pending.authorizationId ?? null,
nsu: pending.nsu ?? null,
tid: pending.tid ?? null,
acquirer: "MyProvider",
code: "PENDING",
message: "等待用户操作",
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
// 错误:所有异步方式都硬编码为7天
delayToCancel: 604800,
paymentUrl: pending.qrCodeUrl ?? pending.boletoUrl ?? undefined,
});
return;
}
// ...
}1. Gateway → POST /payments → Connector (returns status: "undefined")
2. Acquirer webhook → Connector (payment confirmed)
3. Connector → POST callbackUrl (with X-VTEX-API-AppKey/AppToken headers)
4. Gateway updates payment status to approved/denied1. Gateway → POST /payments → Connector (returns status: "undefined")
2. Acquirer webhook → Connector (payment confirmed)
3. Connector → POST callbackUrl (retry, no payload)
4. Gateway → POST /payments → Connector (returns status: "approved"/"denied")const ASYNC_PAYMENT_METHODS = new Set([
"BankInvoice", // Boleto Bancário
"Pix", // Pix instant payments
]);
function isAsyncPaymentMethod(paymentMethod: string): boolean {
return ASYNC_PAYMENT_METHODS.has(paymentMethod);
}async function handleAcquirerWebhook(req: Request, res: Response): Promise<void> {
const webhookData = req.body;
const acquirerRef = webhookData.transactionId;
const payment = await store.findByAcquirerRef(acquirerRef);
if (!payment || !payment.callbackUrl) {
res.status(404).json({ error: "Payment not found" });
return;
}
const pppStatus = webhookData.status === "paid" ? "approved" : "denied";
// Update local state FIRST
await store.updateStatus(payment.paymentId, pppStatus);
// Notify the Gateway via callbackUrl with retry logic
await notifyGateway(payment.callbackUrl, {
paymentId: payment.paymentId,
status: pppStatus,
});
res.status(200).json({ received: true });
}async function notifyGateway(callbackUrl: string, payload: object): Promise<void> {
const maxRetries = 3;
const baseDelay = 1000; // 1 second
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(callbackUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-VTEX-API-AppKey": process.env.VTEX_APP_KEY!,
"X-VTEX-API-AppToken": process.env.VTEX_APP_TOKEN!,
},
body: JSON.stringify(payload),
});
if (response.ok) return;
console.error(`Callback attempt ${attempt + 1} failed: ${response.status}`);
} catch (error) {
console.error(`Callback attempt ${attempt + 1} error:`, error);
}
if (attempt < maxRetries) {
await new Promise((r) => setTimeout(r, baseDelay * Math.pow(2, attempt)));
}
}
// All retries failed — Gateway will still retry via /payments as safety net
console.error("All callback retries exhausted. Relying on Gateway retry.");
}1. 网关 → POST /payments → 连接器(返回status: "undefined")
2. 收单机构webhook → 连接器(支付确认)
3. 连接器 → POST callbackUrl(携带X-VTEX-API-AppKey/AppToken请求头)
4. 网关将支付状态更新为approved/denied1. 网关 → POST /payments → 连接器(返回status: "undefined")
2. 收单机构webhook → 连接器(支付确认)
3. 连接器 → POST callbackUrl(重试,无请求体)
4. 网关 → POST /payments → 连接器(返回status: "approved"/"denied")const ASYNC_PAYMENT_METHODS = new Set([
"BankInvoice", // Boleto Bancário
"Pix", // Pix即时支付
]);
function isAsyncPaymentMethod(paymentMethod: string): boolean {
return ASYNC_PAYMENT_METHODS.has(paymentMethod);
}async function handleAcquirerWebhook(req: Request, res: Response): Promise<void> {
const webhookData = req.body;
const acquirerRef = webhookData.transactionId;
const payment = await store.findByAcquirerRef(acquirerRef);
if (!payment || !payment.callbackUrl) {
res.status(404).json({ error: "未找到对应支付记录" });
return;
}
const pppStatus = webhookData.status === "paid" ? "approved" : "denied";
// 先更新本地状态
await store.updateStatus(payment.paymentId, pppStatus);
// 通过callbackUrl通知网关,附带重试逻辑
await notifyGateway(payment.callbackUrl, {
paymentId: payment.paymentId,
status: pppStatus,
});
res.status(200).json({ received: true });
}async function notifyGateway(callbackUrl: string, payload: object): Promise<void> {
const maxRetries = 3;
const baseDelay = 1000; // 1秒
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(callbackUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-VTEX-API-AppKey": process.env.VTEX_APP_KEY!,
"X-VTEX-API-AppToken": process.env.VTEX_APP_TOKEN!,
},
body: JSON.stringify(payload),
});
if (response.ok) return;
console.error(`回调第${attempt + 1}次尝试失败:${response.status}`);
} catch (error) {
console.error(`回调第${attempt + 1}次尝试报错:`, error);
}
if (attempt < maxRetries) {
await new Promise((r) => setTimeout(r, baseDelay * Math.pow(2, attempt)));
}
}
// 所有重试失败 — 网关仍会通过/payments重试作为兜底
console.error("所有回调重试失败,依赖网关重试机制兜底。");
}status: "approved"callbackUrlX-VTEX-signatureundefinedcallbackUrlundefinedundefinedapproveddelayToCancelstatus: "approved"callbackUrlX-VTEX-signatureundefinedcallbackUrlundefinedundefinedapprovedstatus: "undefined"callbackUrlcallbackUrlX-VTEX-signaturecallbackUrlX-VTEX-API-AppKeyX-VTEX-API-AppTokenpaymentId"undefined""approved""denied"delayToCanceldelayToCanceldelayToCancelstatus: "undefined"callbackUrlcallbackUrlcallbackUrlX-VTEX-signatureX-VTEX-API-AppKeyX-VTEX-API-AppTokenpaymentId"undefined""approved""denied"delayToCanceldelayToCanceldelayToCancelpayment-provider-protocolpayment-idempotencypaymentIdrequestIdpayment-pci-securitypayment-provider-protocolpayment-idempotencypaymentIdrequestIdpayment-pci-securityundefinedundefined