| 1 | /** |
| 2 | * Moving workspaces off Stripe's test mode before the live key: what a |
| 3 | * detach's result says, as plain lines. No Workers imports, so it is tested. |
| 4 | */ |
| 5 | import type { WorkspaceDetach } from "@g1t/contracts"; |
| 6 | |
| 7 | import { usd } from "./money.ts"; |
| 8 | |
| 9 | /** The slugs left alone by "Detach every workspace" unless staff change them. */ |
| 10 | export const DEFAULT_EXCLUDED = ["syntaqx", "flagon-io"]; |
| 11 | |
| 12 | /** What staff type to detach every workspace (billing checks it too). */ |
| 13 | export const CONFIRM_ALL = "detach"; |
| 14 | |
| 15 | /** "syntaqx, flagon-io" or "syntaqx flagon-io" as slugs; blank is none. */ |
| 16 | export function parseExclude(raw: string): string[] { |
| 17 | return [ |
| 18 | ...new Set( |
| 19 | raw |
| 20 | .split(/[\s,]+/) |
| 21 | .map((slug) => slug.trim().toLowerCase()) |
| 22 | .filter(Boolean), |
| 23 | ), |
| 24 | ]; |
| 25 | } |
| 26 | |
| 27 | function count(n: number, one: string, many: string): string { |
| 28 | return `${n} ${n === 1 ? one : many}`; |
| 29 | } |
| 30 | |
| 31 | /** Each change a detach makes to a workspace, in the order it makes them. */ |
| 32 | export function detachChanges(row: WorkspaceDetach): string[] { |
| 33 | const out: string[] = []; |
| 34 | if (row.customer || row.card_cached) out.push(`Customer${row.card ? ` and card ${row.card}` : ""} forgotten`); |
| 35 | if (row.subscriptions > 0) out.push(`${count(row.subscriptions, "plan", "plans")} canceled`); |
| 36 | if (row.card_checks > 0) out.push(`${count(row.card_checks, "card check", "card checks")} removed (trial kept)`); |
| 37 | if (row.checkouts > 0) out.push(`${count(row.checkouts, "open checkout", "open checkouts")} removed`); |
| 38 | if (row.ai_reload) out.push("AI auto-reload off"); |
| 39 | if (row.autopay_cleared) out.push("Declined autopay cleared"); |
| 40 | if (row.invoices_voided > 0) out.push(`${count(row.invoices_voided, "invoice", "invoices")} void`); |
| 41 | if (row.months_skipped.length > 0) out.push(`${row.months_skipped.join(", ")} closed as skipped`); |
| 42 | if (row.credit_grants_closed > 0) { |
| 43 | out.push(`${count(row.credit_grants_closed, "AI credit grant", "AI credit grants")} closed (${usd(row.credit_closed_micros, { cents: true })} unused)`); |
| 44 | } |
| 45 | if (row.payments_reversed_micros > 0) out.push(`${usd(row.payments_reversed_micros, { cents: true })} of test payments reversed`); |
| 46 | if (row.plan_payments_reversed_micros > 0) out.push(`${usd(row.plan_payments_reversed_micros, { cents: true })} of test plan payments reversed`); |
| 47 | if (row.written_off_micros > 0) out.push(`${usd(row.written_off_micros, { cents: true })} owed written off as goodwill`); |
| 48 | if (row.pending_months_marked > 0) { |
| 49 | out.push( |
| 50 | `${count(row.pending_months_marked, "metered figure", "metered figures")} of past months never charged (${usd(row.pending_marked_micros, { cents: true })})`, |
| 51 | ); |
| 52 | } |
| 53 | if (row.pending_baselined > 0) { |
| 54 | out.push( |
| 55 | `This month's ${count(row.pending_baselined, "metered figure", "metered figures")} so far never charged (${usd(row.pending_baselined_micros, { cents: true })})`, |
| 56 | ); |
| 57 | } |
| 58 | if (row.plan_credit_cleared_micros > 0) out.push(`${usd(row.plan_credit_cleared_micros, { cents: true })} of this month's included usage restored`); |
| 59 | if (row.dispute_owed_cleared_micros > 0) out.push(`${usd(row.dispute_owed_cleared_micros, { cents: true })} owed for lost test disputes cleared`); |
| 60 | if (row.disputes_closed > 0) out.push(`${count(row.disputes_closed, "open test dispute", "open test disputes")} closed`); |
| 61 | if (row.stripe_canceled > 0) out.push(`${count(row.stripe_canceled, "subscription", "subscriptions")} canceled at Stripe`); |
| 62 | for (const error of row.stripe_errors) out.push(`Stripe refused: ${error}`); |
| 63 | return out; |
| 64 | } |