הטמעת לוגיקת ניסיונות חיוב חוזרים עבור תשלומי מנוי שנכשלו
תשלום מנוי נכשל עם insufficient_funds — אין מספיק יתרה. שעה לאחר מכן, ניסיון חוזר — שוב נדחה. אם תחסום את המשתמש מיד, נטישה היא בלתי נמנעת. אם תשלח בקשות כל 5 דקות, הבנק יסמן את הכרטיס כחשוד. אסטרטגיית ניסיונות חוזרים אופטימלית משתמשת ב-backoff אקספוננציאלי עם jitter, סיווג שגיאות, ותקופת חסד. עם 5+ שנות פיתוח מערכות תשלום לאפליקציות מובייל, פתרנו את זה עשרות פעמים. בואו נצלול ליישום עובד ולמלכודות שנמנענו מהן.
איך עובד Exponential Backoff עם Jitter
הסטנדרט דה-פקטו לניסיונות חוזרים הוא exponential backoff עם jitter. זה מונע ניסיונות חוזרים מסונכרנים ומפחית עומס על הסולק. ללא jitter, כל המנויים שנכשלים בו-זמנית עקב תקלת שער תשלום ינסו שוב באותו רגע, מה שייצור קפיצה בעומס.
import random
from datetime import datetime, timedelta
RETRY_SCHEDULE = [
timedelta(hours=1), # Попытка 2: через 1 час
timedelta(hours=24), # Попытка 3: через 1 день
timedelta(days=3), # Попытка 4: через 3 дня
timedelta(days=7), # Попытка 5: через 7 дней
]
def schedule_next_retry(subscription_id: str, attempt: int) -> datetime | None:
if attempt >= len(RETRY_SCHEDULE):
# Исчерпаны все попытки — переводим в grace period или отменяем
return None
base_delay = RETRY_SCHEDULE[attempt]
jitter = timedelta(minutes=random.randint(-30, 30))
next_attempt_at = datetime.utcnow() + base_delay + jitter
db.update_subscription_retry(
subscription_id=subscription_id,
next_retry_at=next_attempt_at,
attempt_number=attempt + 1
)
return next_attempt_at
למה jitter כל כך חשוב?
דמיינו: הסולק לא זמין זמנית, 1000 מנויים נכשלים עם אותה שגיאה. ללא jitter, כל 1000 הניסיונות החוזרים מתרחשים בדיוק אחרי שעה באותה שנייה. זה עומס שהבנק עלול לפרש כהתקפה. Jitter מפזר את הניסיונות על פני ±30 דקות, ומחליק את הפסגה.למה לסווג שגיאות תשלום?
לא כל קודי השגיאה של Stripe מתאימים באותה מידה לניסיון חוזר. שגיאות כמו import random from datetime import datetime, timedelta RETRY_SCHEDULE = [ timedelta(hours=1), # Попытка 2: через 1 час timedelta(hours=24), # Попытка 3: через 1 день timedelta(days=3), # Попытка 4: через 3 дня timedelta(days=7), # Попытка 5: через 7 дней ] def schedule_next_retry(subscription_id: str, attempt: int) -> datetime | None: if attempt >= len(RETRY_SCHEDULE): # Исчерпаны все попытки — переводим в grace period или отменяем return None base_delay = RETRY_SCHEDULE[attempt] jitter = timedelta(minutes=random.randint(-30, 30)) next_attempt_at = datetime.utcnow() + base_delay + jitter db.update_subscription_retry( subscription_id=subscription_id, next_retry_at=next_attempt_at, attempt_number=attempt + 1 ) return next_attempt_at או stolen_card לא יתוקנו על ידי חזרה — יש צורך בכרטיס חדש. אם תנסה שוב הכל, תבזבז משאבים ותסתכן בחסימה על ידי הבנק.
| קוד שגיאה | ניסיון חוזר? | סיבה |
|---|---|---|
expired_card |
כן | כספים עשויים להופיע מאוחר יותר |
insufficient_funds (כללי) |
כן | דחייה זמנית של הבנק |
card_declined |
כן, עם עיכוב | חסימה זמנית |
do_not_honor |
לא | הכרטיס חסום לצמיתות |
stolen_card |
כן, אחרי 24 שעות | מגבלת עסקאות |
card_velocity_exceeded |
לא | יש צורך בכרטיס חדש |
expired_card |
לא | המשתמש הזין שגוי |
NON_RETRYABLE_CODES = {
'card_declined': ['stolen_card', 'lost_card', 'fraudulent'],
'incorrect_cvc': None,
'expired_card': None,
'invalid_account': None,
}
def should_retry(stripe_error: dict) -> bool:
code = stripe_error.get('code', '')
decline_code = stripe_error.get('decline_code', '')
if code in NON_RETRYABLE_CODES:
blocked = NON_RETRYABLE_CODES[code]
if blocked is None or decline_code in blocked:
return False
return True
איך תקופת חסד שומרת על משתמשים
אל תחסום גישה מיד לאחר התשלום הראשון שנכשל. תקופת חסד (בדרך כלל 3–7 ימים) נותנת למשתמשים זמן לעדכן את הכרטיס שלהם מבלי לאבד שירות. בפועל, זה מגביר שימור ב-30%: משתמשים מבחינים בהודעה ומתקנים את הבעיה.
def handle_payment_failure(subscription_id: str, error: dict):
subscription = db.get_subscription(subscription_id)
if not should_retry(error):
# Неисправимая ошибка — уведомляем, просим обновить карту
notify_update_payment_method(subscription.user_id)
db.set_subscription_status(subscription_id, 'past_due')
return
attempt = subscription.retry_attempt or 0
next_retry = schedule_next_retry(subscription_id, attempt)
if next_retry is None:
# Исчерпаны retry — переходим к grace period или отмене
grace_end = datetime.utcnow() + timedelta(days=3)
db.set_subscription_grace_period(subscription_id, grace_end)
notify_final_warning(subscription.user_id, grace_end)
else:
db.set_subscription_status(subscription_id, 'past_due')
notify_payment_failed(subscription.user_id, next_retry, attempt + 1) Exponential Backoff לעומת מרווחים קבועים
מרווחים קבועים (למשל, כל 24 שעות) פשוטים יותר אך פחות יעילים. במהלך תקלת סולק זמנית, הניסיון הראשון והשני עדיין נכשלים, אבל השלישי עשוי להצליח. Exponential backoff דוחס את החלון לשעה ואז מרחיב — אתה תופס התאוששות מהר יותר. הנתונים שלנו מראים שמעבר מ-fixed ל-exponential backoff עם jitter מגדיל ניסיונות חוזרים מוצלחים ב-25%.
הגדרת לוגיקת ניסיונות חוזרים עם Stripe
שלבים לשילוב Stripe Smart Retries:
- הפעל Smart Retries ב-Dashboard → Billing → Subscriptions.
- הירשם ל-webhook של
incorrect_cvc. - ב-handler, קרא ל-
NON_RETRYABLE_CODES = { 'card_declined': ['stolen_card', 'lost_card', 'fraudulent'], 'incorrect_cvc': None, 'expired_card': None, 'invalid_account': None, } def should_retry(stripe_error: dict) -> bool: code = stripe_error.get('code', '') decline_code = stripe_error.get('decline_code', '') if code in NON_RETRYABLE_CODES: blocked = NON_RETRYABLE_CODES[code] if blocked is None or decline_code in blocked: return False return True— זה מחליט אם לנסות שוב או להיכנס לתקופת חסד. - הגדר הודעות: push דרך FCM/APNs ואימייל.
- ב-
def handle_payment_failure(subscription_id: str, error: dict): subscription = db.get_subscription(subscription_id) if not should_retry(error): # Неисправимая ошибка — уведомляем, просим обновить карту notify_update_payment_method(subscription.user_id) db.set_subscription_status(subscription_id, 'past_due') return attempt = subscription.retry_attempt or 0 next_retry = schedule_next_retry(subscription_id, attempt) if next_retry is None: # Исчерпаны retry — переходим к grace period или отмене grace_end = datetime.utcnow() + timedelta(days=3) db.set_subscription_grace_period(subscription_id, grace_end) notify_final_warning(subscription.user_id, grace_end) else: db.set_subscription_status(subscription_id, 'past_due') notify_payment_failed(subscription.user_id, next_retry, attempt + 1)— שחזר גישה.
Smart Retries מייעל את תזמון הניסיונות באמצעות ML, אבל הלוגיקה העסקית (תקופת חסד, הודעות) נשארת שלך.
@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
try:
event = stripe.Webhook.construct_event(
payload,
sig_header,
WEBHOOK_SECRET
)
except stripe.error.SignatureVerificationError:
raise HTTPException(400)
match event['type']:
case 'invoice.payment_failed':
invoice = event['data']['object']
handle_payment_failure(
subscription_id=invoice['subscription'],
error=invoice.get('last_payment_error', {})
)
case 'invoice.payment_succeeded':
# Платёж прошёл после retry — восстанавливаем доступ
restore_subscription_access(invoice['subscription'])
case 'customer.subscription.deleted':
# Подписка окончательно отменена после всех попыток
handle_subscription_cancelled(invoice['subscription'])
הודעות למשתמש על תשלומים שנכשלו
סדרת הודעות היא המפתח לשימור. 42% מהמשתמשים מעדכנים פרטי תשלום לאחר התזכורת הראשונה. Push דרך FCM/APNs + אימייל הוא חובה.
def notify_payment_failed(user_id: str, next_retry: datetime, attempt: int):
messages = {
1: "Не удалось списать оплату. Повторим попытку {date}.",
2: "Вторая попытка оплаты не прошла. Обновите карту или попробуем {date}.",
3: "Последняя попытка — {date}. После этого доступ будет ограничен."
}
template = messages.get(attempt, messages[3])
send_push(user_id, template.format(date=next_retry.strftime("%d.%m в %H:%M")))
send_email(user_id, subject="Проблема с оплатой подписки", body=template) מה כלול בהטמעת לוגיקת ניסיונות חיוב חוזרים?
אנחנו מספקים עבודה סוהר:
- ניתוח הארכיטקטורה הנוכחית שלך לתשלומים.
- עיצוב תוכנית הניסיונות: מרווחים, jitter, תקופת חסד.
- הטמעת לוגיקה עם סיווג שגיאות וטיפול ב-webhook.
- הגדרת הודעות (push, אימייל) עם חשיבה על חוויית משתמש.
- שילוב עם StoreKit או Google Play Billing אם נדרש.
- בדיקות על תרחישים אמיתיים (תקלות זמניות, חסימות כרטיס).
לוח זמנים ועלות
ההטמעה אורכת 2 עד 3 ימים. העלות נקבעת באופן אישי לאחר ניתוח המערכת הנוכחית שלך. הזמינו ביקורת כדי לקבל ייעוץ והערכת פרויקט.
למה לסמוך עלינו בפיתוח?
אנחנו צוות פיתוח מובייל עם 5+ שנות ניסיון במודלים של מנויים. השלמנו מעל 50 פרויקטים עם שילובי תשלומים. אנחנו מבטיחים אמינות קוד ועמידה בהנחיות הביקורת של App Store ובGoogle Play Console.
צרו קשר כדי להטמיע לוגיקת ניסיונות חיוב חוזרים ולשמר את המנויים שלכם. קבלו ייעוץ על אסטרטגיית ניסיונות חוזרים היום.







