server: checkout accepts promotion codes (allow_promotion_codes) (#1321)
A dashboard-minted promo code (e.g. an operator comp) can now ride the normal space-upgrade checkout โ a discounted space is entitlement-paid, so it stops counting against the free-space quota. Includes the session's SSH ledger why-entry.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
4 files changed,
+16
−0
billing.go+4 −0changelog/2026-07-23-checkout-promo-codes.md+2 −0cloud_billing_test.go+3 −0web/status/ssh/2026-07-23-checked-cloud-provisioner-quota-config-f.json+7 −0
diff --git a/billing.go b/billing.go| index 0e2ed43..49a90d3 100644 |
| --- a/billing.go |
| +++ b/billing.go |
| @@ -424,6 +424,10 @@ func (b *stripeBilling) createCheckoutSession(spaceID int64, tier billingTier, b |
| form.Set("subscription_data[metadata][band]", band) |
| form.Set("success_url", successURL) |
| form.Set("cancel_url", cancelURL) |
| + // Promotion codes: lets a dashboard-minted code (e.g. an operator comp โ |
| + // a discounted space is entitlement-paid, so it stops counting against the |
| + // free-space quota) ride the normal checkout instead of needing a side door. |
| + form.Set("allow_promotion_codes", "true") |
| // Stripe Tax (docs/pricing-bands.md ยง VAT): consumer Prices are created |
| // tax-inclusive (the sticker is the total), Business tiers ex-VAT โ so a |
| // Business checkout also collects an optional VAT ID for the EU reverse |
diff --git a/changelog/2026-07-23-checkout-promo-codes.md b/changelog/2026-07-23-checkout-promo-codes.md| new file mode 100644 |
| index 0000000..53d6a06 |
| --- /dev/null |
| +++ b/changelog/2026-07-23-checkout-promo-codes.md |
| @@ -0,0 +1,2 @@ |
| +๐ค Upgrading a space now accepts promotion codes โ if you've been given a |
| +discount code, there's an "Add promotion code" field on the checkout page. |
diff --git a/cloud_billing_test.go b/cloud_billing_test.go| index 1725245..eb6a59c 100644 |
| --- a/cloud_billing_test.go |
| +++ b/cloud_billing_test.go |
| @@ -132,6 +132,9 @@ func TestCheckoutMintsSession(t *testing.T) { |
| if !strings.Contains(f.Get("success_url"), "upgraded="+host) { |
| t.Errorf("success_url = %q", f.Get("success_url")) |
| } |
| + if f.Get("allow_promotion_codes") != "true" { |
| + t.Errorf("allow_promotion_codes = %q, want true (dashboard promo codes must be redeemable)", f.Get("allow_promotion_codes")) |
| + } |
| for k := range f { |
| if strings.Contains(k, "customer") || strings.Contains(k, "email") { |
| t.Errorf("checkout form leaks a customer field: %s", k) |
diff --git a/web/status/ssh/2026-07-23-checked-cloud-provisioner-quota-config-f.json b/web/status/ssh/2026-07-23-checked-cloud-provisioner-quota-config-f.json| new file mode 100644 |
| index 0000000..b525d01 |
| --- /dev/null |
| +++ b/web/status/ssh/2026-07-23-checked-cloud-provisioner-quota-config-f.json |
| @@ -0,0 +1,7 @@ |
| +{ |
| + "date": "2026-07-23", |
| + "operator": "Paul Campbell", |
| + "window": "โ20:05โ20:20 UTC (two short sessions)", |
| + "why": "Checked cloud provisioner quota config for the space-quota question (read-only; /data/cloud.env turned out operator-unreadable โ no values seen, no secrets transited the session). No deploys.", |
| + "refs": [] |
| +} |