طلبات الشراء والاستعادة

هذا هو الجزء الذي تنتقل فيه الأموال. اقرأه مرة واحدة، بعناية.

معرّف العملية ملكك

يحمل كل طلب شراء معرّف عملية تختاره أنت — UUID جديدًا لكل عملية شراء — ويُرسَل في الترويسة Idempotency-Key. احفظه، مع طلب الشراء كما هو تمامًا، قبل أن ترسله.

إرسال المعرّف نفسه مرة أخرى لا يشتري مرتين أبدًا: بل يستأنف طلب الشراء نفسه ويعيد نتيجته الحقيقية. هذا ما يجعل إعادة المحاولة آمنة — حتى بعد إعادة تشغيل برنامجك — ولهذا لا تخترع حزم SDK المعرّف نيابةً عنك أبدًا: فالمعرّف الذي يُختلَق عند كل محاولة سيحوّل ردًّا ضائعًا تتبعه إعادة محاولة إلى عملية شراء ثانية.

var operationId = Guid.NewGuid();
await db.RecordOrderIntentAsync(operationId, walletId, request, ct);   // BEFORE the call

var outcome = await anis.Orders.CreateAsync(walletId, operationId, request, ct);

معرّف العملية فريد على مستوى أنيس كلّه. إرسال معرّف واحد مع محتوى طلب شراء مختلف يُرفض بالرمز idempotency_conflict — وهو أيضًا ما يتلقاه الاستئناف إذا اختلف محتواه عن المحتوى المحفوظ. استأنف بالمحتوى الذي حفظته تمامًا.

السعر الذي ترسله

أرسل قيمة unitPrice للبطاقة من الكتالوج، كما قرأتها تمامًا، في الحقل expectedUnitPrice — فهي السعر الذي تدفعه هذه المحفظة، والسعر الذي يُتحقَّق من طلب الشراء على أساسه. أما الأسعار الأخرى (businessPrice وpersonalPrice وspecialOfferPrice) فهي للعرض فقط؛ وطلب الشراء بأحدها قد يُرفض بالرمز price_changed.

يجب أن يساوي expectedTotal حاصل ضرب expectedUnitPrice في quantity تمامًا. المبالغ نصوص عشرية بثلاث خانات بعد الفاصلة تمامًا ("10.500") — احسبها بنوع عشري (decimal)، ولا تستخدم الفاصلة العائمة أبدًا. ويجب أن يكون كلا السعرين اللذين ترسلهما أكبر من صفر، بلا إشارة، وبلا أصفار في البداية، وبما لا يزيد على 15 خانة قبل الفاصلة؛ وأي قيمة أخرى تُرفض بالرمز validation_failed، ولا يُشترى شيء. (أما في ردود أنيس، فرصيد المحفظة وحده قد يكون سالبًا، حين تستخدم المحفظة الدين المسموح.) وابتداءً من الإصدار 1.1.0 من الحزمة، ترفض حزمة ‎.NET سعر الوحدة الذي لا يزيد على صفر قبل الإرسال، بالاستثناء ArgumentOutOfRangeException.

{
  "cardId": "8d4b1e73-9a25-4c60-8f37-6b2e9d5a1c48",
  "quantity": 2,
  "expectedUnitPrice": { "amount": "10.500", "currency": "LYD" },
  "expectedTotal": { "amount": "21.000", "currency": "LYD" },
  "externalReference": "your-order-123"
}

الحقل externalReference اختياري — مرجعك الخاص، من 1 إلى 100 حرف من الحروف اللاتينية والأرقام والمسافة و- _ . : / #. قيمة quantity لا تقل عن 1 ولا تزيد على الحد الأقصى لطلب الشراء الواحد (100). القيمة الافتراضية لـ useAllowedDebt هي false؛ اجعلها true فقط إذا كنت تقصد إنفاق الدَّين المسموح به للحساب.

تحقّق من حدود الكمية قبل الطلب. تحمل كل بطاقة في الكتالوج minimumQuantity وmaximumQuantity — قيمتهما دائمًا 1 وحدّ طلب الشراء على المنصة، وهو 100. والكمية quantity خارجهما تُرفض بالرمز quantity_unavailable. وقد تحمل الفئة الفرعية أيضًا disclaimer، وهي ملاحظة أنيس الحالية لمشتري بطاقاتها: اعرضها على عميلك النهائي قبل أن يشتري.

الردود التي قد يتلقاها طلب الشراء

الردّ معناه ما العمل
201 مع رموز البطاقات اكتمل طلب الشراء احفظ الرموز قبل أي شيء آخر
200 مع رموز البطاقات حصل الاستئناف على الاكتمال الذي ضاع ردّه الأول احفظ الرموز — فهذه أول مرة تراها
201 أو 200 مع Idempotency-Replayed: true سبق إرسال نتيجة طلب الشراء هذا إذا كنت قد حفظت الرموز من ذلك الردّ الأول، فلا شيء عليك. وإن لم يصلك قط — بسبب انتهاء المهلة — فقد كانت الرموز فيه: اكشفها بمعرّف فاتورة طلب الشراء
202 قُبِل، ولا نتيجة بعد أرسل طلب الشراء نفسه مرة أخرى بعد Retry-After
رفض انظر أدناه يتوقف على ما إذا كان طلب الشراء قد قُدِّم

الردّ الذي يُبلغ عن الاكتمال أولًا هو وحده الذي يحمل رموز البطاقات. أي تكرار لاحق يحمل طلب الشراء من دونها، وقراءة طلب الشراء لا تحملها أبدًا. لذا إذا ضاع ذلك الردّ الأول — بسبب انتهاء المهلة، أو انقطاع الاتصال، أو ردّ تعذّر التحقق منه — فإن طلب الشراء مدفوع الثمن، ولا يمكن الحصول على رموزه إلا بالكشف بمعرّف فاتورة طلب الشراء، وهذا يتطلب الصلاحية cards:reveal.

اطلب صلاحية cards:reveal قبل الانتقال إلى التشغيل الفعلي

من دونها، لا يستطيع طلب شراء ضاع ردّه الأول أن يعطيك رموزه مرة أخرى.

ما يقوله كل ردّ عن طلب الشراء

كل ردّ على طلب شراء — الأول وتكراره والقراءة — يقول أي طلب هو: cardId وquantity وexternalReference الخاص بك (إن أرسلته). وبعد أن يكتمل الطلب يحمل total أيضًا. فالقراءة أو التكرار يخبرك أي عملية بيع لديك هذه وكم كلّفت، دون الرجوع إلى قاعدة بياناتك. أما رموز البطاقات فلا تكون إلا في الردّ الأول.

{
  "operationId": "c1f6a3b2-5d47-4e89-9a10-3b7e2d8f4c65",
  "status": "completed",
  "invoiceId": "5a2e9c47-1b83-4f06-8d2a-7c4b9e1f3a60",
  "externalReference": "your-order-123",
  "cardId": "8d4b1e73-9a25-4c60-8f37-6b2e9d5a1c48",
  "quantity": 2,
  "total": { "amount": "21.000", "currency": "LYD" }
}

قراءة طلب شراء فشل تقول لك السبب: failureCode هو رمز الرفض الذي سجّلته أنيس. ابنِ منطقك عليه كما على أي رمز خطأ آخر — لم يُشترَ شيء. ويُحذف حين لا يكون لذلك الرفض رمز علني. أما إرسال مفتاح طلب فاشل مرة أخرى فيُجاب برفضه نفسه كما كان.

الاستعادة هي إرسال طلب الشراء نفسه مرة أخرى

يحمل الردّ 202 الترويسة Location التي تشير إلى طلب الشراء، فيكون الميل الطبيعي هو الاستعلام عنه بشكل متكرر. قراءة طلب الشراء تُخبرك بحالته؛ ولا تدفعه إلى الأمام. طريقة استعادة طلب الشراء هي أن ترسل طلب الشراء نفسه تمامًا مرة أخرى — بمعرّف العملية نفسه، والمحتوى نفسه، موقّعًا من جديد.

افعل ذلك كلما لم تعرف ما الذي حدث:

  • انتهت مهلة استدعائك، أو انقطع الاتصال؛
  • تعذّر التحقق من الردّ؛
  • يقول الرفض إن طلب الشراء قد يكتمل رغم ذلك (انظر أدناه).

الاستئناف آمن دائمًا. أما معرّف عملية جديد في هذه الحالات فليس آمنًا — فقد يشتري البطاقات مرة ثانية.

بين استئنافين، انتظر على الأقل مدة Retry-After في آخر ردّ حين يحملها (تعطيك حزمة ‎.NET إياها في RetryAfter أو SuggestedDelay).

// resuming: true when this call repeats an attempt whose outcome you did not learn
var outcome = resuming
    ? await anis.Orders.ResumeAsync(walletId, operationId, request, ct)
    : await anis.Orders.CreateAsync(walletId, operationId, request, ct);

await (outcome switch
{
    OrderCompleted c when c.CodesWithheld => ReportCodesWithheldAsync(operationId, ct),  // paid, codes withheld: do not buy again; write to support@anis.ly
    OrderCompleted c      => vault.StoreAsync(c.Credentials, ct),                // store codes FIRST; this answer carries them once
    OrderReplayed r       => EnsureStoredAsync(r.Order, ct),                     // if the first answer was lost, reveal by invoice id
    OrderProcessing p     => ResumeLaterAsync(operationId, p.RetryAfter),        // may still complete
    OrderOutcomeUnknown u => ResumeLaterAsync(operationId, u.SuggestedDelay),    // no answer, untrusted answer, a rate limit, an access refusal, or a fresh refusal on resume
    OrderNotPlaced n      => CloseAsNotPlacedAsync(operationId, n.Refusal),      // nothing bought; fix the cause, new order, new id
    _                     => throw new InvalidOperationException($"Unhandled order outcome {outcome}"),
});

تطبّق حزمة ‎.NET قاعدة الاستئناف الواردة أدناه نيابةً عنك: رفضُ الاستئناف غير الموسوم بـ Idempotency-Replayed يعود في صورة OrderOutcomeUnknown، ولا يعود في صورة OrderNotPlaced أبدًا. وابتداءً من الإصدار 1.1.0 من الحزمة، يعود CreateAsync الذي رُفض بسبب حدّ معدّل الاستدعاءات في صورة OrderOutcomeUnknown أيضًا (كانت الإصدارات الأقدم تعيده في صورة OrderNotPlaced): استأنفه بالمعرّف نفسه. وابتداءً من الإصدار 1.1.0 من الحزمة، يُرجع CreateAsync أيضًا الرموز invalid_credentials وinsufficient_scope وwallet_not_granted على شكل OrderOutcomeUnknown (مهلة مقترحة 60 ثانية)، كما كان الاستئناف يفعل دائمًا: أعِد الوصول أولًا، ثم استأنف بالمعرّف نفسه. ويعود الاستدعاء الموقّع غير سليم البنية، malformed_signed_request، بالطريقة نفسها: أصلح طريقة توقيع الاستدعاء، ثم استأنف بالمعرّف نفسه.

استُنفدت محاولات الاستعادة

طلب الشراء الذي حالته recoveryExhausted ليس فاشلًا — لم تتمكن أنيس من معرفة نتيجته بعد، وربما يكون قد اكتمل. لا تقدّمه مرة أخرى بمعرّف جديد. واصل الاستئناف بالمعرّف نفسه، ببطء — دقائق لا ثوانٍ: تحاول أنيس إتمام طلب الشراء حتى خمس مرات في كل 15 دقيقة، ومدة Retry-After في الردّ (نحو دقيقة لطلب كهذا) هي أقل ما تنتظره. فقد يعيد استئناف لاحق الاكتمال مع رموزه. اكتب إلى support@anis.ly واذكر معرّف العملية.

حين يُرفض طلب الشراء: هل قُدِّم؟

كل صفحة خطأ تجيب عن هذا السؤال أولًا.

  • هذا الاستدعاء لم يشترِ شيئًا. أصلح السبب، ثم قدّم طلب شراء جديدًا بمعرّف عملية جديد.
  • لكن عند الاستئناف، فإن الرفض غير الموسوم بـ Idempotency-Replayed قد تقرّر قبل أن تنظر أنيس في طلب الشراء، ولذلك لا يقول شيئًا عن المحاولة السابقة: إذا كانت نتيجة تلك المحاولة مجهولة، فهي لا تزال مجهولة — واصل الاستئناف بالمعرّف نفسه.
  • قد يكتمل رغم ذلك. لم يُتَّخذ أي قرار: dependency_unavailable، أو request_timeout، أو internal_error، أو replay_detected (وصلت نسخة مطابقة من استدعائك قبله). استأنف بمعرّف العملية نفسه.
  • حدّ معدّل الاستدعاءات لا يعني «لم يُشترَ شيء». الرمز rate_limited يردّ هذه النسخة فقط، ولا يقول شيئًا عن نسخة سابقة بالمعرّف نفسه — استدعاء انتهت مهلته، أو إعادة محاولة أجراها عميل HTTP لديك من تلقاء نفسه — وقد تكون مرّت فعلًا. انتظر Retry-After، ثم استأنف بالمعرّف نفسه، لا بمعرّف جديد أبدًا.
  • رُفض قبل أن تنظر أنيس في طلب الشراء. الرموز invalid_credentials وinsufficient_scope وwallet_not_granted وmalformed_signed_request لا تقول شيئًا عن محاولة سابقة بمعرّف العملية نفسه. أعِد الوصول أولًا (ومع malformed_signed_request: أصلح طريقة توقيع الاستدعاء)، ثم استأنف بالمعرّف نفسه (أو اقرأه بالاستدعاء GET /orders/{operationId}). لا تقدّمه أبدًا بمعرّف جديد.

الرفض الموسوم بـ Idempotency-Replayed: true هو الردّ المسجَّل لطلب شراء مغلق بالفعل — لم يُشترَ شيء، وإرسال المعرّف نفسه مرة أخرى يعيد الردّ نفسه إلى الأبد. أغلقه من جهتك؛ فأي محاولة جديدة تحتاج إلى معرّف جديد.

الرفض هل قُدِّم بهذا الاستدعاء؟ ما العمل
price_changed لا أعد قراءة الكتالوج؛ قدّم طلب شراء جديدًا بقيمة unitPrice الجديدة
insufficient_balance لا اشحن الرصيد؛ ثم طلب شراء جديد
quantity_unavailable لا أعد قراءة الكتالوج؛ ثم طلب شراء جديد
card_unavailable لا اقرأ الكتالوج مجددًا وتحقق من الإتاحة لهذا الحساب وهذه المحفظة؛ فقد تُرفض بطاقة معروضة فيه، فاختر بطاقة مؤهلة لطلب شراء جديد
allowed_debt_consent_required لا طلب شراء جديد مع useAllowedDebt: true، فقط إذا كنت تقصد ذلك
owner_limit_exceeded لا رمز محجوز، لا ترسله أنيس حاليًا. إن وصلك يومًا: لا تُعِد المحاولة في حلقة؛ طلب شراء جديد بعد أن يتغيّر الحدّ
daily_limit_exceeded لا لا تُعِد المحاولة في حلقة؛ طلب شراء جديد بعد أن يتغيّر الحدّ
spending_limit_exceeded لا لا تُعِد المحاولة في حلقة؛ بعد Retry-After طلب شراء جديد بمعرّف جديد
rate_limited قد يكتمل رغم ذلك انتظر Retry-After، ثم استأنف بالمعرّف نفسه
invalid_credentials، insufficient_scope، wallet_not_granted لا — لكن ربما قدّمته نسخة سابقة أعِد الوصول أولًا، ثم استأنف بالمعرّف نفسه (أو اقرأه بالاستدعاء GET /orders/{operationId})؛ لا بمعرّف جديد أبدًا
malformed_signed_request لا — لكن ربما قدّمته نسخة سابقة أصلح طريقة توقيع الاستدعاء أولًا، ثم استأنف بالمعرّف نفسه (أو اقرأه بالاستدعاء GET /orders/{operationId})؛ لا بمعرّف جديد أبدًا
idempotency_conflict لا إذا كنت تقصد الاستئناف، فأرسل المحتوى المحفوظ تمامًا؛ ولعملية شراء جديدة، استخدم معرّفًا جديدًا
dependency_unavailable، request_timeout، internal_error، replay_detected قد يكتمل رغم ذلك استأنف بالمعرّف نفسه

قد تُباع بعض البطاقات في مناطق معيّنة فقط. إذا لم تكن البطاقة تُباع في موقع حسابك، يُرفض طلب شرائها بالرمز card_unavailable، حتى لو ظهرت في الكتالوج.

أرسل طلبات الشراء على المحفظة الواحدة واحدًا تلو الآخر، لا عدة طلبات في وقت واحد.

انتهاء المهلة ليس failed أبدًا. الرفض التجاري القاطع وحده هو failed، وطلب الشراء المكتمل لا يصبح فاشلًا بعد ذلك أبدًا.

حين تضيع الردود أو تصل نسختان

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

  • «سبق التسليم» من دون رموز. الاستئناف الذي يردّ بـ Idempotency-Replayed: true ومن دون رموز — وهو OrderReplayed في الحزمة — يعني أن طلب الشراء قد قُدِّم وأن رموزه كانت في ردّ لم يصلك قط. لا تشترِ مرة ثانية: اكشف الرموز بمعرّف فاتورة طلب الشراء (وهذا يتطلب cards:reveal). والأمر نفسه ينطبق على dependency_unavailable أو request_timeout موقّعٍ تبيّن بعده أن طلب الشراء اكتمل: يعيده الاستئناف من دون رموز، والكشف هو طريقك إليها. أما replay_detected فيعني أن نسخة مطابقة سبقت إلى هناك؛ يُستأنف كأي نتيجة مجهولة، وينتهي الأمر إلى الشيء نفسه.
  • اكتمل مع حجب الرموز. قد يكتمل طلب الشراء أيضًا من دون أي رموز: بطاقة فيه أُبطلت أو استُردّ ثمنها، أو لا يمكن إصدار رمزها. طلب الشراء مدفوع ومكتمل — فلا تشترِه مرة أخرى. وتقول أنيس ذلك صراحةً: الردّ الذي يُبلغ أولًا عن طلب كهذا يحمل codesWithheld: true (وهي لا تساوي false أبدًا، والتكرار أو القراءة لا يذكرانها). وعامل طلب الشراء المكتمل الذي لا رموز فيه إطلاقًا بالطريقة نفسها، سواء وُجدت العلامة أم لا. وكشف الرموز المحجوبة يُرفض أيضًا؛ أما بقية بطاقات الطلب فقد يمكن كشفها واحدةً واحدة. اكتب إلى support@anis.ly واذكر معرّف العملية. (في ‎.NET تكون OrderCompleted.CodesWithheld مساوية لـ true.)
  • نسخة تصل متأخرة، أو وصول تغيّر. إعادة الإرسال التي تصل إلى أنيس بعد وقت expires في توقيعها (60 ثانية في حِزم أنيس البرمجية)، أو بعد أكثر من 300 ثانية من توقيعها، تُرفض بالرمز invalid_credentials؛ وقد يكون موظفو أنيس أيضًا قد ألغوا المفتاح، أو أزالوا صلاحية (insufficient_scope)، أو أزالوا المحفظة من تطبيقك (wallet_not_granted) بينما كانت نسخة سابقة قيد الشراء. كلٌّ منها يُرفض قبل أن تنظر أنيس في طلب الشراء، فهو لا يقول شيئًا عن محاولة سابقة بمعرّف العملية نفسه — فقد تكون تلك النسخة قدّمت طلب الشراء فعلًا. أعِد الوصول أولًا، ثم استأنف بالمعرّف نفسه، موقّعًا من جديد (أو اقرأه بالاستدعاء GET /orders/{operationId}، ويتطلب orders:read، أو orders:create لطلباتك أنت). لا تقدّمه أبدًا بمعرّف عملية جديد. المعرّف الجديد للرفض القاطع الوارد أعلاه، أو لرفض مسجَّل موسوم بـ Idempotency-Replayed.
  • wallet_not_granted عند الاستئناف. ربما أزال موظفو أنيس هذه المحفظة من تطبيقك بينما كان طلب الشراء معلّقًا، فيتلقى كل استئناف الرفض نفسه إلى أن تُمنح المحفظة من جديد. اقرأ طلب الشراء بالاستدعاء GET /orders/{operationId} — فالقراءة لا تتوقف على المحفظة — واكتب إلى support@anis.ly واذكر معرّف العملية. لا تقدّم طلب شراء جديدًا.

البطاقات المملوكة والكشف

قائمة البطاقات المملوكة أو قراءة بطاقة منها لا تُظهر رموزها أبدًا، لكنها تعطيك ما تحتاجه لخدمة عميلك: unitPrice (ما دفعته، ولا يغيّره استرداد لاحق)، وexpiryDate، وfaceValue، وsubcategory، وinvoiceNumber المطبوع على الفاتورة — اذكر هذا الرقم حين تكتب إلى الدعم. وتساوي credentialAvailable القيمة true فقط حين يعطيك كشف البطاقة رمزًا؛ والبطاقة المستردّة أو غير الصالحة تساوي false، فلا تعرضها.

والكشف (بطاقة واحدة أو فاتورة كاملة) يسمّي ما يكشفه: إضافةً إلى الرموز تحمل كل بطاقة card (المعرّف والاسم) وinvoiceId وpurchasedAt وexpiryDate. أبلغ عميلك النهائي بتاريخ الانتهاء. وتحمل soldCards في الردّ الأول على طلب الشراء expiryDate أيضًا.

مشتريات العدادات والشحن المباشر

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