بیک گراؤنڈ ٹاسک کے طور پر اسٹرائپ ویب ہکس کو قابل اعتماد طریقے سے کیسے پروسیس کیا جائے۔

آپ نے پٹی ترتیب دی ہے۔ ادائیگی کام کرتی ہے۔ صارفین ادائیگی کر سکتے ہیں۔ لیکن کیا ہوتا ہے ~ بعد ادا

ویب ہک ہینڈلر وہ جگہ ہے جہاں زیادہ تر ادائیگی کے انضمام خود بخود رک جاتے ہیں۔ رسائی دینے کی کوشش کرتے وقت سرور کریش ہو جاتا ہے۔ جب میں نے تصدیقی پیغام بھیجنے کی کوشش کی تو میری ای میل سروس بند تھی۔ لکھنے کے دوران ڈیٹا بیس کا وقت ختم ہو گیا۔

اسٹرائپ پورے ویب ہک کو دوبارہ آزماتا ہے، لیکن ہینڈلر نے کریش ہونے سے پہلے ہی ایک تصدیقی ای میل بھیج دیا ہے۔ اب صارف کو دو ای میلز موصول ہوئی ہیں لیکن ان تک رسائی حاصل نہیں کر سکتے۔

یہ مضمون آپ کو دکھاتا ہے کہ اس مسئلے کو کیسے حل کیا جائے۔ آپ ایک ویب ہک ہینڈلر بنانے کا طریقہ سیکھیں گے جو ادائیگی کے بعد کی منطق کو پائیدار، آزادانہ طور پر دوبارہ آزمائے گئے مراحل میں تقسیم کرکے ناکامی سے بچ جائے۔ یہ پیٹرن کسی بھی ملٹی سٹیپ ویب ہک پروسیسنگ کے لیے کام کرتا ہے، نہ کہ صرف پٹی کے لیے۔

یہاں آپ کیا سیکھیں گے:

  • کیوں پٹی والے ویب ہکس پروڈکشن میں خاموشی سے ناکام ہو جاتے ہیں۔

  • کیسے سادہ ان لائن ہینڈلرز حقیقی زندگی کے حالات میں کام نہیں کرتے

  • پیٹرن: ویب ہک وصول کرتا ہے، توثیق کرتا ہے، اور قطاریں (مزید نہیں)۔

  • انفرادی طور پر شناخت شدہ اقدامات کے ساتھ ایک پائیدار خریداری کا بہاؤ کیسے بنایا جائے۔

  • اسی پیٹرن کے ساتھ رقم کی واپسی اور ادائیگی کی چھوٹ پر کارروائی کیسے کی جائے۔

  • مقامی طور پر اپنے ویب ہک ہینڈلر کی جانچ کیسے کریں۔

شرائط

پیروی کرنے کے لیے، یہاں آپ کو جاننے کی ضرورت ہے:

  • Node.js اور TypeScript

  • مقامی پٹی انضمام (ادائیگی سیشن، ویب ہکس)

  • ایس کیو ایل ڈیٹا بیس (مثال کے طور پر بوندا باندی ORM کے ساتھ PostgreSQL استعمال کرتے ہیں)

  • npm یا Node.js پیکیج مینیجر

Ingest یا پائیدار عملدرآمد کے ساتھ کسی پیشگی تجربے کی ضرورت نہیں ہے۔ یہ مضمون شروع سے دونوں کی وضاحت کرتا ہے۔

آپ کو انسٹالیشن کے لیے کیا ضرورت ہے۔

کوڈ کی مثالوں کو چلانے کے لیے، درج ذیل پیکیجز انسٹال کریں:

npm install inngest stripe drizzle-orm @react-email/components resend

مقامی ویب ہکس کی جانچ کے لیے آپ کو اسٹرائپ CLI کی بھی ضرورت ہوگی۔ ہومبریو کے ذریعے میک او ایس پر انسٹال کریں (brew install stripe/stripe-cli/stripe) یا دوسرے پلیٹ فارمز کے لیے اسٹرائپ دستاویزات میں دی گئی ہدایات پر عمل کریں۔

انڈیکس

اسٹرائپ ویب ہکس خاموشی سے کیوں ناکام ہوجاتے ہیں۔

خوشی کا راستہ آسان ہے۔ ایک بار جب گاہک ادائیگی کرتا ہے، پٹی checkout.session.completed ایک واقعہ سرور کو پہنچایا جاتا ہے اور ایک ہینڈلر اس پر کارروائی کرتا ہے۔ یہ ہمیشہ ترقی کے دوران کام کرتا ہے۔

پیداوار مختلف ہے۔ ایک ویب ہک ہینڈلر کو عام طور پر کامیاب ادائیگی کے بعد کئی کام انجام دینے کی ضرورت ہوتی ہے۔ اپنے ڈیٹا بیس میں صارفین کو تلاش کریں، خریداریوں کو ریکارڈ کریں، تصدیقی ای میلز بھیجیں، منتظمین کو مطلع کریں، مصنوعات تک رسائی فراہم کریں (GitHub invites یا API کیز کے ذریعے)، اور فالو اپ ای میلز کا شیڈول بنائیں۔ یہ 5-6 کام ہیں جن میں 3-4 بیرونی خدمات شامل ہیں۔

ناکامی کے طریقے جو آخر کار ویب ہک ہینڈلر میں ختم ہوتے ہیں وہ ہیں:

1. پروسیسنگ کے دوران سرور کریش ہو جاتا ہے۔

ڈیٹا بیس لکھنا کامیاب رہا، لیکن ای میل نہیں بھیجی گئی۔ جب اسٹرائپ ویب ہک کو دوبارہ آزماتا ہے تو ہینڈلر دوبارہ چلائے گا۔

اب آپ کے پاس یا تو ڈپلیکیٹ ڈیٹا بیس اندراج ہے یا پھر ایک انوکھی رکاوٹ کی غلطی ہے جو دوبارہ کوشش کو روکتی ہے۔

2. جب ایک بیرونی سروس عارضی طور پر بند ہوتی ہے۔

میرا ای میل فراہم کنندہ 500 واپس کرتا ہے۔ GitHub API کال کی شرح محدود ہے۔ تجزیہ سروس کا وقت ختم ہو گیا۔

ویب ہک ہینڈلر فائر کرتا ہے اور اسٹرائپ پوری چیز کی دوبارہ کوشش کرتا ہے۔ تاہم، پہلے سے ہی کامیاب اقدامات (ڈیٹا بیس لکھنا، پہلا ای میل) دوبارہ عمل میں لایا جائے گا۔

3. ہینڈلر ٹائم آؤٹ

اسٹرائپ کو تقریباً 20 سیکنڈ میں 2xx جواب کی توقع ہے۔ اگر ہینڈلر بہت زیادہ کرتا ہے، تو اسٹرائپ اسے ناکام کے طور پر نشان زد کرتا ہے اور دوبارہ کوشش کرتا ہے۔ وقت ختم ہونے سے پہلے ہینڈلر جزوی طور پر مکمل ہو سکتا ہے۔

4. رول بیک کے بغیر جزوی تکمیل

یہ بدترین ناکامی کا موڈ ہے۔ مرحلہ 1-3 کامیاب۔ مرحلہ 4 ناکام۔ پٹی کو دوبارہ آزمایا جاتا ہے اور اقدامات 1-3 کو دوبارہ عمل میں لایا جاتا ہے۔

صارفین کو دو تصدیقی ای میلز موصول ہوں گی۔ ڈیٹا بیس میں ڈپلیکیٹ ریکارڈ موجود ہیں۔ تاہم، مرحلہ 4 اب بھی ناکام ہو جاتا ہے کیونکہ بنیادی مسئلہ (ریٹ محدود کرنا، سروس کی بندش) کو حل نہیں کیا گیا ہے۔

5. دوبارہ کوشش کرنے پر ریس کے حالات

اسٹرائپ آپ کی طرف سے غلطی کے بغیر ایک ہی ایونٹ کو ایک سے زیادہ بار فراہم کر سکتا ہے۔ نیٹ ورک کی خرابیاں، لوڈ بیلنس کا ٹائم آؤٹ، اور اسٹرائپ کی اپنی دوبارہ کوشش کی منطق کا مطلب ہے کہ ہینڈلرز کو ڈپلیکیٹ ڈیلیوری کے لیے تیار رہنا چاہیے۔ اگر ہینڈلر تمام مراحل میں قابل نہیں ہے تو، نقل جزوی تکمیل کے مسئلے کو مزید پیچیدہ بناتی ہے۔

اسٹرائپ کا دوبارہ کوشش کرنے کا طرز عمل اچھی طرح سے ڈیزائن کیا گیا ہے۔ یہ ایکسپونینشل بیک آف کا استعمال کرتا ہے اور کئی دنوں میں درجنوں بار دوبارہ کوشش کرتا ہے۔ لیکن پٹی مکمل ویب ہک کی ترسیل.

ہم نہیں جانتے کہ آیا ہینڈلر نے 1-3 کے مراحل مکمل کر لیے ہیں اور اسے صرف مرحلہ 4 پر دوبارہ کوشش کرنے کی ضرورت ہے۔ یہ فرق کرنا آپ کی ذمہ داری ہے۔

بنیادی مسئلہ یہ ہے کہ ویب ہک ہینڈلر ایک ہی درخواست میں بہت زیادہ کام کرتا ہے۔ ہر بیرونی کال ناکامی کا ایک ممکنہ نقطہ ہے اور ان کے درمیان کوئی چیک پوائنٹ نہیں ہے۔ اگر کوئی ناکام ہو جاتا ہے، تو آپ اس بات کا سراغ نہیں لگا سکتے کہ کون سے پہلے ہی کامیاب ہو چکے ہیں۔

بولی اپروچ (اور یہ کیوں ناکام ہوتا ہے)

ایک عام ویب ہک ہینڈلر اس طرح نظر آتا ہے: میں نے کوڈ بیسز، ٹیوٹوریلز، اور اسٹیک اوور فلو جوابات میں اس پیٹرن کے سینکڑوں تغیرات دیکھے ہیں۔

app.post("/api/payments/webhook", async (req, res) => {
  const event = stripe.webhooks.constructEvent(
    req.body,
    req.headers["stripe-signature"],
    process.env.STRIPE_WEBHOOK_SECRET
  );

  if (event.type === "checkout.session.completed") {
    const session = event.data.object;

    // Step 1: Look up the user
    const user = await db.users.findOne({ id: session.metadata.userId });

    // Step 2: Record the purchase
    await db.purchases.insert({
      userId: user.id,
      stripeSessionId: session.id,
      amount: session.amount_total,
      status: "completed",
    });

    // Step 3: Send confirmation email
    await sendEmail({
      to: user.email,
      subject: "Purchase confirmed!",
      template: "purchase-confirmation",
    });

    // Step 4: Grant product access (GitHub repo invitation)
    await addCollaborator(user.githubUsername);

    // Step 5: Send access email
    await sendEmail({
      to: user.email,
      subject: "Your repository access is ready!",
      template: "repo-access",
    });

    // Step 6: Track analytics
    await analytics.track(user.id, "purchase_completed", {
      amount: session.amount_total,
    });
  }

  res.json({ received: true });
});

یہ صاف نظر آتا ہے۔ اوپر سے نیچے تک پڑھیں۔ تمام ٹیوٹوریلز یہ سکھاتے ہیں۔

اب دیکھتے ہیں کہ اگر مرحلہ 4 ناکام ہو جاتا ہے تو کیا ہوتا ہے۔ شاید GitHub کا API رفتار محدود ہے اور addCollaborator جب میں اسے کال کرتا ہوں تو مجھے ایک غلطی ہوتی ہے: ہینڈلر 500 کو اسٹرائپ پر واپس کرتا ہے۔

ناکامی کے بعد کی حیثیت یہ ہے:

  • صارف ڈیٹا بیس میں موجود ہے (مرحلہ 1 ایک سادہ سوال تھا، لہذا کوئی مسئلہ نہیں ہے)۔

  • خریداری کا ریکارڈ بنایا گیا ہے (مرحلہ 2 کامیاب)۔

  • ایک تصدیقی ای میل بھیج دیا گیا ہے (مرحلہ 3 کامیاب)۔

  • پچھلی GitHub رسائی کی اجازتوں میں شامل ہیں: ~ نہیں منظور شدہ (مرحلہ 4 ناکام)۔

  • آپ کی رسائی کا ای میل یہ ہے: ~ نہیں بھیجا گیا (مرحلہ 5 پر عمل نہیں ہوا)۔

  • تجزیہ ہے۔ ~ نہیں ٹریک کیا گیا (مرحلہ 6 نافذ نہیں ہوا)۔

اسٹرائپ ویب ہک کو دوبارہ آزمائے گا۔ ہینڈلر اوپر سے دوبارہ چلایا جاتا ہے۔

  • مرحلہ 1: صارف سے دوبارہ استفسار کریں۔ ٹھیک ہے

  • مرحلہ 2: خریداری کا دوسرا ریکارڈ داخل کرنے کی کوشش کریں۔ اگر آپ کے پاس انوکھی رکاوٹیں ہیں۔ stripeSessionIdمیں یہ پھینک رہا ہوں۔ اگر نہیں، تو اب ایک ڈپلیکیٹ بنایا جائے گا۔

  • مرحلہ 3: تصدیقی ای میل دوبارہ بھیجیں۔ گاہک دوسرا کلک کرتا ہے "خریداری کی تصدیق کریں!” آپ کو پیغام موصول ہوتا ہے۔ ای میل

  • مرحلہ 4: GitHub تک دوبارہ رسائی کی کوشش کریں۔ اس بار یہ کام ہو سکتا ہے یا نہیں۔

  • مرحلہ 5-6: مرحلہ 4 پر منحصر ہے، یہ چل سکتا ہے یا نہیں چل سکتا۔

آپ اسے آئیڈیمپوٹینسی چیک کے ساتھ پیچ کر سکتے ہیں۔ "اگر آپ کی خریدی ہوئی چیز پہلے سے موجود ہے تو مرحلہ 2 کو چھوڑ دیں۔” لیکن اب ہینڈلر ہر قدم کے لیے مشروط منطق سے بھرا ہوا ہے۔ اور میرے پاس اب بھی ڈپلیکیٹ ای میل کا مسئلہ ہے۔ کیونکہ چیک کرنے کا کوئی طریقہ نہیں ہے "کیا میں نے یہ ای میل پہلے ہی بھیج دیا ہے؟” اپنا ٹریکنگ سسٹم بنائے بغیر۔

یہ نقطہ نظر پیمانہ نہیں ہے۔ ہر نئے قدم میں ایک اور ناکامی موڈ، ایک اور آئیڈیمپوٹینسی چیک، اور ایک اور ایج کیس شامل ہوتا ہے۔

پیٹرنز: ویب ہکس سے لے کر واقعات تک مستقل خصوصیات تک

حل یہ ہے کہ خدشات کو الگ کیا جائے۔ ایک ویب ہک ہینڈلر کو بالکل ایک کام کرنا چاہئے: آنے والے واقعات کی توثیق کریں اور پروسیسنگ کے لئے ان کی قطار لگائیں۔ اور کچھ نہیں ہے۔

تمام اصل کام (ڈیٹا بیس رائٹ، ای میلز، API کالز، اینالیٹکس) کو ایک پائیدار بیک گراؤنڈ فنکشن میں منتقل کیا جاتا ہے جہاں ہر قدم کا انفرادی طور پر معائنہ، دوبارہ کوشش اور ٹریک کیا جاتا ہے۔

بہاؤ مندرجہ ذیل ہے:

Stripe webhook
    |
    v
Webhook endpoint (validate signature, extract event, enqueue)
    |
    v
Background job system (receives event)
    |
    v
Durable function
    |-- Step 1: Look up user and purchase (checkpointed)
    |-- Step 2: Track analytics (checkpointed)
    |-- Step 3: Send confirmation email (checkpointed)
    |-- Step 4: Send admin notification (checkpointed)
    |-- Step 5: Grant GitHub access (checkpointed)
    |-- Step 6: Track GitHub access (checkpointed)
    |-- Step 7: Update purchase record (checkpointed)
    |-- Step 8: Send repo access email (checkpointed)
    |-- Step 9: Schedule follow-up sequence (checkpointed)

ہر قدم کا احاطہ کرتا ہے step.run() یہ ایک پائیدار چوکی ہے۔ اگر مرحلہ 5 ناکام ہو جاتا ہے:

  • اس طرح کے اقدامات 1 سے 4 پر عمل کریں: ~ نہیں دوبارہ کریں۔ نتائج محفوظ ہیں۔

  • مرحلہ 5 اپنے دوبارہ کوشش کرنے والے کاؤنٹر کا استعمال کرتے ہوئے آزادانہ طور پر دوبارہ کوشش کریں۔

  • اگر مرحلہ 5 کامیاب ہوتا ہے تو، 6 سے 9 تک کے مراحل جاری رکھیں۔

"مسلسل پھانسی” کا مطلب یہی ہے۔ ناکامی کے بعد بھی فعالیت میں پیش رفت برقرار ہے۔ فنکشن لیول کی دوبارہ کوششوں کے بجائے مرحلہ وار دوبارہ کوششیں ہوتی ہیں۔ کوئی ڈپلیکیٹ ای میلز نہیں ہیں۔ کوئی ڈپلیکیٹ ڈیٹا بیس رائٹ نہیں ہے۔ کوئی جزوی تکمیل نہیں۔

میں اس کے لیے Ingest استعمال کرتا ہوں۔ ایک ایونٹ سے چلنے والا، پائیدار عملدرآمد کا پلیٹ فارم جو باکس سے باہر اسٹیج لیول چیک پوائنٹنگ فراہم کرتا ہے۔ استعمال کرتے ہوئے فنکشن کی وضاحت کریں: step.run() بلاکنگ اور انجسٹ دوبارہ کوشش کرنے کی منطق، ریاست کی استقامت، اور مشاہدے کو ہینڈل کرتا ہے۔ کوئی Redis، کوئی کارکن عمل نہیں، کوئی حسب ضرورت دوبارہ کوشش کا کوڈ نہیں۔

دوسرے ٹولز بھی اسی طرح کے نتائج حاصل کر سکتے ہیں (مثال کے طور پر عارضی)، لیکن Ingest کے ڈویلپر کے TypeScript کے تجربے نے مجھے اپنی طرف متوجہ کیا۔ عام غیر مطابقت پذیر افعال لکھیں۔ کہ step.run() چادر واحد اضافہ ہے۔

ویب ہک اینڈ پوائنٹ کیسے ترتیب دیا جائے۔

ویب ہک اینڈ پوائنٹس کم سے کم ہونے چاہئیں۔ یہ دستخط کی تصدیق کرتا ہے، ایونٹ کا ڈیٹا نکالتا ہے، اسے بیک گراؤنڈ ٹاسک سسٹم میں بھیجتا ہے، اور فوری طور پر 200 واپس کرتا ہے۔

میرے پروڈکشن کوڈ بیس میں ویب ہک کا اصل نقطہ یہ ہے:

import { constructWebhookEvent } from "@/lib/payments";
import { inngest } from "@/lib/jobs";

app.post("/api/payments/webhook", async ({ request, set }) => {
  const body = await request.text();
  const sig = request.headers.get("stripe-signature");

  if (!sig) {
    set.status = 400;
    return { error: "Missing signature" };
  }

  try {
    const event = await constructWebhookEvent(body, sig);
    console.log(`[Webhook] Received ${event.type}`);

    if (event.type === "charge.refunded") {
      const charge = event.data.object;
      await inngest.send({
        name: "stripe/charge.refunded",
        data: {
          chargeId: charge.id,
          paymentIntentId: charge.payment_intent,
          amountRefunded: charge.amount_refunded,
          originalAmount: charge.amount,
          currency: charge.currency,
        },
      });
    }

    if (event.type === "checkout.session.expired") {
      const session = event.data.object;
      await inngest.send({
        name: "stripe/checkout.session.expired",
        data: {
          sessionId: session.id,
          customerEmail: session.customer_email,
        },
      });
    }

    return { received: true };
  } catch (error) {
    console.error("[Webhook] Stripe verification failed:", error);
    set.status = 400;
    return { error: "Webhook verification failed" };
  }
});

نوٹس کریں کہ یہ ہینڈلر کیا کرتا ہے۔ ~ نہیں کیا کریں: صارفین سے استفسار نہ کریں، ڈیٹا بیس پر نہ لکھیں، ای میلز بھیجیں، یا بیرونی APIs کو کال نہ کریں۔ پٹی دستخط کی توثیق کرتی ہے، متعلقہ فیلڈز کو نکالتی ہے، اور داخل کردہ واقعات کو Ingest پر بھیجتی ہے۔ پورا ہینڈلر ملی سیکنڈ میں مکمل ہوتا ہے۔

کہ constructWebhookEvent فنکشن پٹی کے دستخط کی تصدیق کو لپیٹتا ہے۔

import Stripe from "stripe";

export async function constructWebhookEvent(
  payload: string | Buffer,
  signature: string
) {
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
  if (!webhookSecret) {
    throw new Error("STRIPE_WEBHOOK_SECRET is not set");
  }
  const client = new Stripe(process.env.STRIPE_SECRET_KEY);
  return client.webhooks.constructEventAsync(payload, signature, webhookSecret);
}

ایک اہم تفصیل: خام درخواست جسم (سٹرنگ یا بفر کے طور پر) پٹی میں دستخط کی تصدیق کے لیے استعمال کیا جاتا ہے۔ اگر فریم ورک خام سٹرنگ تک رسائی سے پہلے باڈی کو JSON کے طور پر پارس کرتا ہے تو دستخط کی توثیق ناکام ہو جائے گی۔ یہ "ویب ہک دستخط کی توثیق میں ناکامی” کی خرابی کی سب سے زیادہ ممکنہ وجہ ہے۔

انجسٹ کلائنٹ سیٹ اپ کم سے کم ہے۔

import { Inngest } from "inngest";

export const inngest = new Inngest({
  id: "my-app",
});

خاص طور پر خریداری کے بہاؤ کے لیے، دوسرے اینڈ پوائنٹس ایونٹس بھیجتے ہیں ("بلنگ” کا راستہ جسے فرنٹ اینڈ کال کرتا ہے جب گاہک اسٹرائپ ادائیگی سے واپس آتا ہے)۔ لیکن اصول وہی ہیں۔ توثیق، قطار، واپسی.

// After verifying payment status with Stripe
await inngest.send({
  name: "purchase/completed",
  data: {
    userId: session.user.id,
    tier,
    sessionId,
  },
});

ایک پائیدار خریداری کے بہاؤ کو کیسے بنایا جائے۔

یہ مضمون کا نقطہ ہے۔ کہ handlePurchaseCompleted یہ خصوصیت ادائیگی کے بعد 9 انفرادی طور پر چیک کیے گئے مراحل کے ذریعے آپ کی خریداری پر کارروائی کرتی ہے۔ تمام اقدامات اصل پروڈکشن کوڈ ہیں۔

ذیل کی مثال میں، ہم ایک نجی GitHub ذخیرہ تک رسائی فراہم کرتے ہیں کیونکہ یہ وہی چیز ہے جسے ہم فروخت کرتے ہیں۔

پروڈکٹ کے لیے "تک رسائی فراہم کرنے” کے مراحل مختلف ہوتے ہیں: صارف کو پرو ممبرشپ میں اپ گریڈ کرنا، API کریڈٹس فراہم کرنا، کورس کو غیر مقفل کرنا، سبسکرپشن کو چالو کرنا وغیرہ۔ ایک پائیدار قدم کا نمونہ ایک جیسا ہی ہوتا ہے قطع نظر اس کے کہ آپ جو کچھ بھی فراہم کرتے ہیں۔

اگر مرحلہ 5 ناکام ہو جاتا ہے (مثال کے طور پر، اگر آپ کا ای میل فراہم کنندہ بند ہے)، Ingest صرف مرحلہ 5 کی دوبارہ کوشش کرے گا۔ مرحلہ 1 سے 4 تک دوبارہ نہیں چلایا جائے گا کیونکہ وہ پہلے ہی چیک پوائنٹ کر چکے ہیں۔ مرحلہ 6 سے 9 تک، مرحلہ 5 کے کامیاب ہونے تک انتظار کریں۔

import { eq } from "drizzle-orm";
import { createElement } from "react";

import { inngest } from "@/lib/jobs/client";
import { trackServerEvent } from "@/lib/analytics/server";
import { brand } from "@/lib/brand";
import { db, purchases, users } from "@/lib/db";
import {
  sendEmail,
  PurchaseConfirmationEmail,
  AdminPurchaseNotificationEmail,
  RepoAccessGrantedEmail,
} from "@/lib/email";
import { addCollaborator } from "@/lib/github";

export const handlePurchaseCompleted = inngest.createFunction(
  { id: "purchase-completed", triggers: [{ event: "purchase/completed" }] },
  async ({ event, step }) => {
    const { userId, tier, sessionId } = event.data;

    // Step 1: Look up user and purchase details
    const { user, purchase } = await step.run(
      "lookup-user-and-purchase",
      async () => {
        const userResult = await db
          .select({
            id: users.id,
            email: users.email,
            name: users.name,
            githubUsername: users.githubUsername,
          })
          .from(users)
          .where(eq(users.id, userId))
          .limit(1);

        const foundUser = userResult[0];
        if (!foundUser) {
          throw new Error(`User not found: ${userId}`);
        }

        const purchaseResult = await db
          .select({
            amount: purchases.amount,
            currency: purchases.currency,
            stripePaymentIntentId: purchases.stripePaymentIntentId,
          })
          .from(purchases)
          .where(eq(purchases.stripeCheckoutSessionId, sessionId))
          .limit(1);

        const foundPurchase = purchaseResult[0];

        return {
          user: foundUser,
          purchase: foundPurchase ?? {
            amount: 0,
            currency: "usd",
            stripePaymentIntentId: null,
          },
        };
      }
    );

    // Step 2: Track purchase completion in analytics
    await step.run("track-purchase-to-posthog", async () => {
      await trackServerEvent(userId, "purchase_completed_server", {
        tier,
        amount_cents: purchase.amount,
        currency: purchase.currency,
        stripe_session_id: sessionId,
      });
    });

    // Step 3: Send purchase confirmation to customer
    await step.run("send-purchase-confirmation", async () => {
      await sendEmail({
        to: user.email,
        subject: `Your purchase is confirmed!`,
        template: createElement(PurchaseConfirmationEmail, {
          amount: purchase.amount,
          currency: purchase.currency,
          customerEmail: user.email,
        }),
      });
    });

    // Step 4: Send admin notification
    await step.run("send-admin-notification", async () => {
      const adminEmail = process.env.ADMIN_EMAIL;
      if (!adminEmail) return;

      await sendEmail({
        to: adminEmail,
        subject: `New sale: ${user.email}`,
        template: createElement(AdminPurchaseNotificationEmail, {
          amount: purchase.amount,
          currency: purchase.currency,
          customerEmail: user.email,
          customerName: user.name,
          stripeSessionId: purchase.stripePaymentIntentId ?? sessionId,
        }),
      });
    });

    // Early return if user has no GitHub username
    if (!user.githubUsername) {
      return { success: true, userId, tier, githubAccessGranted: false };
    }

    // Step 5: Grant GitHub repository access
    const collaboratorResult = await step.run(
      "add-github-collaborator",
      async () => {
        return addCollaborator(user.githubUsername!);
      }
    );

    // Step 6: Track GitHub access granted
    await step.run("track-github-access", async () => {
      await trackServerEvent(userId, "github_access_granted", {
        tier,
        github_username: user.githubUsername,
        invitation_status: collaboratorResult.status,
      });
    });

    // Step 7: Update purchase record
    await step.run("update-purchase-record", async () => {
      await db
        .update(purchases)
        .set({
          githubAccessGranted: true,
          githubInvitationId: collaboratorResult.status,
          updatedAt: new Date(),
        })
        .where(eq(purchases.stripeCheckoutSessionId, sessionId));
    });

    // Step 8: Send repo access email
    await step.run("send-repo-access-email", async () => {
      await sendEmail({
        to: user.email,
        subject: `Your repository access is ready!`,
        template: createElement(RepoAccessGrantedEmail, {
          repoUrl: "https://github.com/your-org/your-repo",
        }),
      });
    });

    // Step 9: Schedule follow-up email sequence
    await step.run("schedule-follow-up", async () => {
      const purchaseRecord = await db
        .select({ id: purchases.id })
        .from(purchases)
        .where(eq(purchases.stripeCheckoutSessionId, sessionId))
        .limit(1);

      if (purchaseRecord[0]) {
        await inngest.send({
          name: "purchase/follow-up.scheduled",
          data: {
            userId,
            purchaseId: purchaseRecord[0].id,
            tier,
          },
        });
      }
    });

    return { success: true, userId, tier, githubAccessGranted: true };
  }
);

بہت زیادہ کوڈ ہے۔ میں ہر قدم سے گزروں گا اور وضاحت کروں گا کہ وہ الگ الگ چوکیاں کیوں ہیں۔

مرحلہ 1: صارف کی انکوائری اور خریداری

const { user, purchase } = await step.run(
  "lookup-user-and-purchase",
  async () => {
    // ... database queries ...
    return { user: foundUser, purchase: foundPurchase };
  }
);

اس مرحلے میں، ہم صارف اور خریداری کے ریکارڈ کے لیے ڈیٹا بیس سے استفسار کرتے ہیں۔ اگر ڈیٹا بیس عارضی طور پر دستیاب نہیں ہے، تو یہ مرحلہ خود ہی دوبارہ کوشش کرے گا۔

واپسی کی قیمت (user اور purchase) Ingest کے ذریعے کیش کیا جاتا ہے۔ تمام بعد کے مراحل میں آپ استعمال کر سکتے ہیں: user.email, user.githubUsernameاور purchase.amount ڈیٹا بیس سے دوبارہ استفسار کیے بغیر۔

اگر یہ مرحلہ مستقل طور پر ناکام ہو جاتا ہے (صارف موجود نہیں ہے)، ایک خرابی واقع ہو گی جو پوری فعالیت کو روک دے گی۔ یہ جان بوجھ کر ہے۔ اگر صارف نہ مل سکے تو جاری رکھنے کا کوئی فائدہ نہیں۔

مرحلہ 2: اپنے تجزیات کو ٹریک کریں۔

await step.run("track-purchase-to-posthog", async () => {
  await trackServerEvent(userId, "purchase_completed_server", {
    tier,
    amount_cents: purchase.amount,
  });
});

چونکہ تجزیاتی خدمات کے اپنے ناکامی کے طریقے ہوتے ہیں (ریٹ محدود کرنا، بندش، نیٹ ورک ٹائم آؤٹ)، تجزیات کو ٹریک کرنا ایک الگ مرحلہ ہے۔ اگر PostHog بند ہے، تو آپ اپنی تصدیقی ای میلز کو مسدود نہیں کرنا چاہتے۔

پروڈکشن کوڈ میں، یہ مرحلہ کال کو ٹریس کیچ میں لپیٹتا ہے تاکہ ٹریس کی ناکامی کو پورے فنکشن کو ٹوٹنے سے روکا جا سکے۔ تجزیے کے واقعات غیر اہم ہیں اور واقعات "اچھے ہیں”۔

مرحلہ 3: خریداری کی تصدیقی ای میل بھیجیں۔

await step.run("send-purchase-confirmation", async () => {
  await sendEmail({
    to: user.email,
    subject: `Your purchase is confirmed!`,
    template: createElement(PurchaseConfirmationEmail, {
      amount: purchase.amount,
      currency: purchase.currency,
      customerEmail: user.email,
    }),
  });
});

یہ وہی ہے جو ہم اپنے گاہکوں کے ساتھ تصدیق کرتے ہیں. کیونکہ یہ ایک آزاد کام ہے، یہ ایڈمن کی اطلاع (مرحلہ 4) سے الگ مرحلہ ہے۔ یہاں تک کہ اگر ایڈمنسٹریٹر کا ای میل ناکام ہوجاتا ہے، تب بھی صارف کو ایک تصدیقی پیغام موصول ہوگا۔

کہ sendEmail فنکشن اندرونی طور پر ری ٹرانسمیشن کا استعمال کرتا ہے۔ اگر دوبارہ بھیجنا 500 واپس کرتا ہے، تو اس مرحلے پر دوبارہ کوشش کی جائے گی۔ مرحلہ 2 (تجزیہ) پہلے ہی مکمل اور معائنہ کیا جا چکا ہے، لہذا اسے دوبارہ نہیں چلایا جائے گا۔

مرحلہ 4: ایڈمنسٹریٹر کی اطلاعات بھیجیں۔

await step.run("send-admin-notification", async () => {
  const adminEmail = process.env.ADMIN_EMAIL;
  if (!adminEmail) return;

  await sendEmail({
    to: adminEmail,
    subject: `New sale: ${user.email}`,
    template: createElement(AdminPurchaseNotificationEmail, { /* ... */ }),
  });
});

ایڈمنسٹریٹر کی اطلاعات گاہک کو درپیش کارروائیوں سے مکمل طور پر آزاد ہیں۔ ان کو الگ کرنے کا مطلب ہے کہ ایک میں غلطیاں دوسرے پر اثر انداز نہیں ہوں گی۔

مرحلہ 5: GitHub تک رسائی فراہم کریں۔

const collaboratorResult = await step.run(
  "add-github-collaborator",
  async () => {
    return addCollaborator(user.githubUsername!);
  }
);

یہ وہ قدم ہے جس کے ناکام ہونے کا سب سے زیادہ امکان ہے۔ GitHub کے API میں شرح کی حدیں ہیں۔ اس کا وقت ختم ہوسکتا ہے اور آپ کا GitHub صارف نام غلط ہوسکتا ہے۔

اگر آپ اس قدم کو اپنا بناتے ہیں، تو GitHub API کی خرابیاں تصدیقی ای میلز (مرحلہ 3) یا منتظم اطلاعات (مرحلہ 4) کو دوبارہ بھیجنے کو متحرک نہیں کریں گی۔ اس قدم کو پہلے ہی چیک پوائنٹ کیا جا چکا ہے۔

براہ کرم اس قدم سے پہلے ابتدائی واپسی کو نوٹ کریں۔ اگر صارف کے پاس GitHub کا صارف نام نہیں ہے تو، فنکشن مرحلہ 4 کے بعد جلد واپس آجاتا ہے۔ باقی اقدامات صرف اس صورت میں چلیں گے جب آپ کے پاس رسائی دینے کے لیے GitHub اکاؤنٹ ہو۔

مرحلہ 6: GitHub رسائی کو ٹریک کریں۔

await step.run("track-github-access", async () => {
  await trackServerEvent(userId, "github_access_granted", {
    tier,
    github_username: user.githubUsername,
    invitation_status: collaboratorResult.status,
  });
});

یہ ہے collaboratorResult مرحلہ 5 سے۔ کیونکہ step.run() واپسی کی قیمت کو کیش کریں۔ collaboratorResult.status اگر خصوصیت میں خلل پڑا تھا اور مراحل 5 اور 6 کے درمیان دوبارہ شروع کیا گیا تھا، تو یہ اب بھی یہاں دستیاب ہے۔

مرحلہ 7: اپنی خریداری کی تاریخ کو اپ ڈیٹ کریں۔

await step.run("update-purchase-record", async () => {
  await db
    .update(purchases)
    .set({
      githubAccessGranted: true,
      githubInvitationId: collaboratorResult.status,
      updatedAt: new Date(),
    })
    .where(eq(purchases.stripeCheckoutSessionId, sessionId));
});

ڈیٹا بیس کی اپ ڈیٹس GitHub تک رسائی کی تصدیق کے بعد ہوتی ہیں۔ تم صرف دکھاؤ githubAccessGranted: true یہ تعاون کنندہ کی دعوت کے حقیقت میں کامیاب ہونے کے بعد ہے۔

اگر آپ رسائی دینے سے پہلے ریکارڈ کو اپ ڈیٹ کرتے ہیں اور GitHub مرحلہ ناکام ہو جاتا ہے، تو ڈیٹا بیس ظاہر کرے گا کہ رسائی دی گئی ہے اگرچہ رسائی نہیں دی گئی ہے۔

مرحلہ 8: ریپوزٹری تک رسائی ای میل بھیجیں۔

await step.run("send-repo-access-email", async () => {
  await sendEmail({
    to: user.email,
    subject: `Your repository access is ready!`,
    template: createElement(RepoAccessGrantedEmail, {
      repoUrl: "https://github.com/your-org/your-repo",
    }),
  });
});

یہ ای میل صرف آپ کے GitHub دعوت نامے کی تصدیق کے بعد بھیجی جائے گی (مرحلہ 5) اور ڈیٹا بیس کو اپ ڈیٹ کر دیا گیا ہے (مرحلہ 7)۔ آرڈر اہم ہے۔ اگر کوئی دعوت نامہ نہیں بھیجا گیا ہے تو آپ اپنے صارفین کو یہ نہیں بتانا چاہتے کہ "رسائی تیار ہے”۔

مرحلہ 9: فالو اپ ترتیب کا شیڈول بنائیں

await step.run("schedule-follow-up", async () => {
  const purchaseRecord = await db
    .select({ id: purchases.id })
    .from(purchases)
    .where(eq(purchases.stripeCheckoutSessionId, sessionId))
    .limit(1);

  if (purchaseRecord[0]) {
    await inngest.send({
      name: "purchase/follow-up.scheduled",
      data: {
        userId,
        purchaseId: purchaseRecord[0].id,
        tier,
      },
    });
  }
});

آخری مرحلہ ایک علیحدہ Ingest فنکشن کو متحرک کرتا ہے جو فالو اپ ای میل کی ترتیب کو ہینڈل کرتا ہے (دن 7 کو آن بورڈنگ ٹپس، دن 14 پر فیڈ بیک کی درخواست، اور 30 ​​دن پر تشخیص کی درخواست)۔ یہ ایک واقعہ پر مبنی سلسلہ ہے۔ ایک فنکشن مکمل ہوتا ہے اور دوسرا فنکشن متحرک ہوتا ہے۔

جانشین فنکشن استعمال کرتا ہے: step.sleep() ای میلز کے درمیان انتظار کرنے کے لیے:

export const handlePurchaseFollowUp = inngest.createFunction(
  {
    id: "purchase-follow-up",
    triggers: [{ event: "purchase/follow-up.scheduled" }],
    cancelOn: [
      {
        event: "purchase/follow-up.cancelled",
        match: "data.purchaseId",
      },
    ],
  },
  async ({ event, step }) => {
    const { userId, purchaseId } = event.data;

    await step.sleep("wait-7-days", "7d");

    await step.run("send-day-7-email", async () => {
      // Check eligibility (user exists, not unsubscribed, not refunded)
      // Send onboarding tips email
    });

    await step.sleep("wait-14-days", "7d");

    await step.run("send-day-14-email", async () => {
      // Send feedback request email
    });

    await step.sleep("wait-30-days", "16d");

    await step.run("send-day-30-email", async () => {
      // Send testimonial request email
    });
  }
);

توجہ فرمائیں cancelOn اختیارات۔ اگر آپ کی خریداری کی واپسی کی گئی تھی، تو آپ بھیج سکتے ہیں: purchase/follow-up.cancelled ایک واقعہ رونما ہوتا ہے اور اس کے بعد کا پورا عمل رک جاتا ہے۔ پرانے ای میلز ان صارفین کو نہیں بھیجے جائیں گے جو رقم کی واپسی کی درخواست کرتے ہیں۔

ہر قدم کو الگ الگ کرنے کی ضرورت کیوں ہے؟

اصول سادہ ہیں۔ وہ آپریشن جو بیرونی خدمات کو کال کرتے ہیں یا آزادانہ طور پر ناکام ہو سکتے ہیں ان کے اپنے اقدامات ہونے چاہئیں۔

ڈیٹا بیس کے سوالات ایک قدم ہیں کیونکہ ڈیٹا بیس عارضی طور پر دستیاب نہیں ہے۔ ای میل بھیجنا ایک قدم ہے کیونکہ آپ کا ای میل فراہم کنندہ 500 واپس کر سکتا ہے۔ GitHub API کو کال کرنا ایک قدم ہے کیونکہ اس کی رفتار محدود ہو سکتی ہے۔

دو آپریشنز ایک ہی مرحلے میں ہو سکتے ہیں اگر وہ ہمیشہ کامیاب ہو جائیں یا ایک ساتھ ناکام ہو جائیں (ایک بیرونی کال کا اشتراک کرنا)۔ تاہم، اگر شک ہو تو الگ الگ اقدامات پر آگے بڑھیں۔ اوور ہیڈ نہ ہونے کے برابر ہے اور قابل اعتماد بہتری نمایاں ہے۔

اسی پیٹرن کے لیے رقم کی واپسی پر کارروائی کیسے کریں۔

رقم کی واپسی کا بہاؤ اسی پائیدار قدمی پیٹرن کی پیروی کرتا ہے۔ یہ فنکشن درج ذیل فائل میں موجود ہے۔ handlePurchaseCompletedلہذا، وہ ایک ہی درآمدات کا اشتراک کرتے ہیں (plus removeCollaborator سے @/lib/github اور رقم کی واپسی سے متعلق ای میل ٹیمپلیٹس)۔ یہاں handleRefund فنکشن:

export const handleRefund = inngest.createFunction(
  { id: "refund-processed", triggers: [{ event: "stripe/charge.refunded" }] },
  async ({ event, step }) => {
    const {
      chargeId,
      paymentIntentId,
      amountRefunded,
      originalAmount,
      currency,
    } = event.data;

    const isFullRefund = amountRefunded >= originalAmount;

    // Step 1: Look up the purchase and user
    const { user, purchase } = await step.run(
      "lookup-purchase-by-payment-intent",
      async () => {
        const purchaseResult = await db
          .select({
            id: purchases.id,
            userId: purchases.userId,
            stripePaymentIntentId: purchases.stripePaymentIntentId,
            githubAccessGranted: purchases.githubAccessGranted,
          })
          .from(purchases)
          .where(eq(purchases.stripePaymentIntentId, paymentIntentId))
          .limit(1);

        const foundPurchase = purchaseResult[0];
        if (!foundPurchase) {
          return { user: null, purchase: null };
        }

        const userResult = await db
          .select({
            id: users.id,
            email: users.email,
            name: users.name,
            githubUsername: users.githubUsername,
          })
          .from(users)
          .where(eq(users.id, foundPurchase.userId))
          .limit(1);

        return { user: userResult[0] ?? null, purchase: foundPurchase };
      }
    );

    if (!purchase || !user) {
      return { success: false, reason: "no_matching_purchase" };
    }

    let accessRevoked = false;

    // Step 2: Revoke GitHub access (only for full refunds)
    if (isFullRefund && user.githubUsername && purchase.githubAccessGranted) {
      const revokeResult = await step.run(
        "revoke-github-access",
        async () => {
          return removeCollaborator(user.githubUsername!);
        }
      );
      accessRevoked = revokeResult.success;
    }

    // Step 3: Update purchase status
    await step.run("update-purchase-status", async () => {
      if (isFullRefund) {
        await db
          .update(purchases)
          .set({
            status: "refunded",
            githubAccessGranted: false,
            updatedAt: new Date(),
          })
          .where(eq(purchases.id, purchase.id));
      } else {
        await db
          .update(purchases)
          .set({
            status: "partially_refunded",
            updatedAt: new Date(),
          })
          .where(eq(purchases.id, purchase.id));
      }
    });

    // Step 4: Track refund in analytics
    await step.run("track-refund-event", async () => {
      await trackServerEvent(user.id, "refund_processed", {
        charge_id: chargeId,
        amount_cents: amountRefunded,
        original_amount_cents: originalAmount,
        currency,
        is_full_refund: isFullRefund,
        github_access_revoked: accessRevoked,
      });
    });

    // Step 5: Notify customer
    await step.run("send-customer-notification", async () => {
      if (isFullRefund) {
        await sendEmail({
          to: user.email,
          subject: "Your refund has been processed",
          template: createElement(AccessRevokedEmail, {
            customerEmail: user.email,
            refundAmount: amountRefunded,
            currency,
          }),
        });
      } else {
        await sendEmail({
          to: user.email,
          subject: "Your partial refund has been processed",
          template: createElement(PartialRefundEmail, {
            customerEmail: user.email,
            refundAmount: amountRefunded,
            originalAmount,
            currency,
          }),
        });
      }
    });

    // Step 6: Notify admin
    await step.run("send-admin-notification", async () => {
      const adminEmail = process.env.ADMIN_EMAIL;
      if (!adminEmail) return;

      await sendEmail({
        to: adminEmail,
        subject: `({isFullRefund ? "Full" : "Partial"} refund: ){user.email}`,
        template: createElement(AdminRefundNotificationEmail, {
          customerEmail: user.email,
          customerName: user.name,
          githubUsername: user.githubUsername,
          refundAmount: amountRefunded,
          originalAmount,
          currency,
          stripeChargeId: chargeId,
          accessRevoked,
          isPartialRefund: !isFullRefund,
        }),
      });
    });

    return { success: true, accessRevoked, isFullRefund, userId: user.id };
  }
);

رقم کی واپسی کے عمل میں تین چیزیں قابل ذکر ہیں:

  1. جزوی اور مکمل رقم کی واپسی: یہ فنکشن دونوں کے درمیان فرق کرنے کے لیے ایک سادہ موازنہ کا استعمال کرتا ہے۔ amountRefunded >= originalAmount. جزوی رقم کی واپسی کے لیے، صارفین کو اب بھی رسائی حاصل ہوگی، لیکن ان کی خریداری کی حیثیت تبدیل ہو جائے گی: partially_refunded. مکمل رقم کی واپسی کے لیے، آپ کی GitHub رسائی منسوخ کر دی جائے گی اور اسٹیٹس درج ذیل ہو گا: refunded.

    یہ ڈیٹا بیس کی سالمیت کے لیے اہم ہے۔ ڈاؤن اسٹریم سسٹمز (ڈیش بورڈز، اینالیٹکس، سپورٹ ٹولز) کو درست اسٹیٹس ویلیوز کی ضرورت ہوتی ہے۔

  2. مشروط مرحلہ پر عملدرآمد: "GitHub رسائی کو منسوخ کریں” مرحلہ صرف اس صورت میں چلے گا جب تین شرائط پوری ہوں: اس کا مطلب ہے کہ یہ مکمل رقم کی واپسی ہے، صارف کے پاس GitHub کا صارف نام ہے، اور رسائی پہلے دی گئی تھی۔ Ingest ان اقدامات کو چھوڑ کر صاف طور پر ہینڈل کرتا ہے جن پر عمل کرنے کی ضرورت نہیں ہے۔

    یہ ایک یک سنگی پروسیسر میں گہرائی سے اندر سے جڑے if-else بلاکس سے زیادہ پڑھنے کے قابل ہے۔

  3. صارفین اور منتظمین کے لیے الگ الگ یاددہانی: صارفین کو مختلف ای میلز موصول ہوں گی اس پر منحصر ہے کہ رقم کی واپسی مکمل ہے یا جزوی۔ منتظمین کو ہمیشہ تفصیلی اطلاعات موصول ہوتی ہیں، بشمول بلنگ ID، صارف کا GitHub صارف نام، اور آیا رسائی منسوخ کر دی گئی ہے۔

یہ ایک الگ مرحلہ ہے کیونکہ ایڈمنسٹریٹر کی اطلاع کی ناکامی کی وجہ سے کسٹمر کی اطلاعات کو بلاک نہیں کیا جانا چاہیے۔ آپ کے گاہک کے ای میل کو زیادہ ترجیح حاصل ہے۔

ترک شدہ ادائیگیوں کی بازیافت کیسے کریں۔

لاوارث گاڑیوں کو کہاں سے بازیافت کرنا ہے۔ step.sleep() طریقہ چمکتا ہے۔ جب آپ کا چیک آؤٹ سیشن ختم ہو جائے گا تو اسٹرائپ آپ کو ایک ریکوری ای میل بھیجنے کی کوشش کرے گی۔ لیکن فوری طور پر نہیں۔

گاہکوں کو اپنے طور پر واپس آنے کا وقت دینے کے لیے ایک گھنٹہ یا اس سے زیادہ انتظار کرنے کی توقع کریں۔

export const handleCheckoutExpired = inngest.createFunction(
  {
    id: "checkout-expired",
    triggers: [{ event: "stripe/checkout.session.expired" }],
  },
  async ({ event, step }) => {
    const { customerEmail, sessionId } = event.data;

    if (!customerEmail) {
      return { success: false, reason: "no_email" };
    }

    // Wait 1 hour before sending recovery email
    await step.sleep("wait-before-recovery-email", "1h");

    // Send abandoned cart email
    await step.run("send-abandoned-cart-email", async () => {
      const checkoutUrl = `https://yoursite.com/pricing`;

      await sendEmail({
        to: customerEmail,
        subject: "Your checkout is waiting",
        template: createElement(AbandonedCartEmail, {
          customerEmail,
          checkoutUrl,
        }),
      });
    });

    // Track the event
    await step.run("track-abandoned-cart", async () => {
      await trackServerEvent("anonymous", "abandoned_cart_email_sent", {
        customer_email: customerEmail,
        session_id: sessionId,
      });
    });

    return { success: true, customerEmail };
  }
);

کہ step.sleep("wait-before-recovery-email", "1h") لائنیں کلیدی ہیں۔ یہ کسی کمپیوٹنگ وسائل کو استعمال کیے بغیر فیچر کو ایک گھنٹے کے لیے روک دے گا۔

اندرونی طور پر شیڈولنگ ہینڈلز کو داخل کریں۔ ایک گھنٹے کے بعد، فعالیت دوبارہ شروع ہو جائے گی اور آپ کا ای میل بھیج دیا جائے گا۔

پائیدار عملدرآمد کے بغیر، آپ کو میعاد ختم ہونے والے سیشنز کے لیے ڈیٹا بیس سے استفسار کرنے کے لیے کرون جاب کی ضرورت ہوگی، یا Redis کا استعمال کرتے ہوئے موخر کردہ ٹاسک قطار کی ضرورت ہوگی۔ setTimeout جب سرور دوبارہ شروع ہوتا ہے تو وہ کھو جاتے ہیں۔ کہ step.sleep() نقطہ نظر آسان، زیادہ پڑھنے کے قابل، اور زیادہ قابل اعتماد ہے۔

تقریب کے اوپر ایک گارڈ بھی ہوتا ہے۔ اگر اسٹرائپ کے پاس سیشن کے لیے گاہک کا ای میل نہیں ہے (اگر صارف نے اپنا ای میل داخل کرنے سے پہلے چیک آؤٹ بند کر دیا ہے)، تو فنکشن جلد واپس آجاتا ہے۔ اگر آپ کے پاس بھیجنے کے لیے کوئی پتہ نہیں ہے تو ریکوری ای میل کو شیڈول کرنے کی ضرورت نہیں ہے۔

یہ نمونہ زیادہ پیچیدہ بحالی کے بہاؤ تک پھیلا ہوا ہے۔ آپ 1 سیکنڈ کا اضافہ کر سکتے ہیں۔ step.sleep() اگر گاہک نے ابھی تک خریداری نہیں کی ہے، تو تین دن بعد فالو اپ ریکوری ای میل بھیجیں۔ آپ چیک کر سکتے ہیں کہ آیا کسی صارف نے خریداری مکمل کر لی ہے (ڈیٹا بیس سے استفسار کرکے)۔ step.run()) اگر آپ کے پاس ای میل ہے تو اسے چھوڑ دیں۔

ہر اضافی قدم ایک اضافی قدم ہے۔ step.run() یا step.sleep() کال یہ فنکشن ایک اسکرپٹ کی طرح پڑھتا ہے جو کاروباری منطق کو بیان کرتا ہے، بجائے ایک پیچیدہ کرون جاب اور ڈیٹا بیس کے جھنڈوں کے۔

مقامی طور پر اپنے ویب ہک ہینڈلر کی جانچ کیسے کریں۔

مقامی جانچ سٹرائپ ویب ہکس کے ساتھ سب سے بڑی پریشانیوں میں سے ایک ہے۔ آپ کو اپنے مقامی کمپیوٹر پر ایونٹس بھیجنے کے لیے اسٹرائپ کی ضرورت ہوگی، اور آپ کو ان پر کارروائی کرنے کے لیے بیک گراؤنڈ ٹاسک سسٹم کی ضرورت ہوگی۔ ترتیبات مندرجہ ذیل ہیں:

مقامی طور پر اسٹرائپ ایونٹس کو کیسے بھیجیں۔

اسٹرائپ CLI انسٹال کریں اور ویب ہک ایونٹس کو اپنے مقامی سرور پر بھیجیں۔

stripe listen --forward-to localhost:3000/api/payments/webhook

CLI ویب ہک کے دستخطی راز کو پرنٹ کرتا ہے (اس سے شروع ہوتا ہے: whsec_)۔ اسے اپنے طور پر سیٹ کریں۔ STRIPE_WEBHOOK_SECRET مقامی ترقی کے لیے ماحولیاتی تغیرات۔

آپ ٹیسٹ ایونٹس کو براہ راست متحرک کر سکتے ہیں۔

stripe trigger checkout.session.completed
stripe trigger charge.refunded
stripe trigger checkout.session.expired

انجسٹ ڈیو سرور کو کیسے چلائیں۔

Ingest ایک مقامی ڈویلپمنٹ سرور فراہم کرتا ہے جو حقیقی وقت میں ہر فنکشن، ہر قدم، اور ہر دوبارہ کوشش کو ظاہر کرتا ہے۔

npx inngest-cli@latest dev -u http://localhost:3000/api/inngest

کہ -u جھنڈا انجیسٹ ڈویلپمنٹ سرور کو بتاتا ہے کہ ایپلیکیشن کہاں چل رہی ہے تاکہ یہ خصوصیات کو بازیافت کر سکے۔ کھلا http://localhost:8288 اپنے براؤزر میں Ingest ڈیش بورڈ دیکھیں۔

مرحلہ وار عمل کو کیسے دیکھیں

انجسٹ ڈویلپمنٹ ڈیش بورڈ وہ جگہ ہے جہاں پائیدار عمل درآمد کے نمونے واقعی کلک کرتے ہیں۔ جب آپ اسٹرائپ ایونٹ کو متحرک کرتے ہیں تو آپ دیکھیں گے:

  1. یہ ایک واقعہ ہے جو ‘ایونٹ’ ٹیب میں آتا ہے۔

  2. یہ فنکشن "رن” ٹیب سے شروع ہوتا ہے۔

  3. ان پٹ، آؤٹ پٹ، اور دورانیہ سمیت ہر مرحلہ ایک ایک کرکے انجام دیا جاتا ہے۔

  4. اگر کوئی مرحلہ ناکام ہوجاتا ہے تو، ایک غلطی اور دوبارہ کوشش ظاہر ہوتی ہے۔

یہ مرئیت ایسی چیز ہے جو آپ ان لائن ویب ہک ہینڈلر کے ساتھ حاصل نہیں کر سکتے۔ اگر کوئی گاہک رپورٹ کرتا ہے کہ "میں نے ادائیگی کی، لیکن اس تک رسائی حاصل نہیں کر سکتا”، تو آپ اپنے Ingest ڈیش بورڈ میں فنکشن کو دیکھ سکتے ہیں اور دیکھ سکتے ہیں کہ کون سا مرحلہ ناکام ہوا اور کیوں۔ پیداوار میں اس قسم کا مشاہدہ بہت اہم ہے۔

ناکامی کی تقلید کیسے کریں۔

دوبارہ کوشش کرنے کے رویے کو جانچنے کے لیے، آپ جان بوجھ کر ایک قدم کو ناکام بنا سکتے ہیں۔ مثال کے طور پر، "add-github-collaborator” مرحلہ وقفے وقفے سے ایک غلطی پھینکتا ہے۔

const collaboratorResult = await step.run(
  "add-github-collaborator",
  async () => {
    throw new Error("Simulated GitHub API failure");
  }
);

Ingest ڈیش بورڈ دکھاتا ہے:

  • مرحلہ 1 سے 4 تک کامیاب ہو گئے اور نتائج کیش ہو گئے۔

  • مرحلہ 5 ناکام ہو جاتا ہے اور دوبارہ کوشش کی پالیسی کے مطابق دوبارہ کوشش کی جاتی ہے۔

  • مرحلہ 6 سے 9 تک زیر التواء رہتے ہیں جب تک کہ مرحلہ 5 کامیاب نہیں ہو جاتا۔

کسی بھی خرابی کو ہٹا دیں اور اگلی دوبارہ کوشش پر مرحلہ 5 کامیاب ہو جائے گا۔ اس کے بعد 6 سے 9 تک کے مراحل کو ترتیب سے عمل میں لایا جاتا ہے، لیکن 1 سے 4 تک کے مراحل کو دوبارہ عمل میں نہیں لایا جاتا ہے۔ یہ اصل چوکی سلوک ہے۔

نتیجہ

قابل اعتماد اسٹرائپ ویب ہکس کا نمونہ ایک اصول پر آتا ہے: وصول کرنے اور پروسیسنگ کو الگ کریں۔

ویب ہک اینڈ پوائنٹ پٹی کے دستخط کی توثیق کرتا ہے اور داخل کردہ واقعات کو پس منظر کے ٹاسک سسٹم کو بھیجتا ہے۔ بس۔ پروسیسنگ ایک پائیدار فنکشن میں ہوتی ہے جہاں ہر قدم کا معائنہ کیا جاتا ہے اور انفرادی طور پر دوبارہ کوشش کی جاتی ہے۔

یہ جو فراہم کرتا ہے وہ یہ ہے:

  • کوئی ڈپلیکیٹ ای میل نہیں: وہ اقدامات جو پہلے ہی کامیاب ہو چکے ہیں دوبارہ نہیں چلائے جائیں گے۔

  • کوئی جزوی حالت نہیں: اگر مرحلہ 5 ناکام ہو جاتا ہے تو، مرحلہ 1 سے 4 تک محفوظ رہتا ہے اور مرحلہ 5 کو آزادانہ طور پر دوبارہ آزمایا جاتا ہے۔

  • مکمل مشاہدہ: ہر فنکشن پر عمل درآمد کے لیے، آپ بالکل دیکھ سکتے ہیں کہ کون سا مرحلہ ناکام ہوا اور کیوں۔

  • بلٹ ان تاخیر سے عملدرآمد: step.sleep() کرون جابز کے بغیر ریکوری ای میلز اور فالو اپ سیکونسز پر کارروائی کریں۔

  • قابل ترتیب ورک فلو: ایک خصوصیت کسی ایونٹ کے ذریعے دوسری خصوصیت کو متحرک کر سکتی ہے، ایک سلسلہ بناتا ہے جیسے خریداری کو مکمل کرنا، جس سے 30 دن کی پیروی کی ترتیب ہوتی ہے۔

یہ پیٹرن صرف پٹیوں تک محدود نہیں ہے۔ پائیدار عملدرآمد سے کوئی بھی ملٹی سٹیپ ویب ہُک پروسیسنگ کا فائدہ ہوتا ہے، جیسے کہ CI پائپ لائن کو متحرک کرنے کے لیے GitHub ویب ہُک، ای میل کی ترسیل کو ٹریک کرنے کے لیے ری ڈائریکٹ ویب ہُک، یا خدمات کے درمیان مطابقت پذیر ہونے کے لیے کیلنڈر ویب ہُک۔

اصول وہی ہیں۔ توثیق کریں۔ اسے قطار میں لگائیں۔ مسلسل عمل کریں۔

میں نے اس پیٹرن کو ایڈن اسٹیک کے لیے پروڈکشن میں استعمال کیا ہے، جہاں خریداری کا بہاؤ ادائیگی کی تصدیق سے لے کر GitHub ریپوزٹری کو ملٹی ویک ای میل کی ترتیب تک رسائی دینے تک ہر چیز کو سنبھالتا ہے۔ ہماری 9-مرحلہ خریداری کی خصوصیت نے اس بات کو یقینی بنایا کہ تمام ادائیگیوں پر کارروائی کی گئی، بغیر ایک قدم چھوٹ گئے یا ای میلز کی نقل تیار کی گئی۔

اگر آپ Stripe کا استعمال کرتے ہوئے SaaS بنا رہے ہیں، تو اس مضمون میں ویب ہک اینڈ پوائنٹ کے پیٹرن سے شروع کریں۔ اختتامی نقطوں کو پتلا رکھیں اور پروسیسنگ کو پائیدار مراحل میں منتقل کریں۔ جب گاہک کہتا ہے، "میں نے ادائیگی کر دی، لیکن کچھ نہیں ہوا”، تو آپ صبح 3 بجے کے ڈیبگنگ سیشن سے باہر ہو جاتے ہیں۔

اگر آپ پہلے سے تعمیر شدہ خریداری کے بہاؤ، ریفنڈ پروسیسنگ، اور فالو اپ ای میل کی ترتیب کے ساتھ مکمل اسٹرائپ ویب ہکس اور انجسٹ انٹیگریشن چاہتے ہیں، تو ایڈن اسٹیک میں 30+ اضافی پروڈکشن ٹیسٹ پیٹرن کے ساتھ اس مضمون کی ہر چیز شامل ہے۔

Magnus Rodseth AI پر مبنی ایپلی کیشنز بناتا ہے اور ایڈن اسٹیک30+ کلاڈ ٹیکنالوجیز کے ساتھ پروڈکشن کے لیے تیار اسٹارٹر کٹ جو AI سے چلنے والے SaaS کی ترقی کے لیے پروڈکشن پیٹرن کو انکوڈ کرتی ہے۔

اوپر تک سکرول کریں۔