Merge main into project-descriptions
3 files+35−90/3 viewed
| 179 | 179 | } | |
| 180 | 180 | about={ | |
| 181 | 181 | on | |
| 182 | − | ? `Billing period ${period}. One price for the workspace, never per seat.` | |
| 182 | + | ? `Usage month ${period}${subscription?.periodEnd ? `; the plan renews ${endsOn(subscription.periodEnd)}` : ""}. One price for the workspace, never per seat.` | |
| 183 | 183 | : "The forge is free: repositories, issues, pull requests, reviews, search and the audit log. Checks, workflows and deployments need the plan, or the trial." | |
| 184 | 184 | } | |
| 185 | 185 | aside={ |
| 901 | 901 | ("subscription_data[metadata][workspace]", workspace.to_owned()), | |
| 902 | 902 | ("subscription_data[metadata][feature]", feature.to_owned()), | |
| 903 | 903 | ]; | |
| 904 | − | fields.extend(page_line(monthly_cents, format!("g1t {title} for {workspace}"), None, true)); | |
| 904 | + | fields.extend(page_line(monthly_cents, plan_line_name(title, workspace), None, true)); | |
| 905 | 905 | fields.extend(checkout_tax_fields(customer)); | |
| 906 | 906 | if let Some(customer) = customer { | |
| 907 | 907 | fields.push(("customer", customer.to_owned())); | |
| ⋯ | |||
| 909 | 909 | fields | |
| 910 | 910 | } | |
| 911 | 911 | ||
| 912 | + | /// What a plan is called on Stripe's page and the receipts: "g1t plan for | |
| 913 | + | /// acme", "g1t Deployments plan for acme"; never "g1t g1t". | |
| 914 | + | pub(crate) fn plan_line_name(title: &str, workspace: &str) -> String { | |
| 915 | + | if title == "g1t" { format!("g1t plan for {workspace}") } else { format!("g1t {title} plan for {workspace}") } | |
| 916 | + | } | |
| 917 | + | ||
| 912 | 918 | /// A card check's page's fields: setup mode, nothing charged, so nothing | |
| 913 | 919 | /// to tax. Setup mode takes no line items and no amount. The billing | |
| 914 | 920 | /// address is asked for all the same, and copied onto the customer once | |
| ⋯ | |||
| 1196 | 1202 | fn untaxed_requests_lose_only_automatic_tax() { | |
| 1197 | 1203 | let body = form(&[("mode", "payment".to_owned()), ("automatic_tax[enabled]", "true".to_owned()), ("tax_id_collection[enabled]", "true".to_owned())]); | |
| 1198 | 1204 | assert_eq!(without_automatic_tax(&body), form(&[("mode", "payment".to_owned()), ("tax_id_collection[enabled]", "true".to_owned())])); | |
| 1205 | + | assert_eq!(plan_line_name("g1t", "acme"), "g1t plan for acme"); | |
| 1206 | + | assert_eq!(plan_line_name("Deployments", "acme"), "g1t Deployments plan for acme"); | |
| 1199 | 1207 | assert!(taxed_path("/checkout/sessions") && taxed_path("/invoices") && taxed_path("/subscriptions")); | |
| 1200 | 1208 | // An enrollment converting to the plan turns tax on by updating it. | |
| 1201 | 1209 | assert!(taxed_path("/subscriptions/sub_1") && taxed_path("/subscriptions/sub_1?expand[]=latest_invoice")); | |
| 64 | 64 | } | |
| 65 | 65 | } | |
| 66 | 66 | ||
| 67 | + | /// A value to bind, or SQL NULL. `Option::into` gives JavaScript | |
| 68 | + | /// `undefined` for None, which D1 refuses ("Type 'undefined' not | |
| 69 | + | /// supported"): a customer without a card, or a replay that finished. | |
| 70 | + | fn or_null<T: Into<worker::wasm_bindgen::JsValue>>(value: Option<T>) -> worker::wasm_bindgen::JsValue { | |
| 71 | + | value.map_or(worker::wasm_bindgen::JsValue::NULL, Into::into) | |
| 72 | + | } | |
| 73 | + | ||
| 67 | 74 | impl Billing { | |
| 68 | 75 | /// The workspace's saved card as last synced. Stripe is asked only when | |
| 69 | 76 | /// it never was, or g1t changed the card since; a Stripe that does not | |
| ⋯ | |||
| 130 | 137 | ) | |
| 131 | 138 | .bind(&[ | |
| 132 | 139 | customer.into(), | |
| 133 | − | card.as_ref().map(|c| c.brand.clone()).into(), | |
| 134 | − | card.as_ref().map(|c| c.last4.clone()).into(), | |
| 135 | − | card.as_ref().map(|c| c.exp_month as f64).into(), | |
| 136 | − | card.as_ref().map(|c| c.exp_year as f64).into(), | |
| 140 | + | or_null(card.as_ref().map(|c| c.brand.clone())), | |
| 141 | + | or_null(card.as_ref().map(|c| c.last4.clone())), | |
| 142 | + | or_null(card.as_ref().map(|c| c.exp_month as f64)), | |
| 143 | + | or_null(card.as_ref().map(|c| c.exp_year as f64)), | |
| 137 | 144 | rfc3339(now_ms()).into(), | |
| 138 | 145 | ])? | |
| 139 | 146 | .run() | |
| ⋯ | |||
| 249 | 256 | .bind(&[ | |
| 250 | 257 | mode.into(), (through as f64).into(), rfc3339(now_ms()).into(), | |
| 251 | 258 | crate::optional(if complete { None } else { after.as_deref() }), | |
| 252 | − | (!complete).then_some(since as f64).into(), | |
| 253 | − | (!complete).then_some(until as f64).into(), | |
| 254 | − | if complete { None } else { first_failed.map(|v| v as f64) }.into(), | |
| 259 | + | or_null((!complete).then_some(since as f64)), | |
| 260 | + | or_null((!complete).then_some(until as f64)), | |
| 261 | + | or_null(if complete { None } else { first_failed.map(|v| v as f64) }), | |
| 255 | 262 | ])? | |
| 256 | 263 | .run() | |
| 257 | 264 | .await?; | |
| ⋯ | |||
| 354 | 361 | ||
| 355 | 362 | #[cfg(test)] | |
| 356 | 363 | mod tests { | |
| 364 | + | ||
| 365 | + | #[test] | |
| 366 | + | fn no_binding_turns_none_into_undefined() { | |
| 367 | + | // D1 refuses `undefined`; every optional value goes through or_null. | |
| 368 | + | let source = include_str!("stripe_sync.rs"); | |
| 369 | + | let body = source.split("#[cfg(test)]").next().unwrap(); | |
| 370 | + | for line in body.lines().filter(|l| l.trim_end().ends_with(".into(),")) { | |
| 371 | + | assert!(!line.contains("then_some(") && !line.contains(".map(|"), "{line}"); | |
| 372 | + | } | |
| 373 | + | } | |
| 374 | + | ||
| 357 | 375 | use super::*; | |
| 358 | 376 | ||
| 359 | 377 | #[test] | |
Comments
Sign in to comment.