محرك الاشتراكات في lahsab هو Stripe حيث collectionMethod يساوي دائما send_invoice: تصدر فاتورة مؤرخة، وتوجه تذكيرات، ويدفع الزبون بتحويل CCP أو بريدي موب متى شاء. لا توجد بطاقة يخصم منها، فلا شيء يقتطع من تلقاء نفسه، ومن هذا المبدأ تنبع الآلية كلها.

القاعدة الذهبية

يستطيع Stripe بالبطاقة أن يقدم الساعة عند التجديد مباشرة، لأن لديه بطاقة يخصم منها: الدفع هناك مفترض. أما lahsab فلا يملك ما يخصم منه. تقديم الفترة على فاتورة صدرت ولم تدفع يعني منح الوصول مقابل وعد، وإيهامك بأن الزبون في وضعية سليمة بينما المال لم يصل قط.

لذلك لا تتحرك الساعة أبدا على وعد. تتحرك على دفعة.

عمليا، يختزل كود الصلاحيات عندك في مقارنة واحدة:

const aAcces = user.paidUntil && user.paidUntil > new Date();

حيث paidUntil هو قيمة currentPeriodEnd التي سجلتها عند استقبال invoice.paid.

من يملك ماذا

هذه هي الحدود التي يجب استيعابها قبل كتابة أول سطر.

ما يملكه lahsabما تملكه أنت
ماذاالمال والوقتالصلاحيات (entitlement)
أيمن يدين بماذا، عن أي فترة، هل دفع، ومنذ متى تأخر.ما تفتحه كل خطة، والحصص، والحدود، والأدوار.

lahsab لا يعرف ما تفتحه خطتك «Pro»: لا عدد المشاريع المسموح به ولا الميزات التي تتيحها. تماما مثل Stripe. نقول لك «مدفوع إلى غاية 13 سبتمبر»، وما تفعله بذلك شأنك أنت.

النموذج

خمسة كائنات مستعارة من Stripe: من يعرف أحدهما يعرف الآخر.

الكائنالدور
Customerزبونك. externalId هو معرفه عندك (فريد لكل تاجر وبيئة). phone هو قناة التذكير.
Productما تبيعه. «اشتراك Pro» مثلا.
Priceتسعيرة لمنتج: المبلغ، one_time أو recurring، والوتيرة. غير قابل للتعديل.
Subscriptionالتزام زبون بسعر متكرر. يحمل currentPeriodEnd.
Invoiceما هو مستحق، عن فترة، بتاريخ استحقاق. هذا هو الكائن الذي يدفع.

الاشتراك بنداء واحد

POST /subscriptions هو نقطة الدخول لمسار الشراء. في نداء واحد ينشئ الاشتراك، ويصدر الفاتورة الأولى فورا، ويرجع لك الرابط الذي توجه إليه المشتري. إنه Stripe Checkout الخاص بك.

  1. حدد الزبون. ابحث عنه بمعرفك الخاص (GET /customers?externalId=)، وأنشئه إن لم يوجد. هذا هو نمط lookup / upsert.

  2. أنشئ الاشتراك. نداء POST /subscriptions مع customerId و priceId. يولد الاشتراك بحالة incomplete وتصدر الفاتورة الأولى.

  3. وجه المشتري إلى latestInvoice.hostedInvoiceUrl، صفحة الفاتورة المستضافة لدى lahsab، حيث يختار وسيلة الدفع، ويجد بيانات الدفع الموافقة لها، ويودع وصل الدفع.

  4. انتظر invoice.paid. هناك، وهناك فقط، يمنح الوصول: يتحول الاشتراك إلى active ويتقدم currentPeriodEnd.

تحديد الزبون
# 1. Le client existe-t-il déjà ?
curl "https://api.lahsab.com/customers?externalId=user_abc123" \
  -H "x-api-key: lsk_live_CLE"

# 2. Sinon, on le crée.
curl -X POST https://api.lahsab.com/customers \
  -H "x-api-key: lsk_live_CLE" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: cust-user_abc123" \
  -d '{
    "externalId": "user_abc123",
    "name": "Yacine B.",
    "phone": "0770000000",
    "defaultMethod": "baridimob"
  }'
الاشتراك
curl -X POST https://api.lahsab.com/subscriptions \
  -H "x-api-key: lsk_live_CLE" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: sub-user_abc123-pro-mensuel" \
  -d '{
    "customerId": "c0ffee00-1111-4222-8333-444455556666",
    "priceId": "9f8e7d6c-5b4a-4321-9876-543210fedcba",
    "daysUntilDue": 7
  }'

الرد 201: الاشتراك مع فاتورته الأولى الصادرة سلفا:

{
  "id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
  "environment": "live",
  "status": "incomplete",
  "collectionMethod": "send_invoice",
  "daysUntilDue": 7,
  "invoiceLeadDays": 7,
  "currentPeriodStart": "2026-07-13T00:00:00.000Z",
  "currentPeriodEnd": "2026-08-13T00:00:00.000Z",
  "cancelAtPeriodEnd": false,
  "latestInvoice": {
    "id": "f1e2d3c4-b5a6-4978-8b1c-2d3e4f5a6b7c",
    "number": "LSB-2026-0042",
    "status": "open",
    "amountDue": 2500,
    "dueDate": "2026-07-20T00:00:00.000Z",
    "hostedInvoiceUrl": "https://dashboard.lahsab.com/invoice/f1e2d3c4-b5a6-4978-8b1c-2d3e4f5a6b7c"
  },
  "createdAt": "2026-07-13T10:00:00.000Z"
}

الحالات

الفاتورة

draftopen paidvoiduncollectible

draft حالة عابرة: الفاتورة تعتمد (فترقم) مباشرة بعد إنشائها. paid و void و uncollectible حالات نهائية.

الحالةالمعنى
draftأنشئت، لم ترقم بعد وليست قابلة للدفع.
openمعتمدة، مرقمة، قابلة للدفع.
paidدفعت. يصدر عندها حدث invoice.paid.
voidألغيتها أنت.
uncollectibleاستنفدت فترة السماح: لم تعد تتوقع تحصيلها.

الاشتراك

incompletetrialing activepast_due unpaidcanceled

يدخل الاشتراك من incomplete أو trialing، ويخرج من unpaid أو canceled.

الحالةالمعنى
incompleteصدرت الفاتورة الأولى ولم تدفع قط. لم يمنح أي وصول إطلاقا.
trialingتجربة جارية. currentPeriodEnd يساوي trialEnd.
activeمدفوع إلى غاية currentPeriodEnd.
past_dueانقضى تاريخ الاستحقاق وفاتورة التجديد ما تزال open.
unpaidاستنفدت فترة السماح والفاتورة غير قابلة للتحصيل.
canceledانتهى. قيمة endedAt مسجلة.

الترقيم

عند الاعتماد، تتلقى كل فاتورة رقما: LSB-{ANNÉE}-{séquence}.

LSB-2026-0001
LSB-2026-0002
LSB-2026-0003

التسلسل بلا فجوات، وخاص بكل ثلاثية (تاجر، بيئة، سنة). ويعود إلى 0001 في 1 جانفي.

المتأخرات

lahsab لا يذكّر زبونك مكانك. عندما ينقضي تاريخ الاستحقاق وتبقى الفاتورة open، يصدر حدث invoice.overdue: يتضمن payload الحدث الاسم والهاتف والمبلغ وتاريخ الاستحقاق ورابط الدفع، وهو ما يكفي لصياغة تذكيرك الخاص (رسالة أو مكالمة) دون نداء API إضافي. بعد ذلك، يتحكم إعدادان للتاجر، ضمن إعدادات لوحة التحكم، في المسار الموالي:

الإعدادالقيمة الافتراضيةالدور
gracePeriodDays7المهلة الممنوحة للزبون للدفع بعد تاريخ الاستحقاق.
dunningExhaustedBehaviorcancelمصير الاشتراك عند نهاية فترة السماح.

عند نهاية فترة السماح، تتحول الفاتورة إلى uncollectible، ويصدر lahsab حدث invoice.marked_uncollectible، ويتبع الاشتراك الإعداد:

dunningExhaustedBehaviorعند نهاية فترة السماح
cancelيتحول الاشتراك إلى canceled.
mark_unpaidيتحول إلى unpaid، وتصبح الفاتورة uncollectible.
leave_activeلا نمس شيئا، والتصرف بيدك.

بوابة الزبون

يرجع POST /portal-sessions عنوان URL قصير الصلاحية (ساعة واحدة) يجد فيه زبونك اشتراكه وفواتيره وزر الدفع. إنها صفحة «تسيير اشتراكي» التي لن تحتاج إلى بنائها.

{
  "url": "https://dashboard.lahsab.com/portal/ps_7f3c2e1a9b4d4e6a8c1f2d3e",
  "expiresAt": "2026-07-13T11:00:00.000Z"
}

وماذا بعد؟

  • ربط invoice.paid والتحقق من التوقيعات: Webhooks.
  • كل نقاط النهاية والحقول والأخطاء: مرجع API.