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) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-13 10:58:57 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent aab7e47c35
commit e7e3c35f9e
3 changed files with 33 additions and 4 deletions
@@ -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
+11
View File
@@ -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: {
+4 -4
View File
@@ -10,14 +10,14 @@ import type { BillingPlan } from '@/lib/stripe/client'
const PRICE: Record<BillingPlan, { amount: string; suffix: string; sub: string; cta: string }> = {
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',
},
}