Skip to content

Commit

Billing: Stripe Tax on every payment, the card fee on every card payment, and free_workspaces

Every Checkout page, subscription, invoice and auto-reload is taxed by Stripe Tax (txcd_10103001, prices exclusive), with the address and tax ID collected and saved on the customer. Tax and card fees are kept apart in tax_and_fees (migration 0041): never credited, never cash, their own statement lines, and Tax collected on sudo's Costs. Without an address nothing is charged and the owners are asked for one. The card fee is on by default on every card payment, never on bank transfers or enterprise invoices. free_workspaces tells identity which workspaces are on no paid plan. Enterprise billing address and tax ID from sudo.

syntaqxcommitted Parent5f8154bBrowse files
19 files+1636−2120/19 viewed
+78−3
16911691 pub by: String,
16921692 }
16931693
1694+/// `admin_enterprise_address`: the enterprise's billing address and tax ID,
1695+/// saved on its Stripe customer (made by `admin_enterprise_billing`).
1696+/// Stripe Tax works its invoices' tax out from the address; an invoice is
1697+/// not sent without one. Returns `Outcome<bool>`.
1698+#[derive(Debug, Serialize, Deserialize)]
1699+#[serde(rename_all = "camelCase")]
1700+pub struct AdminEnterpriseAddressArgs {
1701+ pub id: String,
1702+ pub address: PostalAddress,
1703+ #[serde(default, alias = "tax_id_type")]
1704+ pub tax_id_type: Option<String>,
1705+ #[serde(default, alias = "tax_id")]
1706+ pub tax_id: Option<String>,
1707+ pub by: String,
1708+}
1709+
16941710 /// `admin_invoice_enterprise`: sends an enterprise its invoice now, for
16951711 /// what its workspaces owe, rather than waiting for the month to close.
16961712 /// Returns `Outcome<EnterpriseInvoice>`.
17401756 pub pdf_url: Option<String>,
17411757 pub lines: Vec<InvoiceItem>,
17421758 pub created_at: String,
1759+ /// The card processing fee on top of `amount_micros`, when the invoice
1760+ /// is charged to a card; never part of the usage it pays for.
1761+ #[serde(default)]
1762+ pub fee_micros: i64,
1763+ /// The tax Stripe added on top, once it is known (after paying).
1764+ #[serde(default)]
1765+ pub tax_micros: i64,
17431766 }
17441767
17451768 #[derive(Clone, Debug, Serialize, Deserialize)]
18271850 /// What the account's discount took off the line's price.
18281851 #[serde(default)]
18291852 pub discount_micros: i64,
1853+ /// On the `Tax` and `Card processing fees` lines: what was paid with
1854+ /// payments on top of what reached the balance (negative for what a
1855+ /// refund gave back). Never in `charged_micros` or the balance.
1856+ #[serde(default)]
1857+ pub passed_micros: i64,
18301858 }
18311859
18321860 #[derive(Clone, Debug, Serialize, Deserialize)]
18531881 /// carries over to the next invoice. Zero when nothing carried.
18541882 #[serde(default)]
18551883 pub carried_micros: i64,
1884+ /// Tax and card processing fees paid with the month's payments, on top
1885+ /// of `paid_micros`.
1886+ #[serde(default)]
1887+ pub tax_micros: i64,
1888+ #[serde(default)]
1889+ pub card_fee_micros: i64,
18561890 }
18571891
18581892 /// One source that paid for usage before it was charged.
24582492 pub struct Plan {
24592493 pub feature: Feature,
24602494 pub title: String,
2461− /// Charged every month while the plan is on, in cents.
2495+ /// Charged every month while the plan is on, in cents, excluding tax.
24622496 pub monthly_cents: u32,
2497+ /// The card processing fee on top each month, in cents (0 when the
2498+ /// fee is off). Excluding tax, like the price.
2499+ #[serde(default)]
2500+ pub card_fee_cents: u32,
24632501 /// What the monthly price includes, one line each, for people to read.
24642502 pub includes: Vec<String>,
24652503 /// How usage past the allowance is charged, for people to read.
25982636 pub feature: Feature,
25992637 }
26002638
2639+/// `free_workspaces`: which of `workspaces` are free, that is on no paid
2640+/// plan. Paid is the g1t plan, an enterprise's terms, or a discount of
2641+/// 100% (g1t's own workspaces). Identity asks before a workspace is made
2642+/// (a person owns at most one free workspace) and before anyone is added
2643+/// to one (a free workspace cannot invite). Returns `Vec<String>`, the
2644+/// free ones, lower-cased; none where payments are not set up.
2645+#[derive(Debug, Serialize, Deserialize)]
2646+pub struct FreeWorkspacesArgs {
2647+ pub workspaces: Vec<String>,
2648+}
2649+
26012650 /// `charge_feature`: usage of a feature past its plan's allowance, charged
26022651 /// from the workspace's credit at cost plus the margin, whatever
26032652 /// `FREE_WHILE_BUILDING` says. Called by the service that provides it.
27462795 pub cloudflare_cost_micros: i64,
27472796 #[serde(default)]
27482797 pub models_cost_micros: i64,
2798+ /// Tax collected with payments over the range, net of refunds: owed to
2799+ /// the tax authorities, never in cash or revenue.
2800+ #[serde(default)]
2801+ pub tax_collected_micros: i64,
2802+ /// Card processing fees passed on with card payments, net of refunds:
2803+ /// they pay Stripe's fee, so they are not revenue either.
2804+ #[serde(default)]
2805+ pub card_fees_micros: i64,
27492806 }
27502807
27512808 /// A count, cost or leak that does not add up.
28912948 /// and at least `anomaly_floor_micros`, is flagged.
28922949 pub anomaly_factor: f64,
28932950 pub anomaly_floor_micros: i64,
2894− /// Pass Stripe's card fee on as its own line when AI credit is bought
2895− /// by card (`card_fee_percent` and `card_fee_fixed` in the price book).
2951+ /// Pass Stripe's card fee on as its own line on every card payment (the
2952+ /// plan, Security and quality, prepaying, AI credit, auto-reload and
2953+ /// invoices charged to a card), never on a bank transfer or an invoice
2954+ /// sent to be paid (`card_fee_percent` and `card_fee_fixed` in the
2955+ /// price book). On by default.
28962956 #[serde(default = "yes")]
28972957 pub card_fee: bool,
28982958 }
35003560 /// Stripe could not be read: what is shown is what g1t keeps.
35013561 #[serde(default)]
35023562 pub unavailable: Option<String>,
3563+ /// Whether Stripe Tax can place the customer from the address: tax
3564+ /// is worked out from it, and without it nothing is charged.
3565+ #[serde(default)]
3566+ pub tax_location: bool,
3567+ /// Set when g1t did not charge for want of an address (RFC 3339).
3568+ #[serde(default)]
3569+ pub tax_address_needed_at: Option<String>,
3570+ /// Stripe's check of the tax ID: `pending`, `verified`, `unverified` or
3571+ /// `unavailable`.
3572+ #[serde(default)]
3573+ pub tax_id_status: Option<String>,
3574+ /// `none`, `exempt` or `reverse`, as staff set it at Stripe; g1t never
3575+ /// changes it.
3576+ #[serde(default)]
3577+ pub tax_exempt: Option<String>,
35033578 }
35043579
35053580 /// `set_billing_details`: saves the invoice details on the Stripe customer.
+27−1
457457 pdfUrl: string | null;
458458 lines: { description: string; amountMicros: number }[];
459459 createdAt: string;
460+ /** The card processing fee on top of `amountMicros` when charged to a card, and the tax Stripe added once known. */
461+ feeMicros?: number;
462+ taxMicros?: number;
460463 };
461464
462465 /** A month of the ledger, grouped by day or project, a line per kind of charge. */
476479 coveredMicros?: number;
477480 priceMicros?: number;
478481 discountMicros?: number;
482+ /** On `Tax` and `Card processing fees` lines: what was paid with payments on top of what reached the balance. Never charged. */
483+ passedMicros?: number;
479484 }[];
480485 chargedMicros: number;
481486 priceMicros?: number;
495500 covered?: { source: "included" | "trial" | "oss_pool" | "given" | string; label: string; micros: number }[];
496501 /** Owed when the month closed but under the minimum charge: on the next invoice. */
497502 carriedMicros?: number;
503+ /** Tax and card processing fees paid with the month's payments, on top of `paidMicros`. */
504+ taxMicros?: number;
505+ cardFeeMicros?: number;
498506 };
499507 };
500508
627635 stripe(fix?: boolean, by?: string): Promise<StripeStatus>;
628636 /** Where an enterprise's invoices go; makes its Stripe customer. */
629637 enterpriseBilling(id: string, email: string, by: string): Promise<Result<PayingAccount>>;
638+ /** The enterprise's billing address and tax ID, on its Stripe customer: Stripe Tax works its invoices out from them. */
639+ enterpriseAddress(id: string, address: PostalAddress, taxIdType: string | null, taxId: string | null, by: string): Promise<Result<boolean>>;
630640 /** Sends an enterprise its invoice now, for what its workspaces owe. */
631641 invoiceEnterprise(id: string, by: string): Promise<Result<EnterpriseInvoice>>;
632642 /** Exactly these workspaces' accounts, such as one page of the list. */
851861 export type FeaturePlan = {
852862 feature: Feature;
853863 title: string;
854− /** Charged every month while the plan is on, in cents. */
864+ /** Charged every month while the plan is on, in cents, excluding tax. */
855865 monthlyCents: number;
866+ /** The card processing fee on top each month, in cents (0 when off), excluding tax. */
867+ cardFeeCents?: number;
856868 /** What the price includes, one line each. */
857869 includes: string[];
858870 /** How usage past the allowance is charged. */
963975 confirmSubscription(workspace: string, viewer: Viewer, session: string): Promise<Result<FeatureState>>;
964976 /** Ends a plan at the end of its period, or (`resume`) takes that back. Owners only. */
965977 cancelSubscription(actor: User, workspace: string, feature: Feature, resume?: boolean): Promise<Result<FeatureState>>;
978+ /** Which of the workspaces are free (on no paid plan); none where payments are not set up. */
979+ freeWorkspaces(workspaces: string[]): Promise<string[]>;
966980 /** Whether a feature works for a workspace now; a failure with the reason when not. */
967981 hasFeature(workspace: string, feature: Feature): Promise<Result<boolean>>;
968982 /**
12331247 includedMicros?: number;
12341248 cloudflareCostMicros?: number;
12351249 modelsCostMicros?: number;
1250+ /** Tax collected with payments over the range, net of refunds: owed to tax authorities, never cash or revenue. */
1251+ taxCollectedMicros?: number;
1252+ /** Card processing fees passed on with card payments, net of refunds: they pay Stripe's fee, not revenue. */
1253+ cardFeesMicros?: number;
12361254 };
12371255
12381256 /** A count, cost or leak that does not add up. */
15621580 invoices: StripeInvoice[];
15631581 upcoming: UpcomingInvoice;
15641582 unavailable?: string | null;
1583+ /** Whether Stripe Tax can place the customer from the address; without it nothing is charged. */
1584+ taxLocation?: boolean;
1585+ /** Set when g1t did not charge for want of a billing address. */
1586+ taxAddressNeededAt?: string | null;
1587+ /** Stripe's check of the tax ID: pending, verified, unverified or unavailable. */
1588+ taxIdStatus?: string | null;
1589+ /** none, exempt or reverse, as set at Stripe. */
1590+ taxExempt?: string | null;
15651591 };
15661592
15671593 export type BillingDetailsInput = {
+2−0
470470 cancelSubscription: (actor, workspace, feature, resume = false) =>
471471 call("cancel_subscription", { actor, workspace, feature, resume }),
472472 hasFeature: (workspace, feature) => call("has_feature", { workspace, feature }),
473+ freeWorkspaces: (workspaces) => call("free_workspaces", { workspaces }),
473474 chargeFeature: (charge) => call("charge_feature", charge),
474475 billingPortal: (actor, workspace, returnUrl) => call("billing_portal", { actor, workspace, return_url: returnUrl }),
475476 recordSandbox: (usage) => call("record_sandbox", usage),
534535 billingLink: (workspace, by) => call("admin_billing_link", { workspace, by }),
535536 stripe: (fix = false, by) => call("admin_stripe", { fix, by: by ?? null }),
536537 enterpriseBilling: (id, email, by) => call("admin_enterprise_billing", { id, email, by }),
538+ enterpriseAddress: (id, address, taxIdType, taxId, by) => call("admin_enterprise_address", { id, address, taxIdType, taxId, by }),
537539 invoiceEnterprise: (id, by) => call("admin_invoice_enterprise", { id, by }),
538540 accountsFor: (workspaces) => call("admin_accounts", { query: null, workspaces }),
539541 signals: () => call("admin_signals", {}),
+51−0
1+-- Stripe Tax on every payment, and the card processing fee on every card
2+-- payment.
3+--
4+-- Prices are shown and kept excluding tax. Stripe works the tax out on
5+-- every Checkout page, subscription, invoice and off-session charge
6+-- (`automatic_tax`, tax code `txcd_10103001`, tax behavior `exclusive`).
7+-- What reaches a workspace's balance is the payment less its tax and its
8+-- card fee; neither is revenue. Each is kept here, one row per payment and
9+-- kind, so the statement shows them as their own lines and sudo's Costs
10+-- shows tax collected apart from cash.
11+
12+CREATE TABLE IF NOT EXISTS tax_and_fees (
13+ -- `<reference>/tax` or `<reference>/card_fee`. A refund's share is
14+ -- negative, under the refund's reference `refund/<charge>/<refunded>`.
15+ id TEXT PRIMARY KEY,
16+ -- The workspace, or for an enterprise's invoice its billing account.
17+ workspace TEXT NOT NULL,
18+ -- tax or card_fee.
19+ kind TEXT NOT NULL,
20+ -- Positive when collected, negative when refunded.
21+ amount_micros INTEGER NOT NULL,
22+ -- The payment: an invoice, a Checkout page or a PaymentIntent.
23+ reference TEXT NOT NULL,
24+ -- The PaymentIntent that took the money, so a refund finds its tax.
25+ payment_intent TEXT,
26+ -- Stripe Tax's transaction for an off-session charge (auto-reload),
27+ -- which a refund reverses.
28+ tax_transaction TEXT,
29+ created_at TEXT NOT NULL
30+);
31+CREATE INDEX IF NOT EXISTS tax_and_fees_by_workspace ON tax_and_fees (workspace, created_at);
32+CREATE INDEX IF NOT EXISTS tax_and_fees_by_day ON tax_and_fees (created_at);
33+CREATE INDEX IF NOT EXISTS tax_and_fees_by_intent ON tax_and_fees (payment_intent);
34+
35+-- A workspace invoice's card fee and tax, apart from the usage it pays for.
36+ALTER TABLE workspace_invoices ADD COLUMN fee_micros INTEGER NOT NULL DEFAULT 0;
37+ALTER TABLE workspace_invoices ADD COLUMN tax_micros INTEGER NOT NULL DEFAULT 0;
38+
39+-- Set when Stripe Tax could not work out where the customer is (no
40+-- billing address), so g1t did not charge: the Billing page asks an owner
41+-- for the address, and saving it clears this.
42+ALTER TABLE accounts ADD COLUMN tax_address_needed_at TEXT;
43+
44+-- The card processing fee is on for every card payment, not only AI
45+-- credit: the setting's note says so. Never on invoiced or enterprise
46+-- payments, or bank transfers.
47+INSERT OR IGNORE INTO cost_settings (key, value, updated_at, updated_by) VALUES
48+ ('card_fee', 'on', '2026-10-08T00:00:00Z', 'migration');
49+
50+INSERT OR IGNORE INTO price_changes (id, meter, old_cost_micros, new_cost_micros, markup_percent, reason, created_at) VALUES
51+ ('prc_card_fee_all_cards', 'card_fee_percent', 29000, 29000, 0, 'Stripe''s card fee (2.9% + $0.30) is passed on as its own line on every card payment: the plan, Security and quality, prepaying, AI credit and invoices charged to a card. None on bank transfers or invoiced (enterprise) billing. Prices exclude tax; tax is added where it applies', '2026-10-08T00:00:00Z');
+32−1
462462 let fee = i64::from(open.fee_cents.unwrap_or(0)) * 10_000;
463463 self.grant_purchased(&open.workspace, session_id, micros, fee, &open.created_by, session.customer.as_deref())
464464 .await?;
465+ let extras = crate::tax::Extras { tax_cents: session.tax_cents(), fee_cents: fee / 10_000 };
466+ self.record_extras(&open.workspace, session_id, session.payment_intent.as_deref(), extras, None).await?;
465467 Ok(Ok(format!("{}: {} of AI credit bought", open.workspace, cents(micros))))
466468 }
467469
733735 ])?
734736 .run()
735737 .await?;
736− let charge = crate::stripe::SavedCharge {
738+ let untaxed = crate::stripe::SavedCharge {
737739 workspace,
738740 customer: &customer,
739741 payment_method: &method.id,
740742 credit_cents,
741743 fee_cents,
744+ tax_cents: 0,
745+ tax_calculation: None,
742746 key: &key,
743747 };
748+ // No Checkout page or invoice works the tax out here: Stripe Tax's
749+ // calculation does, for the customer's saved address. Without one,
750+ // nothing is charged and the owners are asked for it.
751+ let calculation = match stripe.tax_calculation(&untaxed).await {
752+ Ok(calculation) => calculation,
753+ Err(error) if crate::stripe::is_tax_location_error(&error) => {
754+ self.tax_address_needed(workspace).await?;
755+ self.reload_failed(workspace, Some(&key), amount, "Stripe needs the workspace's billing address to work out tax; add it under Invoice details")
756+ .await?;
757+ return Ok(None);
758+ }
759+ Err(error) => return Err(error),
760+ };
761+ 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 };
744763 let paid = match stripe.charge_saved(&charge).await {
745764 Ok(intent) if intent["status"].as_str() == Some("succeeded") => intent["id"].as_str().map(str::to_owned),
746765 Ok(intent) => {
761780 .run()
762781 .await?;
763782 self.grant_purchased(workspace, &intent, amount, i64::from(fee_cents) * 10_000, "g1t", Some(&customer)).await?;
783+ // Recorded with Stripe Tax once paid, so it is reported and filed;
784+ // 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) => {
788+ worker::console_error!("{workspace}: the tax on auto-reload {intent} was not recorded with Stripe Tax: {error}");
789+ None
790+ }
791+ };
792+ let extras = crate::tax::Extras { tax_cents: i64::from(tax_cents), fee_cents: i64::from(fee_cents) };
793+ self.record_extras(workspace, &intent, Some(&intent), extras, transaction.as_deref()).await?;
794+ self.tax_address_given(workspace).await?;
764795 Ok(Some(amount))
765796 }
766797
+9−0
123123 return Ok(Ok("ignored: settled meanwhile".to_owned()));
124124 }
125125 let workspace = open.workspace;
126+ // The address entered with the card, onto a customer that has none,
127+ // so the plan and invoices that follow can be taxed.
128+ if let (Some(customer), Some(address)) = (session.customer.as_deref(), card.address.as_ref()) {
129+ match stripe.fill_address(customer, address).await {
130+ Ok(true) => self.tax_address_given(&workspace).await?,
131+ Ok(false) => {}
132+ Err(error) => worker::console_error!("{workspace}: the card's billing address was not saved on the customer: {error}"),
133+ }
134+ }
126135 let now = rfc3339(now_ms());
127136 #[derive(Deserialize)]
128137 struct Count {
+19−0
311311 Ok(self.plan_kind(workspace).await? != PlanKind::Free)
312312 }
313313
314+ /// `free_workspaces`: those of the workspaces on no paid plan. With no
315+ /// card processor nothing is free (`plan_kind_for` says paid), so a
316+ /// g1t without payments limits no one.
317+ pub(crate) async fn free_workspaces(&self, a: g1t_contracts::billing::FreeWorkspacesArgs) -> Result<Vec<String>> {
318+ let mut free = vec![];
319+ // A person belongs to a bounded number of workspaces; this caps the
320+ // reads all the same.
321+ for workspace in a.workspaces.iter().take(100) {
322+ let workspace = workspace.trim().to_lowercase();
323+ if workspace.is_empty() || free.contains(&workspace) {
324+ continue;
325+ }
326+ if self.plan_kind(&workspace).await? == PlanKind::Free {
327+ free.push(workspace);
328+ }
329+ }
330+ Ok(free)
331+ }
332+
314333 /// What one monthly allowance has used.
315334 pub(crate) async fn allowance_used(&self, kind: &str, scope: &str, month: &str) -> Result<i64> {
316335 Ok(self
+91−36
3535 if a.name.as_deref().is_some_and(|n| n.trim().chars().count() > 200) {
3636 return Some("Keep the company name under 200 characters.");
3737 }
38− if let Some(address) = &a.address {
39− if !address.country.trim().is_empty() && (address.country.trim().len() != 2 || !address.country.trim().chars().all(|c| c.is_ascii_alphabetic())) {
40− return Some("The country is two letters, such as US or DE.");
41− }
42− let parts = [&address.line1, &address.line2, &address.city, &address.state, &address.postal_code];
43− if parts.iter().any(|p| p.chars().count() > 200) {
44− return Some("Keep each line of the address under 200 characters.");
45− }
38+ if let Some(why) = a.address.as_ref().and_then(address_invalid) {
39+ return Some(why);
4640 }
4741 if a.po_number.as_deref().is_some_and(|p| p.trim().chars().count() > 140) {
4842 return Some("Keep the purchase order under 140 characters.");
5246 {
5347 return Some("Stripe does not write invoices in that language.");
5448 }
55− match (a.tax_id_type.as_deref().map(str::trim), a.tax_id.as_deref().map(str::trim)) {
49+ tax_id_invalid(a.tax_id_type.as_deref(), a.tax_id.as_deref())
50+}
51+
52+/// Puts a tax ID on the customer in place of the one there was; an empty
53+/// value only takes the old one off. Stripe checks it (EU VAT numbers
54+/// against VIES, for one) and Stripe Tax uses it, such as for a reverse
55+/// charge. Why not, as a sentence, when Stripe refuses it.
56+pub(crate) async fn replace_tax_id(
57+ stripe: &crate::stripe::Stripe,
58+ customer: &str,
59+ owner: &str,
60+ kind: &str,
61+ value: &str,
62+) -> std::result::Result<(), String> {
63+ let (kind, value) = (kind.trim(), value.trim());
64+ let existing: Result<Value> = stripe.get(&format!("/customers/{customer}/tax_ids?limit=10")).await;
65+ let existing = existing.ok().and_then(|list| list["data"].as_array().cloned()).unwrap_or_default();
66+ if existing.iter().any(|t| t["type"].as_str() == Some(kind) && t["value"].as_str() == Some(value)) {
67+ return Ok(());
68+ }
69+ if !value.is_empty() {
70+ let key = format!("tax_id/{owner}/{kind}/{value}");
71+ let added: Result<Value> =
72+ stripe.post_idempotent(&format!("/customers/{customer}/tax_ids"), &[("type", kind.to_owned()), ("value", value.to_owned())], &key).await;
73+ if let Err(error) = added {
74+ return Err(crate::stripe::friendly(&error));
75+ }
76+ }
77+ for old in existing {
78+ if let Some(id) = old["id"].as_str() {
79+ let _: Result<Value> = stripe.delete(&format!("/customers/{customer}/tax_ids/{id}")).await;
80+ }
81+ }
82+ Ok(())
83+}
84+
85+/// What is wrong with an address, if anything.
86+pub(crate) fn address_invalid(address: &PostalAddress) -> Option<&'static str> {
87+ if !address.country.trim().is_empty() && (address.country.trim().len() != 2 || !address.country.trim().chars().all(|c| c.is_ascii_alphabetic())) {
88+ return Some("The country is two letters, such as US or DE.");
89+ }
90+ let parts = [&address.line1, &address.line2, &address.city, &address.state, &address.postal_code];
91+ if parts.iter().any(|p| p.chars().count() > 200) {
92+ return Some("Keep each line of the address under 200 characters.");
93+ }
94+ None
95+}
96+
97+/// What is wrong with a tax ID, if anything: a kind Stripe takes, and a
98+/// number of letters, digits and separators.
99+pub(crate) fn tax_id_invalid(kind: Option<&str>, value: Option<&str>) -> Option<&'static str> {
100+ match (kind.map(str::trim), value.map(str::trim)) {
56101 (Some(kind), Some(value)) if !kind.is_empty() && !value.is_empty() => {
57− if kind.len() > 20 || !kind.chars().all(|c| c.is_ascii_lowercase() || c == '_') {
102+ if !TAX_ID_TYPES.contains(&kind) {
58103 return Some("Choose the kind of tax ID from the list.");
59104 }
60− if value.chars().count() > 60 {
61− return Some("That tax ID is too long.");
105+ if value.chars().count() > 60 || !value.chars().all(|c| c.is_ascii_alphanumeric() || " .-/".contains(c)) {
106+ return Some("That is not a tax ID: letters, digits, spaces, dots, hyphens and slashes, up to 60.");
62107 }
108+ None
63109 }
64− (Some(kind), Some(value)) if kind.is_empty() != value.is_empty() => return Some("Give the tax ID's kind and its number together."),
65− _ => {}
110+ (Some(kind), Some(value)) if kind.is_empty() != value.is_empty() => Some("Give the tax ID's kind and its number together."),
111+ _ => None,
66112 }
67− None
68113 }
69114
70115 /// The customer's fields to send for the details given: absent ones left
145190 }),
146191 tax_id_type: tax.and_then(|t| text(&t["type"])),
147192 tax_id: tax.and_then(|t| text(&t["value"])),
193+ tax_id_status: tax.and_then(|t| text(&t["verification"]["status"])),
194+ tax_exempt: text(&customer["tax_exempt"]),
195+ tax_location: crate::stripe::address_places_customer(address)
196+ || crate::stripe::address_places_customer(&customer["shipping"]["address"]),
148197 po_number: text(&customer["metadata"]["po_number"]),
149198 language: customer["preferred_locales"].as_array().and_then(|l| l.first()).and_then(text),
150199 ..BillingDetails::default()
151200 }
152201 }
153202
203+/// The kinds of tax ID Stripe takes on a customer (`tax_ids[type]`) in
204+/// the API version billing is written for.
205+pub(crate) const TAX_ID_TYPES: &[&str] = &[
206+ "ad_nrt", "ae_trn", "al_tin", "am_tin", "ao_tin", "ar_cuit", "au_abn", "au_arn", "ba_tin", "bb_tin", "bg_uic", "bh_vat",
207+ "bo_tin", "br_cnpj", "br_cpf", "bs_tin", "by_tin", "ca_bn", "ca_gst_hst", "ca_pst_bc", "ca_pst_mb", "ca_pst_sk", "ca_qst",
208+ "cd_nif", "ch_uid", "ch_vat", "cl_tin", "cn_tin", "co_nit", "cr_tin", "de_stn", "do_rcn", "ec_ruc", "eg_tin", "es_cif",
209+ "eu_oss_vat", "eu_vat", "gb_vat", "ge_vat", "gn_nif", "hk_br", "hr_oib", "hu_tin", "id_npwp", "il_vat", "in_gst", "is_vat",
210+ "jp_cn", "jp_rn", "jp_trn", "ke_pin", "kh_tin", "kr_brn", "kz_bin", "li_uid", "li_vat", "ma_vat", "md_vat", "me_pib",
211+ "mk_vat", "mr_nif", "mx_rfc", "my_frp", "my_itn", "my_sst", "ng_tin", "no_vat", "no_voec", "np_pan", "nz_gst", "om_vat",
212+ "pe_ruc", "ph_tin", "ro_tin", "rs_pib", "ru_inn", "ru_kpp", "sa_vat", "sg_gst", "sg_uen", "si_tin", "sn_ninea", "sr_fin",
213+ "sv_nit", "th_vat", "tj_tin", "tr_tin", "tw_vat", "tz_vat", "ua_vat", "ug_tin", "us_ein", "uy_ruc", "uz_tin", "uz_vat",
214+ "ve_rif", "vn_tin", "za_vat", "zm_tin", "zw_tin",
215+];
216+
154217 impl Billing {
155218 /// `billing_details`: members only.
156219 pub(crate) async fn billing_details(&self, a: AccountArgs) -> Result<Outcome<BillingDetails>> {
179242 });
180243 details.invoices = invoices.iter().filter_map(invoice_from).collect();
181244 details.upcoming = upcoming;
245+ details.tax_address_needed_at = self.tax_address_needed_at(workspace).await?;
182246 Ok(details)
183247 }
184248 Err(error) => {
209273 let mut subscriptions = 0i64;
210274 for feature in [Feature::Plan, Feature::Security] {
211275 if self.plan_on(workspace, feature).await? {
212− subscriptions += i64::from(self.plan(feature).await?.monthly_cents) * 10_000;
276+ let plan = self.plan(feature).await?;
277+ subscriptions += i64::from(plan.monthly_cents + plan.card_fee_cents) * 10_000;
213278 }
214279 }
215280 let terms = self.terms_of(workspace).await?;
263328 }
264329 }
265330 // A tax ID replaces the one there was.
266− if let (Some(kind), Some(value)) = (a.tax_id_type.as_deref().map(str::trim), a.tax_id.as_deref().map(str::trim)) {
267− let existing: Result<Value> = stripe.get(&format!("/customers/{customer}/tax_ids?limit=10")).await;
268− let existing = existing.ok().and_then(|list| list["data"].as_array().cloned()).unwrap_or_default();
269− let same = existing.iter().any(|t| t["type"].as_str() == Some(kind) && t["value"].as_str() == Some(value));
270− if !same {
271− if !value.is_empty() {
272− let key = format!("tax_id/{workspace}/{kind}/{value}");
273− let added: Result<Value> =
274− stripe.post_idempotent(&format!("/customers/{customer}/tax_ids"), &[("type", kind.to_owned()), ("value", value.to_owned())], &key).await;
275− if let Err(error) = added {
276− return Ok(Outcome::fail(FailureCode::Invalid, crate::stripe::friendly(&error)));
277− }
278− }
279− for old in existing {
280− if let Some(id) = old["id"].as_str() {
281− let _: Result<Value> = stripe.delete(&format!("/customers/{customer}/tax_ids/{id}")).await;
282− }
283− }
284− }
331+ if let (Some(kind), Some(value)) = (a.tax_id_type.as_deref(), a.tax_id.as_deref())
332+ && let Err(why) = replace_tax_id(stripe, &customer, &workspace, kind, value).await
333+ {
334+ return Ok(Outcome::fail(FailureCode::Invalid, why));
285335 }
286336 let account = self.account_of(&workspace).await?;
287337 self.audit(&account.id, "billing_details", &format!("{workspace}: invoice details changed"), &a.actor.username).await?;
288− Ok(Outcome::Ok(self.details_of(&workspace).await?))
338+ let details = self.details_of(&workspace).await?;
339+ // An address Stripe Tax can use lifts the hold on charging.
340+ if details.tax_location {
341+ self.tax_address_given(&workspace).await?;
342+ }
343+ Ok(Outcome::Ok(BillingDetails { tax_address_needed_at: None, ..details }))
289344 }
290345 }
291346
+17−10
129129 feature: Feature::Security,
130130 title: Feature::Security.title().to_owned(),
131131 monthly_cents: (micros / 10_000.0).round().max(0.0) as u32,
132+ card_fee_cents: 0,
132133 includes: vec![
133134 "For every private repository in the workspace; public repositories have it free".to_owned(),
134135 "Custom secret patterns, validity checks with issuers, and delegated push protection bypass".to_owned(),
155156 impl Billing {
156157 /// What the g1t plan costs and includes, as it is sold now, at the
157158 /// price book's prices (the same figures as the pricing page's table).
159+ /// With the card fee on top of its monthly price, as it is charged.
158160 pub(crate) async fn plan(&self, feature: Feature) -> Result<Plan> {
159161 let mut book = std::collections::BTreeMap::new();
160− if feature == Feature::Security {
162+ let mut plan = if feature == Feature::Security {
161163 if let Some((_, price)) = self.price(SECURITY_METER).await? {
162164 book.insert(SECURITY_METER, price);
163165 }
164− return Ok(security_plan_at(&book));
165− }
166− for meter in ["build_second", "app_requests", "app_cpu", "custom_domain_month", "private_storage", "git_operations"] {
167− if let Some((_, price)) = self.price(meter).await? {
168− book.insert(meter, price);
166+ security_plan_at(&book)
167+ } else {
168+ for meter in ["build_second", "app_requests", "app_cpu", "custom_domain_month", "private_storage", "git_operations"] {
169+ if let Some((_, price)) = self.price(meter).await? {
170+ book.insert(meter, price);
171+ }
169172 }
170− }
171− Ok(self.plan_at(&book))
173+ self.plan_at(&book)
174+ };
175+ plan.card_fee_cents = self.card_fee_on(i64::from(plan.monthly_cents)).await? as u32;
176+ Ok(plan)
172177 }
173178
174179 /// The plan at the given prices per unit (micros, after the markup);
182187 feature: Feature::Plan,
183188 title: Feature::Plan.title().to_owned(),
184189 monthly_cents: p.plan_monthly_cents,
190+ card_fee_cents: 0,
185191 includes: vec![
186192 format!(
187193 "{} of usage each month at cost plus {}%, used first",
470476 };
471477 if let (Some(customer), Some(method)) = (customer.as_deref(), saved) {
472478 match stripe
473− .subscribe_with_card(&workspace, feature.as_str(), &plan.title, plan.monthly_cents, customer, &method)
479+ .subscribe_with_card(&workspace, feature.as_str(), &plan.title, plan.monthly_cents, plan.card_fee_cents, customer, &method)
474480 .await
475481 {
476482 Ok(subscription) if matches!(subscription.status.as_str(), "active" | "trialing") => {
504510 feature.as_str(),
505511 &plan.title,
506512 plan.monthly_cents,
513+ plan.card_fee_cents,
507514 customer.as_deref(),
508515 return_url,
509516 )
533540 id: &session.id,
534541 workspace: &workspace,
535542 amount_cents: plan.monthly_cents,
536− fee_cents: 0,
543+ fee_cents: plan.card_fee_cents,
537544 created_by: &a.actor.username,
538545 feature: Some(feature.as_str()),
539546 })
+116−22
6666 error.contains("answered 402") || error.contains("\"card_error\"")
6767 }
6868
69−/// A workspace invoice's draft: charged to the card, and holding only the
70−/// lines put on it, never whatever is pending on the customer.
69+/// A workspace invoice's draft: charged to the card, taxed by Stripe Tax,
70+/// and holding only the lines put on it, never whatever is pending on the
71+/// customer.
7172 fn draft_fields(customer: &str, workspace: &str, reason: &str, period: &str, description: String) -> Vec<(&'static str, String)> {
72− vec![
73+ let mut fields = vec![
7374 ("customer", customer.to_owned()),
7475 ("collection_method", "charge_automatically".to_owned()),
7576 ("auto_advance", "false".to_owned()),
7879 ("metadata[g1t_workspace]", workspace.to_owned()),
7980 ("metadata[reason]", reason.to_owned()),
8081 ("metadata[period]", period.to_owned()),
81− ]
82+ ];
83+ fields.extend(crate::stripe::invoice_tax_fields());
84+ fields
8285 }
8386
84−/// One line, on the draft `invoice`.
87+/// One line, on the draft `invoice`, at g1t's tax code, excluding tax.
8588 fn item_fields(customer: &str, invoice: &str, workspace: &str, description: &str, cents: i64) -> Vec<(&'static str, String)> {
86− vec![
89+ let mut fields = vec![
8790 ("customer", customer.to_owned()),
8891 ("invoice", invoice.to_owned()),
8992 ("amount", cents.to_string()),
9093 ("currency", "usd".to_owned()),
9194 ("description", description.to_owned()),
9295 ("metadata[workspace]", workspace.to_owned()),
93− ]
96+ ];
97+ fields.extend(crate::stripe::item_tax_fields());
98+ fields
9499 }
95100
101+/// Whether Stripe left an invoice a draft because Stripe Tax could not
102+/// place the customer.
103+pub(crate) fn needs_tax_location(invoice: &Value) -> bool {
104+ invoice["automatic_tax"]["status"].as_str() == Some("requires_location_inputs")
105+}
106+
96107 #[derive(Deserialize)]
97108 struct InvoiceRow {
98109 invoice_id: String,
104115 hosted_url: Option<String>,
105116 pdf_url: Option<String>,
106117 created_at: String,
118+ #[serde(default)]
119+ fee_micros: i64,
120+ #[serde(default)]
121+ tax_micros: i64,
107122 }
108123
109124 #[derive(Deserialize)]
126141 amount_paid: i64,
127142 #[serde(default)]
128143 charge: Option<String>,
144+ #[serde(default)]
145+ payment_intent: Option<String>,
146+ /// The tax Stripe added, in cents (`tax`, in this API version).
147+ #[serde(default)]
148+ tax: Option<i64>,
129149 }
130150
151+impl StripeInvoice {
152+ fn tax_cents(&self) -> i64 {
153+ self.tax.unwrap_or(0).max(0)
154+ }
155+}
156+
131157 impl Billing {
132158 /// Invoices the workspace for what it owes, charging its card. `Ok(Err)`
133159 /// says why not, when there was nothing to do or no card.
199225 .map(|u| (u.kind, u.charged.unwrap_or(0)))
200226 .collect();
201227 let lines = invoice_lines(&used, owed);
202− let key = format!("ws-invoice/{workspace}/{reason}/{period}/{}", owed / 10_000);
228+ // Stripe Tax needs to know where the customer is. Without an
229+ // address nothing is charged: the owners are asked for one, and the
230+ // charge goes through once it is there.
231+ match stripe.customer_placed(&customer).await {
232+ Ok(true) => {}
233+ Ok(false) => {
234+ self.tax_address_needed(workspace).await?;
235+ return Ok(Err(crate::tax::address_needed_message(workspace)));
236+ }
237+ Err(error) => return Ok(Err(crate::stripe::friendly(&error))),
238+ }
239+ let cents = line_cents(&lines);
240+ // Charged to a card: Stripe's fee is its own line. A bank account
241+ // set as the way to pay has no card fee.
242+ let by_card = match stripe.default_payment_method(&customer).await {
243+ Ok(method) => method.is_none_or(|m| m.kind == "card"),
244+ Err(_) => true,
245+ };
246+ let fee_cents = if by_card { self.card_fee_on(cents.iter().sum::<i64>()).await? } else { 0 };
247+ let key = format!("ws-invoice/{workspace}/{reason}/{period}/{}/{fee_cents}", owed / 10_000);
203248 let description = match reason {
204249 "month" => format!("g1t usage for {workspace}, {period}"),
205250 _ => format!("g1t usage for {workspace}, charged as it neared its limit"),
210255 // total, or the plan's renewal) on top of their own new lines.
211256 let draft: StripeInvoice =
212257 stripe.post_idempotent("/invoices", &draft_fields(&customer, workspace, reason, period, description), &key).await?;
213− let cents = line_cents(&lines);
214258 for (position, (line, cents)) in lines.iter().zip(&cents).enumerate() {
215259 let fields = item_fields(&customer, &draft.id, workspace, &line.description, *cents);
216260 let _: Value = stripe.post_idempotent("/invoiceitems", &fields, &format!("{key}/item/{position}")).await?;
217261 }
262+ if fee_cents > 0 {
263+ let fields = item_fields(&customer, &draft.id, workspace, crate::stripe::CARD_FEE_LINE, fee_cents);
264+ let _: Value = stripe.post_idempotent("/invoiceitems", &fields, &format!("{key}/item/card_fee")).await?;
265+ }
218266 // A retry finds it finalized already; that is fine.
219− let _ = stripe.post::<Value>(&format!("/invoices/{}/finalize", draft.id), &[]).await;
267+ let finalized = stripe.post::<Value>(&format!("/invoices/{}/finalize", draft.id), &[]).await;
268+ if let Err(error) = &finalized
269+ && crate::stripe::is_tax_location_error(error)
270+ {
271+ self.tax_address_needed(workspace).await?;
272+ return Ok(Err(crate::tax::address_needed_message(workspace)));
273+ }
274+ if let Ok(left) = stripe.get::<Value>(&format!("/invoices/{}", draft.id)).await
275+ && left["status"].as_str() == Some("draft")
276+ && needs_tax_location(&left)
277+ {
278+ self.tax_address_needed(workspace).await?;
279+ return Ok(Err(crate::tax::address_needed_message(workspace)));
280+ }
281+ self.tax_address_given(workspace).await?;
220282 // Charge the card now; a decline comes back as an error.
221283 let paid = stripe.post::<StripeInvoice>(&format!("/invoices/{}/pay", draft.id), &[("off_session", "true".to_owned())]).await;
222284 let invoice: StripeInvoice = stripe.get(&format!("/invoices/{}", draft.id)).await?;
223285 let total = lines.iter().map(|l| l.amount_micros).sum::<i64>();
286+ let fee_micros = fee_cents * 10_000;
287+ let tax_micros = invoice.tax_cents() * 10_000;
224288 let status = if invoice.status.as_deref() == Some("paid") { "paid" } else { "failed" };
225289 // Only the card saying no is a decline, which stops work until it
226290 // is paid. Stripe failing to answer, or answering busy, is g1t's
233297 .db
234298 .prepare(
235299 "INSERT OR REPLACE INTO workspace_invoices
236− (invoice_id, workspace, reason, period, amount_micros, status, hosted_url, pdf_url, through_at, created_at, paid_at)
237− VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
300+ (invoice_id, workspace, reason, period, amount_micros, status, hosted_url, pdf_url, through_at, created_at, paid_at,
301+ fee_micros, tax_micros)
302+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
238303 )
239304 .bind(&[
240305 invoice.id.as_str().into(),
248313 now.as_str().into(),
249314 now.as_str().into(),
250315 crate::optional((status == "paid").then_some(now.as_str())),
316+ (fee_micros as f64).into(),
317+ (tax_micros as f64).into(),
251318 ])?];
252319 for (position, line) in lines.iter().enumerate() {
253320 writes.push(
258325 }
259326 self.db.batch(writes).await?;
260327 if status == "paid" {
261− self.credit_invoice(workspace, &invoice).await?;
328+ self.credit_invoice(workspace, &invoice, fee_cents).await?;
262329 } else {
263330 let error = paid.err().map_or_else(|| "the card was declined".to_owned(), |e| e.to_string().chars().take(200).collect());
264331 self.mark_declined(workspace, &error).await?;
274341 pdf_url: invoice.invoice_pdf,
275342 lines,
276343 created_at: now,
344+ fee_micros,
345+ tax_micros,
277346 }))
278347 }
279348
280− /// Enters an invoice's payment once, with the kind of card that paid.
281− async fn credit_invoice(&self, workspace: &str, invoice: &StripeInvoice) -> Result<bool> {
349+ /// Enters an invoice's payment once, with the kind of card that paid:
350+ /// what it paid for usage, never its tax or card fee, which are kept
351+ /// apart (`tax_and_fees`).
352+ async fn credit_invoice(&self, workspace: &str, invoice: &StripeInvoice, fee_cents: i64) -> Result<bool> {
282353 let seen = self
283354 .db
284355 .prepare("SELECT id FROM ledger WHERE reference = ?")
288359 if seen.is_some() {
289360 return Ok(false);
290361 }
291− let amount = invoice.amount_paid * 10_000;
362+ let extras = crate::tax::Extras { tax_cents: invoice.tax_cents(), fee_cents };
363+ let amount = (invoice.amount_paid - extras.tax_cents - extras.fee_cents).max(0) * 10_000;
292364 if amount <= 0 {
293365 return Ok(false);
294366 }
295− self.enter(workspace, EntryKind::TopUp, amount, &format!("Paid invoice {}", invoice.id), &invoice.id, None, None, None, None)
296− .await?;
367+ let description = if extras == crate::tax::Extras::default() {
368+ format!("Paid invoice {}", invoice.id)
369+ } else {
370+ format!(
371+ "Paid invoice {} (tax {} and card fee {} paid with it)",
372+ invoice.id,
373+ crate::features::cents(extras.tax_cents * 10_000),
374+ crate::features::cents(extras.fee_cents * 10_000)
375+ )
376+ };
377+ self.enter(workspace, EntryKind::TopUp, amount, &description, &invoice.id, None, None, None, None).await?;
378+ self.record_extras(workspace, &invoice.id, invoice.payment_intent.as_deref(), extras, None).await?;
297379 // Prepaid cards pay, but never raise the limit.
298380 if let (Some(stripe), Some(charge)) = (&self.stripe, &invoice.charge)
299381 && let Ok(charge) = stripe.get::<Value>(&format!("/charges/{charge}")).await
325407 #[derive(Deserialize)]
326408 struct Row {
327409 workspace: String,
410+ #[serde(default)]
411+ fee_micros: i64,
328412 }
329413 let Some(row) = self
330414 .db
331− .prepare("SELECT workspace FROM workspace_invoices WHERE invoice_id = ?")
415+ .prepare("SELECT workspace, fee_micros FROM workspace_invoices WHERE invoice_id = ?")
332416 .bind(&[invoice_id.into()])?
333417 .first::<Row>(None)
334418 .await?
338422 let Some(stripe) = &self.stripe else { return Ok(None) };
339423 let invoice: StripeInvoice = stripe.get(&format!("/invoices/{invoice_id}")).await?;
340424 self.db
341− .prepare("UPDATE workspace_invoices SET status = 'paid', paid_at = ? WHERE invoice_id = ?")
342− .bind(&[rfc3339(now_ms()).into(), invoice_id.into()])?
425+ .prepare("UPDATE workspace_invoices SET status = 'paid', paid_at = ?, tax_micros = ? WHERE invoice_id = ?")
426+ .bind(&[rfc3339(now_ms()).into(), ((invoice.tax_cents() * 10_000) as f64).into(), invoice_id.into()])?
343427 .run()
344428 .await?;
345− let credited = self.credit_invoice(&row.workspace, &invoice).await?;
429+ let credited = self.credit_invoice(&row.workspace, &invoice, row.fee_micros / 10_000).await?;
346430 Ok(Some(format!(
347431 "invoice {invoice_id} for {} paid{}",
348432 row.workspace,
381465 pdf_url: row.pdf_url,
382466 lines,
383467 created_at: row.created_at,
468+ fee_micros: row.fee_micros,
469+ tax_micros: row.tax_micros,
384470 });
385471 }
386472 Ok(invoices)
419505 fn an_invoice_holds_only_its_own_lines() {
420506 let draft = draft_fields("cus_1", "acme", "month", "2026-09", "g1t usage".to_owned());
421507 assert!(draft.contains(&("pending_invoice_items_behavior", "exclude".to_owned())));
508+ // Taxed by Stripe Tax, every line at g1t's tax code, excluding tax.
509+ assert!(draft.contains(&("automatic_tax[enabled]", "true".to_owned())));
510+ let line = item_fields("cus_1", "in_1", "acme", crate::stripe::CARD_FEE_LINE, 61);
511+ assert!(line.contains(&("tax_code", crate::stripe::TAX_CODE.to_owned())));
512+ assert!(line.contains(&("tax_behavior", "exclusive".to_owned())));
513+ // Left a draft for want of an address: asked for, never charged.
514+ assert!(needs_tax_location(&serde_json::json!({ "automatic_tax": { "status": "requires_location_inputs" } })));
515+ assert!(!needs_tax_location(&serde_json::json!({ "automatic_tax": { "status": "complete" } })));
422516 let item = item_fields("cus_1", "in_1", "acme", "Sandbox time", 1_234);
423517 assert!(item.contains(&("invoice", "in_1".to_owned())));
424518 assert!(item.contains(&("amount", "1234".to_owned())));
+9−3
487487 .iter()
488488 .map(|row| (row.meter.as_str(), Price::price_for(row.cost_micros, row.markup_percent)))
489489 .collect();
490+ // With the card fee on top of each monthly price, as it is charged.
491+ let card_fee = self.card_fee().await?;
490492 let plans: Vec<_> = g1t_contracts::billing::Feature::ALL
491493 .iter()
492− .map(|feature| match feature {
493− g1t_contracts::billing::Feature::Security => crate::features::security_plan_at(&book),
494− _ => self.plan_at(&book),
494+ .map(|feature| {
495+ let mut plan = match feature {
496+ g1t_contracts::billing::Feature::Security => crate::features::security_plan_at(&book),
497+ _ => self.plan_at(&book),
498+ };
499+ plan.card_fee_cents = crate::tax::fee_for(i64::from(plan.monthly_cents), &card_fee) as u32;
500+ plan
495501 })
496502 .collect();
497503 Ok(PriceBook {
+46−20
4646 mod stripe;
4747 mod stripe_sync;
4848 mod subscriptions;
49+mod tax;
4950 mod tokens;
5051
5152 use g1t_contracts::billing::*;
178179 struct CheckoutRow {
179180 workspace: String,
180181 created_by: String,
182+ #[serde(default)]
183+ fee_cents: Option<u32>,
181184 }
182185
183186 /// A payment page started, as `checkouts` keeps it.
588591 } else {
589592 self.row(&workspace).await?.and_then(|row| row.customer_id)
590593 };
591− let started = match stripe.start_checkout(&workspace, a.amount_cents, customer.as_deref(), &a.return_url, bank_transfer).await {
594+ // By card, Stripe's fee is its own line; a bank transfer has none.
595+ let fee = if bank_transfer { 0 } else { self.card_fee_on(i64::from(a.amount_cents)).await? as u32 };
596+ let started = match stripe.start_checkout(&workspace, a.amount_cents, fee, customer.as_deref(), &a.return_url, bank_transfer).await {
592597 // A customer saved under another Stripe account: start afresh.
593598 Err(error) if customer.is_some() && stripe::is_missing(&error) => {
594599 self.forget_customer(&workspace).await?;
595600 let customer = if bank_transfer { self.customer_for(&workspace).await.ok() } else { None };
596− stripe.start_checkout(&workspace, a.amount_cents, customer.as_deref(), &a.return_url, bank_transfer).await
601+ stripe.start_checkout(&workspace, a.amount_cents, fee, customer.as_deref(), &a.return_url, bank_transfer).await
597602 }
598603 other => other,
599604 };
600− self.page_opened(started, &workspace, a.amount_cents, 0, &a.actor.username, None).await
605+ self.page_opened(started, &workspace, a.amount_cents, fee, &a.actor.username, None).await
601606 }
602607
603608 /// A payment page Stripe started (or refused), recorded in `checkouts`
647652 // A prepayment only: a plan's or a card check's page is
648653 // settled where it was started, never credited as money
649654 // paid in advance.
650− "SELECT workspace, created_by FROM checkouts
655+ "SELECT workspace, created_by, fee_cents FROM checkouts
651656 WHERE id = ? AND workspace = ? AND status = 'open' AND feature IS NULL",
652657 )
653658 .bind(&[a.session.as_str().into(), workspace.as_str().into()])?
661666 Ok(session) => session,
662667 Err(error) => return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error))),
663668 };
664− let paid = session
665− .amount_total
666− .filter(|_| session.payment_status == "paid");
667− if let Some(cents) = paid {
669+ if session.payment_status == "paid" && session.amount_total.is_some() {
668670 // Only whoever flips it from open to paid enters the credit.
669671 let claimed = self
670672 .db
676678 .first::<Touched>(None)
677679 .await?;
678680 if claimed.is_some() {
679− self.enter(
680− &checkout.workspace,
681− EntryKind::TopUp,
682− i64::from(cents) * MICROS_PER_DOLLAR / 100,
683− "Paid in advance",
684− &session.id,
685− None,
686− None,
687− Some(&checkout.created_by),
688− session.customer.as_deref(),
689− )
690− .await?;
681+ self.credit_prepayment(&checkout.workspace, &session, checkout.fee_cents.unwrap_or(0), &checkout.created_by).await?;
691682 }
692683 }
693684 Ok(Outcome::Ok(self.standing(&workspace).await?))
694685 }
695686
687+ /// Enters a paid prepayment page: what its lines came to before tax,
688+ /// less the card fee, as credit; its tax and fee kept apart. Once the
689+ /// page is claimed `open → paid`. The credit in cents.
690+ pub(crate) async fn credit_prepayment(&self, workspace: &str, session: &stripe::Session, fee_cents: u32, by: &str) -> Result<i64> {
691+ let fee = i64::from(fee_cents);
692+ let cents = tax::prepay_credit_cents(session.before_tax_cents(), fee);
693+ let tax_cents = session.tax_cents();
694+ let description = if tax_cents > 0 || fee > 0 {
695+ format!(
696+ "Paid in advance (tax {} and card fee {} paid with it)",
697+ features::cents(tax_cents * 10_000),
698+ features::cents(fee * 10_000)
699+ )
700+ } else {
701+ "Paid in advance".to_owned()
702+ };
703+ self.enter(
704+ workspace,
705+ EntryKind::TopUp,
706+ cents * MICROS_PER_DOLLAR / 100,
707+ &description,
708+ &session.id,
709+ None,
710+ None,
711+ Some(by),
712+ session.customer.as_deref(),
713+ )
714+ .await?;
715+ self.record_extras(workspace, &session.id, session.payment_intent.as_deref(), tax::Extras { tax_cents, fee_cents: fee }, None)
716+ .await?;
717+ Ok(cents)
718+ }
719+
696720 /// Drops a saved customer the card processor no longer knows.
697721 pub(crate) async fn forget_customer(&self, workspace: &str) -> Result<()> {
698722 self.db
12871311 "cancel_subscription" => reply(&billing.cancel_subscription(args(body)?).await?),
12881312 "close_workspace" => reply(&billing.close_workspace(args(body)?).await?),
12891313 "has_feature" => reply(&billing.has_feature(args(body)?).await?),
1314+ "free_workspaces" => reply(&billing.free_workspaces(args(body)?).await?),
12901315 "charge_feature" => reply(&billing.charge_feature(args(body)?).await?),
12911316 "record_sandbox" => reply(&billing.record_sandbox(args(body)?).await?),
12921317 "limit" => reply(&billing.limit(args(body)?).await?),
12971322 "admin_billing_link" => reply(&billing.admin_billing_link(args(body)?).await?),
12981323 "admin_stripe" => reply(&billing.admin_stripe(args(body)?).await?),
12991324 "admin_enterprise_billing" => reply(&billing.admin_enterprise_billing(args(body)?).await?),
1325+ "admin_enterprise_address" => reply(&billing.admin_enterprise_address(args(body)?).await?),
13001326 "admin_invoice_enterprise" => reply(&billing.admin_invoice_enterprise(args(body)?).await?),
13011327 "stripe_webhook" => reply(&billing.stripe_webhook(args(body)?).await?),
13021328 "invoices" => reply(&billing.invoices(args(body)?).await?),
+4−0
17381738 let (draws, refunds) = self.credit_effects(&since, &until).await?;
17391739 overall.credits_used_micros = draws.iter().map(|(_, d)| d.micros).sum();
17401740 overall.credits_refunded_micros = refunds.iter().map(|r| r.micros).sum();
1741+ // Tax and card fees came in with payments but are neither cash nor
1742+ // revenue: balances and plan payments are credited without them
1743+ // (tax.rs), so cash above never holds them. Shown apart.
1744+ (overall.tax_collected_micros, overall.card_fees_micros) = self.extras_between(&since, &until).await?;
17411745 let mut products: Vec<ProductMargin> = products.into_values().collect();
17421746 products.sort_by_key(|p| std::cmp::Reverse(p.cost_micros.max(p.value_micros)));
17431747
+2−1
118118 "UPDATE spikes SET workspace = ?1 WHERE workspace = ?2",
119119 "UPDATE limit_requests SET workspace = ?1 WHERE workspace = ?2",
120120 "UPDATE plan_payments SET workspace = ?1 WHERE workspace = ?2",
121+ "UPDATE tax_and_fees SET workspace = ?1 WHERE workspace = ?2",
121122 // One card check per workspace; alerts sent, one per level a month.
122123 "UPDATE OR IGNORE card_checks SET workspace = ?1 WHERE workspace = ?2",
123124 "DELETE FROM card_checks WHERE workspace = ?2",
314315 "allowance_use", "trial_grants", "credit_grants", "storage_days",
315316 "reservations", "spikes", "limit_requests", "plan_payments", "card_checks", "alerts_sent",
316317 "package_storage_days", "pending_days", "token_usage", "price_notices", "closed_workspaces",
317− "workspace_costs", "own_counts", "ai_reload", "ai_reloads",
318+ "workspace_costs", "own_counts", "ai_reload", "ai_reloads", "tax_and_fees",
318319 ] {
319320 assert!(all.contains(&format!("FROM {table} WHERE workspace = ?2"))
320321 || all.contains(&format!("UPDATE {table} SET"))
+2−1
2929 "DELETE FROM checkouts WHERE workspace = ?1",
3030 "DELETE FROM ai_reload WHERE workspace = ?1",
3131 "DELETE FROM ai_reloads WHERE workspace = ?1",
32+ "DELETE FROM tax_and_fees WHERE workspace = ?1",
3233 "DELETE FROM accounts WHERE workspace = ?1",
3334 "DELETE FROM plan_payments WHERE workspace = ?1",
3435 "DELETE FROM subscriptions WHERE workspace = ?1",
138139 "billing_accounts", "allowance_use", "trial_grants", "credit_grants", "storage_days", "package_storage_days",
139140 "token_usage", "reservations", "spikes", "limit_requests", "plan_payments",
140141 "card_checks", "alerts_sent", "price_notices", "closed_workspaces", "workspace_costs",
141− "margin_alerts", "budget_alerts", "ai_reload", "ai_reloads",
142+ "margin_alerts", "budget_alerts", "ai_reload", "ai_reloads", "tax_and_fees",
142143 ] {
143144 assert!(!kept.contains(&table));
144145 assert!(all.contains(&format!("DELETE FROM {table} WHERE")), "{table}");
+93−11
7474 "AI credit" => 8,
7575 "Credits from g1t" => 9,
7676 "Refunds" => 10,
77+ "Tax" => 8,
78+ "Card processing fees" => 8,
7779 _ => 11,
7880 }
7981 }
8082
8183 /// Payments, credits and refunds: money in, which has no price.
8284 pub(crate) fn is_money_in(kind: &str) -> bool {
83− matches!(kind, "Payments" | "AI credit" | "Credits from g1t" | "Refunds")
85+ matches!(kind, "Payments" | "AI credit" | "Credits from g1t" | "Refunds" | "Tax" | "Card processing fees")
8486 }
8587
8688 /// A usage line at its price: what was charged, what paid for it first,
189191 month: String,
190192 }
191193
194+#[derive(Deserialize)]
195+struct ExtraRow {
196+ group_key: Option<String>,
197+ kind: String,
198+ count: u32,
199+ amount: Option<i64>,
200+}
201+
202+/// How the statement names a `tax_and_fees` kind.
203+pub(crate) fn extra_kind(kind: &str) -> &'static str {
204+ if kind == "tax" { "Tax" } else { "Card processing fees" }
205+}
206+
207+/// Puts a line in its group, starting the group if it is new.
208+fn add_line(groups: &mut Vec<StatementGroup>, key: String, line: StatementLine) {
209+ match groups.iter_mut().find(|g| g.key == key) {
210+ Some(group) => group.lines.push(line),
211+ None => groups.push(StatementGroup {
212+ label: if key.is_empty() { "Not one project".to_owned() } else { key.clone() },
213+ key,
214+ lines: vec![line],
215+ charged_micros: 0,
216+ price_micros: 0,
217+ discount_micros: 0,
218+ }),
219+ }
220+}
221+
192222 impl Billing {
193223 pub(crate) async fn statement(&self, a: StatementArgs) -> Result<Outcome<Statement>> {
194224 let workspace = a.workspace.to_lowercase();
234264 discount_micros: discount,
235265 kind: row.kind,
236266 count: row.count,
267+ passed_micros: 0,
237268 };
238− match groups.iter_mut().find(|g| g.key == key) {
239− Some(group) => group.lines.push(line),
240− None => groups.push(StatementGroup {
241− label: if key.is_empty() { "Not one project".to_owned() } else { key.clone() },
242− key,
243− lines: vec![line],
244− charged_micros: 0,
245− price_micros: 0,
246− discount_micros: 0,
247− }),
269+ add_line(&mut groups, key, line);
270+ }
271+ // Tax and card fees paid with the month's payments: their own
272+ // lines, beside the payment, never in what was charged or paid.
273+ let extras = self
274+ .db
275+ .prepare(format!(
276+ "SELECT {} AS group_key, kind, COUNT(*) AS count, SUM(amount_micros) AS amount
277+ FROM tax_and_fees WHERE workspace = ?1 AND created_at >= ?2 AND created_at < ?3 GROUP BY 1, 2",
278+ if by_project { "''" } else { "substr(created_at, 1, 10)" }
279+ ))
280+ .bind(&[workspace.as_str().into(), from.as_str().into(), until.as_str().into()])?
281+ .all()
282+ .await?
283+ .results::<ExtraRow>()?;
284+ let (mut tax_micros, mut card_fee_micros) = (0, 0);
285+ for extra in extras {
286+ let amount = extra.amount.unwrap_or(0);
287+ if extra.kind == "tax" {
288+ tax_micros += amount;
289+ } else {
290+ card_fee_micros += amount;
248291 }
292+ let line = StatementLine {
293+ kind: extra_kind(&extra.kind).to_owned(),
294+ count: extra.count,
295+ charged_micros: 0,
296+ cost_micros: 0,
297+ covered_micros: 0,
298+ price_micros: 0,
299+ discount_micros: 0,
300+ passed_micros: amount,
301+ };
302+ add_line(&mut groups, extra.group_key.unwrap_or_default(), line);
249303 }
250304 for group in &mut groups {
251305 group.lines.sort_by_key(|line| kind_order(&line.kind));
269323 entries: lines.map(|l| l.count).sum(),
270324 covered: covered_lines(covered),
271325 carried_micros: self.carried(&workspace, &month).await?,
326+ tax_micros,
327+ card_fee_micros,
272328 };
273329 let months = self
274330 .db
437493 use super::*;
438494
439495 #[test]
496+ fn tax_and_card_fees_are_their_own_lines_beside_payments_never_charges() {
497+ assert_eq!(extra_kind("tax"), "Tax");
498+ assert_eq!(extra_kind("card_fee"), "Card processing fees");
499+ for kind in ["Tax", "Card processing fees"] {
500+ // Money that came in with a payment: no price, no usage.
501+ assert!(is_money_in(kind));
502+ assert_eq!(kind_order(kind), kind_order("Payments"));
503+ }
504+ let mut groups = vec![];
505+ let line = |kind: &str, passed: i64| StatementLine {
506+ kind: kind.to_owned(),
507+ count: 1,
508+ charged_micros: 0,
509+ cost_micros: 0,
510+ covered_micros: 0,
511+ price_micros: 0,
512+ discount_micros: 0,
513+ passed_micros: passed,
514+ };
515+ add_line(&mut groups, "2026-10-08".into(), line("Tax", 1_640_000));
516+ add_line(&mut groups, "2026-10-08".into(), line("Card processing fees", 920_000));
517+ assert_eq!(groups.len(), 1);
518+ assert!(groups[0].lines.iter().all(|l| l.charged_micros == 0));
519+ }
520+
521+ #[test]
440522 fn a_month_runs_to_the_first_of_the_next() {
441523 assert_eq!(month_range("2026-10"), Some(("2026-10-01".into(), "2026-11-01".into())));
442524 assert_eq!(month_range("2026-12"), Some(("2026-12-01".into(), "2027-01-01".into())));
+586−65
2323 /// code change: read Stripe's upgrade notes for every field billing reads.
2424 pub(crate) const STRIPE_VERSION: &str = "2025-02-24.acacia";
2525
26+/// Stripe Tax's code for what g1t sells, on every product, price and
27+/// invoice line: software as a service, for business use. A card fee line
28+/// carries it too, since a fee for paying for a sale is taxed as the sale.
29+pub(crate) const TAX_CODE: &str = "txcd_10103001";
30+
31+/// What every payment page (payment or subscription mode) asks of Stripe
32+/// Tax: tax worked out on top of the price shown, from a billing address
33+/// it always collects, with the buyer's tax ID if they have one. With a
34+/// customer already, what was entered is saved on it, so invoices and
35+/// off-session charges later find the address too. Checkout refuses
36+/// `automatic_tax` for a customer with no address unless it may save one
37+/// (`customer_update[address]`), and `tax_id_collection` unless it may save
38+/// the name (`customer_update[name]`).
39+pub(crate) fn checkout_tax_fields(customer: Option<&str>) -> Vec<(&'static str, String)> {
40+ let mut fields = vec![
41+ ("automatic_tax[enabled]", "true".to_owned()),
42+ ("billing_address_collection", "required".to_owned()),
43+ ("tax_id_collection[enabled]", "true".to_owned()),
44+ ];
45+ if customer.is_some() {
46+ fields.extend([("customer_update[address]", "auto".to_owned()), ("customer_update[name]", "auto".to_owned())]);
47+ }
48+ fields
49+}
50+
51+/// Whether Stripe refused because it could not tell where the customer is
52+/// for tax: no address, or not enough of one.
53+pub(crate) fn is_tax_location_error(error: &Error) -> bool {
54+ let text = error.to_string();
55+ text.contains("customer_tax_location_invalid") || text.contains("requires_location_inputs")
56+}
57+
58+/// Whether an address is enough for Stripe Tax to place the customer: a
59+/// country, and in the United States a postal code (in Canada a postal
60+/// code or a province).
61+pub(crate) fn address_places_customer(address: &serde_json::Value) -> bool {
62+ let text = |key: &str| address[key].as_str().map(str::trim).unwrap_or_default().to_owned();
63+ match text("country").to_uppercase().as_str() {
64+ "" => false,
65+ "US" => !text("postal_code").is_empty(),
66+ "CA" => !text("postal_code").is_empty() || !text("state").is_empty(),
67+ _ => true,
68+ }
69+}
70+
2671 /// What a Stripe failure says, for the person on the page: Stripe's own
2772 /// message when it gave one (never the request or any key), else that it
2873 /// could not be reached.
57102 pub url: Option<String>,
58103 /// `paid` once the money has been taken.
59104 pub payment_status: String,
60− /// What was paid, in cents.
105+ /// What was paid, in cents, tax included.
61106 pub amount_total: Option<u32>,
107+ /// What the lines came to before tax, in cents.
108+ #[serde(default)]
109+ pub amount_subtotal: Option<u32>,
110+ /// The tax Stripe added (`amount_tax`), among other totals.
111+ #[serde(default)]
112+ pub total_details: Option<TotalDetails>,
113+ /// For a payment page: the payment that took the money.
114+ #[serde(default)]
115+ pub payment_intent: Option<String>,
62116 pub customer: Option<String>,
63117 /// For a plan's page: the subscription it started.
64118 #[serde(default)]
68122 pub setup_intent: Option<String>,
69123 }
70124
125+#[derive(Default, Deserialize)]
126+pub struct TotalDetails {
127+ #[serde(default)]
128+ pub amount_tax: i64,
129+}
130+
131+impl Session {
132+ /// The tax Stripe added on the page, in cents.
133+ pub fn tax_cents(&self) -> i64 {
134+ self.total_details.as_ref().map_or(0, |t| t.amount_tax.max(0))
135+ }
136+
137+ /// What the page's lines came to before tax, in cents: the total less
138+ /// the tax when Stripe gives no subtotal.
139+ pub fn before_tax_cents(&self) -> i64 {
140+ match self.amount_subtotal {
141+ Some(subtotal) => i64::from(subtotal),
142+ None => (i64::from(self.amount_total.unwrap_or(0)) - self.tax_cents()).max(0),
143+ }
144+ }
145+}
146+
71147 /// A card saved and verified: what a card check found.
72148 #[derive(Debug, Deserialize)]
73149 pub struct CheckedCard {
78154 /// `credit`, `debit`, `prepaid` or `unknown`.
79155 pub funding: Option<String>,
80156 pub country: Option<String>,
157+ /// The billing address entered with the card, as Stripe keeps it.
158+ #[serde(default)]
159+ pub address: Option<serde_json::Value>,
81160 }
82161
83162 /// g1t's settings for Stripe's hosted billing page.
325404 &self,
326405 workspace: &str,
327406 amount_cents: u32,
407+ fee_cents: u32,
328408 customer: Option<&str>,
329409 return_url: &str,
330410 bank_transfer: bool,
331411 ) -> Result<Session> {
332− let fields = prepay_fields(workspace, amount_cents, customer, return_url, bank_transfer);
333− let key = page_key("prepay", workspace, &format!("{amount_cents}/{bank_transfer}"), g1t_kit::now_ms());
412+ let fields = prepay_fields(workspace, amount_cents, fee_cents, customer, return_url, bank_transfer);
413+ let key = page_key(
414+ "prepay",
415+ workspace,
416+ &format!("{amount_cents}/{fee_cents}/{bank_transfer}/{}", customer.unwrap_or("new")),
417+ g1t_kit::now_ms(),
418+ );
334419 self.send(Method::Post, "/checkout/sessions", Some(form(&fields)), Some(&key)).await
335420 }
336421
337− /// Starts a page on which a feature's monthly plan is paid for by card.
422+ /// Starts a page on which a feature's monthly plan is paid for by card,
423+ /// with the card fee as a monthly line of its own.
424+ #[allow(clippy::too_many_arguments)]
338425 pub async fn start_subscription(
339426 &self,
340427 workspace: &str,
341428 feature: &str,
342429 title: &str,
343430 monthly_cents: u32,
431+ fee_cents: u32,
344432 customer: Option<&str>,
345433 return_url: &str,
346434 ) -> Result<Session> {
347− let fields = subscription_fields(workspace, feature, title, monthly_cents, customer, return_url);
348− let key = page_key("plan", workspace, &format!("{feature}/{monthly_cents}/{}", customer.unwrap_or("new")), g1t_kit::now_ms());
435+ let fields = subscription_fields(workspace, feature, title, monthly_cents, fee_cents, customer, return_url);
436+ let key = page_key(
437+ "plan",
438+ workspace,
439+ &format!("{feature}/{monthly_cents}/{fee_cents}/{}", customer.unwrap_or("new")),
440+ g1t_kit::now_ms(),
441+ );
349442 self.send(Method::Post, "/checkout/sessions", Some(form(&fields)), Some(&key)).await
350443 }
351444
406499 pub async fn charge_saved(&self, charge: &SavedCharge<'_>) -> Result<serde_json::Value> {
407500 self.send(Method::Post, "/payment_intents", Some(form(&saved_charge_fields(charge))), Some(charge.key)).await
408501 }
502+
503+ /// Stripe Tax's figure for an off-session charge, which no Checkout
504+ /// page or invoice works out: the credit and the card fee, each at
505+ /// g1t's tax code and excluding tax, for the customer's saved address.
506+ /// A customer Stripe cannot place fails with
507+ /// `customer_tax_location_invalid` (`is_tax_location_error`).
508+ pub async fn tax_calculation(&self, charge: &SavedCharge<'_>) -> Result<TaxCalculation> {
509+ let key = format!("{}/tax", charge.key);
510+ self.send(Method::Post, "/tax/calculations", Some(form(&tax_calculation_fields(charge))), Some(&key)).await
511+ }
512+
513+ /// Records a calculation as a tax transaction once its payment went
514+ /// through, so Stripe Tax reports and files it; `reference` is the
515+ /// PaymentIntent. Its id, for reversing it on a refund.
516+ pub async fn record_tax(&self, calculation: &str, reference: &str) -> Result<String> {
517+ #[derive(Deserialize)]
518+ struct Transaction {
519+ id: String,
520+ }
521+ let fields = [("calculation", calculation.to_owned()), ("reference", reference.to_owned())];
522+ let made: Transaction = self
523+ .send(Method::Post, "/tax/transactions/create_from_calculation", Some(form(&fields)), Some(&format!("tax/{reference}")))
524+ .await?;
525+ Ok(made.id)
526+ }
527+
528+ /// Reverses a tax transaction in part for a refund: `refunded_cents` of
529+ /// the payment, tax included, taken back across all of it.
530+ pub async fn reverse_tax(&self, transaction: &str, reference: &str, refunded_cents: i64) -> Result<()> {
531+ let _: serde_json::Value = self
532+ .send(
533+ Method::Post,
534+ "/tax/transactions/create_reversal",
535+ Some(form(&reversal_fields(transaction, reference, refunded_cents))),
536+ Some(&format!("tax-reversal/{reference}")),
537+ )
538+ .await?;
539+ Ok(())
540+ }
541+
542+ /// Copies the billing address entered with a card onto the customer
543+ /// when the customer has none Stripe Tax can use, so later invoices and
544+ /// charges can be taxed. Never replaces an address an owner gave.
545+ pub async fn fill_address(&self, customer: &str, address: &serde_json::Value) -> Result<bool> {
546+ if !address_places_customer(address) {
547+ return Ok(false);
548+ }
549+ let found = self.customer(customer).await?;
550+ if address_places_customer(&found["address"]) {
551+ return Ok(false);
552+ }
553+ let _: serde_json::Value = self.post(&format!("/customers/{}", encode(customer)), &address_fields(address)).await?;
554+ Ok(true)
555+ }
556+
557+ /// Whether the customer's address is enough for Stripe Tax.
558+ pub async fn customer_placed(&self, customer: &str) -> Result<bool> {
559+ let found = self.customer(customer).await?;
560+ Ok(address_places_customer(&found["address"]) || address_places_customer(&found["shipping"]["address"]))
561+ }
562+}
563+
564+/// Stripe Tax's answer for an off-session charge.
565+#[derive(Debug, Deserialize)]
566+pub struct TaxCalculation {
567+ pub id: String,
568+ /// The tax on top, in cents.
569+ #[serde(default)]
570+ pub tax_amount_exclusive: i64,
571+}
572+
573+/// A customer's address from an address Stripe gave.
574+pub(crate) fn address_fields(address: &serde_json::Value) -> Vec<(&'static str, String)> {
575+ let text = |key: &str| address[key].as_str().unwrap_or_default().to_owned();
576+ vec![
577+ ("address[line1]", text("line1")),
578+ ("address[line2]", text("line2")),
579+ ("address[city]", text("city")),
580+ ("address[state]", text("state")),
581+ ("address[postal_code]", text("postal_code")),
582+ ("address[country]", text("country")),
583+ ]
584+}
585+
586+/// A tax calculation's fields: the credit and the card fee as lines at
587+/// g1t's tax code, prices excluding tax.
588+pub(crate) fn tax_calculation_fields(c: &SavedCharge<'_>) -> Vec<(&'static str, String)> {
589+ let mut fields = vec![
590+ ("currency", "usd".to_owned()),
591+ ("customer", c.customer.to_owned()),
592+ ("line_items[0][amount]", c.credit_cents.to_string()),
593+ ("line_items[0][reference]", "ai_credit".to_owned()),
594+ ("line_items[0][tax_code]", TAX_CODE.to_owned()),
595+ ("line_items[0][tax_behavior]", "exclusive".to_owned()),
596+ ];
597+ if c.fee_cents > 0 {
598+ fields.extend([
599+ ("line_items[1][amount]", c.fee_cents.to_string()),
600+ ("line_items[1][reference]", "card_fee".to_owned()),
601+ ("line_items[1][tax_code]", TAX_CODE.to_owned()),
602+ ("line_items[1][tax_behavior]", "exclusive".to_owned()),
603+ ]);
604+ }
605+ fields
409606 }
410607
608+/// A refund's tax reversal: a part of the payment, as a negative amount.
609+pub(crate) fn reversal_fields(transaction: &str, reference: &str, refunded_cents: i64) -> Vec<(&'static str, String)> {
610+ vec![
611+ ("mode", "partial".to_owned()),
612+ ("original_transaction", transaction.to_owned()),
613+ ("reference", reference.to_owned()),
614+ ("flat_amount", (-refunded_cents.abs()).to_string()),
615+ ]
616+}
617+
411618 /// An AI credit purchase, as its payment page needs it.
412619 pub struct CreditPurchase<'a> {
413620 pub workspace: &'a str,
424631 pub payment_method: &'a str,
425632 pub credit_cents: u32,
426633 pub fee_cents: u32,
634+ /// Tax on top, from `tax_calculation`.
635+ pub tax_cents: u32,
636+ /// The calculation the tax came from, kept on the payment.
637+ pub tax_calculation: Option<&'a str>,
427638 pub key: &'a str,
428639 }
429640
468679 format!("{return_url}{separator}{name}={{CHECKOUT_SESSION_ID}}")
469680 }
470681
471−/// A prepayment page's fields.
682+/// The name of the card fee's line everywhere it appears: Checkout pages,
683+/// subscriptions and invoices. Revenue figures find it by this name.
684+pub(crate) const CARD_FEE_LINE: &str = "Card processing fee";
685+
686+/// The form keys of one line on a payment page, for the first two lines
687+/// (what is bought, and the card fee).
688+const LINE_KEYS: [[&str; 8]; 2] = [
689+ [
690+ "line_items[0][quantity]",
691+ "line_items[0][price_data][currency]",
692+ "line_items[0][price_data][unit_amount]",
693+ "line_items[0][price_data][tax_behavior]",
694+ "line_items[0][price_data][product_data][name]",
695+ "line_items[0][price_data][product_data][tax_code]",
696+ "line_items[0][price_data][product_data][description]",
697+ "line_items[0][price_data][recurring][interval]",
698+ ],
699+ [
700+ "line_items[1][quantity]",
701+ "line_items[1][price_data][currency]",
702+ "line_items[1][price_data][unit_amount]",
703+ "line_items[1][price_data][tax_behavior]",
704+ "line_items[1][price_data][product_data][name]",
705+ "line_items[1][price_data][product_data][tax_code]",
706+ "line_items[1][price_data][product_data][description]",
707+ "line_items[1][price_data][recurring][interval]",
708+ ],
709+];
710+
711+/// One line on a payment page at g1t's tax code, its price excluding tax;
712+/// monthly when `monthly`.
713+fn page_line(index: usize, cents: u32, name: String, description: Option<String>, monthly: bool) -> Vec<(&'static str, String)> {
714+ let keys = LINE_KEYS[index.min(1)];
715+ let mut fields = vec![
716+ (keys[0], "1".to_owned()),
717+ (keys[1], "usd".to_owned()),
718+ (keys[2], cents.to_string()),
719+ (keys[3], "exclusive".to_owned()),
720+ (keys[4], name),
721+ (keys[5], TAX_CODE.to_owned()),
722+ ];
723+ if let Some(description) = description {
724+ fields.push((keys[6], description));
725+ }
726+ if monthly {
727+ fields.push((keys[7], "month".to_owned()));
728+ }
729+ fields
730+}
731+
732+/// The card fee's line on a payment page, when there is a fee.
733+fn fee_line(fee_cents: u32, monthly: bool) -> Vec<(&'static str, String)> {
734+ if fee_cents == 0 {
735+ return vec![];
736+ }
737+ page_line(1, fee_cents, CARD_FEE_LINE.to_owned(), Some("Stripe's fee for taking the payment by card, passed on at cost".to_owned()), monthly)
738+}
739+
740+/// A prepayment page's fields. By card, the card fee is its own line; by
741+/// bank transfer there is none.
472742 pub(crate) fn prepay_fields(
473743 workspace: &str,
474744 amount_cents: u32,
745+ fee_cents: u32,
475746 customer: Option<&str>,
476747 return_url: &str,
477748 bank_transfer: bool,
495766 ("cancel_url", return_url.to_owned()),
496767 ("client_reference_id", workspace.to_owned()),
497768 ("metadata[workspace]", workspace.to_owned()),
498− ("line_items[0][quantity]", "1".to_owned()),
499− ("line_items[0][price_data][currency]", "usd".to_owned()),
500− ("line_items[0][price_data][unit_amount]", amount_cents.to_string()),
501− ("line_items[0][price_data][product_data][name]", format!("g1t usage paid in advance for {workspace}")),
502769 ]);
770+ fields.extend(page_line(0, amount_cents, format!("g1t usage paid in advance for {workspace}"), None, false));
771+ if !bank_transfer {
772+ fields.extend(fee_line(fee_cents, false));
773+ }
774+ fields.extend(checkout_tax_fields(customer));
503775 match customer {
504776 Some(customer) => fields.push(("customer", customer.to_owned())),
505777 None => fields.push(("customer_creation", "always".to_owned())),
507779 fields
508780 }
509781
510−/// A plan's page's fields. In subscription mode Stripe makes the customer
511−/// itself when there is none; `customer_creation` is for payment mode only.
782+/// A plan's page's fields: the plan, and the card fee as a monthly line of
783+/// its own. In subscription mode Stripe makes the customer itself when
784+/// there is none; `customer_creation` is for payment mode only. The
785+/// subscription it starts keeps `automatic_tax`, so every renewal is taxed.
512786 pub(crate) fn subscription_fields(
513787 workspace: &str,
514788 feature: &str,
515789 title: &str,
516790 monthly_cents: u32,
791+ fee_cents: u32,
517792 customer: Option<&str>,
518793 return_url: &str,
519794 ) -> Vec<(&'static str, String)> {
527802 ("metadata[feature]", feature.to_owned()),
528803 ("subscription_data[metadata][workspace]", workspace.to_owned()),
529804 ("subscription_data[metadata][feature]", feature.to_owned()),
530− ("line_items[0][quantity]", "1".to_owned()),
531− ("line_items[0][price_data][currency]", "usd".to_owned()),
532− ("line_items[0][price_data][unit_amount]", monthly_cents.to_string()),
533− ("line_items[0][price_data][recurring][interval]", "month".to_owned()),
534− ("line_items[0][price_data][product_data][name]", format!("g1t {title} for {workspace}")),
535805 ];
806+ fields.extend(page_line(0, monthly_cents, format!("g1t {title} for {workspace}"), None, true));
807+ fields.extend(fee_line(fee_cents, true));
808+ fields.extend(checkout_tax_fields(customer));
536809 if let Some(customer) = customer {
537810 fields.push(("customer", customer.to_owned()));
538811 }
539812 fields
540813 }
541814
542−/// A card check's page's fields: setup mode, nothing charged. Setup mode
543−/// takes no line items and no amount.
815+/// A card check's page's fields: setup mode, nothing charged, so nothing
816+/// to tax. Setup mode takes no line items and no amount. The billing
817+/// address is asked for all the same, and copied onto the customer once
818+/// the card is checked (`fill_address`), so the plan and invoices that
819+/// follow can be taxed.
544820 pub(crate) fn card_check_fields(workspace: &str, customer: &str, return_url: &str) -> Vec<(&'static str, String)> {
545821 vec![
546822 ("mode", "setup".to_owned()),
547823 ("customer", customer.to_owned()),
548824 ("payment_method_types[0]", "card".to_owned()),
549825 ("payment_method_options[card][request_three_d_secure]", "any".to_owned()),
826+ ("billing_address_collection", "required".to_owned()),
550827 ("success_url", back_to(return_url, "card_check")),
551828 ("cancel_url", return_url.to_owned()),
552829 ("client_reference_id", workspace.to_owned()),
574851 ("client_reference_id", p.workspace.to_owned()),
575852 ("metadata[workspace]", p.workspace.to_owned()),
576853 ("metadata[purpose]", "ai_credit".to_owned()),
577− ("line_items[0][quantity]", "1".to_owned()),
578− ("line_items[0][price_data][currency]", "usd".to_owned()),
579− ("line_items[0][price_data][unit_amount]", p.credit_cents.to_string()),
580− ("line_items[0][price_data][product_data][name]", "g1t AI credit".to_owned()),
581− (
582− "line_items[0][price_data][product_data][description]",
583− format!("Prepaid credit for Agent and AI Gateway usage in {}; expires a year after purchase", p.workspace),
584− ),
585854 ];
586− if p.fee_cents > 0 {
587− fields.extend([
588− ("line_items[1][quantity]", "1".to_owned()),
589− ("line_items[1][price_data][currency]", "usd".to_owned()),
590− ("line_items[1][price_data][unit_amount]", p.fee_cents.to_string()),
591− ("line_items[1][price_data][product_data][name]", "Card processing fee".to_owned()),
592− ]);
593− }
855+ fields.extend(page_line(
856+ 0,
857+ p.credit_cents,
858+ "g1t AI credit".to_owned(),
859+ Some(format!("Prepaid credit for Agent and AI Gateway usage in {}; expires a year after purchase", p.workspace)),
860+ false,
861+ ));
862+ fields.extend(fee_line(p.fee_cents, false));
863+ fields.extend(checkout_tax_fields(p.customer));
594864 match p.customer {
595865 Some(customer) => fields.push(("customer", customer.to_owned())),
596866 None => fields.push(("customer_creation", "always".to_owned())),
598868 fields
599869 }
600870
601−/// An off-session charge's fields.
871+/// An off-session charge's fields: the credit, the card fee and the tax
872+/// Stripe Tax worked out for them, in one payment.
602873 pub(crate) fn saved_charge_fields(c: &SavedCharge<'_>) -> Vec<(&'static str, String)> {
603− vec![
604− ("amount", (c.credit_cents + c.fee_cents).to_string()),
874+ let mut fields = vec![
875+ ("amount", (c.credit_cents + c.fee_cents + c.tax_cents).to_string()),
605876 ("currency", "usd".to_owned()),
606877 ("customer", c.customer.to_owned()),
607878 ("payment_method", c.payment_method.to_owned()),
612883 ("metadata[purpose]", "ai_reload".to_owned()),
613884 ("metadata[credit_cents]", c.credit_cents.to_string()),
614885 ("metadata[fee_cents]", c.fee_cents.to_string()),
615− ]
886+ ("metadata[tax_cents]", c.tax_cents.to_string()),
887+ ];
888+ if let Some(calculation) = c.tax_calculation {
889+ fields.push(("metadata[tax_calculation]", calculation.to_owned()));
890+ }
891+ fields
892+}
893+
894+/// A Stripe invoice's tax settings, for every invoice g1t makes: worked out
895+/// by Stripe Tax.
896+pub(crate) fn invoice_tax_fields() -> Vec<(&'static str, String)> {
897+ vec![("automatic_tax[enabled]", "true".to_owned())]
898+}
899+
900+/// An invoice item's tax settings: g1t's tax code, the amount excluding tax.
901+pub(crate) fn item_tax_fields() -> Vec<(&'static str, String)> {
902+ vec![("tax_behavior", "exclusive".to_owned()), ("tax_code", TAX_CODE.to_owned())]
903+}
904+
905+/// What a paid Stripe invoice comes to, as billing counts it: the tax
906+/// (`tax`, in this API version), the card fee (its lines, by name), and
907+/// what is left for what was sold, all in cents.
908+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
909+pub(crate) struct InvoiceSplit {
910+ pub tax_cents: i64,
911+ pub fee_cents: i64,
912+ pub net_cents: i64,
913+}
914+
915+/// A plan started on a saved card, as its subscription needs it.
916+pub(crate) struct SavedPlan<'a> {
917+ pub workspace: &'a str,
918+ pub feature: &'a str,
919+ pub title: &'a str,
920+ pub monthly_cents: u32,
921+ pub fee_cents: u32,
922+ pub customer: &'a str,
923+ pub payment_method: &'a str,
616924 }
617925
926+/// A subscription's fields, started on a saved card: the plan, the card
927+/// fee as a monthly item when there is one, each excluding tax, and Stripe
928+/// Tax on.
929+pub(crate) fn saved_subscription_fields(p: &SavedPlan<'_>, product: &str, fee_product: Option<&str>) -> Vec<(&'static str, String)> {
930+ let mut fields = vec![
931+ ("customer", p.customer.to_owned()),
932+ ("default_payment_method", p.payment_method.to_owned()),
933+ ("payment_behavior", "error_if_incomplete".to_owned()),
934+ ("automatic_tax[enabled]", "true".to_owned()),
935+ ("items[0][price_data][currency]", "usd".to_owned()),
936+ ("items[0][price_data][product]", product.to_owned()),
937+ ("items[0][price_data][unit_amount]", p.monthly_cents.to_string()),
938+ ("items[0][price_data][recurring][interval]", "month".to_owned()),
939+ ("items[0][price_data][tax_behavior]", "exclusive".to_owned()),
940+ ("metadata[workspace]", p.workspace.to_owned()),
941+ ("metadata[feature]", p.feature.to_owned()),
942+ ("description", format!("{} plan for {}", p.title, p.workspace)),
943+ ];
944+ if let (Some(fee_product), true) = (fee_product, p.fee_cents > 0) {
945+ fields.extend([
946+ ("items[1][price_data][currency]", "usd".to_owned()),
947+ ("items[1][price_data][product]", fee_product.to_owned()),
948+ ("items[1][price_data][unit_amount]", p.fee_cents.to_string()),
949+ ("items[1][price_data][recurring][interval]", "month".to_owned()),
950+ ("items[1][price_data][tax_behavior]", "exclusive".to_owned()),
951+ ]);
952+ }
953+ fields
954+}
955+
956+pub(crate) fn invoice_split(invoice: &serde_json::Value, amount_paid: i64) -> InvoiceSplit {
957+ let tax_cents = invoice["tax"].as_i64().unwrap_or(0).max(0);
958+ let fee_cents: i64 = invoice["lines"]["data"]
959+ .as_array()
960+ .map(|lines| {
961+ lines
962+ .iter()
963+ .filter(|line| line["description"].as_str().is_some_and(|d| d.contains(CARD_FEE_LINE)))
964+ .map(|line| line["amount"].as_i64().unwrap_or(0))
965+ .sum()
966+ })
967+ .unwrap_or(0);
968+ let fee_cents = fee_cents.max(0);
969+ InvoiceSplit { tax_cents, fee_cents, net_cents: (amount_paid - tax_cents - fee_cents).max(0) }
970+}
971+
618972 impl Stripe {
619973 /// What a card check's setup found, once it succeeded.
620974 pub async fn checked_card(&self, setup_intent: &str) -> Result<Option<CheckedCard>> {
632986 country: Option<String>,
633987 }
634988 #[derive(Deserialize)]
989+ struct Billing {
990+ #[serde(default)]
991+ address: Option<serde_json::Value>,
992+ }
993+ #[derive(Deserialize)]
635994 struct PaymentMethod {
636995 card: Option<Card>,
996+ #[serde(default)]
997+ billing_details: Option<Billing>,
637998 }
638999 let setup: Setup = self.call(Method::Get, &format!("/setup_intents/{}", encode(setup_intent)), None).await?;
6391000 let (true, Some(method)) = (setup.status == "succeeded", setup.payment_method) else { return Ok(None) };
6401001 let found: PaymentMethod = self.call(Method::Get, &format!("/payment_methods/{}", encode(&method)), None).await?;
6411002 let card = found.card;
1003+ let address = found.billing_details.and_then(|b| b.address).filter(|a| a.is_object());
6421004 Ok(Some(CheckedCard {
1005+ address,
6431006 payment_method: method,
6441007 fingerprint: card.as_ref().and_then(|c| c.fingerprint.clone()),
6451008 brand: card.as_ref().and_then(|c| c.brand.clone()),
6611024 Ok(())
6621025 }
6631026
664− /// A feature's product at Stripe (the plan's is tagged `plan`), made
665− /// the first time it is needed.
666− async fn plan_product(&self, feature: &str, title: &str) -> Result<String> {
1027+ /// One of g1t's products at Stripe, tagged `metadata[g1t]` (the plan's
1028+ /// is `plan`, the card fee's `card_fee`), made the first time it is
1029+ /// needed, with g1t's tax code. One made before Stripe Tax was on is
1030+ /// given the code when it is found.
1031+ async fn product(&self, tag: &str, name: &str) -> Result<String> {
6671032 #[derive(Deserialize)]
6681033 struct Product {
6691034 id: String,
6701035 #[serde(default)]
6711036 metadata: Option<std::collections::HashMap<String, String>>,
1037+ #[serde(default)]
1038+ tax_code: Option<serde_json::Value>,
6721039 }
6731040 #[derive(Deserialize)]
6741041 struct List {
6751042 data: Vec<Product>,
6761043 }
6771044 let list: List = self.call(Method::Get, "/products?active=true&limit=100", None).await?;
678− let ours = |p: &Product| p.metadata.as_ref().and_then(|m| m.get("g1t")).map(String::as_str) == Some(feature);
1045+ let ours = |p: &Product| p.metadata.as_ref().and_then(|m| m.get("g1t")).map(String::as_str) == Some(tag);
6791046 if let Some(found) = list.data.into_iter().find(ours) {
1047+ let coded = found.tax_code.as_ref().is_some_and(|code| code.as_str() == Some(TAX_CODE) || code["id"].as_str() == Some(TAX_CODE));
1048+ if !coded {
1049+ let _: serde_json::Value = self
1050+ .call(Method::Post, &format!("/products/{}", encode(&found.id)), Some(form(&[("tax_code", TAX_CODE.to_owned())])))
1051+ .await?;
1052+ }
6801053 return Ok(found.id);
6811054 }
6821055 let created: Product = self
6831056 .call(
6841057 Method::Post,
6851058 "/products",
686− Some(form(&[("name", format!("{title} plan")), ("metadata[g1t]", feature.to_owned())])),
1059+ Some(form(&[("name", name.to_owned()), ("metadata[g1t]", tag.to_owned()), ("tax_code", TAX_CODE.to_owned())])),
6871060 )
6881061 .await?;
6891062 Ok(created.id)
6901063 }
6911064
692− /// Starts the monthly plan on a saved card, at once. Fails rather than
693− /// leaving it half-started when the card's bank wants the person again;
694− /// the caller then sends them to Stripe's page.
1065+ /// Starts the monthly plan on a saved card, at once, with tax worked
1066+ /// out by Stripe Tax on every invoice and the card fee as a monthly item
1067+ /// of its own. Fails rather than leaving it half-started when the card's
1068+ /// bank wants the person again, or when Stripe Tax cannot place the
1069+ /// customer (no billing address yet); the caller then sends them to
1070+ /// Stripe's page, which asks for the address.
1071+ #[allow(clippy::too_many_arguments)]
6951072 pub async fn subscribe_with_card(
6961073 &self,
6971074 workspace: &str,
6981075 feature: &str,
6991076 title: &str,
7001077 monthly_cents: u32,
1078+ fee_cents: u32,
7011079 customer: &str,
7021080 payment_method: &str,
7031081 ) -> Result<StripeSubscription> {
704− let product = self.plan_product(feature, title).await?;
705− let fields = [
706− ("customer", customer.to_owned()),
707− ("default_payment_method", payment_method.to_owned()),
708− ("payment_behavior", "error_if_incomplete".to_owned()),
709− ("items[0][price_data][currency]", "usd".to_owned()),
710− ("items[0][price_data][product]", product),
711− ("items[0][price_data][unit_amount]", monthly_cents.to_string()),
712− ("items[0][price_data][recurring][interval]", "month".to_owned()),
713− ("metadata[workspace]", workspace.to_owned()),
714− ("metadata[feature]", feature.to_owned()),
715− ("description", format!("{title} plan for {workspace}")),
716− ];
717− let key = plan_key(workspace, feature, payment_method, g1t_kit::now_ms());
1082+ let product = self.product(feature, &format!("{title} plan")).await?;
1083+ let fee_product = if fee_cents > 0 { Some(self.product("card_fee", CARD_FEE_LINE).await?) } else { None };
1084+ let plan = SavedPlan { workspace, feature, title, monthly_cents, fee_cents, customer, payment_method };
1085+ let fields = saved_subscription_fields(&plan, &product, fee_product.as_deref());
1086+ let key = plan_key(workspace, feature, &format!("{payment_method}/{monthly_cents}/{fee_cents}"), g1t_kit::now_ms());
7181087 self.send(Method::Post, "/subscriptions", Some(form(&fields)), Some(&key)).await
7191088 }
7201089
1090+
7211091 /// Ends a subscription now: one that never started properly.
7221092 pub async fn cancel_now(&self, id: &str) -> Result<StripeSubscription> {
7231093 self.call(Method::Delete, &format!("/subscriptions/{}", encode(id)), None).await
8101180 fn the_plans_page_is_a_subscription_without_payment_mode_fields() {
8111181 let url = "https://g1t.sh/acme/-/billing?plan=plan";
8121182 for customer in [None, Some("cus_1")] {
813− let fields = subscription_fields("acme", "plan", "g1t", 2_000, customer, url);
1183+ let fields = subscription_fields("acme", "plan", "g1t", 2_000, 92, customer, url);
8141184 assert_eq!(has(&fields, "mode").as_deref(), Some("subscription"));
8151185 // customer_creation is for payment mode; Stripe refuses it here.
8161186 assert!(has(&fields, "customer_creation").is_none());
8181188 assert_eq!(has(&fields, "customer").as_deref(), customer);
8191189 assert_eq!(has(&fields, "success_url").unwrap(), "https://g1t.sh/acme/-/billing?plan=plan&session={CHECKOUT_SESSION_ID}");
8201190 assert_eq!(has(&fields, "line_items[0][price_data][recurring][interval]").as_deref(), Some("month"));
821− assert!(has(&fields, "automatic_tax[enabled]").is_none());
1191+ // The card fee is a monthly line of its own.
1192+ assert_eq!(has(&fields, "line_items[1][price_data][unit_amount]").as_deref(), Some("92"));
1193+ assert_eq!(has(&fields, "line_items[1][price_data][recurring][interval]").as_deref(), Some("month"));
1194+ assert_eq!(has(&fields, "line_items[1][price_data][product_data][name]").as_deref(), Some(CARD_FEE_LINE));
8221195 }
1196+ let fields = subscription_fields("acme", "plan", "g1t", 2_000, 0, None, url);
1197+ assert!(has(&fields, "line_items[1][quantity]").is_none());
1198+ }
1199+
1200+ /// Every field a page must carry for Stripe Tax: tax on top of the
1201+ /// price, an address always asked for, the buyer's tax ID, and each
1202+ /// line at g1t's tax code, excluding tax. With a customer, what is
1203+ /// entered is saved on it (Checkout refuses `automatic_tax` and
1204+ /// `tax_id_collection` for a customer otherwise).
1205+ fn assert_taxed_page(fields: &[(&str, String)], customer: Option<&str>, lines: usize) {
1206+ assert_eq!(has(fields, "automatic_tax[enabled]").as_deref(), Some("true"));
1207+ assert_eq!(has(fields, "billing_address_collection").as_deref(), Some("required"));
1208+ assert_eq!(has(fields, "tax_id_collection[enabled]").as_deref(), Some("true"));
1209+ let saved = if customer.is_some() { Some("auto") } else { None };
1210+ assert_eq!(has(fields, "customer_update[address]").as_deref(), saved);
1211+ assert_eq!(has(fields, "customer_update[name]").as_deref(), saved);
1212+ for (line, keys) in LINE_KEYS.iter().enumerate().take(lines) {
1213+ assert_eq!(has(fields, keys[3]).as_deref(), Some("exclusive"), "line {line}");
1214+ assert_eq!(has(fields, keys[5]).as_deref(), Some(TAX_CODE), "line {line}");
1215+ }
1216+ // No key twice: Stripe takes the last, silently.
1217+ let mut names: Vec<&str> = fields.iter().map(|(n, _)| *n).collect();
1218+ names.sort_unstable();
1219+ let count = names.len();
1220+ names.dedup();
1221+ assert_eq!(names.len(), count);
1222+ }
1223+
1224+ #[test]
1225+ fn every_payment_page_is_taxed_by_stripe_tax() {
1226+ let url = "https://g1t.sh/acme/-/billing";
1227+ for customer in [None, Some("cus_1")] {
1228+ // The plan and Security and quality.
1229+ assert_taxed_page(&subscription_fields("acme", "plan", "g1t", 2_000, 92, customer, url), customer, 2);
1230+ assert_taxed_page(&subscription_fields("acme", "security", "Security and quality", 1_000, 61, customer, url), customer, 2);
1231+ // Prepaying by card, with its fee, and by bank transfer, without.
1232+ assert_taxed_page(&prepay_fields("acme", 5_000, 185, customer, url, false), customer, 2);
1233+ assert_taxed_page(&prepay_fields("acme", 100_000, 0, customer, url, true), customer, 1);
1234+ // AI credit.
1235+ let purchase = CreditPurchase { workspace: "acme", credit_cents: 2_500, fee_cents: 106, customer, return_url: url };
1236+ assert_taxed_page(&credit_fields(&purchase), customer, 2);
1237+ }
1238+ // tax_id_collection and customer_update are for payment and
1239+ // subscription mode; a card check charges nothing, so it is not
1240+ // taxed, but it asks for the address the plan will be taxed at.
1241+ let check = card_check_fields("acme", "cus_1", url);
1242+ assert!(has(&check, "automatic_tax[enabled]").is_none());
1243+ assert_eq!(has(&check, "billing_address_collection").as_deref(), Some("required"));
1244+ }
1245+
1246+ #[test]
1247+ fn a_bank_transfer_has_no_card_fee() {
1248+ let fields = prepay_fields("acme", 100_000, 3_100, Some("cus_1"), "https://g1t.sh/acme/-/billing", true);
1249+ assert!(has(&fields, "line_items[1][quantity]").is_none());
1250+ let fields = prepay_fields("acme", 5_000, 185, None, "https://g1t.sh/acme/-/billing", false);
1251+ assert_eq!(has(&fields, "line_items[1][price_data][unit_amount]").as_deref(), Some("185"));
1252+ assert_eq!(has(&fields, "line_items[1][price_data][product_data][name]").as_deref(), Some(CARD_FEE_LINE));
1253+ }
1254+
1255+ #[test]
1256+ fn a_plan_on_a_saved_card_is_taxed_with_its_card_fee() {
1257+ let plan = SavedPlan { workspace: "acme", feature: "plan", title: "g1t", monthly_cents: 2_000, fee_cents: 92, customer: "cus_1", payment_method: "pm_1" };
1258+ let fields = saved_subscription_fields(&plan, "prod_plan", Some("prod_fee"));
1259+ assert_eq!(has(&fields, "automatic_tax[enabled]").as_deref(), Some("true"));
1260+ assert_eq!(has(&fields, "items[0][price_data][tax_behavior]").as_deref(), Some("exclusive"));
1261+ assert_eq!(has(&fields, "items[1][price_data][product]").as_deref(), Some("prod_fee"));
1262+ assert_eq!(has(&fields, "items[1][price_data][unit_amount]").as_deref(), Some("92"));
1263+ assert_eq!(has(&fields, "items[1][price_data][tax_behavior]").as_deref(), Some("exclusive"));
1264+ let fields = saved_subscription_fields(&SavedPlan { fee_cents: 0, ..plan }, "prod_plan", None);
1265+ assert!(has(&fields, "items[1][price_data][product]").is_none());
8231266 }
8241267
8251268 #[test]
1269+ fn invoices_and_their_lines_are_taxed() {
1270+ assert_eq!(has(&invoice_tax_fields(), "automatic_tax[enabled]").as_deref(), Some("true"));
1271+ let item = item_tax_fields();
1272+ assert_eq!(has(&item, "tax_behavior").as_deref(), Some("exclusive"));
1273+ assert_eq!(has(&item, "tax_code").as_deref(), Some(TAX_CODE));
1274+ }
1275+
1276+ #[test]
1277+ fn an_off_session_charge_is_its_credit_fee_and_tax() {
1278+ let charge = SavedCharge {
1279+ workspace: "acme",
1280+ customer: "cus_1",
1281+ payment_method: "pm_1",
1282+ credit_cents: 1_600,
1283+ fee_cents: 78,
1284+ tax_cents: 134,
1285+ tax_calculation: Some("taxcalc_1"),
1286+ key: "reload/acme/2026-10/1",
1287+ };
1288+ let calculation = tax_calculation_fields(&charge);
1289+ assert_eq!(has(&calculation, "customer").as_deref(), Some("cus_1"));
1290+ assert_eq!(has(&calculation, "line_items[0][amount]").as_deref(), Some("1600"));
1291+ assert_eq!(has(&calculation, "line_items[0][tax_behavior]").as_deref(), Some("exclusive"));
1292+ assert_eq!(has(&calculation, "line_items[0][tax_code]").as_deref(), Some(TAX_CODE));
1293+ assert_eq!(has(&calculation, "line_items[1][amount]").as_deref(), Some("78"));
1294+ assert_eq!(has(&calculation, "line_items[1][tax_code]").as_deref(), Some(TAX_CODE));
1295+ let fields = saved_charge_fields(&charge);
1296+ assert_eq!(has(&fields, "amount").as_deref(), Some("1812"));
1297+ assert_eq!(has(&fields, "metadata[tax_calculation]").as_deref(), Some("taxcalc_1"));
1298+ let reversal = reversal_fields("tax_1", "pi_1/re_1", 500);
1299+ assert_eq!(has(&reversal, "flat_amount").as_deref(), Some("-500"));
1300+ assert_eq!(has(&reversal, "mode").as_deref(), Some("partial"));
1301+ }
1302+
1303+ #[test]
1304+ fn a_paid_invoice_splits_into_tax_card_fee_and_what_was_sold() {
1305+ let invoice = serde_json::json!({
1306+ "tax": 180,
1307+ "lines": { "data": [
1308+ { "description": "1 × g1t plan (at $20.00 / month)", "amount": 2000 },
1309+ { "description": "1 × Card processing fee (at $0.92 / month)", "amount": 92 },
1310+ ] }
1311+ });
1312+ assert_eq!(invoice_split(&invoice, 2_272), InvoiceSplit { tax_cents: 180, fee_cents: 92, net_cents: 2_000 });
1313+ // Before Stripe Tax: no tax, no fee, all of it sold.
1314+ assert_eq!(invoice_split(&serde_json::json!({ "tax": null }), 2_000).net_cents, 2_000);
1315+ // A page's tax and what came before it.
1316+ let session: Session = serde_json::from_value(serde_json::json!({
1317+ "id": "cs_1", "payment_status": "paid", "amount_total": 5_585, "amount_subtotal": 5_185,
1318+ "total_details": { "amount_tax": 400 }, "customer": "cus_1", "payment_intent": "pi_1"
1319+ }))
1320+ .unwrap();
1321+ assert_eq!((session.tax_cents(), session.before_tax_cents()), (400, 5_185));
1322+ }
1323+
1324+ #[test]
1325+ fn an_address_places_a_customer_for_tax() {
1326+ use serde_json::json;
1327+ assert!(address_places_customer(&json!({ "country": "DE" })));
1328+ assert!(address_places_customer(&json!({ "country": "US", "postal_code": "94107" })));
1329+ assert!(!address_places_customer(&json!({ "country": "US", "postal_code": "" })));
1330+ assert!(address_places_customer(&json!({ "country": "CA", "state": "ON" })));
1331+ assert!(!address_places_customer(&json!({ "country": "" })));
1332+ assert!(!address_places_customer(&serde_json::Value::Null));
1333+ let error = Error::RustError(r#"the card processor answered 400: {"error":{"code":"customer_tax_location_invalid","message":"x"}}"#.into());
1334+ assert!(is_tax_location_error(&error));
1335+ }
1336+
1337+ #[test]
8261338 fn a_card_check_is_a_setup_page_with_no_amount() {
8271339 let fields = card_check_fields("acme", "cus_1", "https://g1t.sh/acme/-/billing");
8281340 assert_eq!(has(&fields, "mode").as_deref(), Some("setup"));
8501362
8511363 #[test]
8521364 fn an_auto_reload_is_one_off_session_charge() {
853− let charge = SavedCharge { workspace: "acme", customer: "cus_1", payment_method: "pm_1", credit_cents: 1_600, fee_cents: 78, key: "reload/acme/2026-10/1" };
1365+ let charge = SavedCharge {
1366+ workspace: "acme",
1367+ customer: "cus_1",
1368+ payment_method: "pm_1",
1369+ credit_cents: 1_600,
1370+ fee_cents: 78,
1371+ tax_cents: 0,
1372+ tax_calculation: None,
1373+ key: "reload/acme/2026-10/1",
1374+ };
8541375 let fields = saved_charge_fields(&charge);
8551376 assert_eq!(has(&fields, "amount").as_deref(), Some("1678"));
8561377 assert_eq!(has(&fields, "off_session").as_deref(), Some("true"));
+277−0
1+//! Tax and the card processing fee: what is paid with a payment on top of
2+//! what reaches the workspace's balance.
3+//!
4+//! - **Tax.** Stripe Tax works it out on every Checkout page, subscription,
5+//! invoice and off-session charge (`stripe::checkout_tax_fields`,
6+//! `invoice_tax_fields`, `tax_calculation`), at g1t's tax code, every
7+//! price excluding tax. Prices on g1t are shown before tax.
8+//! - **The card fee.** Stripe's fee, grossed up (`ai::card_fee_cents`), as a
9+//! line of its own on every card payment: the plan and Security and
10+//! quality (a monthly item), prepaying, AI credit, auto-reload and
11+//! invoices charged to a card. Never on a bank transfer or an invoice
12+//! sent to be paid (an enterprise's). Switched by the `card_fee` cost
13+//! setting, on by default. Tax applies to it as to what it is paid with.
14+//! - **Neither is revenue.** A payment's balance credit is what it paid
15+//! less its tax and fee; each is kept in `tax_and_fees`, shown as its own
16+//! line on the statement, and in sudo's Costs as tax collected, never as
17+//! cash.
18+//! - **No address.** When Stripe Tax cannot place a customer, g1t does not
19+//! charge: it marks the account (`accounts.tax_address_needed_at`), tells
20+//! the owners once, and the Billing page asks for the address. Saving
21+//! billing details with one clears it.
22+
23+use g1t_contracts::billing::CardFee;
24+use g1t_contracts::time::rfc3339;
25+use g1t_kit::now_ms;
26+use serde::Deserialize;
27+use worker::Result;
28+
29+use crate::Billing;
30+
31+/// A payment's tax and card fee, in cents, apart from what it paid for.
32+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33+pub(crate) struct Extras {
34+ pub tax_cents: i64,
35+ pub fee_cents: i64,
36+}
37+
38+/// What a prepayment credits: what its lines came to before tax, less the
39+/// card fee line, in cents. Never less than nothing.
40+pub(crate) fn prepay_credit_cents(before_tax_cents: i64, fee_cents: i64) -> i64 {
41+ (before_tax_cents - fee_cents.max(0)).max(0)
42+}
43+
44+/// What of a refund came off the balance, and what was tax and fee given
45+/// back with it: the refund split in the proportion the payment was.
46+/// `extras_cents` is the payment's tax and fee together.
47+pub(crate) fn refund_split(refunded_cents: i64, paid_cents: i64, tax_cents: i64, fee_cents: i64) -> (i64, i64, i64) {
48+ if paid_cents <= 0 || refunded_cents <= 0 {
49+ return (refunded_cents.max(0), 0, 0);
50+ }
51+ let refunded = refunded_cents.min(paid_cents);
52+ let tax = (i128::from(refunded) * i128::from(tax_cents.max(0)) / i128::from(paid_cents)) as i64;
53+ let fee = (i128::from(refunded) * i128::from(fee_cents.max(0)) / i128::from(paid_cents)) as i64;
54+ (refunded - tax - fee, tax, fee)
55+}
56+
57+/// The card fee on a card payment of `cents`, when the setting is on.
58+pub(crate) fn fee_for(cents: i64, fee: &CardFee) -> i64 {
59+ if cents <= 0 {
60+ return 0;
61+ }
62+ i64::from(crate::ai::card_fee_cents(u32::try_from(cents).unwrap_or(u32::MAX), fee))
63+}
64+
65+/// What the owners are told when Stripe Tax cannot place the workspace.
66+pub(crate) fn address_needed_message(workspace: &str) -> String {
67+ format!(
68+ "Add {workspace}'s billing address under Invoice details on the Billing page (/{workspace}/-/billing#details). Stripe needs it to work out tax, so g1t did not charge the card; nothing is lost, and the charge goes through once the address is there."
69+ )
70+}
71+
72+#[derive(Deserialize)]
73+struct Sum {
74+ micros: Option<f64>,
75+}
76+
77+impl Billing {
78+ /// Keeps a payment's tax and card fee apart from what it paid for, once
79+ /// per payment and kind.
80+ pub(crate) async fn record_extras(
81+ &self,
82+ workspace: &str,
83+ reference: &str,
84+ payment_intent: Option<&str>,
85+ extras: Extras,
86+ tax_transaction: Option<&str>,
87+ ) -> Result<()> {
88+ let now = rfc3339(now_ms());
89+ let mut writes = vec![];
90+ for (kind, cents) in [("tax", extras.tax_cents), ("card_fee", extras.fee_cents)] {
91+ if cents == 0 {
92+ continue;
93+ }
94+ writes.push(
95+ self.db
96+ .prepare(
97+ "INSERT OR IGNORE INTO tax_and_fees (id, workspace, kind, amount_micros, reference, payment_intent, tax_transaction, created_at)
98+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
99+ )
100+ .bind(&[
101+ format!("{reference}/{kind}").into(),
102+ workspace.into(),
103+ kind.into(),
104+ ((cents * 10_000) as f64).into(),
105+ reference.into(),
106+ crate::optional(payment_intent),
107+ crate::optional(if kind == "tax" { tax_transaction } else { None }),
108+ now.as_str().into(),
109+ ])?,
110+ );
111+ }
112+ if !writes.is_empty() {
113+ self.db.batch(writes).await?;
114+ }
115+ Ok(())
116+ }
117+
118+ /// A payment's tax and fee, as kept, by the PaymentIntent that took it
119+ /// or the invoice it paid; and its tax transaction, if any.
120+ pub(crate) async fn extras_of(&self, payment_intent: Option<&str>, invoice: Option<&str>) -> Result<(Extras, Option<String>)> {
121+ #[derive(Deserialize)]
122+ struct Row {
123+ kind: String,
124+ amount_micros: i64,
125+ tax_transaction: Option<String>,
126+ }
127+ let rows = self
128+ .db
129+ .prepare(
130+ "SELECT kind, amount_micros, tax_transaction FROM tax_and_fees
131+ WHERE ((payment_intent IS NOT NULL AND payment_intent = ?1) OR reference = ?2) AND reference NOT LIKE 'refund/%'",
132+ )
133+ .bind(&[payment_intent.unwrap_or("").into(), invoice.unwrap_or("").into()])?
134+ .all()
135+ .await?
136+ .results::<Row>()?;
137+ let mut extras = Extras::default();
138+ let mut transaction = None;
139+ for row in rows {
140+ match row.kind.as_str() {
141+ "tax" => {
142+ extras.tax_cents += row.amount_micros / 10_000;
143+ transaction = transaction.or(row.tax_transaction);
144+ }
145+ _ => extras.fee_cents += row.amount_micros / 10_000,
146+ }
147+ }
148+ Ok((extras, transaction))
149+ }
150+
151+ /// The card fee on a card payment, as the price book and the `card_fee`
152+ /// setting have it now.
153+ pub(crate) async fn card_fee_on(&self, cents: i64) -> Result<i64> {
154+ Ok(fee_for(cents, &self.card_fee().await?))
155+ }
156+
157+ /// Marks that Stripe Tax could not place the workspace, and tells its
158+ /// owners the first time.
159+ pub(crate) async fn tax_address_needed(&self, workspace: &str) -> Result<()> {
160+ let now = rfc3339(now_ms());
161+ #[derive(Deserialize)]
162+ struct Row {
163+ #[allow(dead_code)]
164+ workspace: String,
165+ }
166+ let first = self
167+ .db
168+ .prepare("UPDATE accounts SET tax_address_needed_at = ? WHERE workspace = ? AND tax_address_needed_at IS NULL RETURNING workspace")
169+ .bind(&[now.as_str().into(), workspace.into()])?
170+ .first::<Row>(None)
171+ .await?
172+ .is_some();
173+ if first && let Some(identity) = &self.identity {
174+ crate::limits::notify_with(
175+ identity,
176+ workspace,
177+ &format!("g1t: add a billing address for {workspace}"),
178+ &address_needed_message(workspace),
179+ "Add the address",
180+ &format!("https://g1t.sh/{workspace}/-/billing#details"),
181+ "You get this because you own this workspace on g1t. Tax is explained at https://docs.g1t.sh/guides/usage-and-billing/#tax",
182+ )
183+ .await;
184+ }
185+ Ok(())
186+ }
187+
188+ /// Clears the mark once an address is saved.
189+ pub(crate) async fn tax_address_given(&self, workspace: &str) -> Result<()> {
190+ self.db
191+ .prepare("UPDATE accounts SET tax_address_needed_at = NULL WHERE workspace = ?")
192+ .bind(&[workspace.into()])?
193+ .run()
194+ .await?;
195+ Ok(())
196+ }
197+
198+ /// When Stripe Tax last could not place the workspace, while it still
199+ /// cannot.
200+ pub(crate) async fn tax_address_needed_at(&self, workspace: &str) -> Result<Option<String>> {
201+ #[derive(Deserialize)]
202+ struct Row {
203+ tax_address_needed_at: Option<String>,
204+ }
205+ Ok(self
206+ .db
207+ .prepare("SELECT tax_address_needed_at FROM accounts WHERE workspace = ?")
208+ .bind(&[workspace.into()])?
209+ .first::<Row>(None)
210+ .await?
211+ .and_then(|row| row.tax_address_needed_at))
212+ }
213+
214+ /// Tax and card fees kept between two days (inclusive, `YYYY-MM-DD`),
215+ /// in micros, for sudo: (tax, fees).
216+ pub(crate) async fn extras_between(&self, from: &str, to: &str) -> Result<(i64, i64)> {
217+ let sum = |kind: &'static str| async move {
218+ Ok::<i64, worker::Error>(
219+ self.db
220+ .prepare("SELECT SUM(amount_micros) AS micros FROM tax_and_fees WHERE kind = ? AND substr(created_at, 1, 10) BETWEEN ? AND ?")
221+ .bind(&[kind.into(), from.into(), to.into()])?
222+ .first::<Sum>(None)
223+ .await?
224+ .and_then(|s| s.micros)
225+ .unwrap_or(0.0) as i64,
226+ )
227+ };
228+ Ok((sum("tax").await?, sum("card_fee").await?))
229+ }
230+}
231+
232+#[cfg(test)]
233+mod tests {
234+ use super::*;
235+
236+ fn fee(on: bool) -> CardFee {
237+ CardFee { on, percent_micros: 29_000.0, fixed_cents: 30 }
238+ }
239+
240+ #[test]
241+ fn a_prepayment_credits_what_it_bought_never_its_tax_or_fee() {
242+ // $50 bought, $1.85 card fee, $4 tax: $50 credited.
243+ assert_eq!(prepay_credit_cents(5_185, 185), 5_000);
244+ assert_eq!(prepay_credit_cents(100_000, 0), 100_000);
245+ assert_eq!(prepay_credit_cents(100, 500), 0);
246+ }
247+
248+ #[test]
249+ fn a_refund_gives_back_tax_and_fee_in_proportion() {
250+ // $54.00 paid: $50 credit, $1.85 fee, $2.15 tax; half refunded.
251+ let (balance, tax, fee) = refund_split(2_700, 5_400, 215, 185);
252+ assert_eq!(balance + tax + fee, 2_700);
253+ assert_eq!((tax, fee), (107, 92));
254+ // A payment with neither: all of it off the balance.
255+ assert_eq!(refund_split(1_000, 1_000, 0, 0), (1_000, 0, 0));
256+ // Never more than was paid.
257+ assert_eq!(refund_split(9_000, 5_400, 215, 185).0, 5_000);
258+ }
259+
260+ #[test]
261+ fn the_card_fee_is_on_card_payments_unless_switched_off() {
262+ // A $20 plan: $0.91, so that $20 is left after Stripe's 2.9% + 30¢.
263+ assert_eq!(fee_for(2_000, &fee(true)), 91);
264+ assert_eq!(fee_for(2_000, &fee(false)), 0);
265+ assert_eq!(fee_for(0, &fee(true)), 0);
266+ assert_eq!(fee_for(-500, &fee(true)), 0);
267+ // The default is on: a setting never written reads as on.
268+ assert!(g1t_contracts::billing::CostSettings::default().card_fee);
269+ }
270+
271+ #[test]
272+ fn the_owners_are_told_where_to_add_the_address() {
273+ let message = address_needed_message("acme");
274+ assert!(message.contains("/acme/-/billing#details"));
275+ assert!(message.contains("tax"));
276+ }
277+}
+175−38
2222 //! overdue, their work stops until it is paid.
2323
2424 use g1t_contracts::billing::{
25− AdminEnterpriseBillingArgs, AdminInvoiceEnterpriseArgs, AdminStripeArgs, BillingAccount, EnterpriseInvoice,
25+ AdminEnterpriseAddressArgs, AdminEnterpriseBillingArgs, AdminInvoiceEnterpriseArgs, AdminStripeArgs, BillingAccount, EnterpriseInvoice,
2626 EntryKind, InvoiceLine, StripeEventSummary, StripeStatus, StripeWebhook, StripeWebhookArgs,
2727 };
2828 use g1t_contracts::time::rfc3339;
296296 }
297297 "invoice.paid" => {
298298 if let Some(subscription) = invoice_subscription(object) {
299− self.plan_paid(subscription, &text("id"), object["amount_paid"].as_i64().unwrap_or(0)).await?;
299+ self.plan_paid(subscription, object).await?;
300300 self.settle_subscription(subscription).await?
301301 } else if let Some(done) = self.workspace_invoice_paid(&text("id")).await? {
302302 done
303303 } else {
304− self.enterprise_invoice_paid(&text("id")).await?
304+ self.enterprise_invoice_paid(object).await?
305305 }
306306 }
307307 "invoice.payment_failed" => match (invoice_subscription(object), object["metadata"]["g1t_workspace"].as_str()) {
341341 workspace: String,
342342 created_by: String,
343343 feature: Option<String>,
344+ #[serde(default)]
345+ fee_cents: Option<u32>,
344346 }
345347 let Some(open) = self
346348 .db
347− .prepare("SELECT workspace, created_by, feature FROM checkouts WHERE id = ? AND status = 'open'")
349+ .prepare("SELECT workspace, created_by, feature, fee_cents FROM checkouts WHERE id = ? AND status = 'open'")
348350 .bind(&[session_id.into()])?
349351 .first::<Open>(None)
350352 .await?
382384 }
383385 match open.feature.as_deref() {
384386 None => {
385− let cents = i64::from(session.amount_total.unwrap_or(0));
386− self.enter(
387− &open.workspace,
388− EntryKind::TopUp,
389− cents * 10_000,
390− "Paid in advance",
391− &session.id,
392− None,
393− None,
394− Some(&open.created_by),
395− session.customer.as_deref(),
396− )
397− .await?;
387+ let cents = self.credit_prepayment(&open.workspace, &session, open.fee_cents.unwrap_or(0), &open.created_by).await?;
398388 Ok(format!("credited {} to {}", crate::features::dollars(cents * 10_000), open.workspace))
399389 }
400390 Some(feature) => {
420410
421411 /// The plan's monthly price, paid: revenue that never goes through the
422412 /// ledger, recorded once per invoice for sudo's figures and for trust.
423− async fn plan_paid(&self, subscription_id: &str, invoice_id: &str, amount_cents: i64) -> Result<()> {
413+ /// Only the plan's own price is revenue: the invoice's tax and card fee
414+ /// are kept apart (`tax_and_fees`).
415+ async fn plan_paid(&self, subscription_id: &str, invoice: &Value) -> Result<()> {
416+ let invoice_id = invoice["id"].as_str().unwrap_or_default();
417+ let split = crate::stripe::invoice_split(invoice, invoice["amount_paid"].as_i64().unwrap_or(0));
418+ let amount_cents = split.net_cents;
424419 if amount_cents <= 0 || invoice_id.is_empty() {
425420 return Ok(());
426421 }
422+ #[derive(Deserialize)]
423+ struct Owner {
424+ workspace: String,
425+ }
426+ if let Some(owner) = self
427+ .db
428+ .prepare("SELECT workspace FROM subscriptions WHERE subscription_id = ?")
429+ .bind(&[subscription_id.into()])?
430+ .first::<Owner>(None)
431+ .await?
432+ {
433+ let extras = crate::tax::Extras { tax_cents: split.tax_cents, fee_cents: split.fee_cents };
434+ self.record_extras(&owner.workspace, invoice_id, invoice["payment_intent"].as_str(), extras, None).await?;
435+ }
427436 self.db
428437 .prepare(
429438 "INSERT INTO plan_payments (invoice_id, workspace, amount_micros, paid_at)
520529 .await?
521530 .and_then(|s| s.micros)
522531 .unwrap_or(0);
523− let new = refunded * 10_000 - already;
524− if new <= 0 {
532+ // A refund gives back the payment's tax and card fee in proportion:
533+ // only the rest comes off the balance, which never held them.
534+ let (extras, transaction) = self.extras_of(charge["payment_intent"].as_str(), charge["invoice"].as_str()).await?;
535+ let paid = charge["amount"].as_i64().unwrap_or(refunded);
536+ let (balance_cents, tax_cents, fee_cents) = crate::tax::refund_split(refunded, paid, extras.tax_cents, extras.fee_cents);
537+ let (given_tax, given_fee) = self.refunded_extras(charge_id).await?;
538+ let new = balance_cents * 10_000 - already;
539+ let new_extras = crate::tax::Extras { tax_cents: -(tax_cents - given_tax), fee_cents: -(fee_cents - given_fee) };
540+ if new <= 0 && new_extras == crate::tax::Extras::default() {
525541 return Ok("ignored: refund already recorded".to_owned());
526542 }
527− self.enter(
528− &workspace,
529− EntryKind::TopUp,
530− -new,
531− "Refunded to the card",
532− &format!("refund/{charge_id}/{refunded}"),
533− None,
534− None,
535− None,
536− None,
537− )
538− .await?;
539− Ok(format!("refund of {} recorded for {workspace}", crate::features::dollars(new)))
543+ let reference = format!("refund/{charge_id}/{refunded}");
544+ if new > 0 {
545+ self.enter(&workspace, EntryKind::TopUp, -new, "Refunded to the card", &reference, None, None, None, None).await?;
546+ }
547+ self.record_extras(&workspace, &reference, charge["payment_intent"].as_str(), new_extras, None).await?;
548+ // An off-session charge's tax was recorded by g1t (auto-reload):
549+ // reverse what this refund gave back. Checkout's and invoices' tax
550+ // Stripe Tax keeps itself.
551+ if let (Some(stripe), Some(transaction)) = (&self.stripe, transaction.as_deref()) {
552+ let given_back = new.max(0) / 10_000 - new_extras.tax_cents - new_extras.fee_cents;
553+ if given_back > 0
554+ && let Err(error) = stripe.reverse_tax(transaction, &reference, given_back).await
555+ {
556+ worker::console_error!("{workspace}: the tax on refund {reference} was not reversed: {error}");
557+ }
558+ }
559+ Ok(format!("refund of {} recorded for {workspace}", crate::features::dollars(new.max(0))))
560+ }
561+
562+ /// What earlier refunds of a charge gave back of its tax and card fee,
563+ /// in cents, as positive amounts.
564+ async fn refunded_extras(&self, charge_id: &str) -> Result<(i64, i64)> {
565+ #[derive(Deserialize)]
566+ struct Row {
567+ kind: String,
568+ micros: Option<i64>,
569+ }
570+ let rows = self
571+ .db
572+ .prepare("SELECT kind, SUM(amount_micros) AS micros FROM tax_and_fees WHERE reference LIKE ? GROUP BY kind")
573+ .bind(&[format!("refund/{charge_id}/%").into()])?
574+ .all()
575+ .await?
576+ .results::<Row>()?;
577+ let of = |kind: &str| rows.iter().filter(|r| r.kind == kind).map(|r| -r.micros.unwrap_or(0) / 10_000).sum::<i64>();
578+ Ok((of("tax"), of("card_fee")))
540579 }
541580
542581 /// A disputed payment stops the workspace's work until it is resolved;
633672 })
634673 }
635674
675+ /// `admin_enterprise_address`: the address Stripe Tax works the
676+ /// enterprise's invoices out from, and its tax ID, on its customer.
677+ pub(crate) async fn admin_enterprise_address(&self, a: AdminEnterpriseAddressArgs) -> Result<Outcome<bool>> {
678+ if a.by.trim().is_empty() {
679+ return Ok(Outcome::fail(FailureCode::Invalid, "Say who is changing it."));
680+ }
681+ if let Some(why) = crate::details::address_invalid(&a.address).or_else(|| crate::details::tax_id_invalid(a.tax_id_type.as_deref(), a.tax_id.as_deref())) {
682+ return Ok(Outcome::fail(FailureCode::Invalid, why));
683+ }
684+ let place = serde_json::json!({ "country": a.address.country.trim(), "postal_code": a.address.postal_code.trim(), "state": a.address.state.trim() });
685+ if !crate::stripe::address_places_customer(&place) {
686+ return Ok(Outcome::fail(FailureCode::Invalid, "Give at least the country, and in the US the ZIP code: Stripe Tax needs them."));
687+ }
688+ let Some(stripe) = &self.stripe else {
689+ return Ok(Outcome::fail(FailureCode::Conflict, "Payments are not set up on this g1t."));
690+ };
691+ #[derive(Deserialize)]
692+ struct Row {
693+ kind: String,
694+ customer_id: Option<String>,
695+ }
696+ let Some(row) = self
697+ .db
698+ .prepare("SELECT kind, customer_id FROM billing_accounts WHERE id = ?")
699+ .bind(&[a.id.as_str().into()])?
700+ .first::<Row>(None)
701+ .await?
702+ .filter(|row| row.kind == "enterprise")
703+ else {
704+ return Ok(Outcome::fail(FailureCode::NotFound, "No such enterprise."));
705+ };
706+ let Some(customer) = row.customer_id else {
707+ return Ok(Outcome::fail(FailureCode::Conflict, "Set where its invoices go first: that makes its Stripe customer."));
708+ };
709+ let fields: Vec<(&str, String)> = vec![
710+ ("address[line1]", a.address.line1.trim().to_owned()),
711+ ("address[line2]", a.address.line2.trim().to_owned()),
712+ ("address[city]", a.address.city.trim().to_owned()),
713+ ("address[state]", a.address.state.trim().to_owned()),
714+ ("address[postal_code]", a.address.postal_code.trim().to_owned()),
715+ ("address[country]", a.address.country.trim().to_uppercase()),
716+ ];
717+ if let Err(error) = stripe.post::<Value>(&format!("/customers/{customer}"), &fields).await {
718+ return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error)));
719+ }
720+ if let (Some(kind), Some(value)) = (a.tax_id_type.as_deref(), a.tax_id.as_deref())
721+ && let Err(why) = crate::details::replace_tax_id(stripe, &customer, &a.id, kind, value).await
722+ {
723+ return Ok(Outcome::fail(FailureCode::Invalid, why));
724+ }
725+ self.audit(&a.id, "billing_address", &format!("Billing address set ({})", a.address.country.trim().to_uppercase()), &a.by).await?;
726+ Ok(Outcome::Ok(true))
727+ }
728+
636729 pub(crate) async fn admin_invoice_enterprise(&self, a: AdminInvoiceEnterpriseArgs) -> Result<Outcome<EnterpriseInvoice>> {
637730 if a.by.trim().is_empty() {
638731 return Ok(Outcome::fail(FailureCode::Invalid, "Say who is sending it."));
721814 if lines.is_empty() {
722815 return Ok(Err("Its workspaces owe nothing to invoice.".into()));
723816 }
724− let fields = [
817+ // Stripe Tax places the customer by its address; without one the
818+ // invoice could not be finalized, so it is not started.
819+ match stripe.customer_placed(&customer).await {
820+ Ok(true) => {}
821+ Ok(false) => {
822+ return Ok(Err(format!(
823+ "Add {}'s billing address first (Invoices → Billing address): Stripe needs it to work out tax.",
824+ account.name
825+ )));
826+ }
827+ Err(error) => return Ok(Err(crate::stripe::friendly(&error))),
828+ }
829+ // Invoiced, never by card: no card fee. Tax on top, by Stripe Tax.
830+ let mut fields = vec![
725831 ("customer", customer.clone()),
726832 ("collection_method", "send_invoice".to_owned()),
727833 ("days_until_due", "30".to_owned()),
730836 ("metadata[g1t_enterprise]", id.to_owned()),
731837 ("metadata[period]", period.to_owned()),
732838 ];
839+ fields.extend(crate::stripe::invoice_tax_fields());
733840 #[derive(Deserialize)]
734841 struct Invoice {
735842 id: String,
737844 hosted_invoice_url: Option<String>,
738845 #[serde(default)]
739846 amount_due: i64,
847+ /// The tax Stripe added, in cents (`tax`, in this API version).
848+ #[serde(default)]
849+ tax: Option<i64>,
740850 }
741851 // The draft first, keyed on what it bills, and its lines put on it:
742852 // an attempt that failed half way is found again, never billed again
746856 let key = enterprise_invoice_key(id, period, &keyed);
747857 let draft: Invoice = stripe.post_idempotent("/invoices", &fields, &key).await?;
748858 for (line, cents) in lines.iter().zip(&cents) {
749− let fields = [
859+ let mut fields = vec![
750860 ("customer", customer.clone()),
751861 ("invoice", draft.id.clone()),
752862 ("amount", cents.to_string()),
754864 ("description", format!("{}: g1t usage", line.workspace)),
755865 ("metadata[workspace]", line.workspace.clone()),
756866 ];
867+ fields.extend(crate::stripe::item_tax_fields());
757868 let _: Value = stripe.post_idempotent("/invoiceitems", &fields, &format!("{key}/item/{}", line.workspace)).await?;
758869 }
759870 // A retry finds it finalized already; that is fine.
760871 let _ = stripe.post::<Value>(&format!("/invoices/{}/finalize", draft.id), &[]).await;
761− let sent: Invoice = stripe.post(&format!("/invoices/{}/send", draft.id), &[]).await?;
872+ let sent: Invoice = match stripe.post(&format!("/invoices/{}/send", draft.id), &[]).await {
873+ Ok(sent) => sent,
874+ Err(error) if crate::stripe::is_tax_location_error(&error) => {
875+ return Ok(Err(format!("Stripe could not work out tax for {}: add its billing address, then send it again.", account.name)));
876+ }
877+ Err(error) => return Err(error),
878+ };
762879 let now = rfc3339(now_ms());
763− let total = lines.iter().map(|l| l.amount_micros).sum::<i64>().max(sent.amount_due * 10_000);
880+ // What the workspaces owe, before tax: the tax is the enterprise's
881+ // to pay on top, never their usage, and is kept apart.
882+ let tax_cents = sent.tax.unwrap_or(0).max(0);
883+ let total = lines.iter().map(|l| l.amount_micros).sum::<i64>().max((sent.amount_due - tax_cents) * 10_000);
764884 let mut writes = vec![self
765885 .db
766886 .prepare(
799919
800920 /// An enterprise invoice paid: each workspace is credited its line, and
801921 /// any stop for the invoice is lifted.
802− async fn enterprise_invoice_paid(&self, invoice_id: &str) -> Result<String> {
922+ async fn enterprise_invoice_paid(&self, invoice: &Value) -> Result<String> {
923+ let invoice_id = invoice["id"].as_str().unwrap_or_default();
803924 let claimed = self
804925 .db
805926 .prepare(
827948 )
828949 .await?;
829950 }
951+ // Its tax is the enterprise's, paid on top of what its workspaces
952+ // owed, and never credited to them.
953+ #[derive(Deserialize)]
954+ struct Account {
955+ account_id: String,
956+ }
957+ if let Some(account) = self
958+ .db
959+ .prepare("SELECT account_id FROM enterprise_invoices WHERE invoice_id = ?")
960+ .bind(&[invoice_id.into()])?
961+ .first::<Account>(None)
962+ .await?
963+ {
964+ let extras = crate::tax::Extras { tax_cents: invoice["tax"].as_i64().unwrap_or(0).max(0), fee_cents: 0 };
965+ self.record_extras(&account.account_id, invoice_id, invoice["payment_intent"].as_str(), extras, None).await?;
966+ }
830967 Ok(format!("invoice {invoice_id} paid; {} workspaces credited", lines.len()))
831968 }
832969