نموذج التسعير الذي تختاره لمنتج SaaS ليس مجرد قرار تجاري. إنه يحدد كيفية تخزين بيانات الاشتراك، وكيفية تفعيل الميزات في الوقت الفعلي، وكيفية إحصاء الاستخدام وتقييده، وكيفية التعامل مع حالات الدفع خارج بوابات الدفع الرقمية في لبنان والمنطقة.
نموذج التسعير الذي تختاره لمنتج SaaS ليس قراراً تجارياً فقط. إنه يحدد كيفية تخزين بيانات الاشتراك، وكيفية تفعيل الميزات في الوقت الفعلي، وكيفية إحصاء الاستخدام وتقييده، وكيفية التعامل مع الحالات الطرفية بعد أول 100 مستأجر. معظم الفرق تعامل التسعير كمشكلة إعداد وتكتشف أنه في الواقع مشكلة نمذجة بيانات عند محاولة إضافة مستوى تسعير ثالث.
نموذج البيانات الذي يتعامل مع تعقيد المستويات
أول إصدار من منطق التسعير في معظم خلفيات SaaS يخزن الخطة كحقل نصي في صف المستأجر: plan = "starter" | "growth" | "enterprise". هذا يعمل حتى تحتاج معرفة ما تتضمنه كل خطة، أو ما الذي يتغير عند الترقية، أو ما يحدث عند انتهاء فترة التجربة.
النموذج الذي يتعامل مع التعقيد بشكل نظيف يفصل ثلاثة مفاهيم: تعريف الخطة، والاشتراك، وسجلات الاستخدام:
CREATE TABLE plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
price_cents INTEGER NOT NULL,
currency TEXT NOT NULL DEFAULT 'USD',
interval TEXT NOT NULL DEFAULT 'month',
features JSONB NOT NULL DEFAULT '{}',
limits JSONB NOT NULL DEFAULT '{}',
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
plan_id UUID NOT NULL REFERENCES plans(id),
status TEXT NOT NULL DEFAULT 'active',
current_period_start TIMESTAMPTZ NOT NULL,
current_period_end TIMESTAMPTZ NOT NULL,
stripe_subscription_id TEXT
);
حقل features هو JSONB يحتوي على خريطة أعلام منطقية، وحقل limits يحتوي على حدود رقمية. هذا يتيح تعريف خطط جديدة وتغيير الحدود وإضافة ميزات دون أي تغييرات في الكود.
تفعيل الميزات في Go
فحوصات الميزات يجب أن تكون سريعة. الوسيط أو المعالج الذي يضرب PostgreSQL في كل طلب للتحقق من خطة المستأجر يضيف 5 إلى 15ms لكل استجابة. النمط الذي يعمل: تخزين بيانات الاشتراك في Redis لكل مستأجر وإبطالها عند تغيير الاشتراك.
func (s *SubscriptionService) HasFeature(ctx context.Context, tenantID uuid.UUID, feature string) (bool, error) {
sub, err := s.getCachedSubscription(ctx, tenantID)
if err != nil {
return false, err
}
if sub.Status != "active" && sub.Status != "trialing" {
return false, nil
}
return sub.Features[feature], nil
}
مفتاح التخزين المؤقت هو sub:{tenant_id} مع TTL خمس دقائق. معالج webhook من Stripe يُبطله فوراً عند تغيير الاشتراك.
حدود الاستخدام والتطبيق في الوقت الفعلي
للحدود التي تزداد مع كل عملية كاستدعاءات API الشهرية والمعاملات اليومية، استخدم عدادات Redis:
func (s *UsageService) RecordAndCheckAPICall(ctx context.Context, tenantID uuid.UUID) (bool, error) {
key := fmt.Sprintf("usage:%s:api_calls:%s", tenantID, currentMonth())
pipe := s.redis.Pipeline()
incrCmd := pipe.Incr(ctx, key)
pipe.Expire(ctx, key, 35*24*time.Hour)
_, err := pipe.Exec(ctx)
if err != nil {
return true, nil // الفشل المفتوح لتجنب الحجب عند أخطاء Redis
}
count := incrCmd.Val()
sub, _ := s.getCachedSubscription(ctx, tenantID)
limit := int64(sub.Limits["monthly_api_calls"])
return count <= limit, nil
}
نمط الفشل المفتوح (إرجاع true عند أخطاء Redis) مقصود للحدود غير الحرجة. حجب كل استدعاءات API لأن Redis غير متاح مؤقتاً أسوأ من تجاوز حد استخدام بشكل مؤقت.
التعامل مع واقع الدفع في لبنان والمنطقة
Stripe يتعامل مع فوترة الاشتراكات تلقائياً لمدفوعات البطاقات. في لبنان وأجزاء من الخليج، نسبة ملموسة من العملاء يدفعون عبر تحويل بنكي أو نقداً. هذا يعني أن نظام الاشتراكات يحتاج مساراً يدوياً للتفعيل جانباً مع المسار الآلي عبر Stripe.
حالة اشتراك "pending_payment" تتيح لفريق العمليات تفعيل الحسابات بعد تأكيد المدفوعات غير الإلكترونية. منطق تفعيل الميزات يتحقق من "active" و"trialing" لكن ليس من "pending_payment". هذا يمنع الوصول دون الحاجة إلى تكامل Stripe المعقد لكل طريقة دفع.
لوحة عمليات للإدارة اليدوية لهذه الاشتراكات، تُظهر الاشتراكات المعلقة والمبلغ المستحق وزر تفعيل بنقرة واحدة، هي الفرق بين SaaS يمكنه البيع في لبنان وواحد لا يمكنه.
انتهاء التجربة المجانية
تجارب مجانية تحتاج آلية انتهاء متسقة. النمط الذي يعمل: مهمة cron تعمل كل ساعة، وتجد الاشتراكات حيث status = 'trialing' وtrial_end < NOW()، وتنقلها إلى status = 'trial_expired'.
مهمة الانتهاء هذه يجب أن تكون غير قابلة للتلف: تشغيلها مرتين ينتج نفس النتيجة. هذا مهم عندما تعمل المهمة مرتين بسبب تداخل في النشر.
دروس من الإنتاج
مستويات التسعير مشكلة نمذجة بيانات تتنكر في هيئة مشكلة إعداد. النموذج الذي يتوسع: خطط معرّفة في قاعدة البيانات مع ميزات وحدود JSONB، واشتراكات تحمل الحالة وبيانات الفترة، وتخزين Redis مؤقت لفحوصات الميزات، وعدادات Redis للحدود عالية التكرار، ومسار تفعيل يدوي لأسواق الدفع غير الإلكتروني كلبنان.
الفرق بين النموذج المُنفَّذ بشكل صحيح من البداية والآخر هو الفرق بين إضافة مستوى تسعير رابع بسهولة وإعادة كتابة عشرات مسارات الكود المختلفة.
هل تبني منتج SaaS في لبنان أو المنطقة؟
فريق Voxire يبني منصات SaaS بـ Go وPostgreSQL للشركات في لبنان والمنطقة، بما في ذلك منطق التسعير وإدارة الاشتراكات وتكاملات بوابات الدفع.
https://voxire.com/get-a-quote/



