chore(autonomous-session): consolidate uncommitted work from prior session

Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
This commit is contained in:
2026-05-23 00:52:59 +02:00
parent 43719b49e9
commit 221ae5784e
749 changed files with 7440 additions and 3118 deletions

View File

@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`W7 snapshots heat at canonical inputs > heat: cold (no history) 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: cold (no history) 1`] = `
{
"eoiCount": 0,
"furthestStage": 0,
@@ -10,7 +10,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: cold (no history) 1`]
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: no fallthrough but many interests 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: no fallthrough but many interests 1`] = `
{
"eoiCount": 15,
"furthestStage": 0,
@@ -20,7 +20,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: no fallthrough but ma
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: old fallthrough at deposit stage (recency decayed) 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: old fallthrough at deposit stage (recency decayed) 1`] = `
{
"eoiCount": 15,
"furthestStage": 40,
@@ -30,7 +30,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: old fallthrough at de
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: recent fallthrough at deposit stage (deepest hurt) 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: recent fallthrough at deposit stage (deepest hurt) 1`] = `
{
"eoiCount": 15,
"furthestStage": 40,
@@ -40,7 +40,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: recent fallthrough at
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: recent fallthrough at enquiry stage 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: recent fallthrough at enquiry stage 1`] = `
{
"eoiCount": 0,
"furthestStage": 0,
@@ -50,7 +50,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: recent fallthrough at
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: recent fallthrough at eoi stage 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: recent fallthrough at eoi stage 1`] = `
{
"eoiCount": 5,
"furthestStage": 20,
@@ -60,7 +60,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: recent fallthrough at
}
`;
exports[`W7 snapshots heat at canonical inputs > heat: typical mid-funnel hot lead 1`] = `
exports[`W7 snapshots - heat at canonical inputs > heat: typical mid-funnel hot lead 1`] = `
{
"eoiCount": 10,
"furthestStage": 30,
@@ -70,7 +70,7 @@ exports[`W7 snapshots — heat at canonical inputs > heat: typical mid-funnel ho
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=0, lost=0, stage=0) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=0, lost=0, stage=0) is stable 1`] = `
{
"in": {
"activeInterestCount": 0,
@@ -81,7 +81,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=0, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=0, lost=1, stage=0) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=0, lost=1, stage=0) is stable 1`] = `
{
"in": {
"activeInterestCount": 0,
@@ -92,7 +92,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=0, lost=1, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=0, lost=5, stage=0) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=0, lost=5, stage=0) is stable 1`] = `
{
"in": {
"activeInterestCount": 0,
@@ -103,7 +103,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=0, lost=5, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=0, stage=1) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=0, stage=1) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -114,7 +114,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=0, stage=3) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=0, stage=3) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -125,7 +125,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=0, stage=4) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=0, stage=4) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -136,7 +136,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=0, stage=5) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=0, stage=5) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -147,7 +147,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=0, stage=6) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=0, stage=6) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -158,7 +158,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=1, lost=5, stage=6) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=1, lost=5, stage=6) is stable 1`] = `
{
"in": {
"activeInterestCount": 1,
@@ -169,7 +169,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=1, lost=5, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=2, lost=0, stage=5) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=2, lost=0, stage=5) is stable 1`] = `
{
"in": {
"activeInterestCount": 2,
@@ -180,7 +180,7 @@ exports[`W7 snapshots — tier-ladder boundaries > tier(active=2, lost=0, stage=
}
`;
exports[`W7 snapshots tier-ladder boundaries > tier(active=3, lost=2, stage=4) is stable 1`] = `
exports[`W7 snapshots - tier-ladder boundaries > tier(active=3, lost=2, stage=4) is stable 1`] = `
{
"in": {
"activeInterestCount": 3,

View File

@@ -39,7 +39,7 @@ async function buildAcroFormPdf(): Promise<Buffer> {
return Buffer.from(bytes);
}
describe('parseBerthPdf AcroForm tier', () => {
describe('parseBerthPdf - AcroForm tier', () => {
it('extracts named fields and skips OCR', async () => {
const buf = await buildAcroFormPdf();
const result = await parseBerthPdf(buf, { skipOcr: true });

View File

@@ -1,5 +1,5 @@
/**
* Unit tests for the berth PDF parser (Phase 6b see plan §4.7b, §14.6).
* Unit tests for the berth PDF parser (Phase 6b - see plan §4.7b, §14.6).
*
* Covers:
* - Magic-byte check (`%PDF-`).
@@ -58,7 +58,7 @@ describe('parseHumanDate', () => {
});
});
describe('extractFromOcrText sample berth A1', () => {
describe('extractFromOcrText - sample berth A1', () => {
// Mirrors the layout of Berth_Spec_Sheet_A1.pdf documented in plan §9.2.
const sample = `
PORT NIMARA
@@ -144,7 +144,7 @@ Access: Car to Vessel (max. 3 ton)
});
});
describe('extractFromOcrText imperial/metric drift warning', () => {
describe('extractFromOcrText - imperial/metric drift warning', () => {
it('flags a >1% mismatch', () => {
const { warnings } = extractFromOcrText('Length: 100 ft / 50m');
expect(warnings.some((w) => /mismatch/i.test(w))).toBe(true);

View File

@@ -188,12 +188,12 @@ describe('computeHeat', () => {
});
});
// ─── W7 snapshot lockfile locks current tier-ladder boundaries and heat
// ─── W7 snapshot lockfile - locks current tier-ladder boundaries and heat
// ordering so weight-tuning changes can't silently shift outputs. The
// existing toBe / toBeCloseTo tests above cover correctness; these
// inline snapshots are the regression-catching tripwires.
describe('W7 snapshots tier-ladder boundaries', () => {
describe('W7 snapshots - tier-ladder boundaries', () => {
it.each([
[0, 0, 0],
[0, 1, 0],
@@ -217,7 +217,7 @@ describe('W7 snapshots — tier-ladder boundaries', () => {
);
});
describe('W7 snapshots heat at canonical inputs', () => {
describe('W7 snapshots - heat at canonical inputs', () => {
const NOW = new Date('2026-05-05T00:00:00Z');
const w = DEFAULT_RECOMMENDER_SETTINGS;
@@ -238,7 +238,7 @@ describe('W7 snapshots — heat at canonical inputs', () => {
w,
NOW,
);
// Snapshot the rounded breakdown exact float math (toBeCloseTo)
// Snapshot the rounded breakdown - exact float math (toBeCloseTo)
// is covered above; this locks the relative ordering + magnitude.
expect({
total: Math.round(h.total * 1000) / 1000,

View File

@@ -20,7 +20,7 @@ import { companyMemberships } from '@/lib/db/schema/companies';
// ─── createPending ───────────────────────────────────────────────────────────
describe('berth-reservations.service createPending', () => {
describe('berth-reservations.service - createPending', () => {
it('creates pending reservation for client-owned yacht', async () => {
const port = await makePort();
const berth = await makeBerth({ portId: port.id });
@@ -203,7 +203,7 @@ describe('berth-reservations.service — createPending', () => {
// ─── Lifecycle transitions ───────────────────────────────────────────────────
describe('berth-reservations.service lifecycle transitions', () => {
describe('berth-reservations.service - lifecycle transitions', () => {
async function setup() {
const port = await makePort();
const berth = await makeBerth({ portId: port.id });
@@ -330,7 +330,7 @@ describe('berth-reservations.service — lifecycle transitions', () => {
// ─── listReservations ────────────────────────────────────────────────────────
describe('berth-reservations.service listReservations', () => {
describe('berth-reservations.service - listReservations', () => {
async function makeReservation(portId: string, opts?: { berthId?: string }) {
const berth = opts?.berthId ? { id: opts.berthId } : await makeBerth({ portId });
const client = await makeClient({ portId });
@@ -418,7 +418,7 @@ describe('berth-reservations.service — listReservations', () => {
// ─── Self-check: DB state is as expected after cancel ────────────────────────
describe('berth-reservations.service DB state', () => {
describe('berth-reservations.service - DB state', () => {
it('cancel persists status=cancelled in the database', async () => {
const port = await makePort();
const berth = await makeBerth({ portId: port.id });

View File

@@ -14,7 +14,7 @@ import { companies } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { ConflictError, NotFoundError } from '@/lib/errors';
describe('companies.service createCompany', () => {
describe('companies.service - createCompany', () => {
it('creates a company with minimal required fields', async () => {
const port = await makePort();
@@ -68,7 +68,7 @@ describe('companies.service — createCompany', () => {
});
});
describe('companies.service upsertByName', () => {
describe('companies.service - upsertByName', () => {
it('returns existing company on case-insensitive match', async () => {
const port = await makePort();
const original = await createCompany(
@@ -109,7 +109,7 @@ describe('companies.service — upsertByName', () => {
});
});
describe('companies.service updateCompany', () => {
describe('companies.service - updateCompany', () => {
it('updates fields', async () => {
const port = await makePort();
const company = await makeCompany({
@@ -147,7 +147,7 @@ describe('companies.service — updateCompany', () => {
});
});
describe('companies.service archiveCompany', () => {
describe('companies.service - archiveCompany', () => {
it('sets archivedAt to a non-null timestamp', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });
@@ -173,7 +173,7 @@ describe('companies.service — archiveCompany', () => {
});
});
describe('companies.service listCompanies', () => {
describe('companies.service - listCompanies', () => {
it('is tenant-scoped', async () => {
const portA = await makePort();
const portB = await makePort();
@@ -229,7 +229,7 @@ describe('companies.service — listCompanies', () => {
});
});
describe('companies.service autocomplete', () => {
describe('companies.service - autocomplete', () => {
it('matches by name', async () => {
const port = await makePort();
await makeCompany({ portId: port.id, overrides: { name: 'Phoenix Ltd' } });
@@ -248,7 +248,7 @@ describe('companies.service — autocomplete', () => {
});
});
describe('companies.service getCompanyById', () => {
describe('companies.service - getCompanyById', () => {
it('returns the company when same tenant', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });

View File

@@ -13,7 +13,7 @@ import { companyMemberships } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
describe('company-memberships.service addMembership', () => {
describe('company-memberships.service - addMembership', () => {
it('creates a membership for a valid client + company in the same port', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });
@@ -122,7 +122,7 @@ describe('company-memberships.service — addMembership', () => {
});
});
describe('company-memberships.service updateMembership', () => {
describe('company-memberships.service - updateMembership', () => {
it('updates fields', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });
@@ -180,7 +180,7 @@ describe('company-memberships.service — updateMembership', () => {
});
});
describe('company-memberships.service setPrimary', () => {
describe('company-memberships.service - setPrimary', () => {
it('sets only one membership as primary per company (atomic un-primary others)', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });
@@ -222,7 +222,7 @@ describe('company-memberships.service — setPrimary', () => {
makeAuditMeta({ portId: port.id }),
);
// Mark all primary via the service only the last call should leave a
// Mark all primary via the service - only the last call should leave a
// single primary survivor (m3).
await setPrimary(m1.id, port.id, makeAuditMeta({ portId: port.id }));
await setPrimary(m2.id, port.id, makeAuditMeta({ portId: port.id }));
@@ -262,7 +262,7 @@ describe('company-memberships.service — setPrimary', () => {
});
});
describe('company-memberships.service endMembership', () => {
describe('company-memberships.service - endMembership', () => {
it('sets endDate', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });
@@ -293,7 +293,7 @@ describe('company-memberships.service — endMembership', () => {
});
});
describe('company-memberships.service listByCompany / listByClient', () => {
describe('company-memberships.service - listByCompany / listByClient', () => {
it('returns active memberships only by default', async () => {
const port = await makePort();
const company = await makeCompany({ portId: port.id });

View File

@@ -97,7 +97,7 @@ describe('buildDocumensoPayload', () => {
Length: '45 ft',
Width: '14 ft',
Draft: '6 ft',
// Berth Number carries the formatBerthRange output single-
// Berth Number carries the formatBerthRange output - single-
// berth EOI duplicates the primary mooring; multi-berth shows
// the compact range. The separate 'Berth Range' formValue key
// was retired 2026-05-14 (the Documenso template never had
@@ -134,7 +134,7 @@ describe('buildDocumensoPayload', () => {
it('renders empty Section 3 when yacht and berth are not linked', () => {
// Also explicitly clear the berth-range fallback that defaults to
// the primary mooring when there's no berth AND no bundle, the
// the primary mooring - when there's no berth AND no bundle, the
// form field renders as empty.
const ctx = makeContext({ yacht: null, berth: null, eoiBerthRange: '' });
const payload = buildDocumensoPayload(ctx, OPTIONS);

View File

@@ -115,7 +115,11 @@ describe('placeFields v2 dispatch', () => {
'env-123',
[
{
recipientId: 'rec-a',
// v2 recipient ids are numeric - Documenso's distribute response
// returns them as numbers. The CRM custom-document-upload
// service preserves them as strings or numbers; the v2 placeFields
// coercion normalises to number for the upstream payload.
recipientId: '42',
type: 'SIGNATURE',
pageNumber: 1,
pageX: 25,
@@ -134,9 +138,13 @@ describe('placeFields v2 dispatch', () => {
expect((init as RequestInit).method).toBe('POST');
const body = JSON.parse(String((init as RequestInit).body)) as any;
expect(body.envelopeId).toBe('env-123');
expect(body.fields[0]).toMatchObject({
recipientId: 'rec-a',
// 2026-05-22: Documenso v2 expects the array under `data` (trpc-style
// createMany input), not `fields`. recipientId is a number, and the
// page-index key is `page` (not `pageNumber`).
expect(body.data[0]).toMatchObject({
recipientId: 42,
type: 'SIGNATURE',
page: 1,
positionX: 25,
positionY: 88,
width: 20,
@@ -262,17 +270,22 @@ describe('placeDefaultSignatureFields integration', () => {
await placeDefaultSignatureFields(
'env-x',
[
{ id: 'r1', pageNumber: 4 },
{ id: 'r2', pageNumber: 4 },
{ id: 'r3', pageNumber: 4 },
{ id: '101', pageNumber: 4 },
{ id: '102', pageNumber: 4 },
{ id: '103', pageNumber: 4 },
],
'port-1',
);
expect(fetchMock).toHaveBeenCalledTimes(1);
const body = JSON.parse(String((fetchMock.mock.calls[0]![1] as RequestInit).body)) as any;
expect(body.fields).toHaveLength(3);
expect(body.fields.every((f: { type: string }) => f.type === 'SIGNATURE')).toBe(true);
expect(body.fields.every((f: { pageNumber: number }) => f.pageNumber === 4)).toBe(true);
// 2026-05-22: Documenso v2 expects `data` (not `fields`), `page`
// (not `pageNumber`), and numeric recipientIds.
expect(body.data).toHaveLength(3);
expect(body.data.every((f: { type: string }) => f.type === 'SIGNATURE')).toBe(true);
expect(body.data.every((f: { page: number }) => f.page === 4)).toBe(true);
expect(
body.data.every((f: { recipientId: unknown }) => typeof f.recipientId === 'number'),
).toBe(true);
});
it('skips the API call entirely with zero recipients', async () => {

View File

@@ -32,7 +32,7 @@ describe('extractSigningToken', () => {
});
it('rejects path tails with disallowed characters', () => {
// Real tokens are URL-safe base64 no spaces, no punctuation
// Real tokens are URL-safe base64 - no spaces, no punctuation
expect(extractSigningToken('https://example.com/sign/has%20space')).toBeNull();
});

View File

@@ -3,13 +3,13 @@ import { describe, it, expect } from 'vitest';
import { transformSigningUrl } from '@/lib/services/document-signing-emails.service';
/**
* Phase 5 pin the URL-wrapping contract.
* Phase 5 - pin the URL-wrapping contract.
*
* The marketing website at portnimara.com/sign/[type]/[token] expects
* specific path segments (`client | cc | developer | witness`) and the
* Documenso webhook returns raw URLs of the form
* `https://signatures.portnimara.com/sign/<token>`. transformSigningUrl
* is the seam between the two these tests guard the role-to-URL-
* is the seam between the two - these tests guard the role-to-URL-
* segment mapping so a future refactor can't silently break the
* embedded signing pages.
*/
@@ -38,7 +38,7 @@ describe('transformSigningUrl', () => {
);
});
it('maps approver → /sign/cc/<token> website only handles {client, cc, developer, witness}', () => {
it('maps approver → /sign/cc/<token> - website only handles {client, cc, developer, witness}', () => {
expect(transformSigningUrl(RAW, HOST, 'approver')).toBe(
'https://portnimara.com/sign/cc/vbT8hi3jKQmrFP_LN1WcS',
);
@@ -50,7 +50,7 @@ describe('transformSigningUrl', () => {
);
});
it('maps other → /sign/cc/<token> funnels through CC page with passive copy', () => {
it('maps other → /sign/cc/<token> - funnels through CC page with passive copy', () => {
expect(transformSigningUrl(RAW, HOST, 'other')).toBe(
'https://portnimara.com/sign/cc/vbT8hi3jKQmrFP_LN1WcS',
);
@@ -65,7 +65,7 @@ describe('transformSigningUrl', () => {
);
});
it('preserves the token verbatim no URL encoding / re-shaping', () => {
it('preserves the token verbatim - no URL encoding / re-shaping', () => {
const odd = 'https://sig.example.com/sign/Aa_-Zz09_-XYZ';
expect(transformSigningUrl(odd, HOST, 'developer')).toBe(
'https://portnimara.com/sign/developer/Aa_-Zz09_-XYZ',

View File

@@ -305,7 +305,7 @@ describe('buildEoiContext', () => {
it('throws ValidationError when client has no email', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
// Address only, no email gate should fail.
// Address only, no email - gate should fail.
await db.insert(clientAddresses).values({
clientId: client.id,
portId: port.id,
@@ -324,7 +324,7 @@ describe('buildEoiContext', () => {
it('throws ValidationError when client has no primary address', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
// Email only, no address gate should fail.
// Email only, no address - gate should fail.
await db.insert(clientContacts).values({
clientId: client.id,
channel: 'email',

View File

@@ -1,7 +1,6 @@
import { describe, it, expect, beforeAll } from 'vitest';
describe('portal.service getPortalUserYachts', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
describe('portal.service - getPortalUserYachts', () => {
let getPortalUserYachts: (clientId: string, portId: string) => Promise<Array<any>>;
let makeClient: typeof import('../../helpers/factories').makeClient;
@@ -87,7 +86,7 @@ describe('portal.service — getPortalUserYachts', () => {
// defensive by forcing the yacht's current owner to company after the
// direct query path has already cached it. We simulate the case by
// creating a client-owned yacht, then manually flipping owner to a
// company the client is a member of if both queries ran they'd both
// company the client is a member of - if both queries ran they'd both
// match, but dedup by id ensures only one entry.
const port = await makePort();
const client = await makeClient({ portId: port.id });
@@ -104,7 +103,7 @@ describe('portal.service — getPortalUserYachts', () => {
ownerId: client.id,
name: 'Ambiguous',
});
// Flip the denormalized owner to the company (without updating history)
// Flip the denormalized owner to the company (without updating history) -
// this is artificial but exercises the dedup branch.
await db
.update(yachts)
@@ -142,7 +141,7 @@ describe('portal.service — getPortalUserYachts', () => {
const portA = await makePort();
const portB = await makePort();
const clientInA = await makeClient({ portId: portA.id });
// Directly-owned yacht in portB with the SAME client id must not leak
// Directly-owned yacht in portB with the SAME client id - must not leak
// because getPortalUserYachts filters on portId.
// We insert a yacht row in portB with ownerId=clientInA.id. The FK on
// yachts.currentOwnerId isn't to clients, so this is valid.
@@ -172,8 +171,7 @@ describe('portal.service — getPortalUserYachts', () => {
});
});
describe('portal.service getPortalUserMemberships', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
describe('portal.service - getPortalUserMemberships', () => {
let getPortalUserMemberships: (clientId: string, portId: string) => Promise<Array<any>>;
let makeClient: typeof import('../../helpers/factories').makeClient;
@@ -223,7 +221,7 @@ describe('portal.service — getPortalUserMemberships', () => {
const portA = await makePort();
const portB = await makePort();
const client = await makeClient({ portId: portA.id });
// Company in portB but membership references clientId on portA.
// Company in portB - but membership references clientId on portA.
const companyInB = await makeCompany({ portId: portB.id });
await makeMembership({
companyId: companyInB.id,
@@ -240,8 +238,7 @@ describe('portal.service — getPortalUserMemberships', () => {
});
});
describe('portal.service getPortalUserReservations', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
describe('portal.service - getPortalUserReservations', () => {
let getPortalUserReservations: (clientId: string, portId: string) => Promise<Array<any>>;
let makeClient: typeof import('../../helpers/factories').makeClient;

View File

@@ -91,7 +91,7 @@ describe('computeTotalForecast', () => {
],
{ enquiry: 0.3 },
);
// 'unknown_stage' canonicalizes to 'enquiry' (fallback) so it ALSO
// 'unknown_stage' canonicalizes to 'enquiry' (fallback) - so it ALSO
// gets the enquiry weight. Verifies canonicalization stays consistent
// between rollup and forecast so the totals reconcile.
// 1000*0.3 + 2000*0.3 = 300 + 600 = 900

View File

@@ -7,13 +7,13 @@ import { yachts, companies } from '@/lib/db/schema';
import { makePort, makeClient, makeYacht, makeCompany } from '../../helpers/factories';
// Default opts super admin so every bucket runs without per-resource
// Default opts - super admin so every bucket runs without per-resource
// permission gating getting in the way of the assertions.
const ADMIN_OPTS = { permissions: null, isSuperAdmin: true } as const;
// ─── Yachts ──────────────────────────────────────────────────────────────────
describe('search.service yachts', () => {
describe('search.service - yachts', () => {
it('matches yachts by name (case-insensitive)', async () => {
const port = await makePort();
const owner = await makeClient({ portId: port.id });
@@ -106,7 +106,7 @@ describe('search.service — yachts', () => {
// ─── Companies ───────────────────────────────────────────────────────────────
describe('search.service companies', () => {
describe('search.service - companies', () => {
it('matches companies by name', async () => {
const port = await makePort();
await makeCompany({ portId: port.id, overrides: { name: 'Poseidon Maritime Ltd' } });
@@ -165,7 +165,7 @@ describe('search.service — companies', () => {
// ─── Combined ────────────────────────────────────────────────────────────────
describe('search.service combined', () => {
describe('search.service - combined', () => {
it('returns clients, yachts, and companies for a query that matches multiple', async () => {
const port = await makePort();
const client = await makeClient({
@@ -242,7 +242,7 @@ describe('normalizePhoneQuery', () => {
// ─── Partial name matching ───────────────────────────────────────────────────
describe('search.service partial name matching', () => {
describe('search.service - partial name matching', () => {
it('matches "joh smi" against "John Smith" via tokenized prefix tsquery', async () => {
const port = await makePort();
await makeClient({ portId: port.id, overrides: { fullName: 'John Smith' } });
@@ -263,7 +263,7 @@ describe('search.service — partial name matching', () => {
});
});
describe('search.service bucket totals', () => {
describe('search.service - bucket totals', () => {
it('emits per-bucket totals so the UI can render "show more" links', async () => {
const port = await makePort();
await makeClient({ portId: port.id, overrides: { fullName: 'TotalsCheck One' } });

View File

@@ -18,7 +18,7 @@ import { db } from '@/lib/db';
import { yachts, yachtOwnershipHistory } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
describe('yachts.service createYacht', () => {
describe('yachts.service - createYacht', () => {
it('creates a yacht with a client owner and opens an ownership history row', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
@@ -79,7 +79,7 @@ describe('yachts.service — createYacht', () => {
});
});
describe('yachts.service updateYacht', () => {
describe('yachts.service - updateYacht', () => {
it('updates name and notes', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
@@ -144,7 +144,7 @@ describe('yachts.service — updateYacht', () => {
});
});
describe('yachts.service archiveYacht', () => {
describe('yachts.service - archiveYacht', () => {
it('sets archivedAt to a non-null timestamp', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
@@ -174,7 +174,7 @@ describe('yachts.service — archiveYacht', () => {
});
});
describe('yachts.service listYachts', () => {
describe('yachts.service - listYachts', () => {
it('is scoped to port (tenant isolation)', async () => {
const portA = await makePort();
const portB = await makePort();
@@ -269,7 +269,7 @@ describe('yachts.service — listYachts', () => {
});
});
describe('yachts.service listYachtsForOwner', () => {
describe('yachts.service - listYachtsForOwner', () => {
it('returns all yachts owned by a given client', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });
@@ -305,7 +305,7 @@ describe('yachts.service — listYachtsForOwner', () => {
});
});
describe('yachts.service autocomplete', () => {
describe('yachts.service - autocomplete', () => {
it('matches by name (ILIKE)', async () => {
const port = await makePort();
const client = await makeClient({ portId: port.id });