- 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>
82 lines
3.2 KiB
TypeScript
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>;
|