feat(billing): Customer Portal + page Paramètres + Standard→Premium via portal

- useCustomerPortal hook (mutation + redirect full-page)
- AccountBillingSection: badge plan + bouton Gérer mon abonnement (Standard/Premium)
- ParametresPage: page conteneur /parametres avec section billing
- PricingPage: Standard→Premium redirige vers Customer Portal (prorata natif Stripe)
- Tests: 212 → 219 verts (+7)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-26 05:38:36 +03:00
parent bda7feb196
commit de16deede3
8 changed files with 453 additions and 17 deletions

View file

@ -0,0 +1,97 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import type { ReactNode } from 'react'
// ─── Mocks ───────────────────────────────────────────────────────────────────
const { createCustomerPortalSessionMock } = vi.hoisted(() => ({
createCustomerPortalSessionMock: vi.fn(),
}))
vi.mock('../api', () => ({
createCustomerPortalSession: createCustomerPortalSessionMock,
}))
import { useCustomerPortal } from '../hooks/useCustomerPortal'
afterEach(() => {
createCustomerPortalSessionMock.mockReset()
})
function wrapper({ children }: { children: ReactNode }) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
})
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
// ─── Tests ───────────────────────────────────────────────────────────────────
describe('useCustomerPortal', () => {
it("openPortal() succès → window.location.href set sur l'URL portal", async () => {
const originalLocation = window.location
const hrefSetter = vi.fn()
Object.defineProperty(window, 'location', {
configurable: true,
writable: true,
value: {
...originalLocation,
get href() {
return originalLocation.href
},
set href(v: string) {
hrefSetter(v)
},
},
})
createCustomerPortalSessionMock.mockResolvedValue({
url: 'https://billing.stripe.com/p/session/abc',
})
const { result } = renderHook(() => useCustomerPortal(), { wrapper })
act(() => {
result.current.openPortal()
})
await waitFor(() => {
expect(hrefSetter).toHaveBeenCalledWith('https://billing.stripe.com/p/session/abc')
})
Object.defineProperty(window, 'location', {
configurable: true,
writable: true,
value: originalLocation,
})
})
it('erreur backend → message backend propagé dans `error`', async () => {
createCustomerPortalSessionMock.mockRejectedValue(
new Error('Aucun abonnement actif trouvé. Souscrivez dabord à un plan.'),
)
const { result } = renderHook(() => useCustomerPortal(), { wrapper })
act(() => {
result.current.openPortal()
})
await waitFor(() => {
expect(result.current.error).toMatch(/Aucun abonnement actif trouvé/)
})
})
it('isLoading vrai pendant la mutation pending', async () => {
createCustomerPortalSessionMock.mockReturnValue(new Promise(() => {}))
const { result } = renderHook(() => useCustomerPortal(), { wrapper })
expect(result.current.isLoading).toBe(false)
act(() => {
result.current.openPortal()
})
await waitFor(() => {
expect(result.current.isLoading).toBe(true)
})
})
})