Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { z } from 'zod';
|
|
|
|
|
|
2026-05-06 14:56:59 +02:00
|
|
|
import { baseListQuerySchema } from '@/lib/api/list-query';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { PIPELINE_STAGES, LEAD_CATEGORIES } from '@/lib/constants';
|
chore(cleanup): Phase 1 — gap closure across audit, alerts, soft-delete, perms
Multi-area cleanup pass closing partial-implementation gaps surfaced by the
post-i18n audit. No behavior changes for happy-path users; closes real
correctness/security holes.
PR1a Public yacht-interest endpoint i18n. /api/public/interests now accepts
phoneE164/phoneCountry, nationalityIso, address.{countryIso, subdivisionIso},
and company.{incorporationCountryIso, incorporationSubdivisionIso}.
Server-side parsePhone() fallback for legacy raw phone strings.
PR1b Alert rule registry trim. Two rule slots ('document.expiring_soon',
'audit.suspicious_login') were registered but evaluators returned [].
Both required schema/instrumentation that hadn't landed. Removed from
the registry; comments record the dependencies needed to revive them.
Effective rule count: 8 active.
PR1c vi.mock hoist + flake fix. Hoisted vi.mock calls to top-level in 5
integration test files; webhook-delivery uses vi.hoisted for the
queue-add ref. Vitest no longer warns about non-top-level mocks.
Deflaked the 'short value' assertion in security-encryption.test.ts
by switching plaintext from 'ab' to 'XY' (non-hex chars). 5/5 runs green.
PR1d Soft-delete reference audit. listClientOptions and listYachtsForOwner
now filter by isNull(archivedAt). Berths use status (no archivedAt).
PR1e Permission-matrix audit script + report. scripts/audit-permissions.ts
walks every src/app/api/v1/**/route.ts and reports handlers without a
withPermission() wrapper. Initial run found 33 violations.
- Allow-listed 17 with explicit reasons (self-data, admin, alerts,
search, currency, ai, custom-fields — some marked TODO).
- Wrapped 7 routes with concrete permissions: clients/options
(clients:view), berths/options (berths:view), dashboard/*
(reports:view_dashboard), analytics (reports:view_analytics).
Audit report at docs/runbooks/permission-audit.md. Script exits
non-zero on any unallow-listed violation so it can become a CI gate.
Vitest: 741 -> 741 (no new tests; existing suite covers the changes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:48:22 +02:00
|
|
|
import {
|
|
|
|
|
optionalCountryIsoSchema,
|
|
|
|
|
optionalPhoneE164Schema,
|
|
|
|
|
optionalSubdivisionIsoSchema,
|
|
|
|
|
} from '@/lib/validators/i18n';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
// ─── Create ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-05-05 02:49:01 +02:00
|
|
|
/**
|
|
|
|
|
* Desired-dimension input. Strings/numbers are coerced to a positive
|
|
|
|
|
* decimal (string-typed for postgres `numeric` column compatibility);
|
|
|
|
|
* empty strings collapse to `undefined` so a blank form field doesn't
|
|
|
|
|
* round-trip "" → numeric error on the API.
|
|
|
|
|
*/
|
|
|
|
|
const optionalDesiredDimSchema = z
|
|
|
|
|
.union([z.string(), z.number()])
|
|
|
|
|
.optional()
|
|
|
|
|
.transform((v) => {
|
|
|
|
|
if (v === undefined || v === null || v === '') return undefined;
|
|
|
|
|
const n = typeof v === 'number' ? v : parseFloat(v);
|
|
|
|
|
if (!Number.isFinite(n) || n <= 0) return undefined;
|
|
|
|
|
return String(Math.round(n * 100) / 100);
|
|
|
|
|
});
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
export const createInterestSchema = z.object({
|
|
|
|
|
clientId: z.string().min(1),
|
2026-04-24 15:34:44 +02:00
|
|
|
yachtId: z.string().optional(),
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
berthId: z.string().optional(),
|
|
|
|
|
pipelineStage: z.enum(PIPELINE_STAGES).default('open'),
|
|
|
|
|
leadCategory: z.enum(LEAD_CATEGORIES).optional(),
|
|
|
|
|
source: z.string().optional(),
|
|
|
|
|
notes: z.string().optional(),
|
|
|
|
|
tagIds: z.array(z.string()).optional().default([]),
|
|
|
|
|
reminderEnabled: z.boolean().optional().default(false),
|
|
|
|
|
reminderDays: z.number().int().min(1).optional(),
|
2026-05-05 02:49:01 +02:00
|
|
|
desiredLengthFt: optionalDesiredDimSchema,
|
|
|
|
|
desiredWidthFt: optionalDesiredDimSchema,
|
|
|
|
|
desiredDraftFt: optionalDesiredDimSchema,
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Update ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export const updateInterestSchema = createInterestSchema
|
|
|
|
|
.omit({ clientId: true, tagIds: true })
|
|
|
|
|
.partial();
|
|
|
|
|
|
|
|
|
|
// ─── Change Stage ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export const changeStageSchema = z.object({
|
|
|
|
|
pipelineStage: z.enum(PIPELINE_STAGES),
|
|
|
|
|
reason: z.string().optional(),
|
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
|
|
|
/** Bypass the canTransitionStage transition table. Requires the caller
|
|
|
|
|
* to hold the `interests.override_stage` permission. Reason becomes
|
|
|
|
|
* required when override=true (recorded in the audit log). */
|
|
|
|
|
override: z.boolean().optional(),
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
});
|
|
|
|
|
|
2026-05-02 00:01:33 +02:00
|
|
|
// ─── Outcome (Won / Lost) ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export const INTEREST_OUTCOMES = [
|
|
|
|
|
'won',
|
|
|
|
|
'lost_other_marina',
|
|
|
|
|
'lost_unqualified',
|
|
|
|
|
'lost_no_response',
|
|
|
|
|
'cancelled',
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
export type InterestOutcome = (typeof INTEREST_OUTCOMES)[number];
|
|
|
|
|
|
|
|
|
|
export const setOutcomeSchema = z.object({
|
|
|
|
|
outcome: z.enum(INTEREST_OUTCOMES),
|
|
|
|
|
reason: z.string().max(2000).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const clearOutcomeSchema = z.object({
|
|
|
|
|
// Stage to revert to when reopening (defaults to in_communication).
|
|
|
|
|
reopenStage: z.enum(PIPELINE_STAGES).optional(),
|
|
|
|
|
});
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
// ─── List ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export const listInterestsSchema = baseListQuerySchema.extend({
|
|
|
|
|
clientId: z.string().optional(),
|
2026-04-24 15:34:44 +02:00
|
|
|
yachtId: z.string().optional(),
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
berthId: z.string().optional(),
|
|
|
|
|
pipelineStage: z
|
|
|
|
|
.string()
|
|
|
|
|
.transform((v) => v.split(',').filter(Boolean))
|
|
|
|
|
.optional(),
|
|
|
|
|
leadCategory: z.enum(LEAD_CATEGORIES).optional(),
|
|
|
|
|
eoiStatus: z.string().optional(),
|
|
|
|
|
tagIds: z
|
|
|
|
|
.string()
|
|
|
|
|
.transform((v) => v.split(',').filter(Boolean))
|
|
|
|
|
.optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Waiting List ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export const waitingListAddSchema = z.object({
|
|
|
|
|
clientId: z.string().min(1),
|
|
|
|
|
priority: z.enum(['normal', 'high']).default('normal'),
|
|
|
|
|
notifyPref: z.enum(['email', 'in_app', 'both']).default('email'),
|
|
|
|
|
notes: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Generate Recommendations ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export const generateRecommendationsSchema = z.object({
|
|
|
|
|
interestId: z.string().min(1),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Public Interest ──────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-04-14 12:53:13 -04:00
|
|
|
const addressSchema = z.object({
|
|
|
|
|
street: z.string().max(500).optional(),
|
|
|
|
|
city: z.string().max(200).optional(),
|
chore(cleanup): Phase 1 — gap closure across audit, alerts, soft-delete, perms
Multi-area cleanup pass closing partial-implementation gaps surfaced by the
post-i18n audit. No behavior changes for happy-path users; closes real
correctness/security holes.
PR1a Public yacht-interest endpoint i18n. /api/public/interests now accepts
phoneE164/phoneCountry, nationalityIso, address.{countryIso, subdivisionIso},
and company.{incorporationCountryIso, incorporationSubdivisionIso}.
Server-side parsePhone() fallback for legacy raw phone strings.
PR1b Alert rule registry trim. Two rule slots ('document.expiring_soon',
'audit.suspicious_login') were registered but evaluators returned [].
Both required schema/instrumentation that hadn't landed. Removed from
the registry; comments record the dependencies needed to revive them.
Effective rule count: 8 active.
PR1c vi.mock hoist + flake fix. Hoisted vi.mock calls to top-level in 5
integration test files; webhook-delivery uses vi.hoisted for the
queue-add ref. Vitest no longer warns about non-top-level mocks.
Deflaked the 'short value' assertion in security-encryption.test.ts
by switching plaintext from 'ab' to 'XY' (non-hex chars). 5/5 runs green.
PR1d Soft-delete reference audit. listClientOptions and listYachtsForOwner
now filter by isNull(archivedAt). Berths use status (no archivedAt).
PR1e Permission-matrix audit script + report. scripts/audit-permissions.ts
walks every src/app/api/v1/**/route.ts and reports handlers without a
withPermission() wrapper. Initial run found 33 violations.
- Allow-listed 17 with explicit reasons (self-data, admin, alerts,
search, currency, ai, custom-fields — some marked TODO).
- Wrapped 7 routes with concrete permissions: clients/options
(clients:view), berths/options (berths:view), dashboard/*
(reports:view_dashboard), analytics (reports:view_analytics).
Audit report at docs/runbooks/permission-audit.md. Script exits
non-zero on any unallow-listed violation so it can become a CI gate.
Vitest: 741 -> 741 (no new tests; existing suite covers the changes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:48:22 +02:00
|
|
|
/** ISO 3166-2 subdivision code (e.g. 'PL-MZ'). */
|
|
|
|
|
subdivisionIso: optionalSubdivisionIsoSchema.optional(),
|
2026-04-14 12:53:13 -04:00
|
|
|
postalCode: z.string().max(50).optional(),
|
chore(cleanup): Phase 1 — gap closure across audit, alerts, soft-delete, perms
Multi-area cleanup pass closing partial-implementation gaps surfaced by the
post-i18n audit. No behavior changes for happy-path users; closes real
correctness/security holes.
PR1a Public yacht-interest endpoint i18n. /api/public/interests now accepts
phoneE164/phoneCountry, nationalityIso, address.{countryIso, subdivisionIso},
and company.{incorporationCountryIso, incorporationSubdivisionIso}.
Server-side parsePhone() fallback for legacy raw phone strings.
PR1b Alert rule registry trim. Two rule slots ('document.expiring_soon',
'audit.suspicious_login') were registered but evaluators returned [].
Both required schema/instrumentation that hadn't landed. Removed from
the registry; comments record the dependencies needed to revive them.
Effective rule count: 8 active.
PR1c vi.mock hoist + flake fix. Hoisted vi.mock calls to top-level in 5
integration test files; webhook-delivery uses vi.hoisted for the
queue-add ref. Vitest no longer warns about non-top-level mocks.
Deflaked the 'short value' assertion in security-encryption.test.ts
by switching plaintext from 'ab' to 'XY' (non-hex chars). 5/5 runs green.
PR1d Soft-delete reference audit. listClientOptions and listYachtsForOwner
now filter by isNull(archivedAt). Berths use status (no archivedAt).
PR1e Permission-matrix audit script + report. scripts/audit-permissions.ts
walks every src/app/api/v1/**/route.ts and reports handlers without a
withPermission() wrapper. Initial run found 33 violations.
- Allow-listed 17 with explicit reasons (self-data, admin, alerts,
search, currency, ai, custom-fields — some marked TODO).
- Wrapped 7 routes with concrete permissions: clients/options
(clients:view), berths/options (berths:view), dashboard/*
(reports:view_dashboard), analytics (reports:view_analytics).
Audit report at docs/runbooks/permission-audit.md. Script exits
non-zero on any unallow-listed violation so it can become a CI gate.
Vitest: 741 -> 741 (no new tests; existing suite covers the changes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:48:22 +02:00
|
|
|
/** ISO-3166-1 alpha-2 country code. */
|
|
|
|
|
countryIso: optionalCountryIsoSchema.optional(),
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
});
|
|
|
|
|
|
2026-04-24 15:42:45 +02:00
|
|
|
// Nested yacht block. Public submissions must now include yacht data because the
|
|
|
|
|
// route inserts a yacht row as part of the trio (client + yacht + interest).
|
|
|
|
|
const publicYachtSchema = z.object({
|
|
|
|
|
name: z.string().min(1).max(200),
|
|
|
|
|
hullNumber: z.string().max(100).optional(),
|
|
|
|
|
registration: z.string().max(100).optional(),
|
|
|
|
|
flag: z.string().max(100).optional(),
|
|
|
|
|
yearBuilt: z.coerce.number().int().min(1800).max(2100).optional(),
|
|
|
|
|
lengthFt: z.coerce.number().positive().optional(),
|
|
|
|
|
widthFt: z.coerce.number().positive().optional(),
|
|
|
|
|
draftFt: z.coerce.number().positive().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Optional company block. If provided, the route upserts a company row (match
|
|
|
|
|
// case-insensitively by (portId, name)) and adds an active membership linking
|
|
|
|
|
// the submitting client to the company with the chosen role.
|
|
|
|
|
const publicCompanySchema = z.object({
|
|
|
|
|
name: z.string().min(1).max(200),
|
|
|
|
|
legalName: z.string().max(200).optional(),
|
|
|
|
|
taxId: z.string().max(100).optional(),
|
chore(cleanup): Phase 1 — gap closure across audit, alerts, soft-delete, perms
Multi-area cleanup pass closing partial-implementation gaps surfaced by the
post-i18n audit. No behavior changes for happy-path users; closes real
correctness/security holes.
PR1a Public yacht-interest endpoint i18n. /api/public/interests now accepts
phoneE164/phoneCountry, nationalityIso, address.{countryIso, subdivisionIso},
and company.{incorporationCountryIso, incorporationSubdivisionIso}.
Server-side parsePhone() fallback for legacy raw phone strings.
PR1b Alert rule registry trim. Two rule slots ('document.expiring_soon',
'audit.suspicious_login') were registered but evaluators returned [].
Both required schema/instrumentation that hadn't landed. Removed from
the registry; comments record the dependencies needed to revive them.
Effective rule count: 8 active.
PR1c vi.mock hoist + flake fix. Hoisted vi.mock calls to top-level in 5
integration test files; webhook-delivery uses vi.hoisted for the
queue-add ref. Vitest no longer warns about non-top-level mocks.
Deflaked the 'short value' assertion in security-encryption.test.ts
by switching plaintext from 'ab' to 'XY' (non-hex chars). 5/5 runs green.
PR1d Soft-delete reference audit. listClientOptions and listYachtsForOwner
now filter by isNull(archivedAt). Berths use status (no archivedAt).
PR1e Permission-matrix audit script + report. scripts/audit-permissions.ts
walks every src/app/api/v1/**/route.ts and reports handlers without a
withPermission() wrapper. Initial run found 33 violations.
- Allow-listed 17 with explicit reasons (self-data, admin, alerts,
search, currency, ai, custom-fields — some marked TODO).
- Wrapped 7 routes with concrete permissions: clients/options
(clients:view), berths/options (berths:view), dashboard/*
(reports:view_dashboard), analytics (reports:view_analytics).
Audit report at docs/runbooks/permission-audit.md. Script exits
non-zero on any unallow-listed violation so it can become a CI gate.
Vitest: 741 -> 741 (no new tests; existing suite covers the changes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:48:22 +02:00
|
|
|
/** ISO-3166-1 alpha-2 country of incorporation. */
|
|
|
|
|
incorporationCountryIso: optionalCountryIsoSchema.optional(),
|
|
|
|
|
/** ISO 3166-2 state/province of incorporation. */
|
|
|
|
|
incorporationSubdivisionIso: optionalSubdivisionIsoSchema.optional(),
|
2026-04-24 15:42:45 +02:00
|
|
|
role: z
|
|
|
|
|
.enum([
|
|
|
|
|
'director',
|
|
|
|
|
'officer',
|
|
|
|
|
'broker',
|
|
|
|
|
'representative',
|
|
|
|
|
'legal_counsel',
|
|
|
|
|
'employee',
|
|
|
|
|
'shareholder',
|
|
|
|
|
'other',
|
|
|
|
|
])
|
|
|
|
|
.optional()
|
|
|
|
|
.default('representative'),
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-14 12:53:13 -04:00
|
|
|
export const publicInterestSchema = z
|
|
|
|
|
.object({
|
|
|
|
|
// New: first/last split
|
|
|
|
|
firstName: z.string().min(1).max(100).optional(),
|
|
|
|
|
lastName: z.string().min(1).max(100).optional(),
|
|
|
|
|
// Backward compat
|
|
|
|
|
fullName: z.string().min(1).max(200).optional(),
|
|
|
|
|
email: z.string().email(),
|
|
|
|
|
phone: z.string().min(1),
|
chore(cleanup): Phase 1 — gap closure across audit, alerts, soft-delete, perms
Multi-area cleanup pass closing partial-implementation gaps surfaced by the
post-i18n audit. No behavior changes for happy-path users; closes real
correctness/security holes.
PR1a Public yacht-interest endpoint i18n. /api/public/interests now accepts
phoneE164/phoneCountry, nationalityIso, address.{countryIso, subdivisionIso},
and company.{incorporationCountryIso, incorporationSubdivisionIso}.
Server-side parsePhone() fallback for legacy raw phone strings.
PR1b Alert rule registry trim. Two rule slots ('document.expiring_soon',
'audit.suspicious_login') were registered but evaluators returned [].
Both required schema/instrumentation that hadn't landed. Removed from
the registry; comments record the dependencies needed to revive them.
Effective rule count: 8 active.
PR1c vi.mock hoist + flake fix. Hoisted vi.mock calls to top-level in 5
integration test files; webhook-delivery uses vi.hoisted for the
queue-add ref. Vitest no longer warns about non-top-level mocks.
Deflaked the 'short value' assertion in security-encryption.test.ts
by switching plaintext from 'ab' to 'XY' (non-hex chars). 5/5 runs green.
PR1d Soft-delete reference audit. listClientOptions and listYachtsForOwner
now filter by isNull(archivedAt). Berths use status (no archivedAt).
PR1e Permission-matrix audit script + report. scripts/audit-permissions.ts
walks every src/app/api/v1/**/route.ts and reports handlers without a
withPermission() wrapper. Initial run found 33 violations.
- Allow-listed 17 with explicit reasons (self-data, admin, alerts,
search, currency, ai, custom-fields — some marked TODO).
- Wrapped 7 routes with concrete permissions: clients/options
(clients:view), berths/options (berths:view), dashboard/*
(reports:view_dashboard), analytics (reports:view_analytics).
Audit report at docs/runbooks/permission-audit.md. Script exits
non-zero on any unallow-listed violation so it can become a CI gate.
Vitest: 741 -> 741 (no new tests; existing suite covers the changes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:48:22 +02:00
|
|
|
/** Pre-normalized E.164 form, optional for backwards compat. */
|
|
|
|
|
phoneE164: optionalPhoneE164Schema.optional(),
|
|
|
|
|
/** ISO-3166-1 alpha-2 country the phone was parsed against. */
|
|
|
|
|
phoneCountry: optionalCountryIsoSchema.optional(),
|
|
|
|
|
/** ISO-3166-1 alpha-2 nationality. */
|
|
|
|
|
nationalityIso: optionalCountryIsoSchema.optional(),
|
2026-04-14 12:53:13 -04:00
|
|
|
preferredContactMethod: z.enum(['email', 'phone', 'sms']).optional(),
|
|
|
|
|
mooringNumber: z.string().max(50).optional(),
|
2026-04-24 15:42:45 +02:00
|
|
|
// NEW: required structured yacht block. Public submissions after the
|
|
|
|
|
// data-model refactor MUST include yacht data.
|
|
|
|
|
yacht: publicYachtSchema,
|
2026-05-04 22:57:01 +02:00
|
|
|
// NEW: optional company block - creates/upserts a company and adds a
|
2026-04-24 15:42:45 +02:00
|
|
|
// membership linking the submitting client to it.
|
|
|
|
|
company: publicCompanySchema.optional(),
|
|
|
|
|
source: z.literal('website').default('website'),
|
|
|
|
|
notes: z.string().max(2000).optional(),
|
|
|
|
|
address: addressSchema.optional(),
|
2026-04-14 12:53:13 -04:00
|
|
|
})
|
|
|
|
|
.refine((data) => data.fullName || (data.firstName && data.lastName), {
|
|
|
|
|
message: 'Either fullName or both firstName and lastName are required',
|
|
|
|
|
path: ['fullName'],
|
|
|
|
|
});
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
// ─── Reorder Waiting List ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export const reorderWaitingListSchema = z.object({
|
|
|
|
|
entryId: z.string().min(1),
|
|
|
|
|
newPosition: z.coerce.number().int().min(1),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export type CreateInterestInput = z.infer<typeof createInterestSchema>;
|
|
|
|
|
export type UpdateInterestInput = z.infer<typeof updateInterestSchema>;
|
|
|
|
|
export type ChangeStageInput = z.infer<typeof changeStageSchema>;
|
|
|
|
|
export type ListInterestsInput = z.infer<typeof listInterestsSchema>;
|
|
|
|
|
export type WaitingListAddInput = z.infer<typeof waitingListAddSchema>;
|
|
|
|
|
export type PublicInterestInput = z.infer<typeof publicInterestSchema>;
|
|
|
|
|
export type ReorderWaitingListInput = z.infer<typeof reorderWaitingListSchema>;
|
2026-05-02 00:01:33 +02:00
|
|
|
export type SetOutcomeInput = z.infer<typeof setOutcomeSchema>;
|
|
|
|
|
export type ClearOutcomeInput = z.infer<typeof clearOutcomeSchema>;
|