sudo: a billing reset runs the costs analysis again so every figure is fresh; every submit button shows it is working (CSS only); no margin percentage on less than a cent sold
9 files+58−110/9 viewed
| 105 | 105 | animation: none; | |
| 106 | 106 | } | |
| 107 | 107 | } | |
| 108 | + | ||
| 109 | + | /* | |
| 110 | + | * Every other submit button says it is working the same way: a clicked | |
| 111 | + | * button keeps focus while the page waits for the answer, so it dims | |
| 112 | + | * and shows a spinner. The page that comes back is | |
| 113 | + | * fresh (every action redirects), and says what happened. Nothing may | |
| 114 | + | * stop pointer events here: focus comes on mouse-down, before the click. | |
| 115 | + | */ | |
| 116 | + | form button[type="submit"]:not(.run-button):focus:not(:focus-visible) { | |
| 117 | + | opacity: 0.75; | |
| 118 | + | } | |
| 119 | + | form button[type="submit"]:not(.run-button):focus:not(:focus-visible)::before { | |
| 120 | + | content: ""; | |
| 121 | + | flex: none; | |
| 122 | + | width: 0.8rem; | |
| 123 | + | height: 0.8rem; | |
| 124 | + | border-radius: 9999px; | |
| 125 | + | border: 2px solid currentColor; | |
| 126 | + | border-right-color: transparent; | |
| 127 | + | animation: run-spin 0.8s linear infinite; | |
| 128 | + | } | |
| 129 | + | form button[type="submit"]:not(.run-button):focus:not(:focus-visible) > svg { | |
| 130 | + | display: none; | |
| 131 | + | } | |
| 132 | + | @media (prefers-reduced-motion: reduce) { | |
| 133 | + | form button[type="submit"]:focus:not(:focus-visible)::before { | |
| 134 | + | animation: none; | |
| 135 | + | } | |
| 136 | + | } |
| 135 | 135 | const result = await settle(admin.resetBilling(subject.slug, values.confirmation, note.value, staff.email)); | |
| 136 | 136 | if (!result.ok) return failed("reset", `Billing did not answer: ${result.error}`, values); | |
| 137 | 137 | if (!result.value.ok) return failed("reset", result.value.error.message, values); | |
| 138 | − | return back("reset"); | |
| 138 | + | return back(result.value.value.refreshed === false ? "reset-stale" : "reset"); | |
| 139 | 139 | } | |
| 140 | 140 | ||
| 141 | 141 | if (intent === "payment") { |
| 28 | 28 | attach: "Workspace moved onto the enterprise.", | |
| 29 | 29 | detach: "Workspace moved off the enterprise. It pays for itself again.", | |
| 30 | 30 | credit: "Credit issued.", | |
| 31 | − | reset: "Billing reset. The workspace starts again as a new customer; run the costs analysis to redo the margin figures.", | |
| 31 | + | reset: "Billing reset. The workspace starts again as a new customer, and the costs analysis ran again, so every figure is fresh.", | |
| 32 | + | "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.", | |
| 32 | 33 | created: "Enterprise created.", | |
| 33 | 34 | "billing-email": "Saved where the enterprise's invoices go.", | |
| 34 | 35 | sales: "Sales record saved.", |
| 199 | 199 | const included = o.includedMicros ?? 0; | |
| 200 | 200 | const usageIn = o.usageMicros + included; | |
| 201 | 201 | const usageMargin = o.usageMarginMicros ?? usageIn - usageCost; | |
| 202 | − | const usagePercent = o.usageMarginPercent !== undefined ? o.usageMarginPercent : null; | |
| 202 | + | // Under a cent sold, a percentage says nothing (a few micros against none). | |
| 203 | + | const soldSomething = usageIn >= 10_000; | |
| 204 | + | const usagePercent = soldSomething && o.usageMarginPercent !== undefined ? o.usageMarginPercent : null; | |
| 203 | 205 | const running = o.runningCostMicros ?? 0; | |
| 204 | 206 | const unmapped = o.unmappedCostMicros ?? 0; | |
| 205 | 207 | // Cloudflare's subscriptions are not on the usage bill: the estimate, over the range. | |
| 266 | 268 | <Stat | |
| 267 | 269 | label="Margin on usage sold" | |
| 268 | 270 | value={percentLabel(usagePercent)} | |
| 269 | − | hint={`${usd(usageIn, { cents: true })} paid for usage that cost ${usd(usageCost, { cents: true })}`} | |
| 271 | + | hint={soldSomething ? `${usd(usageIn, { cents: true })} paid for usage that cost ${usd(usageCost, { cents: true })}` : "No usage sold in this range"} | |
| 270 | 272 | tone={marginTone(usagePercent, floor)} | |
| 271 | 273 | /> | |
| 272 | 274 | <Stat |
| 2172 | 2172 | pub struct BillingReset { | |
| 2173 | 2173 | pub workspace: String, | |
| 2174 | 2174 | pub rows: u32, | |
| 2175 | + | /// Whether the costs analysis ran again after it, so the margin | |
| 2176 | + | /// figures no longer hold the workspace's past usage. | |
| 2177 | + | #[serde(default)] | |
| 2178 | + | pub refreshed: bool, | |
| 2175 | 2179 | } | |
| 2176 | 2180 | ||
| 2177 | 2181 | /// One change made in sudo. |
| 364 | 364 | repos' and stay. | |
| 365 | 365 | ||
| 366 | 366 | Billing refuses it while `STRIPE_SECRET_KEY` is a live key, for comped | |
| 367 | − | workspaces, and for a workspace an enterprise pays for. Afterwards press | |
| 368 | − | **Run the analysis now** on Costs & margin so the margin figures drop the | |
| 369 | − | workspace's past usage. | |
| 367 | + | workspaces, and for a workspace an enterprise pays for. It then runs the | |
| 368 | + | costs analysis again (as **Run the analysis now** does), so the margin | |
| 369 | + | figures drop the workspace's past usage at once; if that run does not | |
| 370 | + | finish, the page says so and the button does it. | |
| 370 | 371 | ||
| 371 | 372 | ## Stripe | |
| 372 | 373 |
| 1149 | 1149 | }; | |
| 1150 | 1150 | ||
| 1151 | 1151 | /** What `resetBilling` removed. */ | |
| 1152 | − | export type BillingReset = { workspace: string; rows: number }; | |
| 1152 | + | export type BillingReset = { workspace: string; rows: number; refreshed?: boolean }; | |
| 1153 | 1153 | ||
| 1154 | 1154 | export type WorkspaceCost = { workspace: string; costMicros: number; revenueMicros: number; givenMicros?: number; internal: boolean }; | |
| 1155 | 1155 |
| 1192 | 1192 | "admin_create_enterprise" => reply(&billing.admin_create_enterprise(args(body)?).await?), | |
| 1193 | 1193 | "admin_attach" => reply(&billing.admin_attach(args(body)?).await?), | |
| 1194 | 1194 | "admin_credit" => reply(&billing.admin_credit(args(body)?).await?), | |
| 1195 | − | "admin_reset_billing" => reply(&billing.admin_reset_billing(args(body)?).await?), | |
| 1195 | + | "admin_reset_billing" => reply(&billing.admin_reset_billing(&env, args(body)?).await?), | |
| 1196 | 1196 | "admin_set_allowances" => reply(&billing.admin_set_allowances(args(body)?).await?), | |
| 1197 | 1197 | "entitlements" => reply(&billing.entitlements(args(body)?).await?), | |
| 1198 | 1198 | "audit_retention" => reply(&billing.audit_retention(args(body)?).await?), |
| 56 | 56 | ]; | |
| 57 | 57 | ||
| 58 | 58 | impl Billing { | |
| 59 | − | pub(crate) async fn admin_reset_billing(&self, a: AdminResetBillingArgs) -> Result<Outcome<BillingReset>> { | |
| 59 | + | pub(crate) async fn admin_reset_billing(&self, env: &worker::Env, a: AdminResetBillingArgs) -> Result<Outcome<BillingReset>> { | |
| 60 | 60 | let workspace = a.workspace.trim().to_lowercase(); | |
| 61 | 61 | if workspace.is_empty() || a.by.trim().is_empty() { | |
| 62 | 62 | return Ok(Outcome::fail(FailureCode::Invalid, "A reset needs a workspace and who did it.")); | |
| 105 | 105 | rows += result.meta()?.and_then(|m| m.changes).unwrap_or(0); | |
| 106 | 106 | } | |
| 107 | 107 | self.audit(&account, "reset", &format!("billing of {workspace} reset ({rows} rows): {}", a.note.trim()), &a.by).await?; | |
| 108 | − | Ok(Outcome::Ok(BillingReset { workspace, rows: rows as u32 })) | |
| 108 | + | // The margin figures still hold the workspace's past usage: redo | |
| 109 | + | // them now (the day's analysis: the bill, 31 days, the alerts), so | |
| 110 | + | // the pages show the reset at once. | |
| 111 | + | let refreshed = match self.costs_daily(env, &crate::keeper::Keeper::from_env(env)).await { | |
| 112 | + | Ok(run) => run.problems.is_empty(), | |
| 113 | + | Err(error) => { | |
| 114 | + | worker::console_error!("costs after a reset of {workspace}: {error}"); | |
| 115 | + | false | |
| 116 | + | } | |
| 117 | + | }; | |
| 118 | + | Ok(Outcome::Ok(BillingReset { workspace, rows: rows as u32, refreshed })) | |
| 109 | 119 | } | |
| 110 | 120 | } | |
| 111 | 121 |