طلبات الشراء والاستعادة
هذا هو الجزء الذي تنتقل فيه الأموال. اقرأه مرة واحدة، بعناية.
معرّف العملية ملكك
يحمل كل طلب شراء معرّف عملية تختاره أنت — 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)، ولا تستخدم الفاصلة العائمة أبدًا.
{
"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 فقط إذا كنت تقصد إنفاق الدَّين المسموح به للحساب.
الردود التي قد يتلقاها طلب الشراء
| الردّ | معناه | ما العمل |
|---|---|---|
201 مع رموز البطاقات |
اكتمل طلب الشراء | احفظ الرموز قبل أي شيء آخر |
200 مع رموز البطاقات |
حصل الاستئناف على الاكتمال الذي ضاع ردّه الأول | احفظ الرموز — فهذه أول مرة تراها |
201 أو 200 مع Idempotency-Replayed: true |
سبق إرسال نتيجة طلب الشراء هذا | إذا كنت قد حفظت الرموز من ذلك الردّ الأول، فلا شيء عليك. وإن لم يصلك قط — بسبب انتهاء المهلة — فقد كانت الرموز فيه: اكشفها بمعرّف فاتورة طلب الشراء |
202 |
قُبِل، ولا نتيجة بعد | أرسل طلب الشراء نفسه مرة أخرى بعد Retry-After |
| رفض | انظر أدناه | يتوقف على ما إذا كان طلب الشراء قد قُدِّم |
الردّ الذي يُبلغ عن الاكتمال أولًا هو وحده الذي يحمل رموز البطاقات. أي تكرار لاحق يحمل طلب الشراء من دونها،
وقراءة طلب الشراء لا تحملها أبدًا. لذا إذا ضاع ذلك الردّ الأول — بسبب انتهاء المهلة، أو
انقطاع الاتصال، أو ردّ تعذّر التحقق منه — فإن طلب الشراء مدفوع الثمن، ولا يمكن الحصول على رموزه إلا بالكشف بمعرّف
فاتورة طلب الشراء، وهذا يتطلب الصلاحية cards:reveal.
اطلب صلاحية cards:reveal قبل الانتقال إلى التشغيل الفعلي
من دونها، لا يستطيع طلب شراء ضاع ردّه الأول أن يعطيك رموزه مرة أخرى.
الاستعادة هي إرسال طلب الشراء نفسه مرة أخرى
يحمل الردّ 202 الترويسة Location التي تشير إلى طلب الشراء، فيكون الميل الطبيعي هو الاستعلام عنه بشكل متكرر.
قراءة طلب الشراء تُخبرك بحالته؛ ولا تدفعه إلى الأمام. طريقة استعادة طلب الشراء هي أن ترسل طلب الشراء نفسه
تمامًا مرة أخرى — بمعرّف العملية نفسه، والمحتوى نفسه، موقّعًا من جديد.
افعل ذلك كلما لم تعرف ما الذي حدث:
- انتهت مهلة استدعائك، أو انقطع الاتصال؛
- تعذّر التحقق من الردّ؛
- يقول الرفض إن طلب الشراء قد يكتمل رغم ذلك (انظر أدناه).
الاستئناف آمن دائمًا. أما معرّف عملية جديد في هذه الحالات فليس آمنًا — فقد يشتري البطاقات مرة ثانية.
// resuming: true when this call repeats an attempt whose outcome you did not learn
try
{
var outcome = resuming
? await anis.Orders.ResumeAsync(walletId, operationId, request, ct)
: await anis.Orders.CreateAsync(walletId, operationId, request, ct);
}
catch (AnisApiException failure) when (failure.OrderOutcome == OrderRefusalOutcome.Unknown
|| (resuming && !failure.IsReplayed))
{
await ResumeLaterAsync(operationId); // no decision reached — or a resume refused at the door
}
catch (AnisApiException failure)
{
await CloseAsNotPlacedAsync(operationId, failure); // nothing bought: fix the cause, new order, new id
}
catch (Exception e) when (e is TaskCanceledException or HttpRequestException or UnverifiableResponseException)
{
await ResumeLaterAsync(operationId); // timed out, connection lost, or an untrusted answer
}
استُنفدت محاولات الاستعادة
طلب الشراء الذي حالته recoveryExhausted ليس فاشلًا — لم يتمكن أنيس من معرفة نتيجته بعد، وربما يكون قد
اكتمل. لا تقدّمه مرة أخرى بمعرّف جديد. واصل الاستئناف بالمعرّف نفسه، ببطء (دقائق لا ثوانٍ) — فقد يعيد استئناف
لاحق الاكتمال مع رموزه — وأبلغ أنيس بمعرّف العملية.
حين يُرفض طلب الشراء: هل قُدِّم؟
كل صفحة خطأ تجيب عن هذا السؤال أولًا.
- هذا الاستدعاء لم يشترِ شيئًا. أصلح السبب، ثم قدّم طلب شراء جديدًا بمعرّف عملية جديد. (الرفض عند المدخل — كحدّ معدّل الاستدعاءات أو صلاحية ناقصة — لا يسجّل شيئًا على المعرّف، لذا تنجح إعادة استخدامه أيضًا.)
- لكن عند الاستئناف، فإن الرفض غير الموسوم بـ
Idempotency-Replayedقد تقرّر قبل أن ينظر أنيس في طلب الشراء، ولذلك لا يقول شيئًا عن المحاولة السابقة: إذا كانت نتيجة تلك المحاولة مجهولة، فهي لا تزال مجهولة — واصل الاستئناف بالمعرّف نفسه. - قد يكتمل رغم ذلك. لم يُتَّخذ أي قرار:
dependency_unavailable، أوrequest_timeout، أوinternal_error، أوreplay_detected(وصلت نسخة مطابقة من استدعائك قبله). استأنف بمعرّف العملية نفسه.
الرفض الموسوم بـ Idempotency-Replayed: true هو الردّ المسجَّل لطلب شراء مغلق بالفعل — لم يُشترَ شيء،
وإرسال المعرّف نفسه مرة أخرى يعيد الردّ نفسه إلى الأبد. أغلقه من جهتك؛ فأي محاولة جديدة تحتاج إلى معرّف جديد.
| الرفض | هل قُدِّم بهذا الاستدعاء؟ | ما العمل |
|---|---|---|
price_changed |
لا | أعد قراءة الكتالوج؛ قدّم طلب شراء جديدًا بقيمة unitPrice الجديدة |
insufficient_balance |
لا | اشحن الرصيد؛ ثم طلب شراء جديد |
quantity_unavailable، card_unavailable |
لا | أعد قراءة الكتالوج؛ ثم طلب شراء جديد |
allowed_debt_consent_required |
لا | طلب شراء جديد مع useAllowedDebt: true، فقط إذا كنت تقصد ذلك |
owner_limit_exceeded، daily_limit_exceeded |
لا | لا تُعِد المحاولة في حلقة؛ طلب شراء جديد بعد أن يتغيّر الحدّ |
rate_limited |
لا | انتظر Retry-After، ثم أرسل مرة أخرى |
idempotency_conflict |
لا | إذا كنت تقصد الاستئناف، فأرسل المحتوى المحفوظ تمامًا؛ ولعملية شراء جديدة، استخدم معرّفًا جديدًا |
dependency_unavailable، request_timeout، internal_error، replay_detected |
قد يكتمل رغم ذلك | استأنف بالمعرّف نفسه |
انتهاء المهلة ليس failed أبدًا. الرفض التجاري القاطع وحده هو failed، وطلب الشراء المكتمل لا يصبح فاشلًا بعد
ذلك أبدًا.