Skip to content

Commit

Billing: ask Stripe for tax only where Stripe Tax is active

Stripe refuses a payment page, invoice or subscription with automatic_tax when Stripe Tax is not active for the key's mode, which is the case on the test account g1t.sh still uses (Tax is on in live mode). The client now reads GET /tax/settings at most every ten minutes and, when it is not active, leaves automatic_tax out of those requests and skips the tax calculation on auto-reload (nothing to record). When the settings cannot be read it asks for tax anyway, so a live account never undercharges silently. send_raw is the request itself; send adds the check.

syntaqxcommitted Parent81aa307Browse files
2 files+88−40/2 viewed
+7−4
759759 Err(error) => return Err(error),
760760 };
761761 let tax_cents = u32::try_from(calculation.tax_amount_exclusive.max(0)).unwrap_or(0);
762− let charge = crate::stripe::SavedCharge { tax_cents, tax_calculation: Some(&calculation.id), ..untaxed };
762+ // An empty id: Stripe Tax is not active for this key, so no tax.
763+ let taxed = !calculation.id.is_empty();
764+ let charge = crate::stripe::SavedCharge { tax_cents, tax_calculation: taxed.then_some(calculation.id.as_str()), ..untaxed };
763765 let paid = match stripe.charge_saved(&charge).await {
764766 Ok(intent) if intent["status"].as_str() == Some("succeeded") => intent["id"].as_str().map(str::to_owned),
765767 Ok(intent) => {
782784 self.grant_purchased(workspace, &intent, amount, i64::from(fee_cents) * 10_000, "g1t", Some(&customer)).await?;
783785 // Recorded with Stripe Tax once paid, so it is reported and filed;
784786 // a failure is logged and the payment stands.
785− let transaction = match stripe.record_tax(&calculation.id, &intent).await {
786− Ok(id) => Some(id),
787− Err(error) => {
787+ let transaction = match if taxed { Some(stripe.record_tax(&calculation.id, &intent).await) } else { None } {
788+ None => None,
789+ Some(Ok(id)) => Some(id),
790+ Some(Err(error)) => {
788791 worker::console_error!("{workspace}: the tax on auto-reload {intent} was not recorded with Stripe Tax: {error}");
789792 None
790793 }
+81−0
227227 }
228228
229229 /// `name=value` pairs as a form body.
230+/// Requests that carry `automatic_tax`: payment pages, invoices and
231+/// subscriptions. When Stripe Tax is not active for the key's mode (a test
232+/// account where it was never turned on), Stripe refuses them outright, so
233+/// the field is left out and the payment goes through untaxed.
234+pub(crate) fn taxed_path(path: &str) -> bool {
235+ path == "/checkout/sessions" || path == "/invoices" || path == "/subscriptions"
236+}
237+
238+/// A form body without its `automatic_tax[…]` fields.
239+pub(crate) fn without_automatic_tax(body: &str) -> String {
240+ body.split('&').filter(|pair| !pair.starts_with("automatic_tax")).collect::<Vec<_>>().join("&")
241+}
242+
243+/// How long whether Stripe Tax is active is believed, in milliseconds.
244+const TAX_ACTIVE_FOR_MS: u64 = 10 * 60 * 1000;
245+
246+thread_local! {
247+ /// Whether Stripe Tax is active for the key, and when that was read.
248+ static TAX_ACTIVE: std::cell::Cell<Option<(bool, u64)>> = const { std::cell::Cell::new(None) };
249+}
250+
230251 pub(crate) fn form(fields: &[(&str, String)]) -> String {
231252 fields
232253 .iter()
296317 body: Option<String>,
297318 idempotency_key: Option<&str>,
298319 ) -> Result<T> {
320+ let body = match body {
321+ Some(body) if matches!(method, Method::Post) && taxed_path(path) && body.contains("automatic_tax") && !self.tax_active().await => {
322+ Some(without_automatic_tax(&body))
323+ }
324+ body => body,
325+ };
326+ self.send_raw(method, path, body, idempotency_key).await
327+ }
328+
329+ /// The request itself, as given: `send` without the Stripe Tax check.
330+ async fn send_raw<T: for<'a> Deserialize<'a>>(
331+ &self,
332+ method: Method,
333+ path: &str,
334+ body: Option<String>,
335+ idempotency_key: Option<&str>,
336+ ) -> Result<T> {
299337 let headers = Headers::new();
300338 headers.set("authorization", &format!("Bearer {}", self.key))?;
301339 headers.set("stripe-version", STRIPE_VERSION)?;
322360 response.json().await
323361 }
324362
363+ /// Whether Stripe Tax is active for this key's mode (`GET /tax/settings`,
364+ /// `status` `active`), read at most every ten minutes. When it cannot be
365+ /// read, it is taken as active: asking for tax and being refused says
366+ /// why, where leaving it out would undercharge without a word.
367+ pub async fn tax_active(&self) -> bool {
368+ let now = crate::now_ms();
369+ if let Some((active, at)) = TAX_ACTIVE.with(|cell| cell.get())
370+ && now.saturating_sub(at) < TAX_ACTIVE_FOR_MS
371+ {
372+ return active;
373+ }
374+ #[derive(Deserialize)]
375+ struct Settings {
376+ status: String,
377+ }
378+ let active = match self.send_raw::<Settings>(Method::Get, "/tax/settings", None, None).await {
379+ Ok(settings) => settings.status == "active",
380+ Err(error) => {
381+ worker::console_error!("Stripe Tax settings could not be read; asking for tax anyway: {error}");
382+ true
383+ }
384+ };
385+ TAX_ACTIVE.with(|cell| cell.set(Some((active, now))));
386+ active
387+ }
388+
325389 /// A customer for a workspace that has none yet.
326390 pub async fn create_customer(&self, workspace: &str) -> Result<String> {
327391 #[derive(Deserialize)]
506570 /// A customer Stripe cannot place fails with
507571 /// `customer_tax_location_invalid` (`is_tax_location_error`).
508572 pub async fn tax_calculation(&self, charge: &SavedCharge<'_>) -> Result<TaxCalculation> {
573+ if !self.tax_active().await {
574+ // No Stripe Tax for this key's mode: no tax, and nothing to record.
575+ return Ok(TaxCalculation { id: String::new(), tax_amount_exclusive: 0 });
576+ }
509577 let key = format!("{}/tax", charge.key);
510578 self.send(Method::Post, "/tax/calculations", Some(form(&tax_calculation_fields(charge))), Some(&key)).await
511579 }
11301198 }
11311199
11321200 #[cfg(test)]
1201+mod tax_mode_tests {
1202+ use super::*;
1203+
1204+ #[test]
1205+ fn untaxed_requests_lose_only_automatic_tax() {
1206+ let body = form(&[("mode", "payment".to_owned()), ("automatic_tax[enabled]", "true".to_owned()), ("tax_id_collection[enabled]", "true".to_owned())]);
1207+ assert_eq!(without_automatic_tax(&body), form(&[("mode", "payment".to_owned()), ("tax_id_collection[enabled]", "true".to_owned())]));
1208+ assert!(taxed_path("/checkout/sessions") && taxed_path("/invoices") && taxed_path("/subscriptions"));
1209+ assert!(!taxed_path("/invoices/in_1/finalize") && !taxed_path("/payment_intents"));
1210+ }
1211+}
1212+
1213+#[cfg(test)]
11331214 mod tests {
11341215 use super::*;
11351216