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

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

لیکن یہ اصل ادائیگی کے نظام کا صرف پہلا 10% ہے۔

گاہک کی ادائیگی کے بعد کیا ہوتا ہے؟ آپ کو اپنے ڈیٹا بیس میں خریداری کو ریکارڈ کرنا ہوگا، ایک تصدیقی ای میل بھیجیں، اور پروڈکٹ تک رسائی فراہم کریں (GitHub ریپوزٹری دعوت، API کلید، لائسنس فائل)۔ بطور مینیجر، آپ کو اپنے آپ کو بتانے کی ضرورت ہے۔ آپ کو 2 ہفتوں کے بعد رقم کی واپسی جاری کرنی ہوگی اور اگر کوئی ادائیگی ترک کردیتا ہے تو آپ کو ایک بازیابی ای میل بھیجنا ہوگا۔

یہ ادائیگیوں کا پورا لائف سائیکل ہے اور یہیں سے زیادہ تر SaaS ایپلیکیشنز ٹوٹ جاتی ہیں۔

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

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

  • ایک ڈیٹا بیس اسکیما کو کیسے ڈیزائن کیا جائے جو خریداری کے ہر قدم کو ٹریک کرتا ہو۔

  • سٹرائپ پروڈکٹس اور قیمتیں پروگرام کے مطابق کیسے بنائیں

  • کامیابی/منسوخی پروسیسنگ کے ساتھ ادائیگی کا بہاؤ کیسے بنایا جائے۔

  • دستخطی تصدیق کے ساتھ ویب ہکس کو محفوظ طریقے سے کیسے ہینڈل کریں۔

  • ادائیگی کے بعد کی پروسیسنگ کو پائیدار، آزادانہ طور پر دوبارہ کوشش شدہ مراحل میں کیسے تقسیم کیا جائے۔

  • رسائی کے حقوق کی خودکار واپسی کی وجہ سے مکمل اور جزوی رقم کی واپسی پر کارروائی کیسے کی جائے۔

  • لاوارث چیک آؤٹس سے ریوینیو کی بازیافت کیسے کریں۔

  • ری ایکٹ ای میل اور دوبارہ بھیج کر ٹرانزیکشنل ای میل ٹیمپلیٹس کیسے بنائیں

  • اسٹرائپ CLI اور Ingest کا استعمال کرتے ہوئے مقامی طور پر پورے بہاؤ کی جانچ کیسے کریں۔

انڈیکس

شرطیں

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

  • TypeScript اور Node.js

  • SQL ڈیٹا بیس (مثال کے طور پر PostgreSQL استعمال کرتا ہے)

  • رد عمل کریں (ای میل ٹیمپلیٹس کے لیے)

  • ویب ہکس کی بنیادی تفہیم

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

آپ کو جس چیز کی ضرورت ہے انسٹال کریں۔

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

bun add stripe drizzle-orm @neondatabase/serverless inngest resend @react-email/components

آپ کو بھی ضرورت ہو گی:

ماحولیاتی متغیرات

درج ذیل ماحولیاتی متغیرات کو سیٹ کریں: .env فائل:

# Database
DATABASE_URL=postgresql://...

# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRO_PRICE_ID=price_...

# Email
RESEND_API_KEY=re_...
EMAIL_FROM="Your App "
ADMIN_EMAIL=you@yourapp.com

# App
BETTER_AUTH_URL=http://localhost:3000

ادائیگیوں کا ڈیٹا بیس اسکیما کیسے ڈیزائن کریں۔

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

آپ کی خریداری اس طرح شروع ہوتی ہے: pending جب صارف ‘خریداری’ پر کلک کرتا ہے، تو اسٹرائپ ادائیگی کی تصدیق کرتا ہے اور پھر اس پر منتقل ہوتا ہے: completed. وہاں سے آپ آگے بڑھ سکتے ہیں: refunded یا partially_refunded. نامکمل زیر التواء خریداریوں کی میعاد 24 گھنٹے کے بعد ختم ہو جاتی ہے (ترک شدہ کارٹ)۔

ذیل میں وہ اسکیما ہے جسے ہم پیداوار میں استعمال کرتے ہیں، جس کی وضاحت Drizzle ORM سے کی گئی ہے۔ اس مضمون کی تمام مثالیں اس مخصوص پروڈکٹ کے ذریعہ فروخت کردہ نجی GitHub ذخیرہ تک رسائی فراہم کرتی ہیں۔

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

// src/lib/db/schema.ts
import {
  boolean,
  integer,
  pgEnum,
  pgTable,
  text,
  timestamp,
  varchar,
} from "drizzle-orm/pg-core";

export const purchaseTierEnum = pgEnum("purchase_tier", ["pro"]);
export const purchaseStatusEnum = pgEnum("purchase_status", [
  "completed",
  "partially_refunded",
  "refunded",
]);

export const users = pgTable("users", {
  id: text("id").primaryKey(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  emailVerified: boolean("email_verified").notNull().default(false),
  name: text("name"),
  image: text("image"),
  githubUsername: text("github_username"),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow(),
});

export const purchases = pgTable("purchases", {
  id: text("id")
    .primaryKey()
    .$defaultFn(() => crypto.randomUUID()),
  userId: text("user_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  stripeCheckoutSessionId: text("stripe_checkout_session_id")
    .notNull()
    .unique(),
  stripeCustomerId: text("stripe_customer_id"),
  stripePaymentIntentId: text("stripe_payment_intent_id"),
  tier: purchaseTierEnum("tier").notNull(),
  status: purchaseStatusEnum("status").notNull().default("completed"),
  githubAccessGranted: boolean("github_access_granted")
    .notNull()
    .default(false),
  githubInvitationId: text("github_invitation_id"),
  amount: integer("amount").notNull(),
  currency: text("currency").notNull().default("usd"),
  purchasedAt: timestamp("purchased_at").notNull().defaultNow(),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow(),
});

export type Purchase = typeof purchases.$inferSelect;
export type NewPurchase = typeof purchases.$inferInsert;

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

آپ کو 3 اسٹرائپ ID کالمز کی ضرورت کیوں ہے؟

کہ purchases ٹیبل تین علیحدہ پٹی شناخت کنندگان کو اسٹور کرتا ہے: stripeCheckoutSessionId، stripeCustomerIdاور stripePaymentIntentId.

ہر ایک مختلف مقصد کی خدمت کرتا ہے۔

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

کہ unique() اس کالم کی رکاوٹ ایک غیرمعمولی محافظ ہے۔ اگر کوئی ایک ہی سیشن کی دو بار درخواست کرتا ہے، تو ڈیٹا بیس دوسری داخل کو مسترد کر دے گا۔

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

کہ ادائیگی کا ارادہ ID ریفنڈ ویب ہک ایونٹ میں سٹرائپ یہی بھیجتی ہے۔ جب charge.refunded جب کوئی واقعہ پیش آتا ہے، تو ادائیگی کے ارادے کی ID شامل ہوتی ہے، لیکن ادائیگی کے سیشن کی ID شامل نہیں ہوتی ہے۔ اگر آپ اس فیلڈ کو محفوظ نہیں کرتے ہیں، تو آپ کے ڈیٹا بیس میں ریفنڈز کو خریداریوں سے ملانے کا کوئی طریقہ نہیں ہے۔

ڈیٹا بیس میں رسائی کی حیثیت کو کیوں ٹریک کریں۔

کہ githubAccessGranted اور githubInvitationId میدان غیر ضروری معلوم ہو سکتا ہے۔ آپ GitHub کا API چیک کر سکتے ہیں کہ آیا آپ کو رسائی حاصل ہے۔ تاہم، ہر بار جب آپ کو کسی صارف کی رسائی کی حیثیت چیک کرنے کی ضرورت ہو تو کسی بیرونی API سے استفسار کرنا سست، رفتار سے محدود، اور ناقابل اعتبار ہے۔

آپ کے اپنے ڈیٹا بیس میں رسائی کی حیثیت کا سراغ لگانا آپ کو اس سوال کا جواب دینے کی اجازت دیتا ہے، "کیا اس صارف کو رسائی حاصل ہے؟” ایک انڈیکس استفسار کے ساتھ۔ آپ یہ بھی جان لیں گے کہ رسائی دی گئی ہے یا نہیں، جو کہ رقم کی واپسی کی کارروائی کے لیے بہت اہم ہے۔ اگر githubAccessGranted ہے falseرقم کی واپسی حاصل کرتے وقت آپ کو کچھ بھی منسوخ کرنے کی ضرورت نہیں ہے۔

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

کہ purchaseStatusEnum تین قدریں ہیں: completed، partially_refundedاور refunded.

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

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

ہجرت کیسے بنائیں اور چلائیں۔

اسکیما کی وضاحت کرنے کے بعد، ایک منتقلی فائل بنائیں اور اسے ڈیٹا بیس پر لاگو کریں۔

# Generate migration SQL from schema changes
bun run drizzle-kit generate

# Push schema directly (development only)
bun run drizzle-kit push

# Run migrations (production)
bun run drizzle-kit migrate

Drizzle Kit آپ کے TypeScript اسکیما کا آپ کے ڈیٹا بیس سے موازنہ کرتا ہے اور اسے مطابقت پذیر بنانے کے لیے درکار SQL تیار کرتا ہے۔ تیار کردہ منتقلی فائل کو پروڈکشن میں چلانے سے پہلے اس کا جائزہ لیں۔ اسکیما تبدیلیاں ان چند کارروائیوں میں سے ایک ہیں جنہیں آسانی سے کالعدم نہیں کیا جا سکتا۔

ترقی کے لیے، drizzle-kit push یہ تیز تر ہے کیونکہ یہ منتقلی فائل بنائے بغیر براہ راست تبدیلیوں کا اطلاق کرتا ہے۔ اسے ہمیشہ پیداوار کے لیے استعمال کریں۔ drizzle-kit generate پھر drizzle-kit migrate لہذا ہر اسکیما کی تبدیلی کے لئے ایک ورژن کی تاریخ ہے۔

پٹی کی مصنوعات اور قیمتوں کا تعین کیسے کریں۔

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

// src/lib/payments/seed.ts
import { stripe } from "./index";

const PRODUCTS = [
  {
    name: "My SaaS Product",
    description: "Full access, one-time purchase",
    features: [
      "Full source code access",
      "Production-ready infrastructure",
      "Lifetime updates",
    ],
    metadata: { tier: "pro" },
    prices: [
      {
        lookupKey: "pro_one_time",
        unitAmount: 19900, // $199.00 in cents
        currency: "usd",
        nickname: "Pro One-Time",
      },
    ],
  },
];

async function main() {
  console.log("Seeding Stripe products and prices...n");

  for (const config of PRODUCTS) {
    // Create or find product
    const products = await stripe.products.list({ active: true, limit: 100 });
    let product = products.data.find((p) => p.name === config.name);

    if (!product) {
      product = await stripe.products.create({
        name: config.name,
        description: config.description,
        marketing_features: config.features.map((f) => ({ name: f })),
        metadata: config.metadata,
      });
      console.log(`Created product "({config.name}" (){product.id})`);
    }

    // Create prices
    for (const priceConfig of config.prices) {
      const existing = await stripe.prices.list({
        lookup_keys: [priceConfig.lookupKey],
        active: true,
        limit: 1,
      });

      if (existing.data[0]) {
        console.log(`Price "${priceConfig.lookupKey}" already exists`);
        continue;
      }

      const price = await stripe.prices.create({
        product: product.id,
        unit_amount: priceConfig.unitAmount,
        currency: priceConfig.currency,
        nickname: priceConfig.nickname,
        lookup_key: priceConfig.lookupKey,
        transfer_lookup_key: true,
      });

      console.log(`Created price "({priceConfig.lookupKey}" (){price.id})`);
    }
  }

  console.log("nDone! Add the price ID to your .env as STRIPE_PRO_PRICE_ID");
}

main().catch(console.error);

یہ چلائیں bun run src/lib/payments/seed.ts.

چند باتیں قابل توجہ ہیں۔

  • استعمال کریں lookup_key قیمت کی ID کو ہارڈ کوڈنگ کرنے کے بجائے: ٹیسٹ موڈ اور لائیو موڈ کے لیے قیمت IDs مختلف ہیں۔ تلاش کی چابیاں آپ کو نام سے قیمتوں کا حوالہ دینے کی اجازت دیتی ہیں (pro_one_time) پٹی سے تیار کردہ ID کے بجائے (price_1P...

    کہ transfer_lookup_key: true آپشن کے ساتھ، اسی تلاش کی کلید کے ساتھ نئی قیمت بنانے سے خود بخود پرانی قیمت بدل جائے گی۔

  • قیمتیں سینٹ میں ہیں: Stripe’s API کرنسی کی سب سے چھوٹی اکائی میں رقم کی توقع کرتا ہے۔ USD کے لیے اس کا مطلب ہے۔ 19900 $199.00 کی نمائندگی کرتا ہے۔

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

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

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

اگر ماڈیول لوڈ کرتے وقت API کلید غائب ہو تو اسٹرائپ کلائنٹ بازیافت کو روکنے کے لیے سست ابتداء کا استعمال کرتا ہے۔ یہ تعمیراتی ماحول میں اہم ہے جہاں ماحولیاتی متغیرات سیٹ نہیں ہوتے ہیں۔

// src/lib/payments/index.ts
import Stripe from "stripe";

let stripeClient: Stripe | null = null;

function getStripe(): Stripe {
  if (!stripeClient) {
    const secretKey = process.env.STRIPE_SECRET_KEY;
    if (!secretKey) {
      throw new Error("STRIPE_SECRET_KEY is not set");
    }
    stripeClient = new Stripe(secretKey);
  }
  return stripeClient;
}

export const stripe = new Proxy({} as Stripe, {
  get(_, prop) {
    return Reflect.get(getStripe(), prop);
  },
});

کہ Proxy ریپر یہاں کلیدی نمونہ ہے۔ درخواست کی درآمد کے دوران کوڈ stripe اور پھر درج ذیل طریقہ کو کال کریں: stripe.checkout.sessions.create(...). پراکسی تمام پراپرٹی تک رسائی کو روکتی ہے اور انہیں سست شروع کرنے والے کلائنٹ تک پہنچاتی ہے۔

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

ادائیگی کا بہاؤ کیسے بنایا جائے۔

چیک آؤٹ فلو تین حصوں پر مشتمل ہے: سیشن تخلیق، کسٹمر ری ڈائریکشن، اور ریٹرن پروسیسنگ۔

ادائیگی کا سیشن کیسے بنایا جائے۔

وہ فنکشن جو ایک بار کی ادائیگی کے لیے اسٹرائپ چیک آؤٹ سیشن بناتا ہے:

// src/lib/payments/index.ts
export async function createOneTimeCheckoutSession(params: {
  priceId: string;
  successUrl: string;
  cancelUrl: string;
  metadata: Record;
  customerEmail?: string;
  couponId?: string;
}) {
  const client = getStripe();

  const session = await client.checkout.sessions.create({
    mode: "payment",
    line_items: [{ price: params.priceId, quantity: 1 }],
    success_url: params.successUrl,
    cancel_url: params.cancelUrl,
    metadata: params.metadata,
    ...(params.customerEmail && {
      customer_email: params.customerEmail,
    }),
    ...(params.couponId
      ? { discounts: [{ coupon: params.couponId }] }
      : { allow_promotion_codes: true }),
  });

  return session;
}

یہاں تین تفصیلات اہم ہیں۔

  • کہ mode: "payment" ترتیب اسٹرائپ کو بتاتی ہے کہ یہ ایک بار چارج ہے۔یہ سبسکرپشن نہیں ہے۔ سبسکرپشنز کے لیے، استعمال کریں: mode: "subscription". ادائیگی کے بعد پٹی بھیجنے والے ویب ہک ایونٹس کو موڈ متاثر کرتا ہے۔

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

  • کہ allow_promotion_codes: true چیک آؤٹ پیج پر پرومو کوڈ فیلڈ کو ظاہر کرنے کا آپشن۔ اگر آپ کے پاس درخواست دینے کے لیے مخصوص کوپن ہے (مثال کے طور پر لینڈنگ پیج یو آر ایل پیرامیٹر میں)، تو اسے بذریعہ منتقل کریں: discounts اس کے بجائے۔ آپ دونوں کو بیک وقت استعمال نہیں کر سکتے۔

چیک آؤٹ API اینڈ پوائنٹ کیسے بنایا جائے۔

API کا اختتامی نقطہ جو ادائیگی کا سیشن بناتا ہے اور URL واپس کرتا ہے درج ذیل ہے:

// src/server/api.ts
app.post("/api/payments/checkout", async ({ set }) => {
  const priceId = process.env.STRIPE_PRO_PRICE_ID;

  if (!priceId) {
    set.status = 500;
    return { error: "Price not configured" };
  }

  const baseUrl = process.env.BETTER_AUTH_URL ?? "http://localhost:3000";
  const tier = "pro";

  const checkoutSession = await createOneTimeCheckoutSession({
    priceId,
    successUrl: `${baseUrl}/dashboard?purchase=success&session_id={CHECKOUT_SESSION_ID}`,
    cancelUrl: `${baseUrl}/pricing`,
    metadata: { tier },
  });

  return { url: checkoutSession.url };
});

کہ {CHECKOUT_SESSION_ID} کامیابی کے URL کے پلیس ہولڈرز سٹرائپ ٹیمپلیٹ متغیرات ہیں۔ کسٹمر کو ری ڈائریکٹ کرتے وقت اسٹرائپ اسے اصل سیشن ID سے بدل دیتا ہے۔ اس سے فرنٹ اینڈ کو معلوم ہوتا ہے کہ کون سا سیشن ابھی مکمل ہوا ہے۔

ادائیگی کے بعد اپنی خریداری کا بل کیسے بنائیں

جب گاہک کامیابی کے URL، فرنٹ اینڈ پر واپس آتا ہے۔ session_id اسے URL میں "دعوے” کے اختتامی نقطہ پر بھیجیں۔ یہ اختتامی نقطہ ادائیگی کی تصدیق کرتا ہے اور خریداری کا ریکارڈ بناتا ہے۔

// src/server/api.ts
app.post(
  "/api/purchases/claim",
  async ({ body, request, set }) => {
    const session = await auth.api.getSession({
      headers: request.headers,
    });

    if (!session) {
      set.status = 401;
      return { error: "Unauthorized" };
    }

    const { sessionId } = body;

    // Check if this session was already claimed
    const existing = await db
      .select()
      .from(purchases)
      .where(eq(purchases.stripeCheckoutSessionId, sessionId))
      .limit(1);

    if (existing[0]) {
      return { success: true, alreadyClaimed: true, tier: existing[0].tier };
    }

    // Retrieve the Stripe checkout session to verify payment
    const stripeSession = await retrieveCheckoutSession(sessionId);

    if (stripeSession.payment_status !== "paid") {
      set.status = 400;
      return { error: "Payment not completed" };
    }

    const tier = (stripeSession.metadata?.tier ?? "pro") as PaymentTier;

    // Create purchase record
    await db.insert(purchases).values({
      userId: session.user.id,
      stripeCheckoutSessionId: sessionId,
      stripeCustomerId:
        typeof stripeSession.customer === "string"
          ? stripeSession.customer
          : stripeSession.customer?.id ?? null,
      stripePaymentIntentId:
        typeof stripeSession.payment_intent === "string"
          ? stripeSession.payment_intent
          : stripeSession.payment_intent?.id ?? null,
      tier,
      status: "completed",
      amount: stripeSession.amount_total ?? 0,
      currency: stripeSession.currency ?? "usd",
    });

    // Trigger background processing
    await inngest.send({
      name: "purchase/completed",
      data: {
        userId: session.user.id,
        tier,
        sessionId,
      },
    });

    return { success: true, tier };
  },
  {
    body: t.Object({
      sessionId: t.String(),
    }),
  }
);

یہ اختتامی نقطہ ترتیب میں چار کام انجام دیتا ہے:

  1. پہلے ہم چیک کرتے ہیں کہ آیا سیشن کی پہلے ہی درخواست کی گئی ہے۔ کہ unique() فارماسیوٹیکل stripeCheckoutSessionId آپ کے اسکیما میں ڈپلیکیٹ ریکارڈز کو روکنا، لیکن پہلے چیک کرنا، اس بات کو یقینی بناتا ہے کہ آپ ڈیٹا بیس کی غلطیوں کو پکڑے بغیر صاف جواب دیں گے۔

  2. دوسرا، ہم پٹی کے ساتھ ادائیگی کی تصدیق کرتے ہیں. کلائنٹ کے ڈیٹا پر کبھی بھروسہ نہ کریں۔ فرنٹ اینڈ سیشن آئی ڈی کو پاس کرتا ہے، لیکن آپ کو اس کی تصدیق کے لیے اسٹرائپ کے API کو کال کرنے کی ضرورت ہے۔ payment_status ہے "paid".

  3. تیسرا، خریداری کا ریکارڈ بنائیں۔ اسے کیسے نکالا جاتا ہے اس پر توجہ دیں۔ customer اور payment_intent پٹی سیشن میں۔ آپ کی اسٹرائپ API کی ترتیبات کے لحاظ سے دونوں فیلڈز کو سٹرنگز یا توسیعی آبجیکٹ کے طور پر واپس کیا جاتا ہے، لہذا ٹرنری دونوں صورتوں کو ہینڈل کرتا ہے۔

  4. چوتھا، درج ذیل بھیجیں: purchase/completed یہ Ingest کے بارے میں ایک واقعہ ہے۔ یہ بیک گراؤنڈ پروسیسنگ فلو کو متحرک کرتا ہے جو ای میلز، گرانٹس تک رسائی، تجزیات، اور فالو اپ شیڈولنگ کو ہینڈل کرتا ہے۔ API کا اختتامی نقطہ عمل کیے بغیر واپس آجاتا ہے۔ { success: true } فوری طور پر

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

اس کو تقسیم کرنے سے صارفین فوری طور پر کامیابی کے جوابات دیکھ سکتے ہیں جبکہ پس منظر کے کام جاری رہتے ہیں۔

ویب ہکس کو محفوظ طریقے سے کیسے ہینڈل کریں۔

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

ویب ہک دستخط کی تصدیق کیسے کریں۔

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

// src/lib/payments/index.ts
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 = getStripe();
  return client.webhooks.constructEventAsync(payload, signature, webhookSecret);
}

ایک اہم تفصیل یہ ہے: استعمال کریں constructEventAsync اس کے بجائے constructEvent. غیر مطابقت پذیر ورژن Web Crypto API کا استعمال کرتا ہے، جو کہ Bun اور Cloudflare ورکرز جیسے جدید رن ٹائمز کے ساتھ مطابقت رکھتا ہے۔ ہم وقت ساز ورژن Node.js پر منحصر ہے۔ crypto ماڈیول ہر جگہ دستیاب نہیں ہیں۔

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

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

یہاں پروڈکشن ویب ہک ہینڈلر ہے: اس کا واحد کام واقعات کی توثیق کرنا اور انہیں بیک گراؤنڈ ٹاسک سسٹم تک پہنچانا ہے۔

// src/server/api.ts
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 as {
        id: string;
        payment_intent: string;
        amount: number;
        amount_refunded: number;
        currency: string;
      };
      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 as {
        id: string;
        customer_email: string | null;
      };
      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" };
  }
});

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

پورا ہینڈلر ملی سیکنڈ میں مکمل ہوتا ہے۔

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

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

سین ہینڈلر اسے مکمل طور پر روکتا ہے۔ توثیق کریں، قطار میں شامل کریں، واپس جائیں۔ تمام اصل کام ایک پائیدار پس منظر کے فنکشن میں متضاد طور پر ہوتا ہے۔

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

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,
  },
});

کیا ہوگا اگر ہم پورے اسٹرائپ ایونٹ کو آگے بھیج دیں؟ دو وجوہات۔

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

دوسرا، باؤنڈری سے فیلڈز نکالنے سے ویب ہک ہینڈلر اور بیک گراؤنڈ فنکشن کے درمیان واضح معاہدہ ہوتا ہے۔ اگر پٹی مستقبل کے API ورژن میں ایونٹ آبجیکٹ کی ظاہری شکل کو تبدیل کرتی ہے، تو آپ کو صرف اپنے ویب ہک ہینڈلر میں نکالنے کی منطق کو اپ ڈیٹ کرنے کی ضرورت ہے۔ پس منظر کی خصوصیات کام کرتی رہیں گی کیونکہ وہ صارف کے داخل کردہ ڈیٹا کی شکل پر انحصار کرتی ہیں نہ کہ پٹی پر۔

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

پروڈکشن کے لیے، اسٹرائپ ڈیش بورڈ میں ویب ہکس کو کنفیگر کریں۔

  1. اسٹرائپ ڈیش بورڈ، ڈویلپرز، ویب ہکس پر جائیں۔

  2. اپنے پروڈکشن URL کی طرف اشارہ کرنے والا ایک اختتامی نقطہ شامل کریں۔ https://yourapp.com/api/payments/webhook.

  3. وہ ایونٹ منتخب کریں جسے آپ وصول کرنا چاہتے ہیں: charge.refunded اور checkout.session.expired.

  4. دستخط کرنے کے راز کو کاپی کریں اور اسے اپنے پیداواری ماحول کے متغیرات میں درج ذیل شامل کریں: STRIPE_WEBHOOK_SECRET.

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

ویب ہک ایونٹس وصول کرنے کے لیے

مکمل ادائیگی کے بہاؤ کے لیے، آپ کو اسٹرائپ میں درج ذیل ویب ہُک ایونٹ کو ترتیب دینے کی ضرورت ہوگی۔

واقعہ اگر آگ لگ جائے۔ آپ کیا کرتے ہیں
charge.refunded کسٹمر ریفنڈ وصول کرتا ہے۔ رسائی منسوخ کریں (مکمل رقم کی واپسی) یا اسٹیٹس کو اپ ڈیٹ کریں (جزوی)
checkout.session.expired ادائیگی کے سیشن کا ٹائم آؤٹ (24 گھنٹے) ترک شدہ کارٹ ریکوری ای میل بھیجیں۔

سبسکرپشن پر مبنی بلنگ کے لیے، آپ یہ بھی حاصل کر سکتے ہیں: customer.subscription.updated، customer.subscription.deletedاور invoice.payment_failed. چونکہ یہ مضمون ایک بار کی ادائیگیوں کا احاطہ کرتا ہے، اس لیے مثال اوپر کے دو واقعات پر مرکوز ہے۔

کہ checkout.session.completed واقعات نمایاں طور پر غائب ہیں۔ ایک بار کی ادائیگیوں کے لیے، آپ عام طور پر ویب ہک کے بجائے "بلنگ” اینڈ پوائنٹ (پچھلے حصے میں دکھایا گیا ہے) میں خریداری پر کارروائی کرتے ہیں۔ اس کی وجہ یہ ہے کہ خریداری کو کسی اکاؤنٹ سے منسلک کرنے کے لیے ایک مستند صارف کے سیشن کی ضرورت ہوتی ہے۔

ایک پائیدار پس منظر کے کام کے طور پر خریداریوں پر کارروائی کیسے کریں۔

یہ ادائیگی کے بہاؤ کا دل ہے۔ خریداری کا ریکارڈ بنانے کے بعد purchase/completed ایک بار جب ایونٹ بھیج دیا جاتا ہے، ایک پائیدار فنکشن سنبھالتا ہے اور مکمل ادائیگی کے بعد ورک فلو کو انجام دیتا ہے۔

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

"مسلسل پھانسی” کا مطلب یہی ہے۔ یہ ادائیگی کے نظام کے درمیان فرق ہے جو ترقیاتی ماحول میں کام کرتا ہے اور ادائیگی کے نظام میں جو پیداواری ماحول میں کام کرتا ہے۔

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

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

// src/lib/jobs/client.ts
import { Inngest } from "inngest";

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

اپنے فنکشن کو Ingest subhandler کے ساتھ رجسٹر کریں تاکہ آپ کا ڈویلپمنٹ سرور (اور پروڈکشن) اسے بازیافت کر سکے۔

import { serve } from "inngest/bun";
import { inngest } from "@/lib/jobs/client";
import { stripeFunctions } from "@/lib/jobs/functions/stripe";

const inngestHandler = serve({
  client: inngest,
  functions: [...stripeFunctions],
});

// Mount on your API
app.all("/api/inngest", async (ctx) => {
  return inngestHandler(ctx.request);
});

مکمل خریداری کی خصوصیات میں شامل ہیں:

// src/lib/jobs/functions/stripe.ts
import { eq } from "drizzle-orm";
import { createElement } from "react";

import { inngest } from "../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 as {
      userId: string;
      tier: string;
      sessionId: string;
    };

    // 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 in analytics
    await step.run("track-purchase-to-posthog", async () => {
      try {
        await trackServerEvent(userId, "purchase_completed_server", {
          tier,
          amount_cents: purchase.amount,
          currency: purchase.currency,
          stripe_session_id: sessionId,
          stripe_payment_intent_id: purchase.stripePaymentIntentId,
        });
      } catch (error) {
        console.error(`Failed to track to PostHog:`, error);
      }
    });

    // Step 3: Send purchase confirmation to customer
    await step.run("send-purchase-confirmation", async () => {
      await sendEmail({
        to: user.email,
        subject: `Your ${brand.name} 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 template 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 () => {
      const repoUrl = brand.social.github;
      await sendEmail({
        to: user.email,
        subject: `Your ${brand.name} repository access is ready!`,
        template: createElement(RepoAccessGrantedEmail, { repoUrl }),
      });
    });

    // 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 for user and purchase records
    return { user: foundUser, purchase: foundPurchase };
  }
);

یہ مرحلہ صارف اور خریداری کی تفصیلات کے لیے ڈیٹا بیس سے استفسار کرتا ہے۔ تمام بعد کے اقدامات ان اقدار پر منحصر ہیں: صارف کا ای میل، خریداری کی رقم، اور صارف کا GitHub صارف نام۔

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

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

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

await step.run("track-purchase-to-posthog", async () => {
  try {
    await trackServerEvent(userId, "purchase_completed_server", {
      tier,
      amount_cents: purchase.amount,
      currency: purchase.currency,
    });
  } catch (error) {
    console.error(`Failed to track to PostHog:`, error);
  }
});

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

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

مرحلہ 3 اور 4: ای میل اطلاعات

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

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

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

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

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

مرحلہ 5: مصنوعات تک رسائی فراہم کریں۔

if (!user.githubUsername) {
  return { success: true, userId, tier, githubAccessGranted: false };
}

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

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

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

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

مرحلہ 6-7: ٹریک اور اپ ڈیٹ کریں۔

GitHub تک رسائی دینے کے بعد، فنکشن تجزیات (مرحلہ 6) میں واقعات کو ٹریک کرتا ہے اور ڈیٹا بیس (مرحلہ 7) میں خریداری کے ریکارڈ کو اپ ڈیٹ کرتا ہے۔

ڈیٹا بیس اپ ڈیٹس کو جان بوجھ کر GitHub API کالز کے بعد آرڈر کیا جاتا ہے۔ آپ نے ابھی سیٹ کیا githubAccessGranted: true دعوت اصل میں کامیاب ہونے کے بعد۔ اگر آپ پہلے ریکارڈ کو اپ ڈیٹ کرتے ہیں اور GitHub مرحلہ ناکام ہوجاتا ہے، تو ڈیٹا بیس کہے گا کہ رسائی دی گئی ہے اگرچہ یہ نہیں ہے۔

مرحلہ 8: رسائی ای میل بھیجیں۔

await step.run("send-repo-access-email", async () => {
  const repoUrl = brand.social.github;
  await sendEmail({
    to: user.email,
    subject: `Your ${brand.name} repository access is ready!`,
    template: createElement(RepoAccessGrantedEmail, { repoUrl }),
  });
});

یہ ای میل آپ کے GitHub دعوت نامے کی تصدیق کے بعد ہی بھیجی جائے گی۔ ہجے جان بوجھ کر کیا گیا ہے۔ اگر کوئی دعوت نامہ نہیں بھیجا گیا ہے، تو صارفین کو مطلع نہیں کیا جائے گا کہ "رسائی تیار ہے۔”

مرحلہ 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,
      },
    });
  }
});

آخری مرحلہ ایک علیحدہ فنکشن کو متحرک کرتا ہے جو فالو اپ ای میل کی ترتیب کو ہینڈل کرتا ہے: دن 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 }) => {
    await step.sleep("wait-7-days", "7d");
    await step.run("send-day-7-email", async () => {
      // Send onboarding tips
    });

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

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

مرحلہ علیحدگی کے قواعد

کوئی بھی آپریشن جو ایک بیرونی سروس کو کال کرتا ہے یا آزادانہ طور پر ناکام ہو سکتا ہے اس کا اپنا مرحلہ ہونا چاہیے۔ ڈیٹا بیس کے سوالات ایک قدم ہیں کیونکہ ڈیٹا بیس عارضی طور پر دستیاب نہیں ہے۔ ای میل بھیجنا یا API کو کال کرنا ایک قدم ہے کیونکہ وہ خدمات غلطیاں واپس کر سکتی ہیں یا شرح کی حد تک پہنچ سکتی ہیں۔

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

رقم کی واپسی پر کارروائی کیسے کی جاتی ہے۔

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

مکمل ریفنڈ پروسیسر مندرجہ ذیل ہے:

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

    const chargeId = data.chargeId;
    const paymentIntentId = data.paymentIntentId;
    const currency = data.currency;
    const amountRefunded = data.amountRefunded;
    const originalAmount = data.originalAmount;
    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 () => {
      try {
        await trackServerEvent(user.id, "refund_processed", {
          charge_id: chargeId,
          payment_intent_id: paymentIntentId,
          amount_cents: amountRefunded,
          original_amount_cents: originalAmount,
          currency,
          is_full_refund: isFullRefund,
          github_access_revoked: accessRevoked,
        });
      } catch (error) {
        console.error(`Failed to track to PostHog:`, error);
      }
    });

    // Step 5: Notify customer
    await step.run("send-customer-notification", async () => {
      if (isFullRefund) {
        await sendEmail({
          to: user.email,
          subject: `Your ${brand.name} refund has been processed`,
          template: createElement(AccessRevokedEmail, {
            customerEmail: user.email,
            refundAmount: amountRefunded,
            currency,
          }),
        });
      } else {
        await sendEmail({
          to: user.email,
          subject: `Your ${brand.name} 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 processed: ){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 };
  }
);

مکمل اور جزوی رقم کی واپسی کے درمیان فرق

یہ فنکشن ایک سادہ موازنہ کے ذریعے دونوں کے درمیان فرق کرتا ہے۔

const isFullRefund = amountRefunded >= originalAmount;

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

  1. GitHub رسائی منسوخ کر دی گئی ( removeCollaborator کال کریں)۔

  2. خریداری کی حیثیت اس پر سیٹ ہے: "refunded".

  3. گاہک AccessRevokedEmail یہ وضاحت کرتا ہے کہ رسائی کے حقوق کو ہٹا دیا گیا ہے۔

کے لیے جزوی رقم کی واپسی۔گاہک تک رسائی برقرار رکھتا ہے:

  1. GitHub تک رسائی ہے۔ ~ نہیں منسوخ

  2. خریداری کی حیثیت اس پر سیٹ ہے: "partially_refunded".

  3. گاہک PartialRefundEmail آپ کو واپس کی گئی رقم اور اصل رقم نظر آئے گی۔

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

مشروط اقدامات کیسے کام کرتے ہیں۔

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

if (isFullRefund && user.githubUsername && purchase.githubAccessGranted) {
  const revokeResult = await step.run("revoke-github-access", async () => {
    return removeCollaborator(user.githubUsername!);
  });
  accessRevoked = revokeResult.success;
}

اگر ان شرائط میں سے کوئی بھی غلط ہے، تو وہ مرحلہ مکمل طور پر چھوڑ دیا جاتا ہے۔ Ingest صفائی سے یہ ہینڈل کرتا ہے. یہ خصوصیت مرحلہ 3 کے ساتھ جاری رہتی ہے (خریداری کی حیثیت کو اپ ڈیٹ کریں)۔ accessRevoked اب بھی سیٹ اپ false.

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

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

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

// src/lib/jobs/functions/stripe.ts
export const handleCheckoutExpired = inngest.createFunction(
  {
    id: "checkout-expired",
    triggers: [{ event: "stripe/checkout.session.expired" }],
  },
  async ({ event, step }) => {
    const { customerEmail, sessionId } = event.data as {
      customerEmail: string | null;
      sessionId: string;
    };

    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 baseUrl =
        process.env.BETTER_AUTH_URL ?? "https://your-app.com";
      const checkoutUrl = `${baseUrl}/pricing`;

      await sendEmail({
        to: customerEmail,
        subject: `Your ${brand.name} checkout is waiting`,
        template: createElement(AbandonedCartEmail, {
          customerEmail,
          checkoutUrl,
        }),
      });
    });

    // Track the recovery attempt
    await step.run("track-abandoned-cart", async () => {
      try {
        await trackServerEvent("anonymous", "abandoned_cart_email_sent", {
          customer_email: customerEmail,
          session_id: sessionId,
        });
      } catch (error) {
        console.error(`Failed to track to PostHog:`, error);
      }
    });

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

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

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

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

1 گھنٹے کی تاخیر کیوں درست ہے۔

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

24 گھنٹے انتظار کرنا بہت طویل ہے۔ گاہک آگے بڑھ گیا۔ وہ یا تو آپ کے پروڈکٹ کو بھول گئے ہیں یا کوئی متبادل تلاش کر چکے ہیں۔

جانچ کے ذریعے ہمیں جو بہترین وقت ملا ہے وہ 1 گھنٹہ ہے۔ گاہک کا ارادہ ابھی بھی تازہ ہے، اور ای میل دباؤ ڈالنے کے بجائے مددگار محسوس ہوتا ہے۔

آپ کا مائلیج مختلف ہو سکتا ہے۔ تاخیر قابل ترتیب ہیں۔ تبدیلی "1h" کو "30m" یا "3h" اور دوبارہ تقسیم.

یہ کرون جاب سے بہتر کیوں ہے۔

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

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

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

ری ایکٹ ای میل کے ساتھ لین دین کی ای میلز کیسے بھیجیں۔

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

اپنے ای میل کلائنٹ کو کیسے ترتیب دیں۔

ای میل کلائنٹس ایک سادہ سٹرنگ میں دوبارہ بھیجیں لپیٹتے ہیں۔ sendEmail فنکشن:

// src/lib/email/index.ts
import { render } from "@react-email/components";
import type { ReactElement } from "react";
import { Resend } from "resend";

import { brand } from "@/lib/brand";

let resendClient: Resend | null = null;

function getResend(): Resend {
  if (!resendClient) {
    const apiKey = process.env.RESEND_API_KEY;
    if (!apiKey) {
      throw new Error("RESEND_API_KEY is not set");
    }
    resendClient = new Resend(apiKey);
  }
  return resendClient;
}

interface SendEmailOptions {
  to: string | string[];
  subject: string;
  template: ReactElement;
  from?: string;
  replyTo?: string;
}

export async function sendEmail({
  to,
  subject,
  template,
  from = process.env.EMAIL_FROM ?? brand.emails.from,
  replyTo,
}: SendEmailOptions) {
  const resend = getResend();
  const html = await render(template);

  return resend.emails.send({
    from,
    to,
    subject,
    html,
    replyTo,
  });
}

کہ render() فنکشن @react-email/components React عناصر کو HTML سٹرنگز میں تبدیل کریں۔ یہ HTML وہی ہے جو آپ کے گاہک کے ان باکس میں دوبارہ بھیجتا ہے۔

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

خریداری کی تصدیقی ٹیمپلیٹ کو کیسے مکمل کریں۔

یہاں اصل خریداری کی تصدیقی ای میل ٹیمپلیٹ ہے:

// src/lib/email/emails/purchase-confirmation.tsx
import {
  Body,
  Container,
  Head,
  Heading,
  Hr,
  Html,
  Link,
  Preview,
  Section,
  Text,
} from "@react-email/components";

import { brand } from "@/lib/brand";

interface PurchaseConfirmationEmailProps {
  amount: number;
  currency: string;
  customerEmail: string;
}

const colors = {
  primary: "#d97757",
  background: "#faf9f5",
  foreground: "#30302e",
  muted: "#6b6860",
  border: "#e5e4df",
  card: "#ffffff",
  success: "#16a34a",
  successLight: "#f0fdf4",
};

export default function PurchaseConfirmationEmail({
  amount,
  currency,
  customerEmail,
}: PurchaseConfirmationEmailProps) {
  const formattedAmount = new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: currency.toUpperCase(),
  }).format(amount / 100);

  return (
    
      
      Your {brand.name} purchase is confirmed!
      
        
          
{brand.name}

Payment Successful
Thank you for your purchase! Your payment has been processed successfully. We are now setting up your GitHub repository access. You will receive another email shortly with your access link.
Order Details
Product {brand.name}
Amount {formattedAmount}
Email {customerEmail}
This is a one-time purchase. No recurring charges will be made.
Questions about your purchase? Reply to this email or reach out at{" "} {brand.emails.support}
); } PurchaseConfirmationEmail.PreviewProps = { amount: 9900, currency: "usd", customerEmail: "customer@example.com", } satisfies PurchaseConfirmationEmailProps;

اس سانچے کے بارے میں چند باتیں قابل توجہ ہیں:

  • کرنسی کی فارمیٹنگ ٹیمپلیٹ میں ہوتی ہے۔ کہ amount پرپس سینٹ میں ہیں (وہی فارمیٹ جو ڈیٹا بیس میں محفوظ ہے اور پٹی کے ذریعے واپس کیا گیا ہے)۔ کہ Intl.NumberFormat کال اسے "$99.00” جیسی انسانی پڑھنے کے قابل سٹرنگ میں بدل دیتی ہے اور کرنسی کی فارمیٹنگ منطق کو ایک جگہ پر رکھتی ہے۔

  • کہ PreviewProps مقصد ترقی کے لیے ہے۔ ری ایکٹ ای میل براؤزر میں پیش نظارہ پیش کرنے کے لیے ان پروپس کا استعمال کرتا ہے۔ کہ satisfies کلیدی لفظ اس بات کو یقینی بناتا ہے کہ پیش نظارہ پروپ اجزاء کے انٹرفیس سے میل کھاتا ہے۔

  • تمام طرزیں ان لائن آبجیکٹ ہیں۔ ای میل کلائنٹ کی پٹی زیادہ تر سی ایس ایس کو ٹیگ کرتا ہے اور نظر انداز کرتا ہے۔ Gmail، Outlook، Apple Mail، اور دیگر تمام کلائنٹس میں ای میل کو سٹائل کرنے کا ان لائن طرزیں واحد قابل اعتماد طریقہ ہیں۔

ریپو ایکسیس ٹیمپلیٹ کیسے بنایا جائے۔

آپ کے GitHub دعوت نامے کے کامیاب ہونے کے بعد، آپ کو ریپوزٹری تک رسائی کا ای میل موصول ہوگا۔

// src/lib/email/emails/repo-access-granted.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Hr,
  Html,
  Link,
  Preview,
  Section,
  Text,
} from "@react-email/components";

import { brand } from "@/lib/brand";

interface RepoAccessGrantedEmailProps {
  repoUrl: string;
}

export default function RepoAccessGrantedEmail({
  repoUrl,
}: RepoAccessGrantedEmailProps) {
  return (
    
      
      Your {brand.name} repository access is ready!
      
        
          
{brand.name}

You are in! Your GitHub repository access has been granted. You now have full access to the {brand.name} codebase.
Quick Start 1. Clone the repository to your machine 2. Run{" "} bun install to install dependencies 3. Follow the README for environment setup 4. Run{" "} bun dev to start building

Need help? Reply to this email or reach out at{" "} {brand.emails.support}
); }

اس سانچے میں If you ran into any issues during checkout or have questions about {brand.name}, just reply to this email. I read every message personally.


This email was sent to {customerEmail} because you started a checkout on {brand.name}. If this was not you, you can safely ignore this email. ); }

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

ٹیمپلیٹس مستقل اقدامات کے ساتھ کیسے ضم ہوتے ہیں۔

تمام ای میل ٹیمپلیٹس کو بذریعہ بلایا جاتا ہے: createElement میں step.run() بلاک:

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

کہ createElement کال دیے گئے پرپس کا استعمال کرتے ہوئے ٹیمپلیٹ کے جزو سے ایک React عنصر بناتی ہے۔ کہ sendEmail فنکشن ری ایکٹ ای میل کے ذریعے HTML کو رینڈر کرتا ہے۔ render() ری ٹرانسمیشن کے ذریعے بھیجیں۔

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

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

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

مرحلہ 1: پٹی CLI شروع کریں۔

اسٹرائپ CLI انسٹال کریں اور لاگ ان کریں۔

# macOS
brew install stripe/stripe-cli/stripe

# Authenticate
stripe login

ویب ہک ایونٹس کو مقامی سرور پر فارورڈ کرتا ہے۔

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

CLI اس سے شروع ہونے والے ویب ہک پر دستخط کرنے کے راز کو پرنٹ کرتا ہے: whsec_. یہ آپ کا ہے۔ .env پسند STRIPE_WEBHOOK_SECRET.

مرحلہ 2: Ingest Dev سرور شروع کریں۔

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

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

کھلا http://localhost:8288 آپ کے براؤزر میں۔ یہ ایک انجسٹ ڈیش بورڈ ہے جہاں آپ قدم بہ قدم پائیداری کی خصوصیات دیکھ سکتے ہیں۔

مرحلہ 3: درخواست شروع کریں۔

bun run dev

آپ کی درخواست کو اب اس پر چلنا چاہئے: http://localhost:3000.

مرحلہ 4: اپنی خریداری کے بہاؤ کی جانچ کریں۔

  1. قیمتوں کے صفحہ پر جائیں اور ادائیگی کے بٹن پر کلک کریں۔

  2. پٹی کا ٹیسٹ کارڈ نمبر استعمال کریں۔ 4242 4242 4242 4242 مستقبل کی میعاد ختم ہونے کی تاریخ اور CVC شامل ہے۔

  3. اپنی ادائیگی مکمل کریں۔ پٹی آپ کو کامیابی کے URL پر بھیجتی ہے۔

  4. آپ کا فرنٹ اینڈ ہے /api/purchases/claim سیشن ID کے ساتھ اختتامی نقطہ۔

  5. Ingest ڈیش بورڈ دیکھیں۔ تم ہو purchase-completed فنکشن ٹرگر اور ہر قدم کو ترتیب سے عمل میں لایا جاتا ہے۔

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

  • مرحلہ 1: صارف اور خریداری کے ڈیٹا کے ساتھ "لُک اپ یوزر اور پرچیز” مکمل ہے۔

  • مرحلہ 2: "Track-purchase-to-posthog” مکمل ہوتا ہے (یا اگر PostHog کو کنفیگر نہیں کیا جاتا ہے تو ایک وارننگ لاگ کرتا ہے)۔

  • مرحلہ 3: "بھیجیں-خریداری-تصدیق” مکمل ہو گیا ہے۔ براہ کرم اپنا ای میل چیک کریں۔

  • مرحلہ 4: "ایڈمنسٹریٹر کی اطلاع بھیجیں” مکمل ہے (اگر ADMIN_EMAIL سیٹ)۔

  • مرحلہ 5 سے 9: چلائیں اگر صارف کا GitHub صارف نام اس کے ساتھ وابستہ ہے۔

مرحلہ 5: اپنی رقم کی واپسی کی جانچ کریں۔

اسٹرائپ CLI کے ذریعے ریفنڈز کو متحرک کریں۔

stripe trigger charge.refunded

متبادل طور پر، اپنے اسٹرائپ ڈیش بورڈ پر جائیں، جانچ کی ادائیگی تلاش کریں، اور دستی طور پر رقم کی واپسی جاری کریں۔ پٹی CLI فراہم کرتا ہے: charge.refunded مقامی سرور پر ویب ہک۔

Ingest ڈیش بورڈ دکھاتا ہے: refund-processed ایک فنکشن ٹرگر جس کے اپنے مراحل کے سیٹ ہیں، جیسے استفسار، مشروط رسائی کی منسوخی، اسٹیٹس اپ ڈیٹ، اینالیٹکس ٹریکنگ، ای میل اطلاع وغیرہ۔

مرحلہ 6: چھوڑی ہوئی ٹوکری کی بازیابی کی جانچ کریں۔

ادائیگی کی میعاد ختم ہونے کو متحرک کریں۔

stripe trigger checkout.session.expired

کہ checkout-expired یہ خصوصیت Ingest ڈیش بورڈ میں ظاہر ہوتی ہے۔ 1 گھنٹے کی نیند کے مراحل دکھائے جاتے ہیں۔ ڈیولپمنٹ سرور پر، آپ ڈیش بورڈ میں "اسکیپ” بٹن پر کلک کرکے سلیپ موڈ کو تیزی سے آگے بڑھا سکتے ہیں۔ یہ آپ کو ایک گھنٹہ انتظار کیے بغیر تاخیری ای میلز کی جانچ کرنے کی اجازت دیتا ہے۔

مرحلہ وار ناکامی کی نقل کیسے کریں۔

دوبارہ کوشش کرنے کے رویے کو جانچنے کے لیے، عارضی طور پر درج ذیل مراحل میں سے کسی ایک میں غلطی پیدا کریں:

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

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

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

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

  • فیز 6-9 ابھی باقی ہیں۔

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

نتیجہ

مکمل SaaS ادائیگی کے بہاؤ کی تعمیر اسٹرائپ چیک آؤٹ کو مربوط کرنے سے بالاتر ہے۔ "خریدیں” بٹن سے لے کر "خوش آمدید” ای میل تک کا پورا لائف سائیکل، بشمول کچھ غلط ہونے پر کیا ہوتا ہے۔

اس ٹیوٹوریل میں ہم نے جو بنایا ہے وہ یہ ہے:

  • کوئی راستہ نہیں ڈیٹا بیس سکیما مکمل، جزوی رقم کی واپسی، اور مکمل رقم کی واپسی سمیت تمام حالتوں میں خریداریوں کو ٹریک کریں۔

  • کوئی راستہ نہیں پٹی کی مصنوعات اور قیمت کے بیج سکرپٹ پروگرام کے مطابق کیٹلاگ بنائیں۔

  • کوئی راستہ نہیں ادائیگی کا بہاؤ سیشن کی تخلیق، ادائیگی کی تصدیق، اور غیرمعمولی خریداری کی بلنگ شامل ہے۔

  • کوئی راستہ نہیں پتلا ویب ہک ہینڈلر دستخطوں کی تصدیق کرتا ہے اور واقعات کو پس منظر کے کاموں تک لے جاتا ہے۔

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

  • کوئی راستہ نہیں رقم کی واپسی پروسیسر ہم مکمل اور جزوی رقم کی واپسی کے درمیان فرق کرتے ہیں اور صرف مناسب ہونے پر رسائی منسوخ کرتے ہیں۔

  • نہیں ترک شدہ کارٹ ریکوری فلو براہ کرم اپنا دوستانہ ریکوری ای میل بھیجنے سے پہلے ایک گھنٹہ انتظار کریں۔

  • تین ٹرانزیکشنل ای میل ٹیمپلیٹس ری ایکٹ ای میل کے ساتھ بنایا گیا: خریداریوں کی تصدیق کریں، مخزن تک رسائی فراہم کریں، کارٹ کو ترک کریں۔

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

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

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

آپ کی ضروریات مختلف ہو سکتی ہیں۔ آپ ایک بار کی خریداری کے بجائے سبسکرپشنز بیچ سکتے ہیں، یا GitHub رسائی کے بجائے API کیز کی فراہمی کر سکتے ہیں۔ مخصوص مراحل بدل جاتے ہیں، لیکن فن تعمیر وہی رہتا ہے۔

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

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

Scroll to Top