| 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(", "); |