/** * Shared seed + drive helpers for the end-to-end sales-journey specs. * * These drive the JSON API directly (`page.request.*`) with the * `X-Port-Id` header the `withAuth` helper requires (see * `tests/e2e/smoke/helpers.ts → apiHeaders`). They give the * exhaustive (deterministic) and realapi (Documenso-gated) journey * specs a single source of truth for building a fully-loaded interest * with a priced primary berth — the minimum a deal needs before it can * legally leave the Enquiry stage and walk the pipeline. * * Pipeline stage machine lives in `src/lib/constants.ts` * (`PIPELINE_STAGES` / `STAGE_TRANSITIONS` / `canTransitionStage`): * * enquiry → qualified → nurturing → eoi → reservation * → deposit_paid → contract * * Gates encoded by `changeInterestStage`: * - a yacht must be linked before leaving `enquiry`; * - the primary berth must carry a non-zero `price` before entering * any of eoi / reservation / deposit_paid / contract. */ import { type APIRequestContext, type Page, expect } from '@playwright/test'; import { apiHeaders } from '../smoke/helpers'; export interface SeededDeal { clientId: string; yachtId: string; berthId: string; interestId: string; mooringNumber: string; } interface DataEnvelope { data: T; } /** * Seed a client + yacht + priced berth + interest, all wired together, * via the v1 JSON API. The interest is created with the berth as its * primary link (the create path materialises a primary `interest_berths` * row), and the berth carries a real price so the deal can advance past * Qualified. */ export async function seedFullyLoadedDeal(page: Page, label: string): Promise { const headers = await apiHeaders(page); const stamp = Date.now(); // Mooring numbers must match the canonical `^[A-Z]+\d+$` form; use a // high prefix so we never collide with the global-setup seed berths // (A1, A2, B1). const mooringNumber = `Z${(stamp % 9000) + 1000}`; const clientRes = await page.request.post('/api/v1/clients', { headers, data: { fullName: `${label} Client ${stamp}`, contacts: [{ channel: 'email', value: `journey-${stamp}@example.test`, isPrimary: true }], }, }); expect(clientRes.ok(), `client create: ${clientRes.status()} ${await clientRes.text()}`).toBe( true, ); const clientId = ((await clientRes.json()) as DataEnvelope<{ id: string }>).data.id; const yachtRes = await page.request.post('/api/v1/yachts', { headers, data: { name: `${label} Yacht ${stamp}`, owner: { type: 'client', id: clientId }, }, }); expect(yachtRes.ok(), `yacht create: ${yachtRes.status()} ${await yachtRes.text()}`).toBe(true); const yachtId = ((await yachtRes.json()) as DataEnvelope<{ id: string }>).data.id; const berthRes = await page.request.post('/api/v1/berths', { headers, data: { mooringNumber, area: 'Journey Area', // Priced so the interest can leave Qualified — `changeInterestStage` // rejects a $0 primary berth on any priced stage. price: 250000, priceCurrency: 'EUR', tenureType: 'permanent', }, }); expect(berthRes.ok(), `berth create: ${berthRes.status()} ${await berthRes.text()}`).toBe(true); const berthId = ((await berthRes.json()) as DataEnvelope<{ id: string }>).data.id; const interestRes = await page.request.post('/api/v1/interests', { headers, data: { clientId, yachtId, berthId, pipelineStage: 'enquiry' }, }); expect( interestRes.ok(), `interest create: ${interestRes.status()} ${await interestRes.text()}`, ).toBe(true); const interestId = ((await interestRes.json()) as DataEnvelope<{ id: string }>).data.id; return { clientId, yachtId, berthId, interestId, mooringNumber }; } /** * Drive a single stage transition through the canonical `/stage` PATCH * route and assert the interest now reports the requested stage. Mirrors * the UI: the route enforces `canTransitionStage` + the priced-berth + * yacht gates, so an illegal jump surfaces as a 4xx the caller can assert * on separately. */ export async function changeStage( request: APIRequestContext, headers: Record, interestId: string, pipelineStage: string, ): Promise { const res = await request.patch(`/api/v1/interests/${interestId}/stage`, { headers, data: { pipelineStage }, }); expect(res.ok(), `stage → ${pipelineStage}: ${res.status()} ${await res.text()}`).toBe(true); } /** Fetch the current pipeline stage for an interest via the read API. */ export async function readStage( request: APIRequestContext, headers: Record, interestId: string, ): Promise { const res = await request.get(`/api/v1/interests/${interestId}`, { headers }); expect(res.ok(), `read interest: ${res.status()}`).toBe(true); const body = (await res.json()) as DataEnvelope<{ pipelineStage: string }>; return body.data.pipelineStage; }