يمكن أن تسلّم كل من Apple و Google نفس الاسترداد إلى خادمك أكثر من مرة، وإشعارات الاسترداد المكررة تكلّفك إن تصرفت بناءً على كل واحد منها
تعيد Apple إرسال إشعار الاسترداد حتى خمس مرات، وتعتمد Google Play على Pub/Sub التي تضمن التسليم مرة واحدة على الأقل، لذا يمكن أن يصل الاسترداد نفسه إلى خادمك أكثر من مرة. إليك كيفية التعامل مع إشعارات الاسترداد المكررة دون خصم رصيد أو استهلاك حصة API مرتين.

أهم الخلاصات
- تعيد Apple إرسال إشعار App Store Server Notification V2 خمس مرات، بعد 1 و12 و24 و48 و72 ساعة من المحاولة الأخيرة، كلما لم يجب خادمك بحالة HTTP بين 200 و206. باحتساب المحاولة الأولى، يمكن أن يصل استرداد واحد حتى ست مرات.
- تحمل كل إعادة محاولة حقيقية من Apple نفس notificationUUID، لذا فإن هذا الحقل، وليس معرّف المعاملة، هو مفتاح إزالة التكرار لديك.
- تعمل Real-time Developer Notifications من Google Play على Cloud Pub/Sub، التي تضمن التسليم مرة واحدة على الأقل ولا تضمن أي ترتيب، لذا يمكن أن تصل الرسالة نفسها مرتين أو خارج الترتيب. تخبرك Google بأن تتحقق من messageId للتأكد من التفرد قبل معالجة أي شيء.
- إشعارات CONSUMPTION_REQUEST الدورية من Apple ليست إعادات محاولة. تستمر Apple في إرسال إشعارات جديدة طوال نافذة الاسترداد المفتوحة، لكل منها notificationUUID مختلف، لذا فإن إزالة التكرار بناءً على notificationUUID تُبقي كل واحد منها بشكل صحيح.
- يمكن أن يحمل نفس transactionId من Apple أكثر من قرار، على سبيل المثال REFUND_DECLINED يتبعه لاحقاً REFUND، لذا فإن إزالة التكرار بناءً على معرّف المعاملة وحده تُلقي حدثاً مميزاً كنت بحاجة إليه.
- رفض نسخة مكررة بإرجاع 4xx أو 5xx يجعل المتجر يعيد إرسالها فقط. أزل التكرار داخل قاعدة بياناتك الخاصة وأرجِع دائماً حالة نجاح.
- معالج استرداد ليس idempotent يتصرف مرتين عند التسليم الثاني. يخصم رصيداً مرتين، أو يعكس دفعة مرتين، أو يستهلك حصة Play Developer وApp Store Server API القابلة للفوترة بإعادة فحص استرداد أغلقه بالفعل.
سيتلقى خادمك حدث الاسترداد نفسه أكثر من مرة، وقد صمم المتجران ذلك عن قصد. تعيد Apple إرسال App Store Server Notification حتى خمس مرات عندما لا تجيب نقطة النهاية لديك بوضوح. تسلّم Google Play إشعارات Real-time Developer Notifications عبر Cloud Pub/Sub، التي تعد بالتسليم مرة واحدة على الأقل ولا تعد بشيء عن الترتيب. لذا فإن السؤال ليس أبداً ما إذا كانت نسخة مكررة ستصل. السؤال هو ماذا يفعل رمزك في المرة الثانية التي يرى فيها الاسترداد نفسه. أخطئ في ذلك فتخصم رصيداً مرتين، أو تعكس دفعة مرتين، أو تستهلك حصة API قابلة للفوترة بإعادة فحص استرداد أغلقته بالفعل. إليك كيف تصل إليك إشعارات الاسترداد المكررة فعلياً، وأي التكرارات نسخ مكررة حقيقية وأيها يبدو كذلك فقط، وكيفية التعامل معها بحيث يكون التسليم الثاني مجانياً.
يُسلّم إشعار الاسترداد مرة واحدة على الأقل، وهو ليس نفس التسليم مرة واحدة بالضبط
يعامل المتجران الإشعار المُسلّم على أنه وعد يستمران في محاولة الوفاء به، لا طلقة واحدة يطلقانها وينسيانها. هذا جيد للموثوقية، لأن إشعاراً تفوته أثناء عملية نشر يظل يصل إليك لاحقاً. وهو فخ للصحة، لأن الآلية التي تضمن أنك ستحصل على الحدث في النهاية تضمن أيضاً أنك ستحصل عليه أحياناً مرتين. يجب أن يكون معالجك idempotent، بمعنى أن التسليم الثاني والثالث لاسترداد واحد لا يغيّر شيئاً لم يغيّره الأول بالفعل.
تعيد Apple المحاولة خمس مرات على مدى ثلاثة أيام
عندما ترسل Apple إشعار App Store Server Notification V2، تتوقع أن يجيب خادمك بحالة HTTP في نطاق 200 إلى 206. أي شيء آخر، أي 4xx أو 5xx، يخبر Apple بأن التسليم فشل، فتعيد Apple المحاولة. الجدول ثابت: خمس إعادات محاولة، بعد 1 و12 و24 و48 و72 ساعة من المحاولة السابقة. باحتساب المحاولة الأولى، يمكن أن يصل حدث استرداد واحد حتى ست مرات، موزعة على نحو أسبوع تقريباً. كل واحدة من إعادات المحاولة تلك تحمل نفس notificationUUID. ذلك الحقل هو مفتاح إزالة التكرار لديك. إذا كنت قد سجّلت بالفعل notificationUUID، فإن التسليم الذي بين يديك تكرار، والاستجابة الصحيحة هي ألا تخزّن شيئاً جديداً وأن تُرجِع 200 مع ذلك.
تعمل Google Play على Pub/Sub، التي تعد بمرة واحدة على الأقل ولا تقول شيئاً عن الترتيب
تُنشر إشعارات Real-time Developer Notifications من Google Play على موضوع Cloud Pub/Sub. ضمان التسليم في Pub/Sub هو مرة واحدة على الأقل، ولا تقدم أي ضمان للترتيب على الإطلاق. هذا يعني أن الرسالة نفسها يمكن أن تُسلّم إلى نقطة النهاية لديك أكثر من مرة، وأن رسالتين لنفس عملية الشراء قد تصلان خارج الترتيب. إرشاد Google نفسه صريح: فُك حقل data بترميز base64، واقرأ messageId، وتحقق من أنك لم تره من قبل قبل معالجة أي شيء. messageId مكرر هو تكرار تتخطاه. مع ذلك ينبغي أن يهبط إشعاران مختلفان عن عملية شراء واحدة على السجل نفسه، لذا اجعل مفتاح حالتك المخزنة على purchaseToken أيضاً، ودع حدثاً لاحقاً يحدّث الصف الذي أنشأه حدث سابق.
| المنصة | نموذج التسليم | إزالة التكرار بناءً على | إشارة النجاح | إن لم تؤكد الاستلام |
|---|---|---|---|---|
| App Store Server Notifications V2 | حتى 6 محاولات: الأولى، بالإضافة إلى 5 إعادات محاولة بعد 1 و12 و24 و48 و72 ساعة | notificationUUID | HTTP 200 إلى 206 | تعيد Apple المحاولة وفق الجدول الثابت، ثم تتوقف |
| Google Play RTDN عبر Pub/Sub | مرة واحدة على الأقل، دون ضمان للترتيب | Pub/Sub messageId، مفتاح الكيان على purchaseToken | HTTP 200 للدفع، أو ack صريح | تعيد Pub/Sub الإرسال بعد انتهاء مهلة ack |
التكرارات التي ليست نسخاً مكررة
ليس كل إشعار يبدو كإشعار رأيته من قبل هو إعادة محاولة. سلوكان من Apple يرسلان أحداثاً جديدة فعلاً تشترك في عملية شراء لكن يجب معالجة كل منها، وطيّها معاً بإزالة تكرار ساذجة يُلقي معلومات كنت بحاجة إليها.
ترسل Apple طلبات CONSUMPTION_REQUEST جديدة، لا إعادات محاولة
أثناء طلب استرداد مفتوح على عنصر استهلاكي، لا ترسل Apple طلب CONSUMPTION_REQUEST واحداً وتنتظر. ترسل طلبات جديدة دورياً طوال نافذة الاسترداد المفتوحة كاملة حتى يُغلق الاسترداد. أكّد موظفو Apple أن هذه ليست إعادات محاولة، والدليل هو الحقل الذي تزيل التكرار بناءً عليه: يحمل كل CONSUMPTION_REQUEST جديد notificationUUID مختلفاً. لذا فإن إزالة تكرار مفتاحها notificationUUID تقوم بالعمل الصحيح تلقائياً. تطوي إعادات المحاولة الحقيقية وتُبقي كل مطالبة مميزة. ما يجب ألا تفعله هو إزالة التكرار بناءً على معرّف المعاملة ونوع الإشعار، لأن ذلك سيُسكت كل CONSUMPTION_REQUEST بعد الأول ويكلّفك نافذة الأدلة البالغة 12 ساعة على تلك التي أسقطتها.
يمكن أن تحمل معاملة واحدة أكثر من قرار
يمكن أن ينتج نفس transactionId أكثر من نتيجة استرداد خلال عمره. يمكن أن ترسل Apple REFUND_DECLINED ثم لاحقاً REFUND للمعاملة نفسها، ويبلّغ المطورون عن تلقي ثلاثة إشعارات متعلقة بالاسترداد أو أكثر لمعرّف معاملة واحد. كل منها حدث مميز له notificationUUID الخاص به. إذا كان مفتاح إزالة التكرار لديك هو معرّف المعاملة، فإن القرار الثاني يبدو كنسخة مكررة من الأول ولا تعرف أبداً أن الاسترداد قد مُنح في النهاية. معرّف المعاملة يجمّع الأحداث. لا يعرّفها.

ماذا تكلّفك النسخة المكررة فعلاً
إشعار الاسترداد ليس ضوء حالة. إنه يُطلق إجراءات حقيقية: تلغي الوصول، تخصم رصيداً استهلاكياً، تعكس دفعة لصانع محتوى، تستدعي App Store Server API أو Play Developer API لتأكيد الحالة. شغّل أياً منها مرة ثانية على نسخة مكررة والتكلفة حقيقية.
تتبّع المال. إلغاء الوصول مرتين غير مؤذٍ، لأن الوصول قد زال بالفعل. خصم الرصيد مرتين ليس كذلك: مستخدم اشترى حزمة عملات واستردها يمكن أن يُدفع إلى رصيد سالب يضطر فريق الدعم لديك بعد ذلك إلى تسويته يدوياً. عكس دفعة مرتين يسترد مالاً أعدته بالفعل مرة واحدة، والآن أنت مدين لصانع محتوى باعتذار وتصحيح. وكل نسخة مكررة تعيد معالجتها مقابل API متجر تنفق حصة تحذّر Google صراحة من حمايتها، لذا فإن دفقة من إعادة تسليم Pub/Sub أثناء انقطاع يمكن أن تدفعك إلى تحديد المعدل في اليوم نفسه الذي تقل فيه قدرتك على تحمله.
ترفع نوافذ أدلة الاسترداد المخاطر من جانب Apple. إذا أسكتت إزالة تكرار ساذجة طلبات CONSUMPTION_REQUEST المتكررة التي ترسلها Apple طوال نافذة الاسترداد المفتوحة، فقد تفوتك تلك التي كنت بحاجة للرد عليها، وطلب CONSUMPTION_REQUEST لا ترد عليه خلال 12 ساعة هو استرداد كثيراً ما تمنحه Apple افتراضياً. هذا ليس خصماً مزدوجاً. إنه بيع مفقود إضافة إلى الحوسبة واستدعاءات API والتخزين والدفعات التي أنفقتها بالفعل في تسليم عملية الشراء، ولا يعيد الاسترداد أياً منها.
| نمط الفشل | ما الذي يسير خطأً | ماذا يكلّف |
|---|---|---|
| إعادة خصم رصيد على REFUND مكرر | يصبح الرصيد الاستهلاكي للمستخدم سالباً | وقت دعم يدوي للتسوية، وتجربة عميل سيئة |
| عكس دفعة مرتين | تسترد مالاً أعدته بالفعل مرة واحدة | تصحيح لصانع محتوى وتنظيف محاسبي |
| إعادة المعالجة مقابل API متجر | استدعاءات مكررة تستهلك حصة Play Developer أو App Store Server API | تحديد المعدل أثناء الانقطاع الذي سبّب إعادة التسليم |
| الإفراط في إزالة تكرار CONSUMPTION_REQUEST | تُسقط مطالبة استرداد مميزة كنسخة مكررة زائفة | نافذة 12 ساعة فائتة، فتمنح Apple الاسترداد افتراضياً |
كيفية التعامل مع إشعارات الاسترداد المكررة دون التصرف مرتين
النمط نفسه على المتجرين، بمفتاح مختلف. سجّل التسليم، تحقق من المفتاح قبل أن تتصرف، تصرّف مرة واحدة، وأخبر المتجر دائماً بأنك استلمته.
- أزل التكرار بناءً على معرّف تسليم المتجر، لا المعاملة. استخدم
notificationUUIDلـ Apple وPub/SubmessageIdلـ Google Play. خزّنه بقيد فريد بحيث تخسر نسخة مكررة متزامنة السباق بدلاً من التصرف مرتين. - اجعل الإجراء اللاحق idempotent بشروطه الخاصة. المفتاح على معرّف التسليم يوقف إعادة المعالجة، لكن اكتب الأثر أيضاً بحيث يتحقق الإلغاء أو الخصم أو العكس من الحالة الراهنة أولاً ويكون آمناً للتشغيل مرتين.
- احفظ أولاً، ثم أكّد الاستلام. اكتب الحدث إلى قاعدة بياناتك قبل أن تُرجِع 200 أو تؤكد رسالة Pub/Sub. إن أكّدت أولاً وفشلت الكتابة، يعتبر المتجر الرسالة مُسلّمة ولا يرسلها أبداً مرة أخرى، والآن فقدتها إلى الأبد.
- أرجِع دائماً حالة نجاح، حتى لنسخة مكررة. 200 إلى 206 لـ Apple، و200 لدفع Pub/Sub لـ Google. رفض تكرار بخطأ يجعل المتجر يعيد إرساله فقط.
- اجمع على الكيان، عرّف على الحدث. اجعل مفتاح حالة الشراء المخزنة على
purchaseTokenأوoriginalTransactionIdبحيث تحدّث التسليمات خارج الترتيب صفاً واحداً، لكن عامل كلnotificationUUIDأوmessageIdكحدثه الخاص، لأن عملية شراء واحدة تنتج عدة أحداث بشكل مشروع.
قائمة تحقق قصيرة قبل أن تثق في webhook الاسترداد لديك
- تُزال تكرارات تسليمات Apple بناءً على
notificationUUID، والتكرار لا يكتب شيئاً جديداً لكنه يُرجِع 200 مع ذلك. - تُزال تكرارات تسليمات Google Play بناءً على Pub/Sub
messageId، ويُتحقق منها قبل أي معالجة. - مفتاح حالة الشراء على
purchaseTokenأوoriginalTransactionId، بحيث تهبط الأحداث خارج الترتيب على سجل واحد. - كل أثر جانبي للاسترداد، إلغاء أو خصم أو عكس، آمن للتشغيل أكثر من مرة.
- يكتب معالجك الحدث قبل أن يؤكد الاستلام، لا بعده أبداً.
- تُعامل طلبات CONSUMPTION_REQUEST المتكررة كمطالبات مميزة، لا كنسخ مكررة، بحيث لا تُسقط أي نافذة استرداد مفتوحة.
مرّر نسخة مكررة عبر webhook الخاص بك عمداً وراقب كيف لا تغيّر شيئاً في المرة الثانية. هذا هو الاختبار كله. معالج استرداد آمن للاستدعاء مرتين هو معالج يمكنك التوقف عن القلق بشأنه في اللحظة التي يقرر فيها متجر استدعاءه ست مرات.
الأسئلة الشائعة
- لماذا يتلقى خادمي إشعار استرداد App Store نفسه أكثر من مرة؟
- لأن Apple تعيد إرسال App Store Server Notification V2 حتى خمس مرات، بعد 1 و12 و24 و48 و72 ساعة من المحاولة الأخيرة، كلما لم يستجب خادمك بحالة HTTP بين 200 و206. تحمل كل إعادة محاولة نفس notificationUUID، فيمكنك التعرف عليه وتخطيه.
- أي حقل يجب أن أستخدمه لإزالة تكرار App Store Server Notifications؟
- استخدم notificationUUID. تكرر إعادة المحاولة الحقيقية دائماً نفس notificationUUID، بينما يحصل كل حدث جديد فعلاً، بما في ذلك كل CONSUMPTION_REQUEST جديد، على واحد مختلف، لذا فإن إزالة التكرار بناءً على notificationUUID تتخطى التكرارات دون إسقاط أحداث مميزة.
- هل إشعارات CONSUMPTION_REQUEST المتكررة نسخ مكررة يجب أن أتجاهلها؟
- لا. ترسل Apple إشعارات CONSUMPTION_REQUEST جديدة دورياً طوال نافذة الاسترداد المفتوحة، ويؤكد موظفو Apple أن هذه ليست إعادات محاولة. لكل منها notificationUUID خاص به، لذا عالج كل واحد. إسقاطها يخاطر بتفويت نافذة 12 ساعة التي تمنحها لك Apple للرد.
- كيف أزيل تكرار Google Play Real-time Developer Notifications؟
- اقرأ Pub/Sub messageId من كل إشعار وتحقق منه مقابل تلك التي عالجتها بالفعل قبل التصرف، لأن Pub/Sub تسلّم مرة واحدة على الأقل ويمكن أن ترسل الرسالة نفسها أكثر من مرة. توصي Google بذلك صراحة لتجنب المعالجة المكررة وإهدار حصة API.
- هل يجب أن أُرجِع خطأً لرفض إشعار استرداد مكرر؟
- لا. إرجاع 4xx أو 5xx يخبر المتجر بأن التسليم فشل، فيعيد المحاولة على أي حال. أزل التكرار داخل قاعدة بياناتك الخاصة وأرجِع دائماً حالة نجاح، HTTP 200 إلى 206 لـ Apple أو تأكيد 200 لدفع Google Play.
المصادر وقراءات إضافية
- Apple Developer: App Store Server Notifications V2
- Apple Developer: Responding to App Store Server Notifications
- Apple Developer: notificationUUID
- Apple Developer Forums: App Store Server Notifications V2 retry schedule and CONSUMPTION_REQUEST clarification
- Android Developers: Real-time developer notifications reference
- Android Developers: Purchase lifecycle and RTDNs
- Google Cloud: Pub/Sub subscriber and at-least-once delivery
RefundHalt
الطيار الآلي للاستردادات في App Store وGoogle Play
تابع القراءة
استرداد Family Sharing يعكس دفعة واحدة لكنه قد يترك خمسة أشخاص آخرين يستخدمون تطبيقك، وخادمك وحده هو من يستطيع قطع وصولهم
استرداد Family Sharing يعكس دفعة واحدة لكنه قد يبقي حتى خمسة من أفراد العائلة على ميزاتك المدفوعة. ترسل Apple إشعار REVOKE وتتوقع أن ينهي خادمك ذلك الوصول. إليك كيف تعمل عمليات الاسترداد المشتركة عائليًا وما تكلفة الواحدة منها.
معالجة الاسترداد تتعطل بطرق صامتة، لذا اختبر استرداد المشتريات داخل التطبيق في الـ sandbox قبل أن يفعلها عميل حقيقي
معالجة الاسترداد لديك لا تعمل إلا بعد أن يكون العميل قد رحل بالفعل، لذا يبقى أي خلل فيها غير مرئي حتى يكلفك أموالاً حقيقية. كلا المتجرين يتيحان لك إطلاق استرداد في بيئة اختبار أولاً. إليك كيفية اختبار استرداد المشتريات داخل التطبيق على App Store و Google Play قبل أن يصبح أحدها حقيقياً.