Commit

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

syntaqxcommitted Parentead0d01Browse files
9 files+58−110/9 viewed
+29−0
105105 animation: none;
106106 }
107107 }
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+}
+1−1
135135 const result = await settle(admin.resetBilling(subject.slug, values.confirmation, note.value, staff.email));
136136 if (!result.ok) return failed("reset", `Billing did not answer: ${result.error}`, values);
137137 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");
139139 }
140140
141141 if (intent === "payment") {
+2−1
2828 attach: "Workspace moved onto the enterprise.",
2929 detach: "Workspace moved off the enterprise. It pays for itself again.",
3030 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.",
3233 created: "Enterprise created.",
3334 "billing-email": "Saved where the enterprise's invoices go.",
3435 sales: "Sales record saved.",
+4−2
199199 const included = o.includedMicros ?? 0;
200200 const usageIn = o.usageMicros + included;
201201 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;
203205 const running = o.runningCostMicros ?? 0;
204206 const unmapped = o.unmappedCostMicros ?? 0;
205207 // Cloudflare's subscriptions are not on the usage bill: the estimate, over the range.
266268 <Stat
267269 label="Margin on usage sold"
268270 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"}
270272 tone={marginTone(usagePercent, floor)}
271273 />
272274 <Stat
+4−0
21722172 pub struct BillingReset {
21732173 pub workspace: String,
21742174 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,
21752179 }
21762180
21772181 /// One change made in sudo.
+4−3
364364 repos' and stay.
365365
366366 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.
370371
371372 ## Stripe
372373
+1−1
11491149 };
11501150
11511151 /** What `resetBilling` removed. */
1152−export type BillingReset = { workspace: string; rows: number };
1152+export type BillingReset = { workspace: string; rows: number; refreshed?: boolean };
11531153
11541154 export type WorkspaceCost = { workspace: string; costMicros: number; revenueMicros: number; givenMicros?: number; internal: boolean };
11551155
+1−1
11921192 "admin_create_enterprise" => reply(&billing.admin_create_enterprise(args(body)?).await?),
11931193 "admin_attach" => reply(&billing.admin_attach(args(body)?).await?),
11941194 "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?),
11961196 "admin_set_allowances" => reply(&billing.admin_set_allowances(args(body)?).await?),
11971197 "entitlements" => reply(&billing.entitlements(args(body)?).await?),
11981198 "audit_retention" => reply(&billing.audit_retention(args(body)?).await?),
+12−2
5656 ];
5757
5858 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>> {
6060 let workspace = a.workspace.trim().to_lowercase();
6161 if workspace.is_empty() || a.by.trim().is_empty() {
6262 return Ok(Outcome::fail(FailureCode::Invalid, "A reset needs a workspace and who did it."));
105105 rows += result.meta()?.and_then(|m| m.changes).unwrap_or(0);
106106 }
107107 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 }))
109119 }
110120 }
111121