Billing is ready to take real money, and stays on Stripe's test key until the switch. The g1t plan is $21 a month, all-in, with $10 of usage included: the card processing fee is part of the price, and no payment carries a fee line any more (old fee rows still read as they were). Card, billing address, tax IDs, invoices and cancelling are on Stripe's billing page (Manage billing), whose configuration billing keeps in step; Checkout asks for the billing address and a tax ID; g1t's Billing page keeps usage, the spend limit, budget alerts and prepaying. Members other than owners and billing managers no longer see billing details or invoices, from the page or the API.
Money is credited exactly once and never charged twice: a workspace invoice is recorded as open before it is paid, unpaid ones are settled before a new one is made and what is still owed is invoiced after, a credit is written before its payment is marked paid, a payment that fails for a reason other than the card is tried again while one that needs a card stops work, one workspace's error no longer holds up the month close or autopay for the rest, and a month waits for its metered usage before it closes. Webhooks read invoices, charges and disputes again at the pinned API version, ignore events from the other Stripe mode, reverse plan refunds against the plan, and follow a dispute's current state (a lost one takes the credit back and stops work until repaid, a later win gives it back). Live charges are refused, not sent untaxed, while Stripe Tax is off. Customers get the owner's email, are created once, and are forgotten when Stripe has deleted them. Only payments made since payments went live count toward trust. A CHARGING switch, on here and off for self-hosters, decides whether g1t charges, so a missing Stripe key no longer makes compute free. Usage reported twice spends included usage once, the spend limit counts what the plan's included usage will pay, a staff hold stops running apps, storage is charged by the month's own days, goodwill is granted once per request, and runner minutes are never sold below cost plus the margin. Metering no longer drops usage: runner time is kept until billing confirms it and retried, a build's charge is retried by the deployments sweep, observability data is capped at 1 GiB a month per workspace and charged per GB past it (migration 0059), docs indexing is metered with a monthly cap on free workspaces, package storage is charged at month end as it always should have been, and every month-end report says which month it is for. Staff can detach a workspace from test-mode Stripe before the live key goes in (sudo: each workspace's page, and Detach every workspace on the Stripe page, with a preview): the plan is cancelled, card and customer forgotten, test payments reversed and test-period usage written off as goodwill, with usage history kept, so the first live close charges nothing from the test period (migration 0060). The Stripe page lists a go-live checklist, Stripe Tax and the portal configuration included. Migration 0058 adds disputes and payment safety. The terms, refunds and privacy policies describe g1t as it is: agents you bring on your own provider, what is charged and when, taxes, failed payments, disputes, price-change notice, self-serve deletion; Expo and Google Workspace are listed as subprocessors. Docs, pricing, landing and llms.txt say $21 and no card fee.
- Checked:
- cargo test for g1t-billing (279), g1t-contracts (306) and g1t-api (201), cargo check of the workspace and of billing for wasm32, typecheck and tests of the site (910), sudo (156), contracts (48), telemetry (33), artifacts (126), deployments (89) and the runner (91), production builds of the site, sudo and docs, and three independent reviews of the money paths, whose findings are fixed here.
| 1802 | 1802 | "Change a workspace's budget. Give only what you change; the rest stays as it is. `amount_micros` is the monthly spend limit, up to `max_amount_micros`, or null for the automatic one. `alerts` is some of 50, 75, 90 and 100, in percent of the limit. `pause_at_limit` false makes the limit alert only, without pausing usage; g1t's own ceiling still applies. `webhook` is an https:// address sent a JSON POST for each alert, or null for none. Owners only, as a person: signed in or with a personal access token. A workspace's own token and a run's token can read the budget but never change it. Returns the budget." | |
| 1803 | 1803 | } | |
| 1804 | 1804 | Op::ListInvoices => { | |
| 1805 | − | "A workspace's invoices, newest first. `invoices` is every invoice billed to it (the plan, usage and any AI credit bought earlier), each with its `status`, `total_cents`, `currency` and links to view it and its PDF. `usage_invoices` are g1t's itemised invoices for usage, one when each month closes and one each time the card is charged near the limit, with their `lines` in millionths of a dollar; `amount_micros` is the usage, and the card processing fee (`fee_micros`) and tax (`tax_micros`) are on top. Prices exclude tax: Stripe adds it where it applies. `upcoming` is what the next invoice comes to so far. `unavailable` says why `invoices` could not be read just now, when it could not. Members of the workspace only." | |
| 1805 | + | "A workspace's invoices, newest first. `invoices` is every invoice billed to it (the plan, usage and any AI credit bought earlier), each with its `status`, `total_cents`, `currency` and links to view it and its PDF. `usage_invoices` are g1t's itemised invoices for usage, one when each month closes and one each time the card is charged near the limit, with their `lines` in millionths of a dollar; `amount_micros` is the usage, and tax (`tax_micros`) is on top; there is no card fee, so `fee_micros` is 0 except on invoices from before g1t's prices included it. Prices exclude tax: Stripe adds it where it applies. `upcoming` is what the next invoice comes to so far. `unavailable` says why `invoices` could not be read just now, when it could not. Owners and billing managers only." | |
| 1806 | 1806 | } | |
| 1807 | 1807 | Op::GetBillingDetails => { | |
| 1808 | 1808 | "Who a workspace's invoices are made out to: the billing `email`, `name`, `address`, tax ID (`tax_id_type`, `tax_id`), `po_number` and the invoices' `language`, with the default `payment_method` as far as it is safe to show (its kind, brand, last four digits and expiry). `customer` is false until the workspace has been set up to pay. Tax is worked out from the address: `tax_location` says whether it is enough for that (a country, and in the US a ZIP code), `tax_address_needed_at` is set while g1t is holding a charge for want of one, `tax_id_status` is Stripe's check of the tax ID (`pending`, `verified`, `unverified` or `unavailable`), and `tax_exempt` is `none`, `exempt` or `reverse`. Members of the workspace only." |
| 6814 | 6814 | } | |
| 6815 | 6815 | ], | |
| 6816 | 6816 | "created_at": "2026-10-01T00:05:00.000Z", | |
| 6817 | − | "fee_micros": 1600000, | |
| 6817 | + | "fee_micros": 0, | |
| 6818 | 6818 | "tax_micros": 3510000 | |
| 6819 | 6819 | } | |
| 6820 | 6820 | ], | |
| 6821 | 6821 | "upcoming": { | |
| 6822 | 6822 | "closes_at": "2026-11-01T00:00:00.000Z", | |
| 6823 | − | "subscriptions_micros": 20000000, | |
| 6823 | + | "subscriptions_micros": 21000000, | |
| 6824 | 6824 | "usage_micros": 10210000, | |
| 6825 | 6825 | "total_micros": 30210000 | |
| 6826 | 6826 | }, |
| 448 | 448 | and [git operations](/guides/usage-and-billing/#git-operations). | |
| 449 | 449 | - **Status.** Depends on Cloudflare. | |
| 450 | 450 | ||
| 451 | − | ### Payments are in test mode | |
| 451 | + | ### Payments in test mode | |
| 452 | 452 | ||
| 453 | − | While payments are in test mode, no real card is charged, so a card check | |
| 454 | − | proves nothing. **Status.** Live payments are planned. | |
| 453 | + | A g1t whose payments use Stripe's test mode, such as a self-hosted one set | |
| 454 | + | up with test keys, or g1t.sh before its live payments are switched on, | |
| 455 | + | charges no real card, so a card check there proves nothing. With live | |
| 456 | + | payments, the card check and every charge are real. **Status.** By design | |
| 457 | + | for test setups. | |
| 455 | 458 | ||
| 456 | 459 | ## Accounts, status and self-hosting | |
| 457 | 460 |
| 1975 | 1975 | [runner minutes](/guides/usage-and-billing/#runner-minutes), its time | |
| 1976 | 1976 | rounded up to the whole minute, at the rate of the machine it ran on. A job billing refuses does not start: it is recorded as failed | |
| 1977 | 1977 | with "Not started:" and the reason, such as "Workflows run in g1t's | |
| 1978 | − | sandboxes, which cost real money, so they need the g1t plan ($20 a month) | |
| 1978 | + | sandboxes, which cost real money, so they need the g1t plan ($21 a month) | |
| 1979 | 1979 | or a card check", and what to do about it. The | |
| 1980 | 1980 | Actions page tells people with Write on a repository whose workspace | |
| 1981 | 1981 | cannot run jobs before the first run. |
| 296 | 296 | leaves any that started within the last two hours | |
| 297 | 297 | (`rollout_active_grace_period` in `services/runner/wrangler.jsonc`) to | |
| 298 | 298 | finish first. A workflow job on g1t's machines runs for at most 60 | |
| 299 | − | minutes, so a deploy never cuts one short; an agent run whose guardrails | |
| 300 | − | allow more than two hours can be. | |
| 299 | + | minutes, so a deploy never cuts one short. | |
| 301 | 300 | ||
| 302 | 301 | The standard sandbox is the instance type `standard-3` (2 vCPU, 8 GiB, | |
| 303 | 302 | 16 GB), and so is `runs-on: g1t-2core`, in a class of its own; jobs that |
| 88 | 88 | something else cannot have deployments turned on until that setting | |
| 89 | 89 | changes. | |
| 90 | 90 | ||
| 91 | − | While payments on g1t are in test mode, no real card is charged: use the | |
| 92 | − | test card `4242 4242 4242 4242` with any future date and any code. | |
| 91 | + | On a g1t whose payments use Stripe's test mode, such as a self-hosted one | |
| 92 | + | set up with test keys, no real card is charged: use the test card | |
| 93 | + | `4242 4242 4242 4242` with any future date and any code. With live | |
| 94 | + | payments, the card you enter is charged. | |
| 93 | 95 | ||
| 94 | 96 | ## What deploys | |
| 95 | 97 | ||
| ⋯ | |||
| 761 | 763 | ## What it costs | |
| 762 | 764 | ||
| 763 | 765 | Deployments come with the | |
| 764 | − | [g1t plan](/guides/usage-and-billing/#the-g1t-plan), $20 a month per | |
| 766 | + | [g1t plan](/guides/usage-and-billing/#the-g1t-plan), $21 a month per | |
| 765 | 767 | workspace with $10 of usage included. There are **no quotas**: as many | |
| 766 | 768 | projects and previews as you like, and no count of builds, requests or | |
| 767 | 769 | domains ever stops or pauses an app. What deployments cost g1t is metered | |
| 154 | 154 | was charged plus what your plan's included usage, a trial, a pool or a | |
| 155 | 155 | discount paid of it. The lines are the statement's kinds of charge, such | |
| 156 | 156 | as **Runner minutes, 2-core**, **Self-hosted runner time** and | |
| 157 | − | **Deployments**, most first. Payments, credits, refunds, tax and card fees | |
| 157 | + | **Deployments**, most first. Payments, credits, refunds and tax | |
| 158 | 158 | are money in, not spend, and are left out. Under the total are the days | |
| 159 | 159 | counted and the month so far, at price. | |
| 160 | 160 |
| 184 | 184 | ||
| 185 | 185 | - What the workspace's packages hold is measured each day, public and | |
| 186 | 186 | private apart, each past its own free amount. | |
| 187 | − | - A month's GB-months are those days added up, divided by 30, charged at | |
| 187 | + | - A month's GB-months are those days added up, divided by the days in the month, charged at | |
| 188 | 188 | $0.018 a GB-month as **Package storage** on the | |
| 189 | 189 | [bill](/guides/usage-and-billing/). | |
| 190 | 190 | - Deleted packages and versions, kept for 30 days, do not count. |
| 7 | 7 | nothing, for public and private work. What costs money is compute, the | |
| 8 | 8 | things g1t runs for you: the sandboxes your workflows and checks run in, | |
| 9 | 9 | deployed apps, and, on the plan, storage and git traffic past the free | |
| 10 | − | amounts. Each is charged at what it costs g1t plus 20%, to the workspace | |
| 11 | − | that owns the repository. There is no seat price, and on the plan there | |
| 10 | + | amounts. Each is priced from what it costs g1t, plus 20%: runner minutes | |
| 11 | + | at a fixed rate for each machine, everything else at its cost plus 20%. | |
| 12 | + | It is charged to the workspace that owns the repository. There is no seat price, and on the plan there | |
| 12 | 13 | are no quotas: only your spend limit stops anything. | |
| 13 | 14 | ||
| 14 | 15 | Wherever this page says an owner can do something to a workspace's | |
| ⋯ | |||
| 92 | 93 | ||
| 93 | 94 | ## The g1t plan | |
| 94 | 95 | ||
| 95 | − | One plan, **$20 a month per workspace**, however many people are in it. It is never priced per person. Like every price on g1t, it | |
| 96 | − | excludes tax ([Tax](#tax)), and paid by card it carries Stripe's | |
| 97 | − | [card processing fee](#card-processing-fee) as its own line, $0.91 a month | |
| 98 | − | on $20. | |
| 96 | + | One plan, **$21 a month per workspace**, however many people are in it. It is never priced per person. Like every price on g1t, it | |
| 97 | + | excludes tax ([Tax](#tax)). There is no card processing fee on top: it is | |
| 98 | + | part of the price. | |
| 99 | 99 | ||
| 100 | − | - **$10 of usage each month**, used first: everything g1t runs, at cost | |
| 101 | − | plus 20%. That is 2,500 runner minutes on the standard machine, or | |
| 100 | + | - **$10 of usage each month**, used first: everything g1t runs, at the | |
| 101 | + | [prices below](#what-it-costs) (runner minutes at their flat rate per | |
| 102 | + | machine, everything else at cost plus 20%). That is 2,500 runner minutes on the standard machine, or | |
| 102 | 103 | about 1,428 on `g1t-4core`. | |
| 103 | 104 | - **Everyone in the workspace** at one price, never per person. | |
| 104 | 105 | - **Unlimited** projects, previews and repositories. | |
| ⋯ | |||
| 145 | 146 | | --- | --- | --- | --- | | |
| 146 | 147 | | [Runner minutes](#runner-minutes) on the standard machine (2 vCPU, 8 GiB, 16 GB): workflow jobs, checks, the merge queue | Minute, each job rounded up | From $0.0014 a minute idle to $0.0038 with both vCPUs busy | $0.004 a minute | | |
| 147 | 148 | | [Runner minutes](#runner-minutes) on `g1t-4core` (4 vCPU, 12 GiB, 20 GB) | Minute, each job rounded up | From $0.0020 a minute idle to $0.0068 with all four vCPUs busy | $0.007 a minute | | |
| 148 | − | | Deploy builds | Second | About $0.0015 a minute | About $0.0018 a minute | | |
| 149 | + | | Deploy builds | Second | About $0.0017 a minute | About $0.002 a minute | | |
| 149 | 150 | | App requests | Million | $0.30 | $0.36 | | |
| 150 | 151 | | App CPU time | Million CPU milliseconds | $0.02 | $0.024 | | |
| 151 | 152 | | Custom domains | Domain-month | $0.10 | $0.12 | | |
| ⋯ | |||
| 172 | 173 | ||
| 173 | 174 | ### Start or end the plan | |
| 174 | 175 | ||
| 175 | − | Only an owner can start or end the plan. | |
| 176 | + | Only an owner or a billing manager can start or end the plan. | |
| 176 | 177 | ||
| 177 | 178 | 1. Open **Settings → Billing and plans**, `g1t.sh/<workspace>/-/billing`. | |
| 178 | 179 | 2. On the **g1t** card, choose **Start the g1t plan**. | |
| ⋯ | |||
| 184 | 185 | month's included usage, and the **Upcoming invoice**: the plan and add-ons | |
| 185 | 186 | at their monthly price plus usage still owed after included usage, credit | |
| 186 | 187 | and any discount, from g1t's own ledger (**View upcoming invoice** splits | |
| 187 | − | it). **Usage** and **Invoices** go to each. App traffic, custom domains, | |
| 188 | − | storage and git operations are counted through the month and charged when | |
| 189 | − | it closes. **Downgrade to free at the period's end** ends the plan then, | |
| 190 | − | with nothing more charged after; **Keep the plan** takes that back until | |
| 191 | − | then. If a renewal payment fails, the card says **Payment failed**, and the | |
| 192 | − | plan's features stop until it is paid: update the card with **Manage in | |
| 193 | − | Stripe** under **Payment method**. | |
| 188 | + | it). **Usage** goes to usage by product and project. App traffic, custom | |
| 189 | + | domains, storage and git operations are counted through the month and | |
| 190 | + | charged when it closes. | |
| 194 | 191 | ||
| 192 | + | To end the plan, choose **Manage billing** and cancel it on Stripe's | |
| 193 | + | billing page: it ends at the end of the period, with nothing more charged | |
| 194 | + | after, and the plan's card says **Ends on** and the date. Until then, | |
| 195 | + | **Manage billing** lets you keep it after all. If a renewal payment fails, | |
| 196 | + | the card says **Payment failed**, and the plan's features stop until it is | |
| 197 | + | paid: choose **Update payment method** and change the card on Stripe's | |
| 198 | + | page. | |
| 199 | + | ||
| 195 | 200 | Starting the plan uses the card from the card check, or else the default | |
| 196 | 201 | card on Stripe's billing page, without a second page; with neither, Stripe's | |
| 197 | 202 | page asks for one. If Stripe refuses, the page says why in a sentence. | |
| ⋯ | |||
| 233 | 238 | ## The trial | |
| 234 | 239 | ||
| 235 | 240 | Once its card is checked, a workspace gets **$5 of trial credit**, once. It | |
| 236 | − | pays for usage at cost plus 20%: runner minutes for checks, workflows | |
| 241 | + | pays for usage at the plan's prices: runner minutes for checks, workflows | |
| 237 | 242 | and the merge queue (1,250 on the standard machine), and semantic search. It never pays for deployments. | |
| 238 | 243 | ||
| 239 | 244 | - Each card gets one trial, whichever workspace it is checked in. Prepaid | |
| ⋯ | |||
| 293 | 298 | ||
| 294 | 299 | ## How prices are set | |
| 295 | 300 | ||
| 296 | − | Every price is what g1t pays for the thing, at Cloudflare, plus 20%. The live prices are on | |
| 301 | + | Every price is what g1t pays for the thing, at Cloudflare, plus 20%; | |
| 302 | + | [runner minutes](#runner-minutes) are a fixed rate for each machine, set | |
| 303 | + | the same way from what a minute of it costs. The live prices are on | |
| 297 | 304 | [g1t.sh/pricing](https://g1t.sh/pricing), straight from the price book | |
| 298 | 305 | billing charges from. | |
| 299 | 306 | ||
| ⋯ | |||
| 438 | 445 | ||
| 439 | 446 | | | What it costs g1t | What is counted | | |
| 440 | 447 | | --- | --- | --- | | |
| 441 | − | | Private repository storage | $0.50 a GB-month (Cloudflare Artifacts) | On the plan, each day, what the workspace's private repositories hold past the free 1 GB. A month's GB-months are those days added up, divided by 30. | | |
| 448 | + | | Private repository storage | $0.50 a GB-month (Cloudflare Artifacts) | On the plan, each day, what the workspace's private repositories hold past the free 1 GB. A month's GB-months are those days added up, divided by the days in the month. | | |
| 442 | 449 | | Search embeddings | $0.067 per million tokens (Workers AI) | The text of private repositories, issues and pull requests put in the search index. Public text and searches are not charged. | | |
| 443 | 450 | | Security scans | $0.02 per million CPU milliseconds and $1.00 per million rows written (Workers and D1) | The CPU each history scan and dependency check takes and the rows it writes. | | |
| 444 | 451 | ||
| ⋯ | |||
| 446 | 453 | ||
| 447 | 454 | What a workspace's [`actions/cache`](/guides/actions/#the-cache) entries and its runs' | |
| 448 | 455 | [artifacts](/guides/actions/#artifacts) hold is measured every hour, and each day's largest figure counts. A | |
| 449 | − | month's GB-months are those days added up, divided by 30, charged at | |
| 456 | + | month's GB-months are those days added up, divided by the days in the month, charged at | |
| 450 | 457 | $0.018 a GB-month: R2's $0.015, plus 20%. It is charged from the first | |
| 451 | 458 | byte, to workspaces on the plan only, as **Actions cache storage** under | |
| 452 | 459 | **Git operations & storage**. A free workspace's caches are never charged; | |
| ⋯ | |||
| 589 | 596 | asks. A bank transfer page gives the account details, and the amount | |
| 590 | 597 | counts when the money arrives. | |
| 591 | 598 | ||
| 592 | − | By card, the [card processing fee](#card-processing-fee) is its own line | |
| 593 | − | (the card shows it for $100, $500 and $1,000); a bank transfer has none. | |
| 594 | − | [Tax](#tax) is added where it applies. What you prepay is credited in full: | |
| 595 | − | neither the fee nor the tax comes from it. | |
| 599 | + | There is no fee on top, by card or by bank transfer. [Tax](#tax) is added | |
| 600 | + | where it applies. What you prepay is credited in full: the tax does not | |
| 601 | + | come from it. | |
| 596 | 602 | ||
| 597 | 603 | **Prepaid balance** shows what is paid in advance and not used yet. | |
| 598 | 604 | ||
| ⋯ | |||
| 671 | 677 | charged at once with no minimum. See | |
| 672 | 678 | [deleting a workspace](/guides/workspaces/#what-billing-needs). | |
| 673 | 679 | ||
| 674 | − | Each is charged to the card on file, with the | |
| 675 | − | [card processing fee](#card-processing-fee) and [tax](#tax) as lines of | |
| 676 | − | their own. The Billing page lists them, with links to view each on Stripe | |
| 677 | − | and download its PDF; each one's amount is the usage, with the fee and | |
| 678 | − | tax under it. | |
| 680 | + | Each is charged to the card on file, with [tax](#tax) as a line of its | |
| 681 | + | own and no card fee. Every invoice and receipt, with its PDF, is on | |
| 682 | + | Stripe's billing page: choose **Manage billing** under **Billing on | |
| 683 | + | Stripe**. | |
| 679 | 684 | ||
| 680 | 685 | ## Tax | |
| 681 | 686 | ||
| ⋯ | |||
| 685 | 690 | The plan, add-ons, prepaying and invoices are all taxed the same way, as software as a service for business use. | |
| 686 | 691 | ||
| 687 | 692 | - **The address.** Stripe's payment pages always ask for a billing | |
| 688 | − | address, and save it as the workspace's **Invoice details**. A card check | |
| 693 | + | address, and save it on the workspace's Stripe customer. A card check | |
| 689 | 694 | saves the card's billing address there too, if there is none yet. You | |
| 690 | − | can change it under [Your card and billing | |
| 695 | + | change it on Stripe's billing page: see [Your card and billing | |
| 691 | 696 | details](#your-card-and-billing-details). | |
| 692 | − | - **A business tax ID.** Add it on Stripe's page or under **Invoice | |
| 693 | − | details** (the kind, such as EU VAT, and the number). Stripe checks it | |
| 694 | − | (**Verified by Stripe**, or **Stripe is checking it**) and applies it | |
| 695 | − | where the law says so, such as a reverse charge. | |
| 697 | + | - **A business tax ID.** Add it on Stripe's payment page or on its billing | |
| 698 | + | page (the kind, such as EU VAT, and the number). Stripe checks it and | |
| 699 | + | applies it where the law says so, such as a reverse charge. | |
| 696 | 700 | - **No address, no charge.** Where Stripe has nothing to work tax out | |
| 697 | − | from, g1t does not charge the card. Billing shows **Add a billing | |
| 698 | − | address**, the owners are emailed once, and the charge goes through once | |
| 699 | − | the address is saved. Nothing is lost, and work is not stopped for it. | |
| 701 | + | from, g1t does not charge the card. Billing shows **Add your billing | |
| 702 | + | address in Stripe** with a **Manage billing** button, the owners are | |
| 703 | + | emailed once, and the charge goes through once the address is saved. Nothing is lost, and work is not stopped for it. | |
| 700 | 704 | In the United States the ZIP code is needed; elsewhere the country. | |
| 701 | 705 | - **Tax exempt.** If your organisation is exempt, write to | |
| 702 | 706 | support@g1t.sh with the certificate; Billing then says **Tax exempt**. | |
| ⋯ | |||
| 705 | 709 | Tax is never part of your balance or usage: the statement shows it as its | |
| 706 | 710 | own line, beside the payment it came with. | |
| 707 | 711 | ||
| 708 | − | ## Card processing fee | |
| 709 | − | ||
| 710 | − | Paying by card adds Stripe's fee, **2.9% + $0.30**, as its own line, | |
| 711 | − | **Card processing fee**, worked out so that what is left after Stripe's | |
| 712 | − | fee is exactly what you paid for: $0.91 on the $20 plan, $3.30 on $100 | |
| 713 | − | prepaid. It is shown before you pay, on every card payment: | |
| 712 | + | ## No card processing fee | |
| 714 | 713 | ||
| 715 | − | | Payment | Card fee | | |
| 716 | − | | --- | --- | | |
| 717 | − | | The plan and add-ons, each month | Yes, a monthly line | | |
| 718 | − | | Prepaying by card | Yes | | |
| 719 | − | | Invoices charged to the card | Yes | | |
| 720 | − | | Prepaying by bank transfer | No | | |
| 721 | − | | An enterprise's invoices | No | | |
| 722 | − | ||
| 723 | − | Tax applies to the fee as to what it is paid with. The fee pays Stripe, not | |
| 724 | − | g1t: usage is still charged at cost plus 20%. The statement shows card fees as their own line, | |
| 725 | − | never from your balance. | |
| 714 | + | g1t's prices include what Stripe charges to take a card. Nothing is added | |
| 715 | + | on top of the plan, a prepayment or an invoice, however you pay. Payments | |
| 716 | + | from before the fee was folded into prices may show **Card processing | |
| 717 | + | fees** on the statement; those stay as they were. | |
| 726 | 718 | ||
| 727 | 719 | ### The minimum charge | |
| 728 | 720 | ||
| 729 | 721 | Only the month's close has a minimum: a card is not charged less than | |
| 730 | − | **$5** then, so a payment's fee is never most of what is paid. When a month | |
| 722 | + | **$5** then, so a payment's processing cost is never most of what is paid. When a month | |
| 731 | 723 | closes owing less, the amount carries over to the next invoice that reaches | |
| 732 | 724 | $5, as *Unpaid from earlier*. A charge made because a workspace is at or | |
| 733 | 725 | near a limit always goes through, whatever the amount, so work can carry | |
| ⋯ | |||
| 743 | 735 | ||
| 744 | 736 | ## Your card and billing details | |
| 745 | 737 | ||
| 746 | − | Cards live on **Stripe's billing page**, not on g1t: g1t never sees or | |
| 747 | − | stores card numbers. On **Settings → Billing and plans**: | |
| 738 | + | Your card, billing address, tax IDs, invoices and receipts live on | |
| 739 | + | **Stripe's billing page**, not on g1t: g1t never sees or stores card | |
| 740 | + | numbers. On **Settings → Billing and plans**, owners and billing managers | |
| 741 | + | see **Billing on Stripe**: | |
| 748 | 742 | ||
| 749 | − | - **Payment method** shows the default card (brand, last four digits and | |
| 750 | − | expiry). **Manage in Stripe** opens Stripe's page, where an owner adds, | |
| 751 | − | removes or replaces a card and makes one the default. The plan charges | |
| 752 | − | the default card. | |
| 753 | − | - **Invoice details** are kept on the workspace's Stripe customer and | |
| 754 | − | printed on every invoice: the invoice email, company name, billing | |
| 755 | − | address, tax ID (its kind and number), a purchase order, and the invoice | |
| 756 | − | language. An owner edits them here and chooses **Save invoice details**. | |
| 757 | − | [Tax](#tax) is worked out from the address, so the card says when there | |
| 758 | − | is none yet (in the US it needs the ZIP code), shows Stripe's check of | |
| 759 | − | the tax ID, and says **Tax exempt** or **Reverse charge** when that | |
| 760 | − | applies. | |
| 761 | − | - **Invoices** lists every invoice Stripe sent (the plan, add-ons and | |
| 762 | − | month-end usage), each with **View** and **PDF**. | |
| 763 | − | - **Add-ons** lists what comes with the plan, such as | |
| 764 | − | [Security and quality](/guides/security/pricing/), and whether it is on. | |
| 743 | + | - the default card (brand, last four digits and expiry), read-only; the | |
| 744 | + | plan charges it. | |
| 745 | + | - **Manage billing**, which opens Stripe's billing page. There you add, | |
| 746 | + | remove or replace a card and make one the default; change the billing | |
| 747 | + | email, name, address and phone; add or remove tax IDs; see and download | |
| 748 | + | every invoice and receipt (the plan and month-end usage); and end the | |
| 749 | + | plan at the end of the period, or keep it after all. [Tax](#tax) is | |
| 750 | + | worked out from the address you keep there. | |
| 765 | 751 | ||
| 752 | + | **Add-ons** lists what comes with the plan, such as | |
| 753 | + | [Security and quality](/guides/security/pricing/), and whether it is on. | |
| 754 | + | ||
| 766 | 755 | g1t support never takes card details by phone or email. | |
| 767 | 756 | ||
| 768 | 757 | If a card is declined, work stops until the workspace pays, and the | |
| 769 | − | Billing page and the API say why. Replace the card or prepay to clear it. | |
| 758 | + | Billing page and the API say why. Replace the card under **Manage billing**, | |
| 759 | + | or prepay, to clear it. | |
| 770 | 760 | A payment disputed with the card's bank stops work the same way. | |
| 771 | 761 | ||
| 772 | 762 | Through the API, `GET /workspaces/:workspace/billing_details` and | |
| ⋯ | |||
| 828 | 818 | 30 days and paid by card or bank transfer. If it goes overdue, the | |
| 829 | 819 | workspaces' work stops until it is paid. [Tax](#tax) is added from the | |
| 830 | 820 | enterprise's billing address, which g1t keeps on its Stripe customer, and | |
| 831 | − | an invoice never carries the card processing fee. | |
| 821 | + | there is no card fee. | |
| 832 | 822 | - **A discount**: a percentage off every usage charge, from a few percent | |
| 833 | 823 | to 100%, with a reason, sometimes until a date. Prices themselves stay | |
| 834 | 824 | the public ones. The statement shows every line at its price and the | |
| ⋯ | |||
| 926 | 916 | | Credits from g1t | Credit g1t added (promotional, goodwill or a refund), and what of it expired or was withdrawn. See [Credits from g1t](#credits-from-g1t). | | |
| 927 | 917 | | Refunds | Money given back to your card. | | |
| 928 | 918 | | Tax | Tax paid with that day's payments. Not a charge, and not from your balance. | | |
| 929 | − | | Card processing fees | Card fees paid with that day's payments. Not a charge, and not from your balance. | | |
| 919 | + | | Card processing fees | Only on payments from before the card fee was folded into prices: the fee paid with them. Not a charge, and not from your balance. | | |
| 930 | 920 | ||
| 931 | − | Payments are what reached your balance; the tax and card fee paid with | |
| 932 | − | them are the two lines below them, and the totals say what they came to. | |
| 921 | + | Payments are what reached your balance; the tax paid with them is the | |
| 922 | + | line below them, and the totals say what it came to. | |
| 933 | 923 | ||
| 934 | 924 | - **Covered.** Below the totals, what paid for usage before it was | |
| 935 | 925 | charged, each with its amount: | |
Binary or large file; its contents are not shown.
| 338 | 338 | <span> | |
| 339 | 339 | <span className="block font-medium">The g1t plan, without its price</span> | |
| 340 | 340 | <span className="block text-xs text-muted"> | |
| 341 | − | Its included usage, storage and caps, with no $20 a month. Usage past it is charged as usual. | |
| 341 | + | Its included usage, storage and caps, with no $21 a month. Usage past it is charged as usual. | |
| 342 | 342 | </span> | |
| 343 | 343 | </span> | |
| 344 | 344 | </label> |
| 1 | + | /** | |
| 2 | + | * Moving workspaces off Stripe's test mode before the live key, history | |
| 3 | + | * kept: one workspace on its page, every workspace on the Stripe page. A | |
| 4 | + | * preview (dry run) lists what will change; the change itself needs the | |
| 5 | + | * slug, or "detach", typed. sudo ships no JavaScript: the apply buttons say | |
| 6 | + | * they are running with app.css's run-button rules. | |
| 7 | + | */ | |
| 8 | + | import { Eye, Unplug } from "lucide-react"; | |
| 9 | + | ||
| 10 | + | import type { TestStripeDetach, WorkspaceDetach } from "@g1t/contracts"; | |
| 11 | + | ||
| 12 | + | import { Badge, Button, EmptyState, Field, Input, Notice, Section } from "~/components/ui"; | |
| 13 | + | import { usd } from "~/lib/money"; | |
| 14 | + | import type { SectionError } from "~/lib/review"; | |
| 15 | + | import { CONFIRM_ALL, DEFAULT_EXCLUDED, detachChanges } from "~/lib/test-detach"; | |
| 16 | + | ||
| 17 | + | const WHAT = | |
| 18 | + | "Plans canceled; the Stripe customer, cached card and card checks forgotten (the trial grant stays); open checkouts removed; AI auto-reload and a declined autopay cleared; open invoices void; past months closed as skipped; AI credit closed; test payments reversed and what is owed written off as goodwill. The ledger, statements and audit log keep every line."; | |
| 19 | + | ||
| 20 | + | /** The busy-aware submit for a detach: "Detaching…" while the page waits. */ | |
| 21 | + | function ApplyButton({ label }: { label: string }) { | |
| 22 | + | return ( | |
| 23 | + | <Button type="submit" variant="danger" className="run-button"> | |
| 24 | + | <span className="run-idle inline-flex items-center gap-2"> | |
| 25 | + | <Unplug size={14} /> | |
| 26 | + | {label} | |
| 27 | + | </span> | |
| 28 | + | <span className="run-busy"> | |
| 29 | + | <span className="run-spinner" aria-hidden="true" /> | |
| 30 | + | Detaching… | |
| 31 | + | </span> | |
| 32 | + | </Button> | |
| 33 | + | ); | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | function CancelAtStripe({ defaultChecked = false }: { defaultChecked?: boolean }) { | |
| 37 | + | return ( | |
| 38 | + | <label className="flex items-start gap-2 text-sm"> | |
| 39 | + | <input type="checkbox" name="cancelAtStripe" defaultChecked={defaultChecked} className="mt-0.5 accent-(--g1t-accent)" /> | |
| 40 | + | <span> | |
| 41 | + | <span className="block font-medium">Also cancel the subscriptions at Stripe</span> | |
| 42 | + | <span className="block text-xs text-muted">Test key only. Without it, they are canceled here and left in Stripe's test mode.</span> | |
| 43 | + | </span> | |
| 44 | + | </label> | |
| 45 | + | ); | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | /** One workspace's changes, with its balance before and after. */ | |
| 49 | + | function Changes({ row }: { row: WorkspaceDetach }) { | |
| 50 | + | return ( | |
| 51 | + | <ul className="list-disc space-y-0.5 pl-4 text-sm text-fg-soft"> | |
| 52 | + | {detachChanges(row).map((line) => ( | |
| 53 | + | <li key={line}>{line}</li> | |
| 54 | + | ))} | |
| 55 | + | </ul> | |
| 56 | + | ); | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | /** Every workspace a detach reaches, as a table. */ | |
| 60 | + | export function DetachTable({ rows }: { rows: WorkspaceDetach[] }) { | |
| 61 | + | return ( | |
| 62 | + | <div className="-mx-4 overflow-x-auto sm:-mx-5"> | |
| 63 | + | <table className="w-full min-w-xl text-sm"> | |
| 64 | + | <thead> | |
| 65 | + | <tr className="border-b border-line text-left text-xs text-muted"> | |
| 66 | + | <th className="px-4 py-2 font-medium sm:pl-5">Workspace</th> | |
| 67 | + | <th className="px-4 py-2 font-medium">What changes</th> | |
| 68 | + | <th className="px-4 py-2 text-right font-medium sm:pr-5">Balance</th> | |
| 69 | + | </tr> | |
| 70 | + | </thead> | |
| 71 | + | <tbody> | |
| 72 | + | {rows.map((row) => ( | |
| 73 | + | <tr key={row.workspace} className="border-b border-line align-top last:border-0"> | |
| 74 | + | <td className="px-4 py-2.5 sm:pl-5"> | |
| 75 | + | <a href={`/workspaces/${encodeURIComponent(row.workspace)}`} className="font-mono text-xs hover:underline"> | |
| 76 | + | {row.workspace} | |
| 77 | + | </a> | |
| 78 | + | {row.done && ( | |
| 79 | + | <span className="mt-1 block"> | |
| 80 | + | <Badge tone="mint">Detached</Badge> | |
| 81 | + | </span> | |
| 82 | + | )} | |
| 83 | + | </td> | |
| 84 | + | <td className="px-4 py-2.5"> | |
| 85 | + | <Changes row={row} /> | |
| 86 | + | </td> | |
| 87 | + | <td className="tabular px-4 py-2.5 text-right whitespace-nowrap sm:pr-5"> | |
| 88 | + | <span className="text-faint">{usd(row.balance_before_micros, { cents: true })}</span> | |
| 89 | + | {" → "} | |
| 90 | + | <span className="font-medium">{usd(row.balance_after_micros, { cents: true })}</span> | |
| 91 | + | </td> | |
| 92 | + | </tr> | |
| 93 | + | ))} | |
| 94 | + | </tbody> | |
| 95 | + | </table> | |
| 96 | + | </div> | |
| 97 | + | ); | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | /** The workspace page's section: preview, then confirm with the slug. */ | |
| 101 | + | export function WorkspaceTestDetachSection({ | |
| 102 | + | workspace, | |
| 103 | + | pathname, | |
| 104 | + | preview, | |
| 105 | + | error, | |
| 106 | + | }: { | |
| 107 | + | workspace: string; | |
| 108 | + | pathname: string; | |
| 109 | + | preview: TestStripeDetach | null; | |
| 110 | + | error: SectionError; | |
| 111 | + | }) { | |
| 112 | + | const row = preview?.workspaces.find((w) => w.workspace === workspace) ?? null; | |
| 113 | + | return ( | |
| 114 | + | <Section | |
| 115 | + | id="test-detach" | |
| 116 | + | title="Detach from Stripe test mode" | |
| 117 | + | description={`Before billing takes a live Stripe key. ${WHAT} Refused once the key is live.`} | |
| 118 | + | > | |
| 119 | + | <div className="space-y-4"> | |
| 120 | + | {error && <Notice tone="error">{error.error}</Notice>} | |
| 121 | + | {!preview ? ( | |
| 122 | + | <form method="post" action={`${pathname}#test-detach`}> | |
| 123 | + | <input type="hidden" name="intent" value="test-detach" /> | |
| 124 | + | <Button type="submit" variant="quiet"> | |
| 125 | + | <Eye size={14} /> | |
| 126 | + | Preview what changes | |
| 127 | + | </Button> | |
| 128 | + | </form> | |
| 129 | + | ) : !row ? ( | |
| 130 | + | <Notice tone="ok">Nothing from Stripe's test mode is left on {workspace}: a detach would change nothing.</Notice> | |
| 131 | + | ) : ( | |
| 132 | + | <> | |
| 133 | + | <Changes row={row} /> | |
| 134 | + | <p className="tabular text-sm text-muted"> | |
| 135 | + | Balance {usd(row.balance_before_micros, { cents: true })} → {usd(row.balance_after_micros, { cents: true })}. | |
| 136 | + | </p> | |
| 137 | + | <form method="post" action={`${pathname}#test-detach`} className="space-y-4"> | |
| 138 | + | <input type="hidden" name="intent" value="test-detach" /> | |
| 139 | + | <input type="hidden" name="confirm" value="yes" /> | |
| 140 | + | {row.subscriptions > 0 && <CancelAtStripe defaultChecked={error?.values?.cancelAtStripe === "on"} />} | |
| 141 | + | <Field | |
| 142 | + | label="Confirm" | |
| 143 | + | hint={ | |
| 144 | + | <> | |
| 145 | + | Type the workspace's slug (<span className="font-mono text-muted">{workspace}</span>) to detach it. Kept in the audit log. | |
| 146 | + | </> | |
| 147 | + | } | |
| 148 | + | > | |
| 149 | + | <Input name="confirmation" required placeholder={workspace} className="font-mono" /> | |
| 150 | + | </Field> | |
| 151 | + | <div className="flex justify-end"> | |
| 152 | + | <ApplyButton label="Detach from test mode" /> | |
| 153 | + | </div> | |
| 154 | + | </form> | |
| 155 | + | </> | |
| 156 | + | )} | |
| 157 | + | </div> | |
| 158 | + | </Section> | |
| 159 | + | ); | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | /** The Stripe page's section: every workspace, previewed, then confirmed. */ | |
| 163 | + | export function EveryWorkspaceDetachSection({ | |
| 164 | + | live, | |
| 165 | + | result, | |
| 166 | + | error, | |
| 167 | + | exclude, | |
| 168 | + | }: { | |
| 169 | + | live: boolean; | |
| 170 | + | result: TestStripeDetach | null; | |
| 171 | + | error: string | null; | |
| 172 | + | exclude: string | null; | |
| 173 | + | }) { | |
| 174 | + | const excludeValue = exclude ?? DEFAULT_EXCLUDED.join(", "); | |
| 175 | + | const preview = result?.dry_run ? result : null; | |
| 176 | + | const applied = result && !result.dry_run ? result : null; | |
| 177 | + | return ( | |
| 178 | + | <Section | |
| 179 | + | id="test-detach" | |
| 180 | + | title="Detach every workspace from test mode" | |
| 181 | + | description={`Run before switching billing to the live key. ${WHAT} Comped and enterprise workspaces are skipped, and so is anything excluded below.`} | |
| 182 | + | className="mt-6" | |
| 183 | + | > | |
| 184 | + | {live ? ( | |
| 185 | + | <Notice tone="info">Billing is on the live key: test-mode data can no longer be detached.</Notice> | |
| 186 | + | ) : ( | |
| 187 | + | <div className="space-y-4"> | |
| 188 | + | {error && <Notice tone="error">{error}</Notice>} | |
| 189 | + | <form method="post" action="/stripe#test-detach" className="flex flex-col gap-2 sm:flex-row sm:items-end"> | |
| 190 | + | <input type="hidden" name="intent" value="test-detach-preview" /> | |
| 191 | + | <Field label="Leave alone" hint="Slugs, separated by commas. Blank leaves nothing out." className="grow"> | |
| 192 | + | <Input name="exclude" defaultValue={excludeValue} className="font-mono" /> | |
| 193 | + | </Field> | |
| 194 | + | <Button type="submit" variant="quiet" className="shrink-0"> | |
| 195 | + | <Eye size={14} /> | |
| 196 | + | Preview | |
| 197 | + | </Button> | |
| 198 | + | </form> | |
| 199 | + | ||
| 200 | + | {applied && ( | |
| 201 | + | <> | |
| 202 | + | <Notice tone={applied.remaining > 0 ? "warn" : "ok"}> | |
| 203 | + | Detached {applied.workspaces.length} workspace{applied.workspaces.length === 1 ? "" : "s"}. | |
| 204 | + | {applied.remaining > 0 ? ` ${applied.remaining} more to go: preview and detach again.` : " Each is in its audit log."} | |
| 205 | + | </Notice> | |
| 206 | + | {applied.workspaces.length > 0 && <DetachTable rows={applied.workspaces} />} | |
| 207 | + | </> | |
| 208 | + | )} | |
| 209 | + | ||
| 210 | + | {preview && | |
| 211 | + | (preview.workspaces.length === 0 ? ( | |
| 212 | + | <EmptyState title="Nothing to detach">No workspace outside the exclusions holds anything from Stripe's test mode.</EmptyState> | |
| 213 | + | ) : ( | |
| 214 | + | <> | |
| 215 | + | <p className="text-sm text-muted"> | |
| 216 | + | {preview.workspaces.length} workspace{preview.workspaces.length === 1 ? "" : "s"} will change | |
| 217 | + | {preview.workspaces.length > 25 ? ", 25 a run" : ""}: | |
| 218 | + | </p> | |
| 219 | + | <DetachTable rows={preview.workspaces} /> | |
| 220 | + | <form method="post" action="/stripe#test-detach" className="space-y-4 border-t border-line pt-4"> | |
| 221 | + | <input type="hidden" name="intent" value="test-detach-all" /> | |
| 222 | + | <input type="hidden" name="exclude" value={excludeValue} /> | |
| 223 | + | {preview.workspaces.some((w) => w.subscriptions > 0) && <CancelAtStripe />} | |
| 224 | + | <Field label="Confirm" hint={<>Type <span className="font-mono text-muted">{CONFIRM_ALL}</span> to detach every workspace listed. Each is kept in its audit log.</>}> | |
| 225 | + | <Input name="confirmation" required placeholder={CONFIRM_ALL} className="font-mono" /> | |
| 226 | + | </Field> | |
| 227 | + | <div className="flex justify-end"> | |
| 228 | + | <ApplyButton label={`Detach ${Math.min(preview.workspaces.length, 25)} workspace${preview.workspaces.length === 1 ? "" : "s"}`} /> | |
| 229 | + | </div> | |
| 230 | + | </form> | |
| 231 | + | </> | |
| 232 | + | ))} | |
| 233 | + | ||
| 234 | + | {result && result.skipped.length > 0 && ( | |
| 235 | + | <div> | |
| 236 | + | <p className="mb-1.5 text-xs text-faint">Left alone, though they hold test-mode data</p> | |
| 237 | + | <ul className="space-y-0.5 text-sm"> | |
| 238 | + | {result.skipped.map((s) => ( | |
| 239 | + | <li key={s.workspace}> | |
| 240 | + | <span className="font-mono text-xs">{s.workspace}</span> <span className="text-muted">— {s.reason}</span> | |
| 241 | + | </li> | |
| 242 | + | ))} | |
| 243 | + | </ul> | |
| 244 | + | </div> | |
| 245 | + | )} | |
| 246 | + | </div> | |
| 247 | + | )} | |
| 248 | + | </Section> | |
| 249 | + | ); | |
| 250 | + | } |
| 232 | 232 | minDailyCostMicros: minDaily, | |
| 233 | 233 | anomalyFactor, | |
| 234 | 234 | anomalyFloorMicros: anomalyFloor, | |
| 235 | − | cardFee: form.get("cardFee") === "on", | |
| 236 | 235 | }, | |
| 237 | 236 | }; | |
| 238 | 237 | } |
| 191 | 191 | * the account's share of the open-source pool and of trials, and staff's | |
| 192 | 192 | * override of the days of audit log kept. A blank amount means the default | |
| 193 | 193 | * (for the audit log, the plan's). A hold is a line saying why new compute | |
| 194 | − | * is held; blank is no hold. The agent overrides (agents at once, the run | |
| 195 | − | * and issue caps) are retired with g1t's own agents: they are always sent | |
| 196 | − | * cleared. | |
| 194 | + | * is held; blank is no hold. A run's cap is not staff's to set here: it is | |
| 195 | + | * always sent cleared, so the plan's applies. The overrides retired with | |
| 196 | + | * g1t's own agents (agents at once, an issue's cap) control nothing and are | |
| 197 | + | * not sent; billing keeps them empty. | |
| 197 | 198 | */ | |
| 198 | 199 | export function parseAllowances(form: FormData): Parsed<Allowances> { | |
| 199 | 200 | const amount = (name: string, what: string, max: number, maxText: string, min = 0): Parsed<number | null> => { |
| 31 | 31 | revoked: "What was left of the credit was taken back. It is on the statement and in the audit log.", | |
| 32 | 32 | reset: "Billing reset. The workspace starts again as a new customer, and the costs analysis ran again, so every figure is fresh.", | |
| 33 | 33 | "reset-stale": "Billing reset. The workspace starts again as a new customer; the costs analysis did not finish, so press Run the analysis now on Costs & margin.", | |
| 34 | + | "test-detach": "Detached from Stripe's test mode. What changed is in the audit log; the ledger keeps every line.", | |
| 35 | + | "test-detach-none": "Nothing from Stripe's test mode was left on this workspace: nothing changed.", | |
| 34 | 36 | created: "Enterprise created.", | |
| 35 | 37 | "billing-email": "Saved where the enterprise's invoices go.", | |
| 36 | 38 | "billing-address": "Billing address saved on its Stripe customer. Its invoices are taxed from it.", |
| 1 | + | /** | |
| 2 | + | * The staff actions that move workspaces off Stripe's test mode before the | |
| 3 | + | * live key: one workspace from its page, every workspace from the Stripe | |
| 4 | + | * page. Each is previewed first (a dry run) and then confirmed by typing; | |
| 5 | + | * billing refuses both on a live key, and checks the confirmation again. | |
| 6 | + | */ | |
| 7 | + | import type { TestStripeDetach } from "@g1t/contracts"; | |
| 8 | + | import { data, redirect } from "react-router"; | |
| 9 | + | ||
| 10 | + | import { text } from "./forms"; | |
| 11 | + | import { admin } from "./services.server"; | |
| 12 | + | import { settle } from "./settle"; | |
| 13 | + | import type { Staff } from "./staff"; | |
| 14 | + | import { CONFIRM_ALL, DEFAULT_EXCLUDED, parseExclude } from "./test-detach"; | |
| 15 | + | ||
| 16 | + | /** What the workspace page's action returns for a detach. */ | |
| 17 | + | export type WorkspaceDetachData = { testDetach: TestStripeDetach; workspace: string }; | |
| 18 | + | ||
| 19 | + | /** What the Stripe page's action returns for a detach of every workspace. */ | |
| 20 | + | export type EveryDetachData = { everyDetach?: TestStripeDetach; everyDetachError?: string; exclude: string }; | |
| 21 | + | ||
| 22 | + | export const WORKSPACE_DETACH_INTENT = "test-detach"; | |
| 23 | + | export const EVERY_DETACH_INTENTS = new Set(["test-detach-preview", "test-detach-all"]); | |
| 24 | + | ||
| 25 | + | async function detach(args: Parameters<typeof admin.detachTestStripe>[0]): Promise<{ ok: true; value: TestStripeDetach } | { ok: false; error: string }> { | |
| 26 | + | const result = await settle(admin.detachTestStripe(args)); | |
| 27 | + | if (!result.ok) return { ok: false, error: `Billing did not answer: ${result.error}` }; | |
| 28 | + | if (!result.value.ok) return { ok: false, error: result.value.error.message }; | |
| 29 | + | return { ok: true, value: result.value.value }; | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | /** One workspace: a preview, then, confirmed with its slug, the detach. */ | |
| 33 | + | export async function workspaceDetachAction(form: FormData, staff: Staff, slug: string, path: string) { | |
| 34 | + | const confirmed = text(form, "confirm") === "yes"; | |
| 35 | + | const values = { confirmation: "", cancelAtStripe: text(form, "cancelAtStripe") }; | |
| 36 | + | const failed = (error: string) => data({ error, section: "test-detach", values }, { status: 422 }); | |
| 37 | + | if (!confirmed) { | |
| 38 | + | const preview = await detach({ workspace: slug, dryRun: true, by: staff.email }); | |
| 39 | + | if (!preview.ok) return failed(preview.error); | |
| 40 | + | return { testDetach: preview.value, workspace: slug } satisfies WorkspaceDetachData; | |
| 41 | + | } | |
| 42 | + | const confirmation = text(form, "confirmation").trim(); | |
| 43 | + | if (confirmation !== slug) return failed(`Type the workspace's slug, ${slug}, exactly, to detach it.`); | |
| 44 | + | const done = await detach({ | |
| 45 | + | workspace: slug, | |
| 46 | + | dryRun: false, | |
| 47 | + | confirm: confirmation, | |
| 48 | + | cancelAtStripe: values.cancelAtStripe === "on", | |
| 49 | + | by: staff.email, | |
| 50 | + | }); | |
| 51 | + | if (!done.ok) return failed(done.error); | |
| 52 | + | return redirect(`${path}?done=${done.value.workspaces.length > 0 ? "test-detach" : "test-detach-none"}#top`); | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | /** Every workspace: a preview with the exclusions, then, with "detach" typed, the detach. */ | |
| 56 | + | export async function everyDetachAction(form: FormData, staff: Staff) { | |
| 57 | + | const intent = text(form, "intent"); | |
| 58 | + | const exclude = text(form, "exclude"); | |
| 59 | + | const slugs = parseExclude(exclude); | |
| 60 | + | if (intent === "test-detach-preview") { | |
| 61 | + | const preview = await detach({ exclude: slugs, dryRun: true, by: staff.email }); | |
| 62 | + | if (!preview.ok) return { everyDetachError: preview.error, exclude } satisfies EveryDetachData; | |
| 63 | + | return { everyDetach: preview.value, exclude } satisfies EveryDetachData; | |
| 64 | + | } | |
| 65 | + | const confirmation = text(form, "confirmation").trim(); | |
| 66 | + | if (confirmation !== CONFIRM_ALL) { | |
| 67 | + | // Show the preview again beside the error, so nothing has to be redone. | |
| 68 | + | const preview = await detach({ exclude: slugs, dryRun: true, by: staff.email }); | |
| 69 | + | const error = `Type ${CONFIRM_ALL} to detach every workspace listed.`; | |
| 70 | + | return { everyDetach: preview.ok ? preview.value : undefined, everyDetachError: error, exclude } satisfies EveryDetachData; | |
| 71 | + | } | |
| 72 | + | const done = await detach({ | |
| 73 | + | exclude: slugs, | |
| 74 | + | dryRun: false, | |
| 75 | + | confirm: confirmation, | |
| 76 | + | cancelAtStripe: text(form, "cancelAtStripe") === "on", | |
| 77 | + | by: staff.email, | |
| 78 | + | }); | |
| 79 | + | if (!done.ok) return { everyDetachError: done.error, exclude } satisfies EveryDetachData; | |
| 80 | + | return { everyDetach: done.value, exclude } satisfies EveryDetachData; | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | export const DEFAULT_EXCLUDE_FIELD = DEFAULT_EXCLUDED.join(", "); |