Loading...
Loading...
Apply when implementing idempotency logic in payment connector code or handling duplicate payment requests. Covers paymentId as idempotency key, payment state machine transitions, retry semantics for cancellation and refund operations, and requestId handling. Use for preventing duplicate charges and ensuring correct Gateway retry behavior across Create Payment, Cancel, Capture, and Refund endpoints.
npx skill4agent add vtexdocs/ai-skills payment-idempotencyundefinedpayment-provider-protocolpayment-async-flowpayment-pci-securitypaymentIdpaymentIdrequestIdpaymentIdstatus: "undefined""approved"undefinedapprovedsettledundefineddeniedapprovedcancelledpaymentIdundefinedpaymentIdpaymentIdpaymentIdasync function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
// Check for existing payment — idempotency guard
const existingPayment = await paymentStore.findByPaymentId(paymentId);
if (existingPayment) {
// Return the exact same response — no new acquirer call
res.status(200).json(existingPayment.response);
return;
}
// First time seeing this paymentId — process with acquirer
const result = await acquirer.authorize(req.body);
const response = {
paymentId,
status: result.status,
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: result.code ?? null,
message: result.message ?? null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
};
// Store the response for future idempotent lookups
await paymentStore.save(paymentId, { request: req.body, response });
res.status(200).json(response);
}async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
// No idempotency check — every call hits the acquirer
// If the Gateway retries this (which it will for undefined status),
// the customer gets charged multiple times
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,
});
}paymentIdauthorizationIdtidnsustatuspaymentIdasync function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
const existing = await paymentStore.findByPaymentId(paymentId);
if (existing) {
// Return the EXACT stored response — same authorizationId, tid, nsu, status
res.status(200).json(existing.response);
return;
}
// ... process new payment and store response
}async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.body;
const existing = await paymentStore.findByPaymentId(paymentId);
if (existing) {
// WRONG: Generating new identifiers for an existing payment
// The Gateway will see different tid/nsu and lose track of the transaction
const newTid = generateNewTid();
res.status(200).json({
...existing.response,
tid: newTid, // Different from original — breaks reconciliation
nsu: generateNewNsu(),
});
return;
}
// ... process new payment
}status: "undefined"status: "approved"status: "denied"approvedundefinedcallbackUrlstatus: "approved"status: "denied""undefined"async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
const isAsyncMethod = ["BankInvoice", "Pix"].includes(paymentMethod);
if (isAsyncMethod) {
const pending = await acquirer.initiateAsyncPayment(req.body);
await paymentStore.save(paymentId, {
status: "undefined",
callbackUrl,
acquirerRef: pending.reference,
});
res.status(200).json({
paymentId,
status: "undefined", // Correct for async
authorizationId: pending.authorizationId ?? null,
nsu: pending.nsu ?? null,
tid: pending.tid ?? null,
acquirer: "MyProvider",
code: "ASYNC-PENDING",
message: "Awaiting customer payment",
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 604800, // 7 days for async
paymentUrl: pending.paymentUrl,
});
return;
}
// Sync methods can return approved/denied immediately
// ...
}async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod } = req.body;
// WRONG: Approving a Pix payment synchronously
// The customer hasn't paid yet — the order will ship without payment
const result = await acquirer.createPixCharge(req.body);
res.status(200).json({
paymentId,
status: "approved", // WRONG — Pix is async, should be "undefined"
authorizationId: result.authorizationId ?? null,
nsu: null,
tid: null,
acquirer: "MyProvider",
code: null,
message: null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: 21600,
});
}interface PaymentRecord {
paymentId: string;
status: "undefined" | "approved" | "denied" | "cancelled" | "settled" | "refunded";
response: Record<string, unknown>;
callbackUrl?: string;
createdAt: Date;
updatedAt: Date;
}
interface OperationRecord {
requestId: string;
paymentId: string;
operation: "cancel" | "capture" | "refund";
response: Record<string, unknown>;
createdAt: Date;
}const store = new PaymentStore();
async function createPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId, paymentMethod, callbackUrl } = req.body;
// Idempotency check
const existing = await store.findByPaymentId(paymentId);
if (existing) {
res.status(200).json(existing.response);
return;
}
const isAsync = ["BankInvoice", "Pix"].includes(paymentMethod);
const result = await acquirer.process(req.body);
const status = isAsync ? "undefined" : result.status;
const response = {
paymentId,
status,
authorizationId: result.authorizationId ?? null,
nsu: result.nsu ?? null,
tid: result.tid ?? null,
acquirer: "MyProvider",
code: result.code ?? null,
message: result.message ?? null,
delayToAutoSettle: 21600,
delayToAutoSettleAfterAntifraud: 1800,
delayToCancel: isAsync ? 604800 : 21600,
...(result.paymentUrl ? { paymentUrl: result.paymentUrl } : {}),
};
await store.save(paymentId, {
paymentId, status, response, callbackUrl,
createdAt: new Date(), updatedAt: new Date(),
});
res.status(200).json(response);
}requestIdasync function cancelPaymentHandler(req: Request, res: Response): Promise<void> {
const { paymentId } = req.params;
const { requestId } = req.body;
// Operation idempotency check
const existingOp = await store.findOperation(requestId);
if (existingOp) {
res.status(200).json(existingOp.response);
return;
}
// State machine validation
const payment = await store.findByPaymentId(paymentId);
if (!payment || !["undefined", "approved"].includes(payment.status)) {
res.status(200).json({
paymentId,
cancellationId: null,
code: "cancel-failed",
message: `Cannot cancel payment in ${payment?.status ?? "unknown"} state`,
requestId,
});
return;
}
const result = await acquirer.cancel(paymentId);
const response = {
paymentId,
cancellationId: result.cancellationId ?? null,
code: result.code ?? null,
message: result.message ?? "Successfully cancelled",
requestId,
};
await store.updateStatus(paymentId, "cancelled");
await store.saveOperation(requestId, {
requestId, paymentId, operation: "cancel", response, createdAt: new Date(),
});
res.status(200).json(response);
}paymentIdundefinedstatus: "approved"MaptidnsuauthorizationIdpaymentIdrequestIdpaymentIdpaymentIdrequestIdstatus: "undefined""approved"delayToCancelpayment-provider-protocolpayment-async-flowpayment-pci-security