Merge pull request #1641 from useautumn/fix/attach-tax-rate-id
fix: attach tax rate id
This commit is contained in:
20
bun.lock
20
bun.lock
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@better-auth/cli": "^1.4.21",
|
||||
"@biomejs/biome": "^2.2.7",
|
||||
"@biomejs/biome": "2.2.2",
|
||||
"@trigger.dev/build": "4.4.6",
|
||||
"@types/node": "^24.9.1",
|
||||
"concurrently": "^9.2.1",
|
||||
@@ -937,23 +937,23 @@
|
||||
|
||||
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.2.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.2", "@biomejs/cli-darwin-x64": "2.2.2", "@biomejs/cli-linux-arm64": "2.2.2", "@biomejs/cli-linux-arm64-musl": "2.2.2", "@biomejs/cli-linux-x64": "2.2.2", "@biomejs/cli-linux-x64-musl": "2.2.2", "@biomejs/cli-win32-arm64": "2.2.2", "@biomejs/cli-win32-x64": "2.2.2" }, "bin": { "biome": "bin/biome" } }, "sha512-j1omAiQWCkhuLgwpMKisNKnsM6W8Xtt1l0WZmqY/dFj8QPNkIoTvk4tSsi40FaAAkBE1PU0AFG2RWFBWenAn+w=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6ePfbCeCPryWu0CXlzsWNZgVz/kBEvHiPyNpmViSt6A2eoDf4kXs3YnwQPzGjy8oBgQulrHcLnJL0nkCh80mlQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tn4JmVO+rXsbRslml7FvKaNrlgUeJot++FkvYIhl1OkslVCofAtS35MPlBMhXgKWF9RNr9cwHanrPTUUXcYGag=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-JfrK3gdmWWTh2J5tq/rcWCOsImVyzUnOS2fkjhiYKCQ+v8PqM+du5cfB7G1kXas+7KQeKSWALv18iQqdtIMvzw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-/MhYg+Bd6renn6i1ylGFL5snYUn/Ct7zoGVKhxnro3bwekiZYE8Kl39BSb0MeuqM+72sThkQv4TnNubU9njQRw=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Ogb+77edO5LEP/xbNicACOWVLt8mgC+E1wmpUakr+O4nKwLt9vXe74YNuT3T1dUBxC/SnrVmlzZFC7kQJEfquQ=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ZCLXcZvjZKSiRY/cFANKg+z6Fhsf9MHOzj+NrDQcM+LbqYRT97LyCLWy2AS+W2vP+i89RyRM+kbGpUzbRTYWig=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-wBe2wItayw1zvtXysmHJQoQqXlTzHSpQRyPpJKiNIR21HzH/CrZRDFic1C1jDdp+zAPtqhNExa0owKMbNwW9cQ=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-DAuHhHekGfiGb6lCcsT4UyxQmVwQiBCBUMwVra/dcOSs9q8OhfaZgey51MlekT3p8UwRqtXQfFuEJBhJNdLZwg=="],
|
||||
|
||||
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
|
||||
|
||||
|
||||
@@ -107,11 +107,9 @@
|
||||
"migrate-functions:prod": "infisical run --env=prod --recursive -- bun scripts/migrations/migrate-functions.ts",
|
||||
"validate-schema": "infisical run --env=prod --recursive -- bun scripts/migrations/validate-schema.ts",
|
||||
"validate-shebangs": "infisical run --env=prod --recursive -- bun scripts/migrations/validate-fullsubject-shebangs.ts",
|
||||
|
||||
"tb": "bun scripts/tinybird/index.ts",
|
||||
"tb:prod": "bun scripts/tinybird/index.ts prod",
|
||||
"tb:prod-legacy": "bun scripts/tinybird/index.ts prod-legacy",
|
||||
|
||||
"trigger:deploy": "bunx trigger.dev deploy",
|
||||
"setupci": "node scripts/setup/setupci.js",
|
||||
"replicate": "bun scripts/db/replicate.ts",
|
||||
@@ -159,7 +157,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@better-auth/cli": "^1.4.21",
|
||||
"@biomejs/biome": "^2.2.7",
|
||||
"@biomejs/biome": "2.2.2",
|
||||
"@trigger.dev/build": "4.4.6",
|
||||
"@types/node": "^24.9.1",
|
||||
"concurrently": "^9.2.1",
|
||||
|
||||
@@ -299,6 +299,7 @@ export const setupAttachBillingContext = async ({
|
||||
params.success_url ?? orgToReturnUrl({ org: ctx.org, env: ctx.env }),
|
||||
checkoutSessionParams: params.checkout_session_params,
|
||||
userMetadata: params.metadata,
|
||||
taxRateId: params.tax_rate_id,
|
||||
|
||||
externalId: params.subscription_id,
|
||||
|
||||
|
||||
@@ -42,10 +42,18 @@ export const buildStripeCheckoutSessionAction = ({
|
||||
? "payment"
|
||||
: "subscription";
|
||||
|
||||
// 3. Build line_items from recurring items and one-off items
|
||||
// Payment-mode checkout has no top-level default_tax_rates, so one-off items take per-line tax_rates.
|
||||
const taxRateId = billingContext.taxRateId;
|
||||
const applyTaxRateToLineItem = (
|
||||
item: Stripe.Checkout.SessionCreateParams.LineItem,
|
||||
): Stripe.Checkout.SessionCreateParams.LineItem =>
|
||||
taxRateId ? { ...item, tax_rates: [taxRateId] } : item;
|
||||
|
||||
const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = [
|
||||
...recurringLineItems.filter((item) => item.quantity !== 0),
|
||||
...oneOffLineItems.filter((item) => item.quantity !== 0),
|
||||
...oneOffLineItems
|
||||
.filter((item) => item.quantity !== 0)
|
||||
.map(applyTaxRateToLineItem),
|
||||
];
|
||||
|
||||
// 4. Trial handling (only for subscription mode)
|
||||
@@ -66,6 +74,7 @@ export const buildStripeCheckoutSessionAction = ({
|
||||
end_behavior: { missing_payment_method: "cancel" },
|
||||
},
|
||||
}),
|
||||
...(taxRateId && { default_tax_rates: [taxRateId] }),
|
||||
metadata: buildAutumnSubscriptionMetadata({
|
||||
actionSource: billingContext.actionSource,
|
||||
}),
|
||||
|
||||
@@ -51,6 +51,10 @@ export const executeStripeSubscriptionOperation = async ({
|
||||
const wantsAutoTax =
|
||||
!!ctx.org.config.automatic_tax && !billingContext.invoiceMode;
|
||||
|
||||
const taxRateParams = billingContext.taxRateId
|
||||
? { default_tax_rates: [billingContext.taxRateId] }
|
||||
: {};
|
||||
|
||||
switch (subscriptionAction.type) {
|
||||
case "update": {
|
||||
let stripeSubscription = billingContext.stripeSubscription;
|
||||
@@ -99,6 +103,7 @@ export const executeStripeSubscriptionOperation = async ({
|
||||
...(updateWillCreateInvoice ? invoiceModeParams : {}),
|
||||
...(autumnMeta && { metadata: autumnMeta }),
|
||||
...(wantsAutoTax ? { automatic_tax: { enabled: true } } : {}),
|
||||
...taxRateParams,
|
||||
expand: ["latest_invoice"],
|
||||
},
|
||||
);
|
||||
@@ -110,6 +115,7 @@ export const executeStripeSubscriptionOperation = async ({
|
||||
...fallbackPaymentMethodParams,
|
||||
...(autumnMeta && { metadata: autumnMeta }),
|
||||
...(wantsAutoTax ? { automatic_tax: { enabled: true } } : {}),
|
||||
...taxRateParams,
|
||||
|
||||
billing_mode: { type: "flexible" },
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Pass an explicit Stripe tax rate ID (`tax_rate_id`) through
|
||||
* `/v1/billing.attach` and assert that:
|
||||
* - The created Stripe subscription carries it in `default_tax_rates`.
|
||||
* - The first invoice applies the 10% rate on top of pro's $20 base.
|
||||
*/
|
||||
|
||||
import { expect, test } from "bun:test";
|
||||
import { getStripeSubscription } from "@tests/integration/billing/utils/stripeSubscriptionUtils.js";
|
||||
import { products } from "@tests/utils/fixtures/products.js";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
|
||||
import chalk from "chalk";
|
||||
import type Stripe from "stripe";
|
||||
|
||||
const subTaxRateIds = (sub: Stripe.Subscription): string[] =>
|
||||
(sub.default_tax_rates ?? []).map((rate) =>
|
||||
typeof rate === "string" ? rate : rate.id,
|
||||
);
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("attach-tax-rate-id (v2 /v1/billing.attach): pro $20/mo + explicit 10% tax_rate_id = $22 invoice")}`,
|
||||
async () => {
|
||||
const customerId = "attach-tax-rate-pro";
|
||||
const proProd = products.pro({ id: "pro", items: [] });
|
||||
|
||||
const { ctx, autumnV2_2 } = await initScenario({
|
||||
customerId,
|
||||
setup: [
|
||||
s.customer({ testClock: false, paymentMethod: "success" }),
|
||||
s.products({ list: [proProd] }),
|
||||
],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const taxRate = await ctx.stripeCli.taxRates.create({
|
||||
display_name: "Test Tax",
|
||||
percentage: 10,
|
||||
inclusive: false,
|
||||
});
|
||||
|
||||
await autumnV2_2.billing.attach({
|
||||
customer_id: customerId,
|
||||
plan_id: proProd.id,
|
||||
tax_rate_id: taxRate.id,
|
||||
});
|
||||
|
||||
const { stripeCli, subscription } = await getStripeSubscription({
|
||||
customerId,
|
||||
});
|
||||
|
||||
expect(subTaxRateIds(subscription)).toContain(taxRate.id);
|
||||
|
||||
const latestInvoiceId =
|
||||
typeof subscription.latest_invoice === "string"
|
||||
? subscription.latest_invoice
|
||||
: (subscription.latest_invoice as Stripe.Invoice).id!;
|
||||
const invoice = await stripeCli.invoices.retrieve(latestInvoiceId);
|
||||
|
||||
expect(invoice.subtotal).toBe(2000);
|
||||
expect(invoice.total).toBe(2200);
|
||||
expect(invoice.total - invoice.subtotal).toBe(200);
|
||||
},
|
||||
);
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("attach-tax-rate-id (v2): tax_rate_id set on pro persists through upgrade to premium")}`,
|
||||
async () => {
|
||||
const customerId = "attach-tax-rate-pro-then-premium";
|
||||
const proProd = products.pro({ id: "pro", items: [] });
|
||||
const premiumProd = products.premium({ id: "premium", items: [] });
|
||||
|
||||
const { ctx, autumnV2_2 } = await initScenario({
|
||||
customerId,
|
||||
setup: [
|
||||
s.customer({ testClock: false, paymentMethod: "success" }),
|
||||
s.products({ list: [proProd, premiumProd] }),
|
||||
],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const taxRate = await ctx.stripeCli.taxRates.create({
|
||||
display_name: "Test Tax",
|
||||
percentage: 10,
|
||||
inclusive: false,
|
||||
});
|
||||
|
||||
await autumnV2_2.billing.attach({
|
||||
customer_id: customerId,
|
||||
plan_id: proProd.id,
|
||||
tax_rate_id: taxRate.id,
|
||||
});
|
||||
|
||||
const afterAttach = await getStripeSubscription({ customerId });
|
||||
expect(subTaxRateIds(afterAttach.subscription)).toContain(taxRate.id);
|
||||
|
||||
await autumnV2_2.billing.attach({
|
||||
customer_id: customerId,
|
||||
plan_id: premiumProd.id,
|
||||
});
|
||||
|
||||
const afterUpgrade = await getStripeSubscription({ customerId });
|
||||
expect(subTaxRateIds(afterUpgrade.subscription)).toContain(taxRate.id);
|
||||
},
|
||||
);
|
||||
@@ -35,6 +35,8 @@ export const ExtAttachParamsV0Schema = BillingParamsBaseV0Schema.extend({
|
||||
// For importing an existing subscription...?
|
||||
processor_subscription_id: z.string().optional(),
|
||||
no_billing_changes: z.boolean().optional(),
|
||||
|
||||
tax_rate_id: z.string().optional(),
|
||||
});
|
||||
|
||||
export const AttachParamsV0Schema = ExtAttachParamsV0Schema.extend({
|
||||
|
||||
@@ -95,6 +95,11 @@ export const AttachParamsV1Schema = BillingParamsBaseV1Schema.extend({
|
||||
description:
|
||||
"If true, the customer's plan is activated immediately even when payment is deferred (invoice mode) or pending (Stripe checkout). For Stripe checkout, the customer_product is inserted before the customer completes the hosted form.",
|
||||
}),
|
||||
|
||||
tax_rate_id: z.string().optional().meta({
|
||||
description:
|
||||
"Stripe tax rate ID (txr_...) to apply as the default tax rate on the created subscription, invoice, or checkout session line items.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type AttachParamsV1 = z.infer<typeof AttachParamsV1Schema>;
|
||||
|
||||
@@ -86,6 +86,7 @@ export interface BillingContext {
|
||||
successUrl?: string;
|
||||
checkoutSessionParams?: Record<string, unknown>;
|
||||
userMetadata?: Record<string, string>;
|
||||
taxRateId?: string;
|
||||
|
||||
skipBillingChanges?: boolean;
|
||||
dryRunStripe?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user