import type { BillingConfig } from "./types.js"; export type TaxDecision = { /** e.g. 22 for 22%. Zero for reverse charge and out-of-scope sales. */ percent: number; /** * No tax arises, and that is a complete answer rather than a missing one. * * A European seller exporting a digital service outside the EU: the place of * supply is the customer's country, which is outside the EU, so no EU VAT is due. * Distinct from `approximate`, where 0% means "we do not know" — this one means * "nothing, and here is why", which is what belongs on the invoice. */ outOfScope?: boolean; /** * Tax IS due here and this library has no rate for the place it is due, so `percent` * is 0 because there is nothing to apply — NOT because nothing is owed. * * The distinction from `outOfScope` is the whole point: there, 0% is the complete * answer; here it is a missing one. Set when the rated place falls outside the 45 * European countries the dataset covers — the US being the case that matters in * practice, reached either domestically (a US-established seller) or through a * declared US `registrations` entry. US sales tax is destination-based across * 13 000+ jurisdictions: * counties, cities and special districts stack on the state rate, and SaaS is * taxable in some states and not others. Illinois is 6.25% in the table while a * Chicago buyer owes ~10.25%. Getting that right needs address → geocode → * jurisdiction boundaries, which is a data operation and not something a local * table can approximate. * * `taxRatesFor` refuses to mint a Stripe rate from an approximate decision, and * there is no flag to make it stop refusing: the two ways out both say something * true (`registrations`, if you do not in fact owe it; `mode: "stripe"`, if you do). */ approximate?: boolean; /** The customer accounts for the VAT (cross-border EU B2B with a valid id). */ reverseCharge: boolean; country: string; /** "vat" or "none". GST/other regimes are outside this dataset's scope. */ type: string; /** * The country's own word for the tax — "IVA", "TVA", "MwSt" — from the dataset. * * On the invoice, in the language the customer uses. Consumers used to hardcode a * per-country map for this (`{ IT: "IVA", FR: "TVA" }`), which stops at the second * market you sell into. */ displayName?: string; }; /** Where you are registered to collect tax. `state` narrows it to one US state. */ export type TaxRegistration = { /** ISO country code — "IT", "GB", "US". */ country: string; /** * A US state code ("CA", "TX"), for a registration that is not country-wide. * * Omitted, the registration covers the whole country, which is what a VAT * registration is. Present, it covers only that state — which is what US nexus * is, and the reason `state` is threaded here from the customer's address. */ state?: string; }; /** * What tax applies to this customer. * * `originCountry` is where YOU are established: it decides domestic vs * cross-border, so an Italian seller charges 22% to an Italian business but * reverse-charges a German one. * * Providing `taxNumber` triggers a live VIES lookup. VIES has outages, and this * deliberately does NOT convert one into an exemption — see the note above. */ export declare function resolveTax(opts: { originCountry: string; country: string; state?: string | null; taxNumber?: string | null; /** * Where you are registered to collect. **Undefined is "the caller did not say", * never "registered nowhere"** — leaving it out keeps the regime rules alone * deciding (domestic, plus the EU cross-border rules), which is what every * deployment predating this option gets. * * Declaring it switches to one rule for everywhere, domestic included: tax is due * where you say you are registered and nowhere else. An explicit `[]` therefore * differs from undefined, and says something useful — a US-established seller with * no nexus anywhere charges 0% on every sale, correctly and without a refusal. * * One obligation is deliberately NOT registration-gated: destination VAT on a sale * from outside the EU to an EU consumer, which arises with no threshold to sit * under, so an empty list cannot wish it away. */ registrations?: readonly TaxRegistration[]; /** * Are you registered for the EU One-Stop Shop? * * It only decides ONE case: a cross-border EU customer with no valid VAT number. * Reverse charge needs a valid id, so without one the sale is treated as B2C — * and then OSS decides whose rate applies. Registered (the default), the * CUSTOMER's; not registered, YOUR OWN, which is what the sub-€10 000 regime * allows and the only rate you can actually remit without a foreign registration. * * Charging the customer's rate while unregistered collects VAT you have nowhere * to pay over — the mirror of under-collecting, and awkward in a different way. * * Nothing else moves: domestic is domestic, valid-id B2B still reverse charges, * and non-EU is still out of scope. */ oss?: boolean; }): Promise; /** Forget the confirmed VAT numbers — for a test, or after a customer disputes one. */ export declare function invalidateVatNumbers(): void; /** * Test seam: replace the VIES lookup. * * **Calling it with nothing does NOT restore the real lookup — it installs one that * refuses.** Restoring the real one would re-arm the network inside a suite that is * offline by design, and the way that bites is months later: someone writes a test * with a `taxNumber` and no stub, it passes on their machine because VIES happens to * be up and the number happens to be live, and it fails in CI on a member state's * outage. A deterministic failure that names the fix is worth more than a real lookup * no test wants. Nothing in production calls this, so the real one is what ships. */ export declare function __setVatValidatorForTests(fn?: (cleanVatNumber: string) => Promise): void; /** Thrown when a charge would carry a rate this library knows to be wrong. */ export declare class ApproximateTaxError extends Error { readonly decision: TaxDecision; constructor(decision: TaxDecision); } /** Forget the resolved TaxRate ids — for when one is archived in the Dashboard * and Stripe starts rejecting it. */ export declare function invalidateTaxRates(): void; export declare function ensureStripeTaxRate(decision: TaxDecision, opts?: { displayName?: string; }): Promise; export type TaxMode = /** This library: `resolveTax` (eu-vat-rates-data + VIES) → an explicit Stripe TaxRate. */ "local" /** Stripe Tax (`automatic_tax`). Requires registrations, or it computes 0%. */ | "stripe" /** No tax on anything the library charges. */ | "none"; /** * The declared mode. **Nothing declared means `"local"`.** * * That default is the whole point, and it used to be `"none"`. Silence meant no tax * on anything the library charged — so a deployment that never thought about VAT * shipped charging none of it, which is the expensive direction: over-charging is * recoverable and under-collecting means owing it yourself, with interest, in every * jurisdiction you sold into. "I did not configure tax" is not a statement that the * sale is untaxed. * * The mode needs to know where you are ESTABLISHED, and that used to be why it * could not default: no `origin`, no rate. It no longer has to be declared — * `originFor` falls back to the Stripe account's own country, which is the country * you gave Stripe when you signed up and the best available answer. So the default * needs no config at all. * * `automatic: true` is still `"stripe"`, and `"none"` is now an explicit opt-out — * correct for an account that genuinely charges no tax, and something you have to * write down rather than arrive at by omission. */ export declare function taxModeOf(tax: BillingConfig["tax"] | undefined): TaxMode; /** * Where the business is established: `config.tax.origin`, else the Stripe account's * country. * * Explicit always wins — an account registered in one country can be established in * another, and only the app knows. The fallback exists so the common case needs no * config, and it NEVER throws: it is on a charge path, so a Stripe blip must cost a * tax rate, not the charge. */ export declare function originFor(tax: BillingConfig["tax"] | undefined): Promise; /** * Test seam + escape hatch after an account's country changes. * * Also re-arms the one-time "no origin" warning. That warning is deliberately * once-per-process (it is a config error, not a per-charge one, so it must not * appear on every invoice) — which makes it unobservable to any test that is not * the first to trigger it. Resetting it here is what keeps the "says so once" * property testable instead of only asserted in a comment. */ export declare function invalidateTaxOrigin(): void; /** * What to put on a charge, derived from `config.tax` alone. * * Returns the same `{ taxRates, automaticTax }` shape every charge site already * takes, so wiring it is one line per site and an explicit argument still wins. * * Under `"local"` the rate comes from the CUSTOMER: their address decides * domestic vs cross-border and their Stripe tax id decides reverse charge. A * customer with no address on file is charged the DOMESTIC rate rather than * nothing — the same direction `resolveTax` takes for an unverifiable VAT number, * because over-charging is recoverable and under-charging means owing it yourself. * * `decision` is the `TaxDecision` the rate was minted from — country, percent, * reverse charge — present ONLY under `"local"`, because that is the one mode where * this library decided anything. Under `"stripe"` Stripe decides, under `"none"` * nobody does, and behind the consumer's own `rates` hook the consumer did; inventing * a decision there would be a second answer to a question already answered elsewhere. * It exists because a consumer re-derived the customer-country cascade beside this * function to learn whether a quote reverse-charges — the cascade this function * already ran and discarded. */ export declare function taxFor(stripeCustomerId: string | null, tax: BillingConfig["tax"] | undefined): Promise<{ taxRates?: string[]; automaticTax?: boolean; decision?: TaxDecision; }>; /** resolveTax + ensureStripeTaxRate: the ids to put on a session's line items. */ export declare function taxRatesFor(opts: { originCountry: string; country: string; state?: string | null; taxNumber?: string | null; displayName?: string; /** See `resolveTax`. Decides whose rate a cross-border EU sale carries when the * customer has no valid VAT id. */ oss?: boolean; /** See `resolveTax`. Where you are registered to collect; undefined means the * regime rules alone decide. */ registrations?: readonly TaxRegistration[]; /** Mandatory invoice wording per outcome. See `TaxNotes`. */ notes?: TaxNotes; }): Promise<{ decision: TaxDecision; rateIds: string[]; }>; /** * The wording an invoice must carry, per outcome. * * Both are legally mandatory where they apply, which is why they belong in config * rather than at a call site: France fines €15 per invoice missing "TVA non * applicable, art. 293 B du CGI", and the CJEU held in C-247/21 that an omitted * reverse-charge mention cannot be cured after the fact. * * Stripe caps a TaxRate `display_name` at 50 characters, so keep these short — the * mention, not the explanation. */ export type TaxNotes = { /** * Shown when NOTHING is due and it is not reverse charge — a small-business * exemption (France's `franchise en base`), or a supply outside the scope of your * VAT regime. * * Without it an untaxed sale carries no tax line at all, which is right for an * account with no such obligation and wrong for one that has it. */ exempt?: string; /** Shown on a reverse-charge line. Defaults to `"Reverse charge"`; a French * seller wants `"Autoliquidation, art. 196 dir. 2006/112/CE"`. */ reverseCharge?: string; }; /** Which note applies to a decision, if any. */ export declare function noteFor(decision: TaxDecision, notes: TaxNotes | undefined): string | undefined; /** * Re-tax an OPEN Checkout Session, for when the customer's country changes. * * This is the piece Stripe Tax would otherwise do: the total has to be right * before payment, but the country isn't known until the address is typed. Line * item `tax_rates` are updatable mid-session, so the browser triggers this (via * the SDK's `runServerUpdate`) and the totals refresh. * * Every line gets the same rate — one plan, one place of supply. */ export declare function updateCheckoutSessionTaxRates(sessionId: string, rateIds: string[]): Promise; //# sourceMappingURL=tax.d.ts.map