import { test, expect } from '@playwright/test' import { fireStripeWebhook, getLastOrg, installStripeMock, resetDb, } from './helpers/api' /** * Scénarios billing trial 14 j avec CB à l'inscription. * * Stripe est mocké au niveau API (cf. test_e2e_controller). La SPA * appelle vraiment `/api/v1/billing/start-trial`, mais la réponse "URL * Stripe" pointe sur notre app (pas sur checkout.stripe.com), pour qu'on * puisse rester dans le navigateur sans dépendre du réseau Stripe. * * Pour valider le flow complet (3DS, prélèvement J+14), cf. le playbook * manuel docs/tech/stripe-trial-e2e-playbook.md. */ async function signupQuick(page: import('@playwright/test').Page) { const email = `bob+${Date.now()}@rubis.test` await page.goto('/signup') await page.getByLabel(/Prénom \/ Nom/i).fill('Bob Martin') 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 }) return { email } } test.describe('Billing trial 14 j', () => { test.beforeEach(async () => { await resetDb() }) test('démarrer l\'essai 14 j redirige vers Stripe Checkout (mock)', async ({ page, }) => { await signupQuick(page) // Navigation manuelle vers l'écran billing (pas encore forcé dans le flow) await page.goto('/onboarding/billing') await expect( page.getByRole('heading', { name: /essayez rubis pro 14 jours/i }), ).toBeVisible() // Click "Démarrer mon essai 14 jours" await page.getByRole('button', { name: /démarrer mon essai 14 jours/i }).click() // Le mock Stripe répond avec l'URL `/onboarding/compte?trial=started&session_id=cs_e2e_mock` await page.waitForURL(/\/onboarding\/compte\?trial=started/, { timeout: 10_000 }) }) test('fallback Free 2 factures continue l\'onboarding sans CB', async ({ page }) => { await signupQuick(page) await page.goto('/onboarding/billing') await page .getByRole('button', { name: /pas de carte.*Free.*2 factures/i }) .click() await expect(page).toHaveURL(/\/onboarding\/compte/) }) test('webhook checkout.completed fait passer l\'org en trialing avec trial_ends_at', async ({ page, }) => { // Note : on ne teste PAS le rendu du banner "Essai Pro" en E2E parce // que TanStack Query (`staleTime: 30_000`) garde le cache initial et // un `page.reload()` blow l'auth en mémoire (refresh par cookie // cross-origin pas fiable sur localhost). Le rendu UI du banner est // déjà couvert par les tests vitest (`useTrialDaysRemaining`, // `useIsAtFreeLimit` bypass trial). Ici on se concentre sur la // chaîne signup → start-trial → webhook → état DB attendu. await signupQuick(page) 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 }) // org créé + Stripe customer posé par start-trial const org = await getLastOrg() expect(org?.id).toBeTruthy() expect(org?.stripe_customer_id).toBe('cus_e2e_mock') // Simule la livraison du webhook checkout.completed await fireStripeWebhook({ type: 'checkout.session.completed', data: { object: { id: 'cs_e2e_mock', subscription: 'sub_e2e_mock', metadata: { organization_id: org!.id }, }, }, }) // L'org doit être en trialing avec trial_ends_at posé const afterWebhook = await getLastOrg() expect(afterWebhook?.subscription_status).toBe('trialing') expect(afterWebhook?.trial_ends_at).toBeTruthy() expect(afterWebhook?.stripe_subscription_id).toBe('sub_e2e_mock') }) test('fermer Stripe Checkout (?trial=cancel) ramène sur onboarding/billing', async ({ page, }) => { await signupQuick(page) await installStripeMock('trial_decline') await page.goto('/onboarding/billing') await page.getByRole('button', { name: /démarrer mon essai 14 jours/i }).click() await page.waitForURL(/\/onboarding\/billing\?trial=cancel/, { timeout: 10_000 }) // Le bouton fallback Free reste disponible await expect( page.getByRole('button', { name: /pas de carte.*Free.*2 factures/i }), ).toBeVisible() }) }) test.describe('Inspection DB après actions', () => { test.beforeEach(async () => { await resetDb() }) test('après start-trial : stripeCustomerId posé sur l\'org', async ({ page }) => { await signupQuick(page) await page.goto('/onboarding/billing') await page.getByRole('button', { name: /démarrer mon essai 14 jours/i }).click() await page.waitForURL(/\/onboarding\/compte/, { timeout: 10_000 }) const org = await getLastOrg() expect(org).not.toBeNull() expect(org!.stripe_customer_id).toBe('cus_e2e_mock') }) })