UnoPayv1.0.0
  1. خانه
  2. پرداخت‌های ناموفق و لغوشده

راهنماها

مدیریت پرداخت‌های ناموفق و لغوشده

UnoPay سه نتیجه گزارش می‌کند و فقط یکی از آن‌ها استثناست. جابه‌جا کردنشان دلیل رایجی است که سیستم‌های پرداخت به سفارش‌هایی استرداد می‌کنند که هرگز پرداخت نشده‌اند.

سه نتیجه

نتیجه، نشانه و پاسخ درست
نتیجه نشانه پاسخ درست
Paid isSuccessful: true Fulfil the order. Store the ref_id.
Declined isSuccessful: false + errorReason Leave unpaid, show the reason, let the payer retry with a fresh payment.
Abandoned Nothing. No callback, no event. Expire it yourself on a timer. Nothing will ever tell you.
Faulted A thrown PaymentError Leave the order pending. The callback may still arrive — do not fail it permanently.
خطای شبکه را شکست تلقی نکنید

یک GatewayNetworkError در هندلر کال‌بک یعنی سرور شما نتیجه را نفهمید — نه اینکه پرداخت شکست خورد. علامت‌زدن سفارش به‌عنوان ناموفق در آنجا می‌تواند وقتی پرداخت‌کننده به‌هرحensus پرداخت را کامل کند باعث تسویه دوبل شود. بگذارید معلق بماند و مغایرت‌گیری کنید.

ماشین حالتی که لبه‌های سخت را تاب می‌آورد

ردیف پرداخت را از ردیف سفارش جدا نگه دارید. یک پرداخت می‌تواند معلق، پرداخت‌شده، ردشده یا منقضی باشد؛ سفارش فقط وقتی پرداخت‌شده تلقی می‌شود که پرداختش باشد.

payments.ts ts
type PaymentState = 'pending' | 'paid' | 'declined' | 'expired';

app.get('/api/payment/callback', async (req, res) => {
  const payment = await findPaymentByAuthority(req.query.Authority);
  if (!payment) return res.status(404).send('unknown authority');

  // A repeated callback is normal: ZarinPal answers 101 the second time.
  // Make the state transition itself idempotent, not just the gateway call.
  if (payment.state === 'paid') {
    return res.send('already confirmed');
  }

  let result;
  try {
    result = await unopay.verifyCallback('zarinpal', {
      method: req.method,
      query: req.query as Record<string, string>,
      body: {},
    });
  } catch (err) {
    // Faulted: stay pending. The payer may still complete the payment.
    console.error('verification faulted', err);
    return res.status(502).send('try again');
  }

  if (result.isSuccessful) {
    await markPaid(payment.id, {
      refId: result.transactionId,      // the bank's reference
      amount: result.settledAmount,      // echoed from the callback
    });
    return res.send('paid');
  }

  await markDeclined(payment.id, result.errorReason ?? 'unknown');
  return res.status(400).send('declined');
});

پرداخت‌های رهاشده هرگز برنمی‌گردند

هیچ‌کس به شما نمی‌گوید. پرداخت‌کننده‌ای که تب صفحه زرین‌پال را می‌بندد هیچ کال‌بکی تولید نمی‌کند، پس نه رویداد لاگر و نه خطا. به یک کار انقضا نیاز دارید، و باید آنچه سفارش نگه داشته را آزاد کنید — موجودی، صندلی، کد تخفیف.

expire.ts ts
// Run on a schedule. Nothing in the SDK does this for you.
const STALE_AFTER_MS = 30 * 60 * 1000;

export async function expireStalePayments() {
  const stale = await db.payments.findMany({
    where: { state: 'pending', createdAt: { lt: new Date(Date.now() - STALE_AFTER_MS) } },
  });

  for (const p of stale) {
    // Guard on state: a callback that lands mid-loop flips this to 'paid'
    // and the update below will simply match nothing.
    await db.payments.updateMany({
      where: { id: p.id, state: 'pending' },
      data: { state: 'expired' },
    });
    await releaseOrderHold(p.orderId);
  }
}

به‌روزرسانی شرطی تمام ترفند است. بدون where: { state: 'pending' }، کال‌بکی که یک میلی‌ثانیه بعد از خواندن ردیف توسط حلقه شما برسد، توسط انقضا بازنویسی می‌شود.