Get a quote

أتمتة دورة حياة الاشتراكات في منصات SaaS

إدارة الاشتراكات يدوياً قد تنجح مع عشرة عملاء، لكنها تنهار عند المئة. أتمتة دورة حياة الاشتراك من التجربة المجانية إلى الإلغاء والتجديد ليست ميزة إضافية، بل الأساس الذي يُحدد ما إذا كانت منصة SaaS ستُوسَّع فعلاً.

حين يكسر النمو الإدارة اليدوية

تخيّل سيناريو مألوف: منصة SaaS تبدأ بعشرين عميلاً. يدفع بعضهم في الوقت المحدد، ويتأخر بعضهم، ويطلب آخرون الترقية أو التخفيض أو الإلغاء. يتابع أحد أعضاء الفريق هذا يدوياً: يرسل رسائل تذكير، يُعدِّل الفواتير، يُوقِّف الحسابات المتأخرة.

ثم يصبح العملاء مئتين. ما كان يستغرق ساعة أسبوعياً يصبح يوماً كاملاً. تبدأ الأخطاء: عميل يُوقَّف حسابه رغم أنه دفع، وآخر يواصل استخدام الخدمة رغم أن دفعته فشلت منذ أسبوعين. هذا ليس مشكلة توظيف، بل مشكلة بنية.

أتمتة الأعمال في منصات SaaS تبدأ بنقطة واحدة: آلة حالات الاشتراك.

تصميم آلة الحالات لدورة حياة الاشتراك

آلة الحالات هي التعريف الرسمي لكل حالة يمكن أن يكون عليها الاشتراك، وكل الانتقالات المسموح بها بين الحالات. بدونها، ينتهي بك الأمر بكود مليء بشروط if/else متشابكة ومجال واسع للأخطاء.

الحالات الأساسية لدورة حياة اشتراك SaaS:

trial -> active (عند إضافة طريقة دفع ناجحة)
trial -> canceled (عند انتهاء التجربة دون تحويل)
active -> past_due (عند فشل عملية الدفع)
active -> canceled (عند طلب الإلغاء)
active -> paused (عند طلب الإيقاف المؤقت)
past_due -> active (عند نجاح إعادة المحاولة)
past_due -> canceled (عند انتهاء فترة السماح)
paused -> active (عند استئناف الاشتراك)
canceled -> active (عند إعادة الاشتراك)

تطبيق هذا في الكود:

type SubscriptionStatus =
  | 'trial'
  | 'active'
  | 'past_due'
  | 'paused'
  | 'canceled';

const ALLOWED_TRANSITIONS: Record<
  SubscriptionStatus,
  SubscriptionStatus[]
> = {
  trial: ['active', 'canceled'],
  active: ['past_due', 'canceled', 'paused'],
  past_due: ['active', 'canceled'],
  paused: ['active', 'canceled'],
  canceled: ['active'],
};

function transition(
  current: SubscriptionStatus,
  next: SubscriptionStatus
): void {
  if (!ALLOWED_TRANSITIONS[current].includes(next)) {
    throw new Error(
      `Transition not allowed: ${current} -> ${next}`
    );
  }
  // تنفيذ الانتقال وتسجيله
}

كل انتقال يُسجَّل في جدول تاريخ مستقل مع توقيت الحدث وسببه. هذا السجل ثمين للغاية عند تشخيص المشاكل أو الرد على شكاوى العملاء.

معالجة أحداث Webhook من مزود الدفع

حين يفشل دفع أو ينجح، يُرسِل مزود الدفع حدث Webhook إلى خادمك. قواعد أساسية لمعالجة Webhooks بأمان:

التحقق من التوقيع أولاً. كل حدث يجب التحقق من أنه صادر فعلاً عن مزود الدفع وليس طلباً مزوراً:

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSig)
  );
}

اجعل المعالجة idempotent. قد يُرسَل الحدث ذاته أكثر من مرة:

async function handlePaymentEvent(event: PaymentEvent) {
  const already = await db.processedEvents.findUnique({
    where: { eventId: event.id }
  });
  if (already) return;

  await db.$transaction(async (tx) => {
    await tx.processedEvents.create({ data: { eventId: event.id } });
    await processEvent(tx, event);
  });
}

أعد الاستجابة 200 فوراً ثم اعمل في الخلفية. مزود الدفع يتوقع استجابة سريعة.

منطق Dunning: ماذا يحدث حين يفشل الدفع

Dunning هو النظام الذي يُدير إعادة محاولات الدفع الفاشلة. جدول إعادة المحاولات الموصى به:

| اليوم | الإجراء | |-------|----------| | يوم 0 | الدفع يفشل، تُرسَل رسالة إشعار للعميل، ينتقل الاشتراك إلى past_due | | يوم 3 | إعادة محاولة تلقائية | | يوم 5 | رسالة تذكير بضرورة تحديث بيانات الدفع | | يوم 7 | إعادة محاولة تلقائية | | يوم 10 | تحذير بقرب انتهاء فترة السماح | | يوم 14 | إعادة محاولة أخيرة، يُلغى الاشتراك إن فشلت |

فترة السماح مهمة لأن كثيراً من حالات فشل الدفع تقنية وليست رفضاً من العميل: بطاقة منتهية الصلاحية، رصيد مؤقتاً غير كافٍ، مشكلة مصرفية. إعطاء العميل 14 يوماً مع رسائل واضحة يُستردّ كثير من هذه الحالات تلقائياً.

لا توقف الخدمة فوراً. إيقاف خدمة عميل يدفع عادة بسبب خطأ تقني مؤقت يضر بالثقة ويخلق ضغطاً على فريق الدعم.

حساب النسبة المئوية للترقية والتخفيض

حين يُغيِّر عميل خطته في منتصف دورة الفوترة، يجب حساب الفرق بدقة:

function calculateProration(
  currentPlanPrice: number,
  newPlanPrice: number,
  daysRemaining: number,
  totalDaysInCycle: number
): number {
  const dailyCurrentRate = currentPlanPrice / totalDaysInCycle;
  const dailyNewRate = newPlanPrice / totalDaysInCycle;
  const unusedCredit = dailyCurrentRate * daysRemaining;
  const newCharge = dailyNewRate * daysRemaining;
  return newCharge - unusedCredit;
}

في حالة الترقية: يُشحَن الفرق فوراً لتفعيل الميزات الإضافية. في حالة التخفيض: يُضاف الرصيد إلى الدورة القادمة. سجِّل كل عملية حساب نسبي بتفاصيلها في قاعدة البيانات.

اختبار سيناريوهات الاشتراك

السيناريوهات الإلزامية في الاختبار:

  • ترقية في آخر يوم من دورة الفوترة
  • تخفيض في أول يوم من دورة الفوترة الجديدة
  • إلغاء ثم إعادة اشتراك خلال فترة السماح
  • فشل الدفع عند تاريخ التجديد تحديداً
  • تغيير الخطة بعد يوم واحد من التجديد
  • عميل يُلغي ثم يشترك بخطة مختلفة

دروس من سياق MENA

معدلات فشل الدفع في منطقة الشرق الأوسط وشمال أفريقيا أعلى مما هو عليه في أسواق أخرى لأسباب هيكلية: البطاقات المسبقة الدفع شائعة، والبنية التحتية المصرفية لها مشاكل موثوقية. صمِّم منطق Dunning ليكون صبوراً وتواصلياً بدلاً من عقابياً فورياً.

ادعم طرق الدفع البديلة. الدفع النقدي وشبكات الكيوسك والمحافظ الرقمية المحلية تستخدمها شريحة واسعة لا تملك وصولاً موثوقاً لبطاقات الدفع.

ابنِ نظام اشتراكك بدقة هندسية

أتمتة دورة حياة الاشتراك في منصات SaaS هي بنية تحتية، لا ميزة. كلما أُرجئ بناؤها، زاد الدين التقني واتسعت الفجوات في الإيراد. في فوكساير، نبني هذه الأنظمة لمنصات SaaS في لبنان والمنطقة، بمعرفة عميقة بمتطلبات السوق المحلية.

https://voxire.com/get-a-quote/

العودة إلى المدونة
Chat on WhatsApp