Files
pn-new-crm/src/lib/validators/tenancies.ts
Matt d32e557e56 feat(tenancies-renew-transfer): tenure-aware renewal + transfer actions
- renewTenancy service:
  - permanent / fee_simple / strata_lot → mutate-in-place (startDate
    moves forward, endDate may extend or null out)
  - fixed_term / seasonal → end the current row at its existing endDate
    + mint a successor with previousTenancyId chain. newEndDate required.
- transferTenancy service: end-and-spawn — end current row at
  transferDate, mint fresh active row with transferredFromTenancyId
  pointing back. New client + yacht cross-validated against port +
  ownership constraint (assertClientOwnsOrRepresentsYacht).
- POST /api/v1/tenancies/[id]/renew + /transfer routes gated on
  tenancies.manage + module-enabled.
- TenancyRenewDialog (tenure-aware copy explains in-place vs successor),
  TenancyTransferDialog (ClientPicker + YachtPicker with owner-scoped
  filter). Both mounted on tenancy-detail.tsx alongside Edit + End.
- Validators: renewTenancySchema + transferTenancySchema in
  src/lib/validators/tenancies.ts.

Verified: tsc clean, 1493/1493 vitest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:13:34 +02:00

82 lines
3.2 KiB
TypeScript

import { z } from 'zod';
import { baseListQuerySchema } from '@/lib/api/list-query';
export const TENANCY_STATUSES = ['pending', 'active', 'ended', 'cancelled'] as const;
export const TENURE_TYPES = ['permanent', 'fixed_term', 'seasonal'] as const;
export const createPendingSchema = z.object({
berthId: z.string().min(1),
clientId: z.string().min(1),
yachtId: z.string().min(1),
interestId: z.string().optional(),
startDate: z.coerce.date(),
tenureType: z.enum(TENURE_TYPES).optional().default('permanent'),
notes: z.string().optional(),
});
export const activateSchema = z.object({
contractFileId: z.string().optional(),
effectiveDate: z.coerce.date().optional(),
});
export const endTenancySchema = z.object({
endDate: z.coerce.date(),
notes: z.string().optional(),
});
export const cancelSchema = z.object({
reason: z.string().optional(),
});
/** PATCH body for the "edit metadata" idiom — touches notes / dates /
* tenure type without crossing a status boundary. Status transitions
* flow through activate / endTenancy / cancel; this is non-transition
* metadata only. */
export const updateTenancySchema = z
.object({
startDate: z.coerce.date().optional(),
endDate: z.coerce.date().nullable().optional(),
tenureType: z.enum(TENURE_TYPES).optional(),
notes: z.string().nullable().optional(),
})
.refine((d) => Object.keys(d).length > 0, { message: 'At least one field must be provided' });
export type UpdateTenancyInput = z.infer<typeof updateTenancySchema>;
/** Renewal input. For permanent-class tenancies the existing row is
* mutated in place; for fixed_term / seasonal the existing row is
* ended at `previousEndDate` and a fresh row is minted with
* previousTenancyId pointing back. */
export const renewTenancySchema = z.object({
/** New start date for the renewal window (or the mutated row when permanent). */
newStartDate: z.coerce.date(),
/** New end date — required for fixed_term / seasonal; optional when permanent. */
newEndDate: z.coerce.date().optional(),
/** Optional notes appended to the successor / mutated row. */
notes: z.string().optional(),
});
export type RenewTenancyInput = z.infer<typeof renewTenancySchema>;
/** Transfer input — hand a berth's active tenancy to a new client/yacht
* pair. The current row is ended, a fresh active row is minted with
* transferredFromTenancyId pointing at the predecessor. */
export const transferTenancySchema = z.object({
newClientId: z.string().min(1),
newYachtId: z.string().min(1),
transferDate: z.coerce.date(),
notes: z.string().optional(),
});
export type TransferTenancyInput = z.infer<typeof transferTenancySchema>;
export const listTenanciesSchema = baseListQuerySchema.extend({
status: z.enum(TENANCY_STATUSES).optional(),
berthId: z.string().optional(),
clientId: z.string().optional(),
yachtId: z.string().optional(),
});
export type CreatePendingInput = z.infer<typeof createPendingSchema>;
export type ActivateInput = z.infer<typeof activateSchema>;
export type EndTenancyInput = z.infer<typeof endTenancySchema>;
export type CancelInput = z.infer<typeof cancelSchema>;
export type ListTenanciesInput = z.infer<typeof listTenanciesSchema>;