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:
co-authored by
Claude Opus 4.8
parent
aab7e47c35
commit
e7e3c35f9e
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user