التوثيق
الدمججلسة الدفع

بنود الفاتورة والتسعير

إزاي بتشتغل `lineItems` على جلسة الدفع: الأسعار الموجودة مقابل `priceData` المضمَّن، والكمية الثابتة والقابلة للتعديل، وبنود المبلغ المخصّص، والعملة، وبوابة الإتاحة.

مصفوفة lineItems في POST /checkout/sessions بتسرد اللي العميل بيدفع مقابله. كل بند بيحمل مرجع سعر، وكمية، وقواعد اختيارية تسمح للعميل يعدّل الكمية وقت الدفع. الجلسة بتجمّعهم في الإجماليات اللي بتظهر على الصفحة المستضافة (amountSubtotal، amountTotal)، ونفس شكل البند ده هو اللي بيرجع في رد جلسة الدفع.

حاجتين بيحدّدوا كل بند فاتورة: أنهي PriceAPI بيشير ليه، وكام واحد من السعر ده العميل هيتخصم عليه. وكل حاجة تانية (العملة، والإتاحة، وسعر صرف العرض) بتطلع من السعر.

الأسعار الموجودة مقابل priceData المضمَّن

البند بيشير لسعر بإحدى طريقتين. قدّم واحدة بالظبط. إرسال الاتنين، أو ولا واحدة، بيتم رفضه.

الحقلإمتى تستخدمه
priceالـ price_* ID بتاع الـ PriceAPI. استخدمه لما الـ SKU موجود في الكتالوج بتاعك.
priceDataمنتج وسعر مؤقت بيتعملوا في اللحظة للجلسة دي. استخدمه للفواتير لمرة واحدة، والطلبات المخصّصة، وصفحات التبرّع.
// Catalog reference
{ "price": "price_test_AbC123", "quantity": 1 }

// Inline ad-hoc price
{
  "priceData": {
    "currency": "EGP",
    "unitAmount": 149900,
    "productData": { "name": "Custom order #4231" }
  },
  "quantity": 1
}

الأسعار المضمَّنة بتتخزّن كصفوف لمرة واحدة مربوطة بالجلسة. مش بتدخل كتالوج المنتجات بتاعك ومش بتظهر في قوايم منتجات لوحة التحكم. لو نفس العنصر ظهر في جلسات كتير، اعمل منتج وسعر حقيقيين مرة واحدة وأشِر ليهم بالـ ID. هتلاقي تتبّع للمخزون، وتحليلات، وإنشاء جلسة أسرع.

priceData.unitAmount بالوحدات الصغرى. 149900 معناه 1,499.00 EGP. شكل priceData الكامل:

الحقلمطلوبملاحظات
currencyنعمISO 4217. لازم يطابق العملة اللي بتظهر للعميل في الجلسة (شوف العملة تحت).
unitAmountنعممبلغ الوحدة بالوحدات الصغرى. بيتضرب في quantity علشان يطلع إجمالي البند.
productData.nameنعمالاسم المعروض في ملخص الطلب على الصفحة المستضافة.
productData.descriptionلاسطر فرعي بيظهر تحت اسم المنتج.
productData.imageلارابط صورة عام. بيتجاب ويتخزّن في السيرفر.
productData.unitLabelلاتسمية مخصّصة لعروض "كمية 2" (زي "مقعد"، "ترخيص").
productData.metadataلاحقيبة مفتاح-قيمة بتاعتك إنت، مبهمة بالنسبة لـ XPay.

الكمية والكمية القابلة للتعديل

دور بند الفاتورة بيتحدّد من حقلين: quantity وadjustableQuantity. مفيش حقل نوع منفصل؛ التوليفة بينهم هي اللي بتقرّر هل العميل لازم يشتري البند، ولا يقدر يشيله، ولا يبدأ من غيره أصلًا.

الدورquantity المبدئيةadjustableQuantityاللي العميل بيشوفه
مطلوب1 أو أكترمقفولة، أو مفتوحة بـ minimum 1 أو أكتربند ثابت مش بيقدر يشيله. العدّاد بيظهر بس لما الكمية تكون قابلة للتعديل.
قابل للإزالة1 أو أكترمفتوحة، minimum 0بيبدأ جوّه الطلب. العميل يقدر ينزّله لـ 0 علشان يشيله.
إضافة اختيارية0مفتوحة، minimum 0بيبدأ بره الطلب، معروض تحت "أضِف لطلبك" مع زرار إضافة.

الحالة الشائعة هي بند ثابت: ظبّط quantity وسيب adjustableQuantity مقفولة.

{ "price": "price_test_AbC123", "quantity": 1 }

علشان تسمح للعميل يغيّر العدد وقت الدفع، فعّل adjustableQuantity:

{
  "price": "price_test_AbC123",
  "quantity": 1,
  "adjustableQuantity": {
    "enabled": true,
    "minimum": 1,
    "maximum": 10,
  },
}

لما adjustableQuantity.enabled يكون true، الصفحة المستضافة بتعرض عدّاد / + جنب البند ده. العدّاد بيحترم minimum (الافتراضي 1) وmaximum (الافتراضي 99). وبيحترم كمان stock المتبقّي للسعر: لو السعر ليه مخزون محدود، العدّاد بيتوقّف عند اللي فاضل، وبيظهر تلميح "باقي N بس" تحت العدّاد لما المخزون يكون 10 أو أقل.

البنود القابلة للإزالة والاختيارية

ظبّط adjustableQuantity.minimum على 0 علشان تسمح للعميل يشيل البند بالكامل من الطلب.

  • قابل للإزالة. ابدأ quantity بـ 1 أو أكتر مع minimum: 0. البند بيبقى جوّه الطلب من البداية، وعند الكمية 1 زرار بيتحوّل لزرار إزالة بينزّله لـ 0.
  • إضافة اختيارية. ابدأ quantity بـ 0 مع minimum: 0. البند بيبدأ بره الطلب، معروض تحت قسم "أضِف لطلبك". العميل بيدخّله بكمية 1 عن طريق زرار إضافة.
// إضافة اختيارية: بتبدأ بره العربة، والعميل يقدر يضيف لحد 3
{
  "price": "price_test_giftwrap",
  "quantity": 0,
  "adjustableQuantity": { "enabled": true, "minimum": 0, "maximum": 3 },
}

quantity بقيمة 0 صحيحة بس على البند اللي adjustableQuantity.enabled بتاعه true وminimum بتاعه 0. أي بند تاني بيتبعت بـ quantity: 0 بيتم رفضه وقت الإنشاء.

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

الصفحة المستضافة بتتولّى أي تغيير في الكمية من الأول للآخر: الإجماليات بتتحدّث على الصفحة والجلسة بتتحدّث في السيرفر. مش هتكتب أي كود للمسار ده. علشان تغيّر بنود الفاتورة من الـ backend بتاعك، استخدم PATCH /checkout/sessions/:id (استبدال كامل لـ lineItems، شوف نظرة عامة ← إيه اللي يتعدّل).

العملة وتعدد العملات

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

XPay بيعالج التسويات بالـ EGP حاليًا. لما السعر يكون بالـ EGP، العملة الواحدة دي بتمشي من الأول للآخر. ولما السعر يكون بعملة تانية (عملة العرض)، الجلسة بتثبّت سعر صرف عند الإنشاء وبتحتفظ بعرض موازي لـ "اللي العميل بيشوفه" جنب قيم المعالجة بالـ EGP.

بشكل ملموس:

  • الجلسة وكل بند فاتورة بيحملوا مسارَين للمبلغ في الرد: مسار المعالجة (amountSubtotal، amountTotal، بوحدات EGP الصغرى) وملحق مرآة للعرض (presentmentDetails.amountSubtotal، presentmentDetails.amountTotal، بعملة العميل).
  • سعر الصرف بيتثبّت عند إنشاء الجلسة. تغييرات الكمية، والخصومات، والتحديثات كلها بتعيد استخدامه. العميل عمره ما بيشوف السعر بيتحرّك وقت الدفع.
  • تطبيق الدفع المستضاف بيقرا قيم العرض لما تكون موجودة وبيعرض الصفحة كلها بعملة واحدة. والسيرفر بتاعك بيقرا قيم المعالجة للسجل والتسوية.

مش محتاج تعمل حسابات صرف على أي ناحية. رد الجلسة بيحمل كل قيمة وهي متحسّبة بالفعل.

إتاحة السعر

كل سعر ليه حقول دورة حياة بتتحكم في إن البند يشتغل وقت الدفع ولا لأ. نفس البوابة بتنطبق عند إنشاء الجلسة، ومع كل تحديث كمية، ومرة تانية لما العميل يبعت الدفع. البند اللي بيتحوّل لغير متاح في وسط الجلسة بيعطّل زر الدفع على الصفحة المستضافة وبيظهر شارة على مستوى البند.

الحالةالسبباللي العميل بيشوفه
archivedالسعر اتأرشف في لوحة التحكم (active: false).شارة "غير متاح"، والبند باهت.
scheduledالسعر ليه startDate في المستقبل.شارة "لسه مش متاح".
expiredالسعر ليه expirationDate عدّى.شارة "منتهي الصلاحية".
sold_outالـ stock المحدود أقل من الكمية المطلوبة.شارة "خلص المخزون".
recurring_unsupportedالـ type بتاع السعر هو RECURRING. الأسعار المتكرّرة مش مقبولة في الدفع.شارة "نوع سعر غير مدعوم".
availableولا واحدة مما سبق.عرض عادي.

المخزون بيتخصم ذرّيًا لما الدفع ينجح، مش لما الجلسة تتنشئ. الجلسة اللي بتفتح والمخزون متاح ممكن يخلص مخزونها قبل ما العميل يدفع. البوابة وقت الإرسال بتمسك السباق ده وبترفض الدفع بخطأ price_sold_out بدل ما تبيع أكتر من المتاح.

stock لما يكون null (الافتراضي) معناه غير محدود. بنود priceData المضمَّنة عمرها ما بتحمل سقف مخزون.

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

بنود المبلغ المخصّص

لما السعر يكون type: CUSTOM، العميل بيدخل المبلغ بنفسه وقت الدفع. استخدمه للتبرّعات، أو تسعير "ادفع اللي إنت عايزه"، أو الفواتير اللي التاجر بيعرف مبلغها في لوحة التحكم قبل إرسال الرابط بلحظة.

سعر المبلغ المخصّص بيحمل تهيئة customUnitAmount:

{
  "id": "price_test_donate",
  "type": "CUSTOM",
  "currency": "EGP",
  "customUnitAmount": {
    "minimum": 5000, // 50.00 EGP
    "maximum": 10000000, // 100,000.00 EGP
    "preset": 50000, // 500.00 EGP shown by default
  },
}

البند اللي بيستقر على سعر CUSTOM بيبدأ مبلغ الجلسة من customUnitAmount.preset (أو 0 لو مفيش preset متظبّط). الصفحة المستضافة بتعرض مبلغ كبير مع زر تغيير المبلغ بيفتح حقل إدخال. لما العميل يأكّد، الصفحة المستضافة بتتحقق من القيمة المدخلة مقابل الحدود وإجماليات الجلسة بتتحدّث في مكانها.

بنود CUSTOM ليها كام قاعدة صارمة:

  • بند CUSTOM واحد على الأكتر في الجلسة، ولازم يكون البند الوحيد. خلطه مع بنود تانية بيتم رفضه.
  • quantity بتتفرض على 1. الكمية القابلة للتعديل مش مسموحة.
  • الخصومات وأكواد الترويج مش مدعومة على الجلسات اللي فيها بند CUSTOM. الفكرة كلها إن العميل هو اللي اختار المبلغ؛ وإضافة كوبون فوقه بيكسر الاتفاق.
  • priceData المضمَّن مينفعش يكون CUSTOM. بس مراجع price_* الموجودة المتعملة في لوحة التحكم أو عن طريق الـ Prices API هي اللي تقدر تستقر على CUSTOM. الحدود (customUnitAmount) بتتظبّط على الـ Price، مش على البند.
  • uiMode: "custom" (Elements SDK) غير متوافق. ابنِ الجلسة بـ unitAmount ثابت بدلًا منه، أو استخدم uiMode: "hosted" / "embedded"، والاتنين بيستخدموا واجهة إدخال المبلغ بتاعة الدفع المستضاف.

إيه اللي على كل بند فاتورة في الرد

كل بند فاتورة في رد جلسة الدفع بيحمل نفس الحقول:

الحقلإيه هو
idمعرّف البند (li_*).
quantityالكمية المستقرّة. 1 لبنود CUSTOM؛ و0 للإضافة الاختيارية اللي العميل ماضافهاش؛ وغير كده كمية البند نفسها، يا القيمة الثابتة اللي التاجر حطّها يا اللي العميل ظبّطه بالعدّاد في البند القابل للتعديل.
priceكائن PriceAPI الكامل، بما فيه منتجه، ونوعه (ONE_TIME / CUSTOM)، وcustomUnitAmount (لو موجود)، وحقول دورة الحياة (active، stock، startDate، expirationDate).
adjustableQuantityبيرجع زي ما اتظبّط عند الإنشاء. بيحرّك عرض العدّاد على الصفحة المستضافة.
amountSubtotalunitAmount × quantity، بعملة معالجة الجلسة.
amountTotalبعد أي خصم أو ضريبة موزّعين على البند ده.
amountDiscountالخصم الموزّع على البند ده. 0 لما مفيش خصم منطبق. مجموعه عبر البنود بيساوي خصم الجلسة.
amountTaxالضريبة الموزّعة على البند ده. 0 لما مفيش ضريبة منطبقة.
currencyعملة حقول amount* اللي فوق. على جلسة الدفع دي عملة المعالجة.
presentmentDetailsملحق بـ unitAmount، وamountSubtotal، وamountDiscount، وamountTotal، وcurrency بعملة العميل. موجود بس لما الجلسة تكون متعددة العملات.

البنود الاختيارية اللي العميل ماضافهاش مش بتبقى جزء من الطلب المكتمل. بمجرد ما الدفع ينجح، الـ GET /checkout/sessions/:id المكتمل وحدث checkout.session.completed بيحملوا بس البنود اللي العميل اشتراها. والبند اللي قاعد على quantity 0 بيتشال من الاتنين.

رايح فين بعد كده

في الصفحة دي