From e7e3c35f9e44ed977f9ec4f3afc6c8d19b248492 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:58:57 +0200 Subject: [PATCH] fix(billing): charge Swedish VAT on Stripe subscriptions (#1011) The subscription price is net (tax_behavior=exclusive in Stripe), so the Checkout session now enables Stripe Tax and collects what the rate needs: - automatic_tax: 25% moms for SE customers, reverse charge for EU-B2B with a valid VAT number; it carries onto the subscription so renewals and the post-trial first charge stay taxed. - tax_id_collection + billing_address_collection: capture the VAT number and address so Stripe issues a compliant momsfaktura. - customer_update: persist name/address onto the pre-created customer. BillingActions now shows "199 kr/man exkl. moms" plus the inkl. price. Co-authored-by: Claude Opus 4.8 (1M context) --- app/api/billing/__tests__/checkout.test.ts | 18 ++++++++++++++++++ app/api/billing/checkout/route.ts | 11 +++++++++++ components/settings/BillingActions.tsx | 8 ++++---- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/app/api/billing/__tests__/checkout.test.ts b/app/api/billing/__tests__/checkout.test.ts index fd91d25d..476dc15e 100644 --- a/app/api/billing/__tests__/checkout.test.ts +++ b/app/api/billing/__tests__/checkout.test.ts @@ -143,6 +143,24 @@ describe('POST /api/billing/checkout', () => { expect(sessionsCreate.mock.calls[0][0].subscription_data.trial_end).toBeUndefined() }) + it('enables automatic VAT, tax-id collection and address capture on the session', async () => { + enqueue({ data: { stripe_customer_id: 'cus_existing' } }) // subscription row + enqueue({ data: null }) // no trial grant + sessionsCreate.mockResolvedValue({ url: 'https://stripe.test/session' }) + + const req = createMockRequest('/api/billing/checkout', { method: 'POST', body: { plan: 'monthly' } }) + await POST(req, routeParams) + + expect(sessionsCreate).toHaveBeenCalledWith( + expect.objectContaining({ + automatic_tax: { enabled: true }, + tax_id_collection: { enabled: true }, + billing_address_collection: 'required', + customer_update: { name: 'auto', address: 'auto' }, + }), + ) + }) + it('creates a Stripe customer when none exists yet', async () => { enqueue({ data: null }) // no existing subscription row enqueue({ data: null }) // no trial grant diff --git a/app/api/billing/checkout/route.ts b/app/api/billing/checkout/route.ts index 7a7d5a5f..9ea43482 100644 --- a/app/api/billing/checkout/route.ts +++ b/app/api/billing/checkout/route.ts @@ -121,6 +121,17 @@ export const POST = withRouteContext('billing.checkout', async (request, ctx) => mode: 'subscription', customer: customerId, line_items: [{ price: priceId, quantity: 1 }], + // Swedish VAT: the Price is net (tax_behavior=exclusive in Stripe), so Stripe + // Tax adds 25% moms for SE customers and applies reverse charge for EU-B2B with + // a valid VAT number. automatic_tax carries onto the created subscription, so + // renewals (and the first charge after a deferred trial) stay taxed. The rate + // can only compute with a customer address, and a compliant momsfaktura needs + // it too; customer_update persists the address + tax id onto the pre-created + // customer for future invoices. + automatic_tax: { enabled: true }, + tax_id_collection: { enabled: true }, + billing_address_collection: 'required', + customer_update: { name: 'auto', address: 'auto' }, client_reference_id: companyId, metadata: { company_id: companyId }, subscription_data: { diff --git a/components/settings/BillingActions.tsx b/components/settings/BillingActions.tsx index b3984cc3..684417f7 100644 --- a/components/settings/BillingActions.tsx +++ b/components/settings/BillingActions.tsx @@ -10,14 +10,14 @@ import type { BillingPlan } from '@/lib/stripe/client' const PRICE: Record = { monthly: { amount: '199 kr', - suffix: '/ mån', - sub: 'Faktureras månadsvis.', + suffix: '/ mån exkl. moms', + sub: '248,75 kr/mån inkl. moms. Faktureras månadsvis.', cta: 'Aktivera abonnemang: 199 kr/mån', }, yearly: { amount: '166 kr', - suffix: '/ mån', - sub: '1 999 kr/år: du betalar för 10 månader.', + suffix: '/ mån exkl. moms', + sub: '1 999 kr/år exkl. moms (2 498,75 kr inkl.). Du betalar för 10 månader.', cta: 'Aktivera årsabonnemang: 1 999 kr/år', }, }