Преминете към основното съдържание

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/..."
}

Логика:

  1. Проверка дали потребителят вече не е на Pro план
  2. Mapping на interval: "annual"/"yearly"/"year" -> Stripe "year", default -> "month"
  3. Резолвиране на price_id от PricingService (кеширани Stripe prices за съответния product)
  4. Lazy Stripe Customer creation: Ако потребителят няма stripe_customer_id, създава Stripe Customer с metadata dictaro_user_id
  5. Създаване на Checkout Session с:
    • mode: "subscription"
    • allow_promotion_codes: true
    • customer: 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.completedUpgrade на потребителя към Pro; запазва stripe_subscription_id. Idempotent -- проверява дали вече е Pro.
customer.subscription.updatedАко status == "active" и потребителят не е Pro -> upgrade.
customer.subscription.deletedDowngrade към 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

Два режима на работа:

  1. С 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=...
  2. Без параметри (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, което позволява на потребителите да въведат промо код при плащане.