@@ -0,0 +1,136 @@ |
| 1 | +# Stripe Billing |
| 2 | + |
| 3 | +Operator runbook for turning on shithub's paid organization flow. |
| 4 | +Pair this with [`../billing.md`](../billing.md) for the product |
| 5 | +contract and entitlement matrix. |
| 6 | + |
| 7 | +## Preconditions |
| 8 | + |
| 9 | +- A verified Stripe account with payouts enabled to a bank account the |
| 10 | + operator controls. |
| 11 | +- A recurring per-seat Product/Price for Team organizations. The v1 |
| 12 | + price is `$4` USD per active organization member per month. |
| 13 | +- Production `auth.base_url` points at the public HTTPS origin. |
| 14 | +- Web and worker processes share the same billing env vars. |
| 15 | +- Database migrations are current. |
| 16 | + |
| 17 | +Do not enable live billing before Stripe account identity, tax, payout, |
| 18 | +and statement descriptor setup are complete. shithub stores only Stripe |
| 19 | +IDs and derived payment summaries; card data stays in Stripe. |
| 20 | + |
| 21 | +## Stripe setup |
| 22 | + |
| 23 | +1. Create a Product named `shithub Team`. |
| 24 | +2. Create a recurring monthly Price: |
| 25 | + - Billing scheme: per unit. |
| 26 | + - Currency: USD. |
| 27 | + - Unit amount: `400`. |
| 28 | + - Usage: licensed quantity. |
| 29 | +3. Copy the Price ID, for example `price_...`. |
| 30 | +4. Create a restricted secret key if possible. It must be able to: |
| 31 | + - create/read Customers, |
| 32 | + - create Checkout Sessions, |
| 33 | + - create Billing Portal Sessions, |
| 34 | + - update Subscription Items, |
| 35 | + - read invoice/subscription objects delivered by webhooks. |
| 36 | +5. Create a webhook endpoint for: |
| 37 | + - `checkout.session.completed` |
| 38 | + - `customer.subscription.created` |
| 39 | + - `customer.subscription.updated` |
| 40 | + - `customer.subscription.deleted` |
| 41 | + - `invoice.finalized` |
| 42 | + - `invoice.payment_succeeded` |
| 43 | + - `invoice.payment_failed` |
| 44 | + - `invoice.voided` |
| 45 | +6. Point the endpoint at: |
| 46 | + `https://<host>/stripe/webhook` |
| 47 | +7. Copy the webhook signing secret, for example `whsec_...`. |
| 48 | + |
| 49 | +Enable Stripe Tax only after the Stripe account is configured for it. |
| 50 | +If it is enabled in shithub while Stripe is not ready, Checkout can fail |
| 51 | +before the user reaches payment. |
| 52 | + |
| 53 | +## Enable test mode |
| 54 | + |
| 55 | +Use Stripe test-mode keys first: |
| 56 | + |
| 57 | +```sh |
| 58 | +SHITHUB_BILLING__ENABLED=true |
| 59 | +SHITHUB_BILLING__GRACE_PERIOD=336h |
| 60 | +SHITHUB_BILLING__STRIPE__SECRET_KEY=sk_test_... |
| 61 | +SHITHUB_BILLING__STRIPE__WEBHOOK_SECRET=whsec_... |
| 62 | +SHITHUB_BILLING__STRIPE__TEAM_PRICE_ID=price_... |
| 63 | +SHITHUB_BILLING__STRIPE__AUTOMATIC_TAX=false |
| 64 | +``` |
| 65 | + |
| 66 | +Then validate and restart: |
| 67 | + |
| 68 | +```sh |
| 69 | +shithubd config validate |
| 70 | +systemctl restart shithubd-web shithubd-worker |
| 71 | +``` |
| 72 | + |
| 73 | +If `config validate` fails, fix the named key before restarting. The |
| 74 | +web process mounts billing routes only when billing is configured; the |
| 75 | +worker uses the same Stripe credentials for seat quantity sync. |
| 76 | + |
| 77 | +## Smoke test |
| 78 | + |
| 79 | +1. Sign in as an owner. |
| 80 | +2. Visit `/organizations/new`. |
| 81 | +3. Confirm the plan picker appears. |
| 82 | +4. Choose Team, create a test organization, and confirm redirect to |
| 83 | + `/organizations/<org>/settings/billing?notice=team-created`. |
| 84 | +5. Click `Upgrade to Team`. |
| 85 | +6. Complete Stripe Checkout with a test card. |
| 86 | +7. Confirm Stripe redirects back to shithub. |
| 87 | +8. Confirm `/organizations/<org>/settings/billing` eventually shows: |
| 88 | + - current plan: Team, |
| 89 | + - subscription: Active, |
| 90 | + - payment source: Stripe customer configured, |
| 91 | + - a billable member count matching the org membership. |
| 92 | +9. Invite or remove a member and verify the worker updates the Stripe |
| 93 | + subscription item quantity. |
| 94 | + |
| 95 | +If the UI remains Free after checkout, inspect webhook receipts and the |
| 96 | +web journal. The most likely causes are a wrong webhook secret, missing |
| 97 | +event subscription, or Stripe delivering events to the wrong host. |
| 98 | + |
| 99 | +## Go live |
| 100 | + |
| 101 | +1. Replace test-mode values with live-mode Stripe keys and Price ID. |
| 102 | +2. Re-run `shithubd config validate`. |
| 103 | +3. Restart web and worker. |
| 104 | +4. Create a low-risk real organization and complete checkout. |
| 105 | +5. Confirm a live invoice appears in Stripe and shithub. |
| 106 | +6. Confirm payouts are enabled in Stripe and the first payout schedule |
| 107 | + is visible. |
| 108 | + |
| 109 | +Do not promise Enterprise, SAML, SCIM, compliance, or custom support |
| 110 | +from the billing UI until the matching product and operational support |
| 111 | +exist. |
| 112 | + |
| 113 | +## Incident checks |
| 114 | + |
| 115 | +| Symptom | First check | |
| 116 | +| --- | --- | |
| 117 | +| Plan picker missing | `billing.enabled` false or web not restarted. | |
| 118 | +| Checkout button 404s | billing routes were not mounted; validate Stripe config and restart web. | |
| 119 | +| Checkout creation fails | secret key, Team Price ID, Stripe Tax, or network reachability. | |
| 120 | +| Webhook returns 400 | wrong `billing.stripe.webhook_secret` or non-Stripe request. | |
| 121 | +| Subscription stays Free | missing subscription webhook events or unmapped customer/subscription metadata. | |
| 122 | +| Seat count stale | worker not running, seat-sync jobs failing, or Stripe key lacks Subscription Item update permission. | |
| 123 | +| Billing portal unavailable | the organization has no Stripe customer yet; start Checkout first. | |
| 124 | + |
| 125 | +## Rollback |
| 126 | + |
| 127 | +To pause paid onboarding without changing stored subscription state: |
| 128 | + |
| 129 | +1. Set `SHITHUB_BILLING__ENABLED=false`. |
| 130 | +2. Restart web and worker. |
| 131 | + |
| 132 | +Existing local billing rows remain in the database. Billing routes |
| 133 | +unmount, plan comparison links become disabled, and entitlement state |
| 134 | +continues to derive from the latest local billing projection. Handle any |
| 135 | +Stripe-side cancellations or refunds in the Stripe dashboard until the |
| 136 | +admin billing tools exist. |