Files
pn-new-crm/tests/helpers/factories.ts

660 lines
20 KiB
TypeScript
Raw Normal View History

/**
* Test factory helpers.
*
* Two flavours:
* 1. Async DB-inserting factories (makePort, makeClient, makeBerth, makeYacht,
* makeCompany, ...) insert a row via the app's `db` handle and return the
* inserted record. Use these from integration tests that need real FK /
* unique-index enforcement. They require DATABASE_URL to be reachable.
* 2. Plain-data helpers (makeAuditMeta, makeCreateClientInput, makeCreate*)
* return in-memory objects suitable for unit tests with mocked `db`.
*/
import { and, eq, sql } from 'drizzle-orm';
import { db } from '@/lib/db';
import { ports, type NewPort, type Port } from '@/lib/db/schema/ports';
import { clients, type NewClient, type Client } from '@/lib/db/schema/clients';
import { berths, type NewBerth, type Berth } from '@/lib/db/schema/berths';
import { yachts, yachtOwnershipHistory, type NewYacht, type Yacht } from '@/lib/db/schema/yachts';
import {
companies,
companyMemberships,
type NewCompany,
type Company,
} from '@/lib/db/schema/companies';
import { berthReservations, type BerthReservation } from '@/lib/db/schema/reservations';
// ─── Port ────────────────────────────────────────────────────────────────────
export async function makePort(args?: { overrides?: Partial<NewPort> }): Promise<Port> {
const suffix = Math.random().toString(36).slice(2, 10);
const [port] = await db
.insert(ports)
.values({
name: args?.overrides?.name ?? `Test Port ${suffix}`,
slug: args?.overrides?.slug ?? `test-port-${suffix}`,
...args?.overrides,
})
.returning();
return port!;
}
// ─── Client ──────────────────────────────────────────────────────────────────
export async function makeClient(args: {
portId: string;
overrides?: Partial<NewClient>;
}): Promise<Client> {
const [client] = await db
.insert(clients)
.values({
portId: args.portId,
fullName: args.overrides?.fullName ?? `Test Client ${Math.random().toString(36).slice(2, 8)}`,
...args.overrides,
})
.returning();
return client!;
}
// ─── Berth ────────────────────────────────────────────────────────────────────
export async function makeBerth(args: {
portId: string;
overrides?: Partial<NewBerth>;
}): Promise<Berth> {
const [berth] = await db
.insert(berths)
.values({
portId: args.portId,
mooringNumber: args.overrides?.mooringNumber ?? `B-${Math.random().toString(36).slice(2, 8)}`,
...args.overrides,
})
.returning();
return berth!;
}
// ─── Yacht ───────────────────────────────────────────────────────────────────
export async function makeYacht(args: {
portId: string;
ownerType: 'client' | 'company';
ownerId: string;
name?: string;
status?: 'active' | 'retired' | 'sold_away';
hullNumber?: string;
registration?: string;
overrides?: Partial<NewYacht>;
}): Promise<Yacht> {
const [yacht] = await db
.insert(yachts)
.values({
portId: args.portId,
name: args.name ?? args.overrides?.name ?? `Yacht ${Math.random().toString(36).slice(2, 8)}`,
currentOwnerType: args.ownerType,
currentOwnerId: args.ownerId,
...(args.status !== undefined ? { status: args.status } : {}),
...(args.hullNumber !== undefined ? { hullNumber: args.hullNumber } : {}),
...(args.registration !== undefined ? { registration: args.registration } : {}),
...args.overrides,
})
.returning();
await db.insert(yachtOwnershipHistory).values({
yachtId: yacht!.id,
ownerType: args.ownerType,
ownerId: args.ownerId,
startDate: new Date(),
endDate: null,
createdBy: 'test',
});
return yacht!;
}
// ─── Company ─────────────────────────────────────────────────────────────────
export async function makeCompany(args: {
portId: string;
overrides?: Partial<NewCompany>;
}): Promise<Company> {
const [company] = await db
.insert(companies)
.values({
portId: args.portId,
name: args.overrides?.name ?? `Company ${Math.random().toString(36).slice(2, 8)}`,
...args.overrides,
})
.returning();
return company!;
}
// ─── Company Membership ──────────────────────────────────────────────────────
export async function makeMembership(args: {
companyId: string;
clientId: string;
role?: string;
roleDetail?: string;
startDate?: Date;
endDate?: Date | null;
isPrimary?: boolean;
notes?: string;
}) {
const [row] = await db
.insert(companyMemberships)
.values({
companyId: args.companyId,
clientId: args.clientId,
role: args.role ?? 'director',
roleDetail: args.roleDetail ?? null,
startDate: args.startDate ?? new Date(),
endDate: args.endDate ?? null,
isPrimary: args.isPrimary ?? false,
notes: args.notes ?? null,
})
.returning();
if (!row) throw new Error('Failed to create membership');
return row;
}
// ─── Berth Reservation ───────────────────────────────────────────────────────
export async function makeReservation(args: {
berthId: string;
portId: string;
clientId: string;
yachtId: string;
status: 'pending' | 'active' | 'ended' | 'cancelled';
startDate?: Date;
endDate?: Date | null;
tenureType?: 'permanent' | 'fixed_term' | 'seasonal';
interestId?: string;
createdBy?: string;
notes?: string;
}): Promise<BerthReservation> {
const [row] = await db
.insert(berthReservations)
.values({
berthId: args.berthId,
portId: args.portId,
clientId: args.clientId,
yachtId: args.yachtId,
interestId: args.interestId ?? null,
status: args.status,
startDate: args.startDate ?? new Date(),
endDate: args.endDate ?? null,
tenureType: args.tenureType ?? 'permanent',
contractFileId: null,
notes: args.notes ?? null,
createdBy: args.createdBy ?? 'seed-user',
})
.returning();
if (!row) throw new Error('Failed to create reservation');
return row;
}
// ─── Yacht Ownership Transfer ────────────────────────────────────────────────
export async function makeOwnershipTransfer(args: {
yachtId: string;
newOwner: { type: 'client' | 'company'; id: string };
effectiveDate?: Date;
transferReason?: string;
transferNotes?: string;
createdBy?: string;
}) {
const effective = args.effectiveDate ?? new Date();
const createdBy = args.createdBy ?? 'seed-user';
return await db.transaction(async (tx) => {
// Close current open row
await tx
.update(yachtOwnershipHistory)
.set({ endDate: effective })
.where(
and(
eq(yachtOwnershipHistory.yachtId, args.yachtId),
sql`${yachtOwnershipHistory.endDate} IS NULL`,
),
);
// Insert new open row
const [newHistory] = await tx
.insert(yachtOwnershipHistory)
.values({
yachtId: args.yachtId,
ownerType: args.newOwner.type,
ownerId: args.newOwner.id,
startDate: effective,
endDate: null,
transferReason: args.transferReason ?? null,
transferNotes: args.transferNotes ?? null,
createdBy,
})
.returning();
// Update yacht's denormalized current owner
const [updated] = await tx
.update(yachts)
.set({
currentOwnerType: args.newOwner.type,
currentOwnerId: args.newOwner.id,
updatedAt: new Date(),
})
.where(eq(yachts.id, args.yachtId))
.returning();
return { history: newHistory!, yacht: updated! };
});
}
// ─── Webhook ──────────────────────────────────────────────────────────────────
export interface WebhookData {
id: string;
portId: string;
name: string;
url: string;
secret: string | null;
events: string[];
isActive: boolean;
createdBy: string;
createdAt: Date;
updatedAt: Date;
}
export function makeWebhook(overrides?: Partial<WebhookData>): WebhookData {
return {
id: crypto.randomUUID(),
portId: crypto.randomUUID(),
name: 'Test Webhook',
url: 'https://example.com/webhook',
secret: null,
events: ['client.created'],
isActive: true,
createdBy: crypto.randomUUID(),
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
};
}
// ─── Audit Log ────────────────────────────────────────────────────────────────
export interface AuditMeta {
userId: string;
portId: string;
ipAddress: string;
userAgent: string;
}
export function makeAuditMeta(overrides?: Partial<AuditMeta>): AuditMeta {
return {
userId: crypto.randomUUID(),
portId: crypto.randomUUID(),
ipAddress: '127.0.0.1',
userAgent: 'vitest/1.0',
...overrides,
};
}
// ─── Auth Context ─────────────────────────────────────────────────────────────
import type { RolePermissions } from '@/lib/db/schema/users';
/** Full permissions — every action allowed. */
export function makeFullPermissions(): RolePermissions {
return {
clients: { view: true, create: true, edit: true, delete: true, merge: true, export: true },
interests: {
view: true,
create: true,
edit: true,
delete: true,
change_stage: true,
feat(interests): manual stage override + Residential Partner system role Manual stage override Sales reps need to skip canTransitionStage rules when the data was entered out of order — e.g. recording a contract_signed deal whose earlier stages were never tracked in the system. - New permission flag interests.override_stage in RolePermissions. Plumbed through the schema TS type, the role-editor UI, the seed file's pre-built roles (super_admin/director/sales_manager get it, sales_agent + viewer don't), and the test factories. - changeStageSchema gains an optional `override` boolean and the service checks it before evaluating canTransitionStage. When override=true the reason field becomes required (min 5 chars) and is recorded in the audit log. - The route handler gates `override` on the new permission so a sales_agent without it can't pass override=true and bypass. - InterestStagePicker auto-detects when the requested transition is blocked by the table and switches into "override mode" — shows an amber warning, requires the reason, button label flips to "Override stage". When the operator lacks the permission, the warning is red and the button is disabled. Residential Partner role Per the smart-archive scoping conversation: external partners who handle residential inquiries shouldn't see marina clients, yachts, berths, or financials. The two residential_* permission groups already exist; this commit just seeds a pre-built system role ("residential_partner") with those flags + minimal own-reminders, so admins can invite a partner today via /admin/users without manually building the permission set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:32:57 +02:00
override_stage: true,
generate_eoi: true,
export: true,
},
berths: { view: true, edit: true, import: true, manage_waiting_list: true },
documents: {
view: true,
create: true,
edit: true,
send_for_signing: true,
upload_signed: true,
delete: true,
},
expenses: {
view: true,
create: true,
edit: true,
delete: true,
export: true,
scan_receipt: true,
},
invoices: {
view: true,
create: true,
edit: true,
delete: true,
send: true,
record_payment: true,
export: true,
},
files: { view: true, upload: true, edit: true, delete: true, manage_folders: true },
email: { view: true, send: true, configure_account: true },
reminders: {
view_own: true,
view_all: true,
create: true,
edit_own: true,
edit_all: true,
assign_others: true,
},
calendar: { connect: true, view_events: true },
reports: { view_dashboard: true, view_analytics: true, export: true },
document_templates: { view: true, generate: true, manage: true },
yachts: { view: true, create: true, edit: true, delete: true, transfer: true },
companies: { view: true, create: true, edit: true, delete: true },
memberships: { view: true, manage: true },
reservations: { view: true, create: true, activate: true, cancel: true },
admin: {
manage_users: true,
view_audit_log: true,
manage_settings: true,
manage_webhooks: true,
manage_reports: true,
manage_custom_fields: true,
manage_forms: true,
manage_tags: true,
system_backup: true,
},
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
residential_clients: { view: true, create: true, edit: true, delete: true },
residential_interests: {
view: true,
create: true,
edit: true,
delete: true,
change_stage: true,
},
};
}
/** Read-only viewer permissions — no create/update/delete. */
export function makeViewerPermissions(): RolePermissions {
return {
clients: { view: true, create: false, edit: false, delete: false, merge: false, export: false },
interests: {
view: true,
create: false,
edit: false,
delete: false,
change_stage: false,
feat(interests): manual stage override + Residential Partner system role Manual stage override Sales reps need to skip canTransitionStage rules when the data was entered out of order — e.g. recording a contract_signed deal whose earlier stages were never tracked in the system. - New permission flag interests.override_stage in RolePermissions. Plumbed through the schema TS type, the role-editor UI, the seed file's pre-built roles (super_admin/director/sales_manager get it, sales_agent + viewer don't), and the test factories. - changeStageSchema gains an optional `override` boolean and the service checks it before evaluating canTransitionStage. When override=true the reason field becomes required (min 5 chars) and is recorded in the audit log. - The route handler gates `override` on the new permission so a sales_agent without it can't pass override=true and bypass. - InterestStagePicker auto-detects when the requested transition is blocked by the table and switches into "override mode" — shows an amber warning, requires the reason, button label flips to "Override stage". When the operator lacks the permission, the warning is red and the button is disabled. Residential Partner role Per the smart-archive scoping conversation: external partners who handle residential inquiries shouldn't see marina clients, yachts, berths, or financials. The two residential_* permission groups already exist; this commit just seeds a pre-built system role ("residential_partner") with those flags + minimal own-reminders, so admins can invite a partner today via /admin/users without manually building the permission set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:32:57 +02:00
override_stage: false,
generate_eoi: false,
export: false,
},
berths: { view: true, edit: false, import: false, manage_waiting_list: false },
documents: {
view: true,
create: false,
edit: false,
send_for_signing: false,
upload_signed: false,
delete: false,
},
expenses: {
view: true,
create: false,
edit: false,
delete: false,
export: false,
scan_receipt: false,
},
invoices: {
view: true,
create: false,
edit: false,
delete: false,
send: false,
record_payment: false,
export: false,
},
files: { view: true, upload: false, edit: false, delete: false, manage_folders: false },
email: { view: true, send: false, configure_account: false },
reminders: {
view_own: true,
view_all: false,
create: false,
edit_own: false,
edit_all: false,
assign_others: false,
},
calendar: { connect: false, view_events: true },
reports: { view_dashboard: true, view_analytics: false, export: false },
document_templates: { view: true, generate: false, manage: false },
yachts: { view: true, create: false, edit: false, delete: false, transfer: false },
companies: { view: true, create: false, edit: false, delete: false },
memberships: { view: true, manage: false },
reservations: { view: true, create: false, activate: false, cancel: false },
admin: {
manage_users: false,
view_audit_log: false,
manage_settings: false,
manage_webhooks: false,
manage_reports: false,
manage_custom_fields: false,
manage_forms: false,
manage_tags: false,
system_backup: false,
},
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
residential_clients: { view: false, create: false, edit: false, delete: false },
residential_interests: {
view: false,
create: false,
edit: false,
delete: false,
change_stage: false,
},
};
}
/** Sales agent permissions — own clients/interests, no admin. */
export function makeSalesAgentPermissions(): RolePermissions {
return {
clients: { view: true, create: true, edit: true, delete: false, merge: false, export: false },
interests: {
view: true,
create: true,
edit: true,
delete: false,
change_stage: true,
feat(interests): manual stage override + Residential Partner system role Manual stage override Sales reps need to skip canTransitionStage rules when the data was entered out of order — e.g. recording a contract_signed deal whose earlier stages were never tracked in the system. - New permission flag interests.override_stage in RolePermissions. Plumbed through the schema TS type, the role-editor UI, the seed file's pre-built roles (super_admin/director/sales_manager get it, sales_agent + viewer don't), and the test factories. - changeStageSchema gains an optional `override` boolean and the service checks it before evaluating canTransitionStage. When override=true the reason field becomes required (min 5 chars) and is recorded in the audit log. - The route handler gates `override` on the new permission so a sales_agent without it can't pass override=true and bypass. - InterestStagePicker auto-detects when the requested transition is blocked by the table and switches into "override mode" — shows an amber warning, requires the reason, button label flips to "Override stage". When the operator lacks the permission, the warning is red and the button is disabled. Residential Partner role Per the smart-archive scoping conversation: external partners who handle residential inquiries shouldn't see marina clients, yachts, berths, or financials. The two residential_* permission groups already exist; this commit just seeds a pre-built system role ("residential_partner") with those flags + minimal own-reminders, so admins can invite a partner today via /admin/users without manually building the permission set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:32:57 +02:00
override_stage: true,
generate_eoi: true,
export: false,
},
berths: { view: true, edit: false, import: false, manage_waiting_list: false },
documents: {
view: true,
create: true,
edit: true,
send_for_signing: true,
upload_signed: true,
delete: false,
},
expenses: {
view: true,
create: true,
edit: true,
delete: false,
export: false,
scan_receipt: true,
},
invoices: {
view: true,
create: false,
edit: false,
delete: false,
send: false,
record_payment: false,
export: false,
},
files: { view: true, upload: true, edit: false, delete: false, manage_folders: false },
email: { view: true, send: true, configure_account: false },
reminders: {
view_own: true,
view_all: false,
create: true,
edit_own: true,
edit_all: false,
assign_others: false,
},
calendar: { connect: true, view_events: true },
reports: { view_dashboard: true, view_analytics: false, export: false },
document_templates: { view: true, generate: true, manage: false },
yachts: { view: true, create: true, edit: true, delete: false, transfer: false },
companies: { view: true, create: true, edit: false, delete: false },
memberships: { view: true, manage: false },
reservations: { view: true, create: true, activate: true, cancel: false },
admin: {
manage_users: false,
view_audit_log: false,
manage_settings: false,
manage_webhooks: false,
manage_reports: false,
manage_custom_fields: false,
manage_forms: false,
manage_tags: false,
system_backup: false,
},
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
residential_clients: { view: false, create: false, edit: false, delete: false },
residential_interests: {
view: false,
create: false,
edit: false,
delete: false,
change_stage: false,
},
};
}
/** Sales manager — can do most things, limited admin. */
export function makeSalesManagerPermissions(): RolePermissions {
return {
clients: { view: true, create: true, edit: true, delete: true, merge: true, export: true },
interests: {
view: true,
create: true,
edit: true,
delete: true,
change_stage: true,
feat(interests): manual stage override + Residential Partner system role Manual stage override Sales reps need to skip canTransitionStage rules when the data was entered out of order — e.g. recording a contract_signed deal whose earlier stages were never tracked in the system. - New permission flag interests.override_stage in RolePermissions. Plumbed through the schema TS type, the role-editor UI, the seed file's pre-built roles (super_admin/director/sales_manager get it, sales_agent + viewer don't), and the test factories. - changeStageSchema gains an optional `override` boolean and the service checks it before evaluating canTransitionStage. When override=true the reason field becomes required (min 5 chars) and is recorded in the audit log. - The route handler gates `override` on the new permission so a sales_agent without it can't pass override=true and bypass. - InterestStagePicker auto-detects when the requested transition is blocked by the table and switches into "override mode" — shows an amber warning, requires the reason, button label flips to "Override stage". When the operator lacks the permission, the warning is red and the button is disabled. Residential Partner role Per the smart-archive scoping conversation: external partners who handle residential inquiries shouldn't see marina clients, yachts, berths, or financials. The two residential_* permission groups already exist; this commit just seeds a pre-built system role ("residential_partner") with those flags + minimal own-reminders, so admins can invite a partner today via /admin/users without manually building the permission set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:32:57 +02:00
override_stage: true,
generate_eoi: true,
export: true,
},
berths: { view: true, edit: true, import: false, manage_waiting_list: true },
documents: {
view: true,
create: true,
edit: true,
send_for_signing: true,
upload_signed: true,
delete: true,
},
expenses: {
view: true,
create: true,
edit: true,
delete: true,
export: true,
scan_receipt: true,
},
invoices: {
view: true,
create: true,
edit: true,
delete: false,
send: true,
record_payment: true,
export: true,
},
files: { view: true, upload: true, edit: true, delete: true, manage_folders: true },
email: { view: true, send: true, configure_account: false },
reminders: {
view_own: true,
view_all: true,
create: true,
edit_own: true,
edit_all: true,
assign_others: true,
},
calendar: { connect: true, view_events: true },
reports: { view_dashboard: true, view_analytics: true, export: true },
document_templates: { view: true, generate: true, manage: false },
yachts: { view: true, create: true, edit: true, delete: false, transfer: true },
companies: { view: true, create: true, edit: true, delete: false },
memberships: { view: true, manage: true },
reservations: { view: true, create: true, activate: true, cancel: true },
admin: {
manage_users: false,
view_audit_log: true,
manage_settings: false,
manage_webhooks: false,
manage_reports: true,
manage_custom_fields: false,
manage_forms: false,
manage_tags: true,
system_backup: false,
},
feat(platform): residential module + admin UI + reliability fixes Residential platform - New schema: residentialClients, residentialInterests (separate from marina/yacht clients) with migration 0010 - Service layer with CRUD + audit + sockets + per-port portal toggle - v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries) - List + detail pages with inline editing for clients and interests - Per-user residentialAccess toggle on userPortRoles (migration 0011) - Permission keys: residential_clients, residential_interests - Sidebar nav + role form integration - Smoke spec covering page loads, UI create flow, public endpoint Admin & shared UI - Admin → Forms (form templates CRUD) with validators + service - Notification preferences page (in-app + email per type) - Email composition + accounts list + threads view - Branded auth shell shared across CRM + portal auth surfaces - Inline editing extended to yacht/company/interest detail pages - InlineTagEditor + per-entity tags endpoints (yachts, companies) - Notes service polymorphic across clients/interests/yachts/companies - Client list columns: yachtCount + companyCount badges - Reservation file-download via presigned URL (replaces stale <a href>) Route handler refactor - Extracted yachts/companies/berths reservation handlers to sibling handlers.ts files (Next.js 15 route.ts only allows specific exports) Reliability fixes - apiFetch double-stringify bug fixed across 13 components (apiFetch already JSON.stringifies its body; passing a stringified body produced double-encoded JSON which failed zod validation) - SocketProvider gated behind useSyncExternalStore-based mount check to avoid useSession() SSR crashes under React 19 + Next 15 - apiFetch falls back to URL-pathname → port-id resolution when the Zustand store hasn't hydrated yet (fresh contexts, e2e tests) - CRM invite flow (schema, service, route, email, dev script) - Dashboard route → [portSlug]/dashboard/page.tsx + redirect - Document the dev-server restart-after-migration gotcha in CLAUDE.md Tests - 5-case residential smoke spec - Integration test updates for new service signatures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
residential_clients: { view: true, create: true, edit: true, delete: true },
residential_interests: {
view: true,
create: true,
edit: true,
delete: true,
change_stage: true,
},
};
}
/** Director — everything except system backup. */
export function makeDirectorPermissions(): RolePermissions {
return {
...makeFullPermissions(),
admin: {
...makeFullPermissions().admin,
system_backup: false,
},
};
}
// ─── Minimal valid CreateClientInput ─────────────────────────────────────────
/** Returns a minimal valid CreateClientInput object for use in service calls. */
export function makeCreateClientInput(overrides?: { fullName?: string; portId?: string }) {
return {
fullName: overrides?.fullName ?? 'Test Client',
contacts: [{ channel: 'email' as const, value: 'test@example.com', isPrimary: true }],
tagIds: [] as string[],
};
}
/** Returns a minimal valid CreateInterestInput object. */
export function makeCreateInterestInput(overrides?: {
clientId?: string;
pipelineStage?:
| 'open'
| 'details_sent'
| 'in_communication'
| 'eoi_sent'
| 'eoi_signed'
| 'deposit_10pct'
| 'contract_sent'
| 'contract_signed'
| 'completed';
}) {
return {
clientId: overrides?.clientId ?? crypto.randomUUID(),
pipelineStage: overrides?.pipelineStage ?? ('open' as const),
reminderEnabled: false,
tagIds: [] as string[],
};
}