Billing (Stripe интеграция)
Dictaro използва Stripe за обработка на абонаменти. Потребителите могат да се абонират за Pro план чрез Stripe Checkout и да управляват абонаментите си чрез Stripe Customer Portal.
Общ поток
Checkout Session Creation
POST /billing/create-checkout (RequireAuth)
Request:
{
"redirect_uri": "http://localhost:9876/callback",
"state": "random_state",
"interval": "monthly" // или "annual"
}
Response:
{
"checkout_url": "https://checkout.stripe.com/c/pay/..."
}
Логика:
- Проверка дали потребителят вече не е на Pro план
- Mapping на
interval:"annual"/"yearly"/"year"-> Stripe"year", default ->"month" - Резолвиране на
price_idот PricingService (кеширани Stripe prices за съответния product) - Lazy Stripe Customer creation: Ако потребителят няма
stripe_customer_id, създава Stripe Customer с metadatadictaro_user_id - Създаване на Checkout Session с:
mode: "subscription"allow_promotion_codes: truecustomer: stripe_customer_id- Success URL:
{BASE_URL}/billing/checkout-success?session_id={CHECKOUT_SESSION_ID}&redirect_uri=...&state=... - Cancel URL:
{BASE_URL}/billing/checkout-cancel
Choose Plan Page (GET /billing/choose-plan)
HTML страница, която показва наличните планове (monthly/annual) от Stripe. JWT token-ът се подава като query parameter (тъй като страницата се отваря директно в browser без Bearer header).
Webhook обработка
POST /billing/webhook
Stripe изпраща webhooks за промени в абонаменти. Server-ът валидира подписа чрез STRIPE_WEBHOOK_SECRET.
Обработвани events:
| Stripe Event | Действие |
|---|---|
checkout.session.completed | Upgrade на потребителя към Pro; запазва stripe_subscription_id. Idempotent -- проверява дали вече е Pro. |
customer.subscription.updated | Ако status == "active" и потребителят не е Pro -> upgrade. |
customer.subscription.deleted | Downgrade към Free; изтрива stripe_subscription_id. |
invoice.payment_failed | Логване на предупреждение (без промяна на план). |
Lookup на потребител
Webhook-овете идентифицират потребителя чрез customer ID от event data -> SELECT ... FROM users WHERE stripe_customer_id = $1.
Checkout Success/Cancel
GET /billing/checkout-success
Два режима на работа:
-
С
session_idиredirect_uri(normal flow от desktop client):- Retrieves Checkout Session от Stripe
- Намира потребителя по
stripe_customer_id - Race condition handling: ако webhook-ът все още не е дошъл, ръчно обновява плана
- Издава нов JWT с
plan = "pro" - Redirect към
redirect_uri?token=NEW_JWT&state=...
-
Без параметри (fallback):
- Показва статична HTML страница "Payment Successful"
GET /billing/checkout-cancel
Показва статична HTML страница "Checkout Cancelled".
Customer Portal
POST /account/portal-url (RequireAuth)
Създава Stripe Customer Portal session и връща URL-а. Позволява на потребителите да управляват абонамента си (upgrade, downgrade, cancel, update payment method).
Response:
{
"url": "https://billing.stripe.com/p/session/..."
}
Изисква потребителят да има stripe_customer_id.
Stripe Sync при Login
При всеки login (OAuth и email), сървърът проверява реалния статус на абонамента в Stripe:
Тази синхронизация гарантира, че Stripe е source of truth за плана, дори ако webhook-ът е бил пропуснат.
Pricing Service
PricingService извлича и кешира активните цени от Stripe за конфигурирания product:
- Cache TTL: 5 минути
- Thread-safe: Използва
sync.RWMutexс double-check locking - Endpoint:
GET /pricing/plans-- публичен, връща масив отPlanInfoобекти
[
{
"price_id": "price_xxx",
"interval": "month",
"amount": 999,
"currency": "eur",
"formatted_price": "€9.99"
}
]
Promotion Codes
Stripe Checkout Sessions се създават с AllowPromotionCodes: true, което позволява на потребителите да въведат промо код при плащане.