Жизнен цикъл на абонамент
Този документ описва пълния жизнен цикъл на Stripe абонамент в Dictaro — от създаване на checkout session до отказ и downgrade.
Архитектура на billing системата
Checkout процес
1. Избор на план
Потребителят избира план чрез страницата GET /billing/choose-plan:
PricingService кешира цените от Stripe за 5 минути (cacheTTL: 5 * time.Minute), за да намали API заявките.
2. Създаване на Checkout Session
Lazy Stripe Customer създаване:
- Stripe Customer се създава само при първия checkout
- Метаданни:
dictaro_user_idза свързване с вътрешния потребител stripe_customer_idсе запазва в users таблицата
Promotion codes:
AllowPromotionCodes: trueпозволява потребителите да въведат промо код при checkout
3. Успешно плащане
Race condition handling: Checkout success redirect може да пристигне преди webhook-а. Сървърът проверява и обновява плана проактивно, за да осигури незабавен достъп.
Webhook обработка
Поддържани webhook events
checkout.session.completed
Функция: HandleCheckoutCompleted
1. Извлича customer_id и subscription_id от event data
2. Търси потребител по stripe_customer_id
3. Idempotent проверка: ако вече е pro с този subscription → skip
4. UPDATE users SET plan='pro', stripe_subscription_id=? WHERE id=?
5. Логва upgrade
customer.subscription.updated
Функция: HandleSubscriptionUpdated
1. Извлича customer_id и status от event data
2. Търси потребител по stripe_customer_id
3. Ако status == 'active' И user.plan != 'pro' → upgrade
4. Потвърждава статус чрез лог
Този event се получава при:
- Подновяване на абонамент
- Промяна на план (monthly ↔ annual)
- Реактивиране след неуспешно плащане
customer.subscription.deleted
Функция: HandleSubscriptionDeleted
1. Извлича customer_id от event data
2. Търси потребител по stripe_customer_id
3. UPDATE users SET plan='free', stripe_subscription_id=NULL
4. Логва downgrade
Този event се получава когато:
- Абонаментът изтече след отказ (
cancel_at_period_end) - Администраторско отменяне от Stripe Dashboard
- Абонаментът е прекратен поради многократно неуспешно плащане
invoice.payment_failed
Функция: HandlePaymentFailed
1. Логва предупреждение с customer_id и subscription_id
2. Не променя плана (Stripe опитва повторно автоматично)
Бележка: Stripe има вградена retry логика за неуспешни плащания. Планът се променя на free едва когато Stripe изпрати subscription.deleted след изчерпване на опитите.
Състояния на абонамент
Stripe sync при login
При всеки OAuth login, сървърът синхронизира състоянието с Stripe:
SyncStripeSubscription осигурява, че Stripe е source of truth за статуса на абонамента. Това обработва случаи, при които webhook е бил пропуснат.
Stripe Customer Portal
Потребителят може от Customer Portal да:
- Промени платежен метод
- Прегледа фактури и разписки
- Откаже абонамент (cancel_at_period_end)
- Реактивира отказан абонамент
- Промени план (monthly ↔ annual)
Return URL: {BASE_URL}/billing/portal-return
Metrics и мониторинг
Системата събира Prometheus метрики за billing операциите:
| Метрика | Labels | Описание |
|---|---|---|
billing_checkouts_total | status: success/error | Общ брой checkout сесии |
billing_webhooks_total | event_type, status | Webhook events по тип и резултат |
Сигурност
- Webhook верификация: Всеки webhook се верифицира чрез
Stripe-Signatureheader иSTRIPE_WEBHOOK_SECRET - Body limit: Максимален размер на webhook payload: 1 MB
- Idempotency:
HandleCheckoutCompletedпроверява за дублиращи се events - Production валидация:
STRIPE_WEBHOOK_SECRETе задължителен в production (GIN_MODE=release) - JWT обновяване: Нов JWT с актуален план се издава при checkout success redirect