operations/billing-providers/polar-setup.md

Polar.sh — Hướng dẫn cấu hình (Org Access Token + Webhook + Super-admin)

Hướng dẫn end-to-end để bật Polar.sh làm billing provider cho subscription. Làm sandbox trước, verify xong mới mirror sang production.

Liên quan: ../../architecture/polar-integration-plan.md · README.md

0. Sandbox vs Production

Polar tách hẳn 2 môi trường, dữ liệu + token + secret KHÔNG dùng chung:

Môi trường Dashboard API server (SDK)
Sandbox (test) https://sandbox.polar.sh https://sandbox-api.polar.sh
Production https://polar.sh https://api.polar.sh

Trong super-admin, toggle Sandbox / test mode quyết định SDK nối server nào (isTest=trueserver: 'sandbox'). Sandbox dashboard có banner vàng: "Changes you make here don't affect your live account · Payments are not processed".

⚠️ Token tạo ở sandbox chỉ chạy với server: 'sandbox'. Bật nhầm toggle = Health check 401.


1. Organization Access Token

Token này để backend gọi Polar API (tạo checkout, cancel sub, health check…).

Polar đã bỏ Personal Access Token. Dùng Organization Access Token (scope theo 1 org, nên organization_id được phép bỏ qua trong API call).

Đường đi

Có 2 lối tới cùng một panel "Create API key":

  • Cách A — Org Settings (chính): https://<sandbox|polar>.polar.sh/dashboard/<org-slug>/settings → cuộn xuống mục Developers ("Manage access tokens to authenticate with the Polar API") → Create token.
  • Cách B — Onboarding: màn Account Review → Checkout integration → "Create an API key" (Required) cũng mở đúng panel này.

KHÔNG dùng User Settings → Developer — đó là cấp account, chứa OAuth Applications + báo "Access tokens have moved → use Organization access tokens". Token mình cần là Organization Access Token (cấp org). ❌ KHÔNG bấm New OAuth App (dành cho OAuth flow của app bên thứ 3, không phải API token).

Điền panel "Create API key"

Field Giá trị
Name Glamvoo Booking API (chỉ để nhận diện)
Expiration No expiration (hoặc dài nhất) ❗ — mặc định panel là 30 days; để vậy thì token hết hạn → checkout/health chết. Nếu buộc có hạn, đặt lịch rotate.
Scopes tick đúng 5 scope dưới. Sandbox có thể Select all; production NÊN chỉ tick 5 cái (ít quyền nhất).

Scopes cần (khớp các call trong polar.adapter.ts):

Scope Dùng cho
products:read Health check (products.list)
checkouts:write Tạo checkout (checkouts.create)
subscriptions:read Lấy sub cho customer portal
subscriptions:write Cancel / đổi seats
customer_sessions:write Tạo customer portal session

Lấy giá trị

  • Bấm tạo → Polar hiện token một lần duy nhất (dạng polar_oat_...).
  • Copy ngay, lưu password manager. Mất thì phải tạo token mới.
  • 👉 Sẽ paste vào field API key ở bước 3. Không paste vào chat/commit.

2. Webhook + Signing Secret

Webhook để Polar đẩy event (subscription created/updated/canceled, order paid…) về API mình; signing secret để verify chữ ký (Standard Webhooks).

Đường đi

Settings (org) → Webhooks (nav trái) → Add Endpoint / Create webhook.

Điền form

Field Giá trị
Name tùy chọn (vd Glamvoo Booking)
URL https://app-dev.novagoo.com/api/webhooks/subscription/polar (sandbox/dev)
Format dropdown "Select a payload format"Raw ❗ (Standard Webhooks — KHÔNG chọn Discord/Slack)
Secret để Polar tự generate

URL = tunnel HTTPS trỏ về API local (Bambora/Polar callback dùng chung). Xem ../caddy-dev-setup.md. Production = domain API thật https://<prod>/api/webhooks/subscription/polar (KHÔNG phải app-dev).

Route POST /webhooks/subscription/:provider nhận mọi BillingProvider enum, nên …/polar hoạt động ngay khi enum có POLAR (đã có).

Events cần tick

Màn "Create webhook" liệt kê event theo nhóm (Benefit, Customer, Order, Product…). Cuộn xuống tìm nhóm Subscription + Order, tick:

Subscription: subscription.created, subscription.updated, subscription.active, subscription.canceled, subscription.uncanceled, subscription.revoked, subscription.past_due

Order: order.created, order.paid, order.refunded

order.updated không cần (mình không map — chỉ dùng created/paid cho renewal, refunded cho refund). Tick thừa cũng vô hại (event không map → ghi inbox rồi bỏ qua), nhưng để gọn thì bỏ.

Bỏ qua hẳn các nhóm Benefit / Benefit Grant / Checkout / Customer / Customer Seat / Member / Organization / Product — không thuộc luồng subscription.

Lấy giá trị

  • Mở lại endpoint vừa tạo → phần Signing SecretReveal/Show → copy.
  • 👉 Sẽ paste vào field Webhook secret ở bước 3. Không paste vào chat.

Webhook chỉ "sống" khi tunnel app-dev.novagoo.com đang chạy API local yarn start:dev đang bật. Lúc tạo endpoint thì chưa cần; lúc test checkout thật (P4) thì cả hai phải bật.


3. Cấu hình Super-admin → Health check → Activate

Vào /admin/superadmin/settings?tab=billingAdd billing provider.

Health check chạy theo config id cụ thể, độc lập trạng thái active — nên kiểm tra credentials TRƯỚC, xanh rồi mới Activate (Activate = go-live).

Field Giá trị
Sandbox / test mode ON (sandbox)
Store ID để trống (field LS-legacy; token đã org-scoped)
Display name Polar Sandbox (tùy)
API base URL để trống (Polar dùng toggle server, không dùng base URL)
Store domain để trống (LS-legacy)
API key Organization Access Token (polar_oat_...) từ bước 1
Webhook secret Signing secret từ bước 2

Sau đó:

  1. Save.
  2. Health check ngay trên dòng config vừa tạo → kỳ vọng ok / xanh (backend gọi products.list với credentials của config đó).
  3. Xanh rồi → Activate dòng Polar (chỉ 1 config active tại 1 thời điểm).

Troubleshooting Health check

Triệu chứng Nguyên nhân thường gặp
401 / invalid token Toggle sandbox/prod sai môi trường với token; hoặc copy thiếu ký tự
403 / forbidden Token thiếu scope products:read
"No active Polar config" Chưa Activate, hoặc config active là provider khác

4. Sau khi Health check xanh (các bước tiếp)

  • Plan mappings (P4.5): trong config → Plan mappings → map planKey = premium_1_monthproduct id Polar.
    • Sandbox Premium product id: 850a5748-9270-44f3-88d2-ed076f3d8d20.
  • ⚠️ TẮT Free trial trên Polar product. Trial 14 ngày do hệ thống mình quản (trial-on-signup) — bật trial trên Polar nữa sẽ double-trial + hoãn charge 14 ngày. Polar chỉ charge khi owner subscribe. Xem ../../architecture/subscription-enforcement-plan.md.
  • Checkout E2E (P4): chạy thử checkout sandbox (cần tunnel + API live), đối chiếu field name payload webhook thật với polar.event-mapper.ts (với Polar trial OFF, status: active không có field trial → trialEndsAt: null hiện tại trong mapper là đúng, không cần sửa).

5. Mirror sang Production (sau khi sandbox xong)

  1. Tạo Organization Access Token tại https://polar.sh/dashboard/<org>/settings (org production, ví dụ novago).
  2. Tạo webhook endpoint trỏ domain production, Format Raw, copy secret mới.
  3. Super-admin: tạo config mới với Sandbox / test mode = OFF, paste token + secret production → Activate → Health check.
  4. Plan mapping trỏ product id production (org novago: 6feccf1c-74e3-4cef-b76f-5d639079bab5).