Skip to content

Compare changes

Choose two branches to see what one has that the other does not, then open a pull request for it.

Open a pull request

1 commit

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