بنود الفاتورة والتسعير
إزاي بتشتغل `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 | بيرجع زي ما اتظبّط عند الإنشاء. بيحرّك عرض العدّاد على الصفحة المستضافة. |
amountSubtotal | unitAmount × quantity، بعملة معالجة الجلسة. |
amountTotal | بعد أي خصم أو ضريبة موزّعين على البند ده. |
amountDiscount | الخصم الموزّع على البند ده. 0 لما مفيش خصم منطبق. مجموعه عبر البنود بيساوي خصم الجلسة. |
amountTax | الضريبة الموزّعة على البند ده. 0 لما مفيش ضريبة منطبقة. |
currency | عملة حقول amount* اللي فوق. على جلسة الدفع دي عملة المعالجة. |
presentmentDetails | ملحق بـ unitAmount، وamountSubtotal، وamountDiscount، وamountTotal، وcurrency بعملة العميل. موجود بس لما الجلسة تكون متعددة العملات. |
البنود الاختيارية اللي العميل ماضافهاش مش بتبقى جزء من الطلب المكتمل. بمجرد ما الدفع ينجح، الـ GET /checkout/sessions/:id المكتمل وحدث checkout.session.completed بيحملوا بس البنود اللي العميل اشتراها. والبند اللي قاعد على quantity 0 بيتشال من الاتنين.