import { test, expect } from '@playwright/test' import { fireStripeWebhook, getLastOrg, resetDb } from './helpers/api' /** * Scénarios billing — transitions de subscription côté DB après les * différents webhooks Stripe (au-delà de l'essai 14 j déjà couvert * dans billing-trial.spec.ts). * * On teste les états observables : * - subscription.updated trialing → active (J+14 paiement OK) * - subscription.updated trialing → past_due (paiement échoué) * - subscription.deleted → org bascule en Free * - invoice.payment_failed → past_due * * Le rendu SPA du status est testé séparément par les tests vitest * (useSubscription / SubscriptionState). */ async function signupAndStartTrial(page: import('@playwright/test').Page, tag: string) { const email = `${tag}+${Date.now()}@rubis.test` await page.goto('/signup') await page.getByLabel(/Prénom \/ Nom/i).fill('Test') await page.getByLabel(/Email professionnel/i).fill(email) await page.getByLabel(/Mot de passe/i).fill('motdepasse-fort-123') await page.getByRole('button', { name: /créer mon compte/i }).click() await page.waitForURL(/\/onboarding\/compte/, { timeout: 10_000 }) // Onboarding pour avoir un user complet await page.getByRole('button', { name: /continuer/i }).click() await page.waitForURL(/\/onboarding\/entreprise/) await page.getByLabel(/nom de l'entreprise/i).fill('Atelier Billing') await page.getByRole('button', { name: /continuer/i }).click() await page.waitForURL(/\/onboarding\/signature/) await page.getByRole('button', { name: /terminer/i }).click() await page.waitForURL('/', { timeout: 10_000 }) // Démarrer l'essai 14 j (pose stripeCustomerId + envoie redirect) await page.goto('/onboarding/billing') await page.getByRole('button', { name: /démarrer mon essai 14 jours/i }).click() await page.waitForURL(/\/onboarding\/compte\?trial=started/, { timeout: 10_000 }) // Webhook checkout.completed → org en trialing avec sub_e2e_mock posé const org = await getLastOrg() await fireStripeWebhook({ type: 'checkout.session.completed', data: { object: { id: 'cs_e2e_mock', subscription: 'sub_e2e_mock', metadata: { organization_id: org!.id }, }, }, }) return org! } test.describe('Billing — transitions de subscription via webhooks', () => { test.beforeEach(async () => { await resetDb() }) test('trial → active : org reste pro avec status active', async ({ page }) => { const orgBefore = await signupAndStartTrial(page, 'bill-active') expect((await getLastOrg())?.subscription_status).toBe('trialing') // Stripe envoie customer.subscription.updated (status='active') à J+14 await fireStripeWebhook({ type: 'customer.subscription.updated', data: { object: { id: 'sub_e2e_mock', customer: 'cus_e2e_mock', status: 'active', items: { data: [ { id: 'si_e2e', price: { id: 'price_pro_monthly_e2e', lookup_key: 'rubis_pro_monthly' }, current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 3600, }, ], }, cancel_at_period_end: false, metadata: { organization_id: orgBefore.id }, }, }, }) const after = await getLastOrg() expect(after?.subscription_status).toBe('active') expect(after?.plan).toBe('pro') }) test('invoice.payment_failed → org passe en past_due (le plan reste Pro)', async ({ page, }) => { await signupAndStartTrial(page, 'bill-pastdue') await fireStripeWebhook({ type: 'invoice.payment_failed', data: { object: { id: 'in_e2e_failed', customer: 'cus_e2e_mock', status: 'open', }, }, }) const after = await getLastOrg() expect(after?.subscription_status).toBe('past_due') // Important : le plan reste Pro (Stripe smart retries = 7 j de tolérance) expect(after?.plan).toBe('pro') }) test('subscription.deleted → org bascule en Free, trial_ends_at conservé', async ({ page, }) => { const orgBefore = await signupAndStartTrial(page, 'bill-deleted') const trialEndsAtBefore = (await getLastOrg())?.trial_ends_at await fireStripeWebhook({ type: 'customer.subscription.deleted', data: { object: { id: 'sub_e2e_mock', customer: 'cus_e2e_mock', }, }, }) const after = await getLastOrg() expect(after?.id).toBe(orgBefore.id) expect(after?.plan).toBe('free') expect(after?.subscription_status).toBe('canceled') expect(after?.stripe_subscription_id).toBeNull() // trial_ends_at conservé pour l'historique (empêche un re-trial) expect(after?.trial_ends_at).toBeTruthy() expect(after?.trial_ends_at).toBe(trialEndsAtBefore) }) })