test(documenso): real-API E2E spec + 2.x response normalization

The documenso-template pathway was returning 201 with documensoId=null
because Documenso 2.x renamed `id` → `documentId` and recipient `id` →
`recipientId` in its API responses. Our DocumensoDocument interface
still expected the legacy v1.13 shape, so destructuring silently yielded
undefined and the documents row got NULL'd.

- Add normalizeDocument() in documenso-client that reads either field
  name and surfaces the legacy `id` form downstream consumers expect
- Apply normalization at every callsite that returns DocumensoDocument
  (createDocument, generateDocumentFromTemplate, sendDocument, getDocument)
- New realapi Playwright project (opt-in: --project=realapi) targeting
  tests/e2e/realapi/, with 2-min timeout for real-network calls
- New spec: documenso-real-api.spec.ts seeds client/yacht/berth/interest
  via the v1 API, fires generate-and-sign through the documenso-template
  pathway, asserts the response carries a documensoId, then GETs the
  document directly from Documenso to confirm it exists with PENDING
  status and recipients populated

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-27 15:25:06 +02:00
parent 4441f1177f
commit 4a859245b7
3 changed files with 176 additions and 4 deletions

View File

@@ -0,0 +1,139 @@
import 'dotenv/config';
import { test, expect } from '@playwright/test';
import { login, apiHeaders } from '../smoke/helpers';
/**
* Real-API end-to-end test for the Documenso documenso-template pathway.
*
* This spec exercises the SEND side of the integration only — it creates a
* fully-loaded interest, fires generate-and-sign, and asserts both
* (a) the CRM persisted a documensoId on the documents row, and
* (b) Documenso itself returns 200 for the freshly-created document.
*
* The receive side (webhook → state update) is validated separately by
* triggering a real signing event in the Documenso UI and watching
* /tmp/dev-server.log; it isn't automated here because Documenso has no
* machine-friendly "sign on behalf of recipient" endpoint we can drive
* deterministically from CI.
*
* Requires DOCUMENSO_API_URL + DOCUMENSO_API_KEY + DOCUMENSO_TEMPLATE_ID_EOI
* in the environment (loaded via dotenv).
*/
const DOCUMENSO_BASE = process.env.DOCUMENSO_API_URL;
const DOCUMENSO_API_KEY = process.env.DOCUMENSO_API_KEY;
test.describe('Documenso real-API: documenso-template pathway', () => {
test.skip(!DOCUMENSO_BASE || !DOCUMENSO_API_KEY, 'DOCUMENSO_API_URL / DOCUMENSO_API_KEY not set');
test.beforeEach(async ({ page }) => {
await login(page, 'super_admin');
});
test('generate-and-sign creates a real Documenso document', async ({ page }) => {
const stamp = Date.now();
const headers = await apiHeaders(page);
// ─── 1. Seed client (with a contact email so Documenso has a recipient) ──
const clientRes = await page.request.post('/api/v1/clients', {
headers,
data: {
fullName: `Documenso E2E Client ${stamp}`,
contacts: [
{ channel: 'email', value: `documenso-e2e-${stamp}@example.test`, isPrimary: true },
],
},
});
expect(clientRes.ok(), `client create: ${clientRes.status()} ${await clientRes.text()}`).toBe(
true,
);
const clientId = (await clientRes.json()).data.id as string;
// ─── 2. Seed yacht owned by that client ──────────────────────────────────
const yachtRes = await page.request.post('/api/v1/yachts', {
headers,
data: {
name: `E2E Yacht ${stamp}`,
owner: { type: 'client', id: clientId },
},
});
expect(yachtRes.ok(), `yacht create: ${yachtRes.status()} ${await yachtRes.text()}`).toBe(true);
const yachtId = (await yachtRes.json()).data.id as string;
// ─── 3. Seed berth ───────────────────────────────────────────────────────
const berthRes = await page.request.post('/api/v1/berths', {
headers,
data: {
mooringNumber: `E2E-${stamp}`,
area: 'Test Area',
},
});
expect(berthRes.ok(), `berth create: ${berthRes.status()} ${await berthRes.text()}`).toBe(true);
const berthId = (await berthRes.json()).data.id as string;
// ─── 4. Create the interest linking all three ────────────────────────────
const interestRes = await page.request.post('/api/v1/interests', {
headers,
data: { clientId, yachtId, berthId, pipelineStage: 'open' },
});
expect(
interestRes.ok(),
`interest create: ${interestRes.status()} ${await interestRes.text()}`,
).toBe(true);
const interestId = (await interestRes.json()).data.id as string;
// ─── 5. Fire generate-and-sign through the documenso-template pathway ────
const signRes = await page.request.post(
'/api/v1/document-templates/documenso-template/generate-and-sign',
{
headers,
data: {
interestId,
clientId,
berthId,
pathway: 'documenso-template',
signers: [],
},
},
);
expect(signRes.ok(), `generate-and-sign: ${signRes.status()} ${await signRes.text()}`).toBe(
true,
);
const body = (await signRes.json()) as {
data: { document: { id: string; documensoId: string | null; status: string } };
};
expect(body.data.document.documensoId, 'documensoId on response').toBeTruthy();
expect(body.data.document.status).toBe('sent');
const documensoId = body.data.document.documensoId!;
// ─── 6. Verify the document exists on the Documenso side ─────────────────
const documensoRes = await page.request.get(
`${DOCUMENSO_BASE}/api/v1/documents/${documensoId}`,
{
headers: { Authorization: `Bearer ${DOCUMENSO_API_KEY}` },
},
);
expect(
documensoRes.ok(),
`documenso GET: ${documensoRes.status()} ${await documensoRes.text()}`,
).toBe(true);
const documensoDoc = (await documensoRes.json()) as {
id: number;
status: string;
recipients?: Array<{ email: string; signingStatus: string }>;
};
expect(String(documensoDoc.id)).toBe(documensoId);
// Freshly-sent doc should be PENDING (or DRAFT if Documenso queued it).
expect(['PENDING', 'DRAFT'], `unexpected status ${documensoDoc.status}`).toContain(
documensoDoc.status,
);
// The template is configured with ≥1 recipient role, so we should have
// at least the client's email populated.
expect(documensoDoc.recipients?.length ?? 0).toBeGreaterThan(0);
});
});