feat(public-interest): atomic client+yacht+company+interest trio
Restructures the public interest endpoint to create the yacht as a first-class row (owned by the new client, or by a newly upserted company when a company block is provided) and writes the yacht_id onto the new interest. All writes now run inside a single transaction instead of the previous unwrapped sequence. The public validator gains structured `yacht` (required) and `company` (optional) sub-objects; legacy flat fields remain in the schema for backward compatibility but are silently ignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,42 @@ const addressSchema = z.object({
|
||||
country: z.string().max(100).optional(),
|
||||
});
|
||||
|
||||
// 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(),
|
||||
incorporationCountry: z.string().max(100).optional(),
|
||||
role: z
|
||||
.enum([
|
||||
'director',
|
||||
'officer',
|
||||
'broker',
|
||||
'representative',
|
||||
'legal_counsel',
|
||||
'employee',
|
||||
'shareholder',
|
||||
'other',
|
||||
])
|
||||
.optional()
|
||||
.default('representative'),
|
||||
});
|
||||
|
||||
export const publicInterestSchema = z
|
||||
.object({
|
||||
// New: first/last split
|
||||
@@ -85,15 +121,26 @@ export const publicInterestSchema = z
|
||||
phone: z.string().min(1),
|
||||
preferredContactMethod: z.enum(['email', 'phone', 'sms']).optional(),
|
||||
mooringNumber: z.string().max(50).optional(),
|
||||
companyName: z.string().optional(),
|
||||
// NEW: required structured yacht block. Public submissions after the
|
||||
// data-model refactor MUST include yacht data.
|
||||
yacht: publicYachtSchema,
|
||||
// NEW: optional company block — creates/upserts a company and adds a
|
||||
// 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(),
|
||||
|
||||
// ─── Deprecated flat fields ────────────────────────────────────────────
|
||||
// Kept in the schema so strict parse does not reject submissions from
|
||||
// legacy callers, but the route IGNORES them in favor of `yacht` / `company`.
|
||||
// Remove once all inbound integrations have migrated.
|
||||
yachtName: z.string().optional(),
|
||||
yachtLengthFt: z.coerce.number().positive().optional(),
|
||||
yachtWidthFt: z.coerce.number().positive().optional(),
|
||||
yachtDraftFt: z.coerce.number().positive().optional(),
|
||||
preferredBerthSize: z.string().optional(),
|
||||
source: z.literal('website').default('website'),
|
||||
notes: z.string().max(2000).optional(),
|
||||
address: addressSchema.optional(),
|
||||
companyName: z.string().optional(),
|
||||
})
|
||||
.refine((data) => data.fullName || (data.firstName && data.lastName), {
|
||||
message: 'Either fullName or both firstName and lastName are required',
|
||||
|
||||
Reference in New Issue
Block a user