73-file atomic rename per docs/tenancies-design.md:
- Migration 0085: rename table + indexes + FK constraints; rename
documents.reservation_id → tenancy_id; migrate jsonb permission maps
(reservations resource → tenancies; collapse create+activate → manage);
rewrite historical audit_logs.entity_type='berth_reservation' →
'berth_tenancy'. FK renames wrapped in DO blocks so dev DBs that pre-date
the FK additions don't abort.
- Schema: berthReservations → berthTenancies; BerthReservation type →
BerthTenancy; indexes idx_br_* / idx_brr_* → idx_bt_*.
- RolePermissions: resource { view, create, activate, cancel } collapses to
{ view, manage, cancel }; all 8 default seed bundles + role-form + matrix
updated.
- Service: berth-reservations.service.ts → berth-tenancies.service.ts;
endReservation → endTenancy; listReservations → listTenancies.
- API: /api/v1/berth-reservations → /api/v1/tenancies (+ nested [id]);
/api/v1/berths/[id]/reservations → /api/v1/berths/[id]/tenancies.
- Validators: reservations.ts → tenancies.ts; RESERVATION_STATUSES →
TENANCY_STATUSES; endReservationSchema → endTenancySchema.
- Routes: /{portSlug}/berth-reservations → /{portSlug}/tenancies;
/portal/my-reservations → /portal/my-tenancies.
- Components: src/components/reservations/* → src/components/tenancies/*;
BerthReservationsTab → BerthTenanciesTab; ClientReservationsTab →
ClientTenanciesTab; ReservationList → TenancyList.
- Socket events: berth_reservation:* → berth_tenancy:*; payload
reservationId → tenancyId.
- Webhook events: berth_reservation.* → berth_tenancy.*.
- Portal: getPortalUserReservations → getPortalUserTenancies;
PortalReservation → PortalTenancy; PortalDashboard.counts.activeReservations
→ activeTenancies; PortalNav label "Reservations" → "Tenancies".
- Dossier: DossierReservation → DossierTenancy; reservationDecisions →
tenancyDecisions across smart-archive-dialog + bulk-archive routes.
- Documents schema: documents.reservationId → documents.tenancyId
(TS + DB column + index + FK constraint).
- Activity feed label berth_reservation → berth_tenancy (matched against
migrated historical audit rows).
KEPT (separate concepts):
- Reservation Agreement document type (the contract sent to clients).
- "Reservation" pipeline stage name.
- {{reservation.*}} merge tokens in template authoring.
- interest.reservationStatus / reservationDocStatus / dateReservationSent
fields (track agreement signing on the deal).
- reservation-agreement-context.ts service (builds merge context for the
Reservation Agreement doc; only its DB imports were renamed).
Verified: tsc clean, 1480/1480 vitest passing, migration applied.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
577 lines
19 KiB
TypeScript
577 lines
19 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { createClientSchema, updateClientSchema } from '@/lib/validators/clients';
|
|
import { createInterestSchema, changeStageSchema } from '@/lib/validators/interests';
|
|
import { updateBerthSchema, updateBerthStatusSchema } from '@/lib/validators/berths';
|
|
import { createInvoiceSchema } from '@/lib/validators/invoices';
|
|
import { createWebhookSchema, updateWebhookSchema } from '@/lib/validators/webhooks';
|
|
import { createFieldSchema, updateFieldSchema } from '@/lib/validators/custom-fields';
|
|
import { createYachtSchema, transferOwnershipSchema } from '@/lib/validators/yachts';
|
|
import { createCompanySchema } from '@/lib/validators/companies';
|
|
import { addMembershipSchema } from '@/lib/validators/company-memberships';
|
|
import { createPendingSchema } from '@/lib/validators/tenancies';
|
|
|
|
// ─── Client schemas ───────────────────────────────────────────────────────────
|
|
|
|
describe('createClientSchema', () => {
|
|
const validClient = {
|
|
fullName: 'Alice Smith',
|
|
contacts: [{ channel: 'email' as const, value: 'alice@example.com' }],
|
|
};
|
|
|
|
it('accepts a valid minimal client', () => {
|
|
expect(createClientSchema.safeParse(validClient).success).toBe(true);
|
|
});
|
|
|
|
it('rejects empty fullName', () => {
|
|
const result = createClientSchema.safeParse({ ...validClient, fullName: '' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects when contacts array is empty', () => {
|
|
const result = createClientSchema.safeParse({ ...validClient, contacts: [] });
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
const paths = result.error.issues.map((i) => i.path.join('.'));
|
|
expect(paths).toContain('contacts');
|
|
}
|
|
});
|
|
|
|
it('rejects invalid contact channel', () => {
|
|
const result = createClientSchema.safeParse({
|
|
...validClient,
|
|
contacts: [{ channel: 'fax', value: '1234' }],
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid email in contact value', () => {
|
|
// channel=email doesn't mandate email format at schema level (value is just string.min(1))
|
|
// But empty value is rejected
|
|
const result = createClientSchema.safeParse({
|
|
...validClient,
|
|
contacts: [{ channel: 'email' as const, value: '' }],
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid source enum', () => {
|
|
const result = createClientSchema.safeParse({ ...validClient, source: 'unknown' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('accepts optional fields', () => {
|
|
const result = createClientSchema.safeParse({
|
|
...validClient,
|
|
nationality: 'AU',
|
|
source: 'manual' as const,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('accepts minimal valid input (no deprecated yacht/company fields)', () => {
|
|
const result = createClientSchema.safeParse({
|
|
fullName: 'Alice',
|
|
contacts: [{ channel: 'email', value: 'a@example.com' }],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('updateClientSchema (partial)', () => {
|
|
it('accepts empty object (all optional)', () => {
|
|
expect(updateClientSchema.safeParse({}).success).toBe(true);
|
|
});
|
|
|
|
it('rejects fullName: empty string even in update', () => {
|
|
const result = updateClientSchema.safeParse({ fullName: '' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Interest schemas ─────────────────────────────────────────────────────────
|
|
|
|
describe('createInterestSchema', () => {
|
|
const validInterest = { clientId: 'client-uuid-1' };
|
|
|
|
it('accepts a valid minimal interest', () => {
|
|
expect(createInterestSchema.safeParse(validInterest).success).toBe(true);
|
|
});
|
|
|
|
it('rejects empty clientId', () => {
|
|
const result = createInterestSchema.safeParse({ clientId: '' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid pipelineStage', () => {
|
|
const result = createInterestSchema.safeParse({
|
|
clientId: 'c1',
|
|
pipelineStage: 'unknown_stage',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('accepts all valid pipeline stages', () => {
|
|
const stages = [
|
|
'enquiry',
|
|
'qualified',
|
|
'nurturing',
|
|
'eoi',
|
|
'reservation',
|
|
'deposit_paid',
|
|
'contract',
|
|
];
|
|
for (const stage of stages) {
|
|
const result = createInterestSchema.safeParse({ clientId: 'c1', pipelineStage: stage });
|
|
expect(result.success, `stage "${stage}" should be valid`).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('rejects reminderDays < 1', () => {
|
|
const result = createInterestSchema.safeParse({ clientId: 'c1', reminderDays: 0 });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('createInterestSchema accepts yachtId', () => {
|
|
const result = createInterestSchema.safeParse({
|
|
clientId: 'c1',
|
|
yachtId: 'y1',
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('changeStageSchema', () => {
|
|
it('accepts a valid stage', () => {
|
|
expect(changeStageSchema.safeParse({ pipelineStage: 'nurturing' }).success).toBe(true);
|
|
});
|
|
|
|
it('rejects invalid stage', () => {
|
|
expect(changeStageSchema.safeParse({ pipelineStage: 'bogus' }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Berth schemas ────────────────────────────────────────────────────────────
|
|
|
|
describe('updateBerthSchema', () => {
|
|
it('accepts empty object (all optional)', () => {
|
|
expect(updateBerthSchema.safeParse({}).success).toBe(true);
|
|
});
|
|
|
|
it('accepts valid tenure type', () => {
|
|
expect(updateBerthSchema.safeParse({ tenureType: 'permanent' }).success).toBe(true);
|
|
});
|
|
|
|
it('rejects invalid tenure type', () => {
|
|
expect(updateBerthSchema.safeParse({ tenureType: 'lease' }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('updateBerthStatusSchema', () => {
|
|
it('accepts valid status with reason', () => {
|
|
expect(
|
|
updateBerthStatusSchema.safeParse({ status: 'available', reason: 'Freed up' }).success,
|
|
).toBe(true);
|
|
});
|
|
|
|
it('rejects invalid status', () => {
|
|
expect(
|
|
updateBerthStatusSchema.safeParse({ status: 'occupied', reason: 'reason' }).success,
|
|
).toBe(false);
|
|
});
|
|
|
|
it('rejects missing reason', () => {
|
|
const result = updateBerthStatusSchema.safeParse({ status: 'available', reason: '' });
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
const paths = result.error.issues.map((i) => i.path.join('.'));
|
|
expect(paths).toContain('reason');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Invoice schemas ──────────────────────────────────────────────────────────
|
|
|
|
describe('createInvoiceSchema', () => {
|
|
const validInvoice = {
|
|
billingEntity: { type: 'client' as const, id: 'client-123' },
|
|
dueDate: '2026-06-01',
|
|
lineItems: [{ description: 'Berth fee', quantity: 1, unitPrice: 5000 }],
|
|
};
|
|
|
|
it('accepts a valid invoice with line items', () => {
|
|
expect(createInvoiceSchema.safeParse(validInvoice).success).toBe(true);
|
|
});
|
|
|
|
it('accepts a valid invoice with billingEntity type=company', () => {
|
|
const result = createInvoiceSchema.safeParse({
|
|
...validInvoice,
|
|
billingEntity: { type: 'company' as const, id: 'company-123' },
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('accepts invoice with only expenseIds', () => {
|
|
const result = createInvoiceSchema.safeParse({
|
|
billingEntity: { type: 'client' as const, id: 'client-123' },
|
|
dueDate: '2026-06-01',
|
|
expenseIds: ['exp-1'],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('rejects invoice with neither lineItems nor expenseIds', () => {
|
|
const result = createInvoiceSchema.safeParse({
|
|
billingEntity: { type: 'client' as const, id: 'client-123' },
|
|
dueDate: '2026-06-01',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects missing billingEntity', () => {
|
|
const result = createInvoiceSchema.safeParse({
|
|
dueDate: '2026-06-01',
|
|
lineItems: [{ description: 'Fee', quantity: 1, unitPrice: 1 }],
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects billingEntity with invalid type', () => {
|
|
const result = createInvoiceSchema.safeParse({
|
|
...validInvoice,
|
|
billingEntity: { type: 'unknown', id: 'id-1' },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects billingEntity with empty id', () => {
|
|
const result = createInvoiceSchema.safeParse({
|
|
...validInvoice,
|
|
billingEntity: { type: 'client', id: '' },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid billingEmail', () => {
|
|
const result = createInvoiceSchema.safeParse({ ...validInvoice, billingEmail: 'not-an-email' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects currency that is not 3 chars', () => {
|
|
const result = createInvoiceSchema.safeParse({ ...validInvoice, currency: 'USDX' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects negative unit price', () => {
|
|
const result = createInvoiceSchema.safeParse({
|
|
...validInvoice,
|
|
lineItems: [{ description: 'Fee', quantity: 1, unitPrice: -1 }],
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Webhook schemas ──────────────────────────────────────────────────────────
|
|
|
|
describe('createWebhookSchema', () => {
|
|
const validWebhook = {
|
|
name: 'My Webhook',
|
|
url: 'https://example.com/hook',
|
|
events: ['client.created'],
|
|
};
|
|
|
|
it('accepts a valid webhook', () => {
|
|
expect(createWebhookSchema.safeParse(validWebhook).success).toBe(true);
|
|
});
|
|
|
|
it('rejects http URL (must be HTTPS)', () => {
|
|
const result = createWebhookSchema.safeParse({
|
|
...validWebhook,
|
|
url: 'http://example.com/hook',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
const messages = result.error.issues.map((i) => i.message);
|
|
expect(messages.some((m) => m.toLowerCase().includes('https'))).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('rejects non-URL string', () => {
|
|
const result = createWebhookSchema.safeParse({ ...validWebhook, url: 'not a url' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects empty events array', () => {
|
|
const result = createWebhookSchema.safeParse({ ...validWebhook, events: [] });
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
const paths = result.error.issues.map((i) => i.path.join('.'));
|
|
expect(paths).toContain('events');
|
|
}
|
|
});
|
|
|
|
it('rejects unknown event name', () => {
|
|
const result = createWebhookSchema.safeParse({ ...validWebhook, events: ['unknown.event'] });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects empty webhook name', () => {
|
|
const result = createWebhookSchema.safeParse({ ...validWebhook, name: '' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('updateWebhookSchema', () => {
|
|
it('accepts empty object (all optional)', () => {
|
|
expect(updateWebhookSchema.safeParse({}).success).toBe(true);
|
|
});
|
|
|
|
it('rejects http URL in update too', () => {
|
|
const result = updateWebhookSchema.safeParse({ url: 'http://example.com/hook' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Custom field schemas ─────────────────────────────────────────────────────
|
|
|
|
describe('createFieldSchema', () => {
|
|
const validTextField = {
|
|
entityType: 'client',
|
|
fieldName: 'preferred_marina',
|
|
fieldLabel: 'Preferred Marina',
|
|
fieldType: 'text',
|
|
};
|
|
|
|
it('accepts a valid text field', () => {
|
|
expect(createFieldSchema.safeParse(validTextField).success).toBe(true);
|
|
});
|
|
|
|
it('rejects fieldName that is not snake_case', () => {
|
|
const result = createFieldSchema.safeParse({ ...validTextField, fieldName: 'PreferredMarina' });
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
const paths = result.error.issues.map((i) => i.path.join('.'));
|
|
expect(paths).toContain('fieldName');
|
|
}
|
|
});
|
|
|
|
it('rejects fieldName with spaces', () => {
|
|
const result = createFieldSchema.safeParse({
|
|
...validTextField,
|
|
fieldName: 'preferred marina',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('accepts select type with selectOptions', () => {
|
|
const result = createFieldSchema.safeParse({
|
|
...validTextField,
|
|
fieldType: 'select',
|
|
selectOptions: ['Option A', 'Option B'],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('rejects select type without selectOptions', () => {
|
|
const result = createFieldSchema.safeParse({ ...validTextField, fieldType: 'select' });
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
const paths = result.error.issues.map((i) => i.path.join('.'));
|
|
expect(paths).toContain('selectOptions');
|
|
}
|
|
});
|
|
|
|
it('rejects invalid fieldType', () => {
|
|
const result = createFieldSchema.safeParse({ ...validTextField, fieldType: 'json' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid entityType', () => {
|
|
const result = createFieldSchema.safeParse({ ...validTextField, entityType: 'invoice' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('updateFieldSchema', () => {
|
|
it('accepts empty object (all optional)', () => {
|
|
expect(updateFieldSchema.safeParse({}).success).toBe(true);
|
|
});
|
|
|
|
it('accepts valid update with fieldLabel', () => {
|
|
expect(updateFieldSchema.safeParse({ fieldLabel: 'New Label' }).success).toBe(true);
|
|
});
|
|
|
|
it('does NOT accept fieldType (immutability by omission)', () => {
|
|
// fieldType is omitted from the schema - it should be stripped or cause a strict failure
|
|
// With Zod default (strip mode), unknown keys are stripped and parse succeeds.
|
|
// The important check is that the parsed output does NOT include fieldType.
|
|
const result = updateFieldSchema.safeParse({ fieldType: 'number' });
|
|
if (result.success) {
|
|
// fieldType should be stripped from output
|
|
expect((result.data as Record<string, unknown>).fieldType).toBeUndefined();
|
|
}
|
|
// If it fails that's also acceptable (strict mode), but the key thing is
|
|
// it cannot be used to mutate fieldType.
|
|
});
|
|
});
|
|
|
|
// ─── Yacht schemas ────────────────────────────────────────────────────────────
|
|
|
|
describe('createYachtSchema', () => {
|
|
it('rejects empty name', () => {
|
|
const result = createYachtSchema.safeParse({
|
|
name: '',
|
|
owner: { type: 'client', id: 'c1' },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('requires owner', () => {
|
|
const result = createYachtSchema.safeParse({ name: 'Sea Breeze' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid yearBuilt', () => {
|
|
const result = createYachtSchema.safeParse({
|
|
name: 'Sea Breeze',
|
|
owner: { type: 'client', id: 'c1' },
|
|
yearBuilt: 1700,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('accepts minimal valid input', () => {
|
|
const result = createYachtSchema.safeParse({
|
|
name: 'Sea Breeze',
|
|
owner: { type: 'client', id: 'c1' },
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('transferOwnershipSchema', () => {
|
|
it('requires newOwner + effectiveDate', () => {
|
|
expect(transferOwnershipSchema.safeParse({}).success).toBe(false);
|
|
});
|
|
|
|
it('accepts valid input', () => {
|
|
const result = transferOwnershipSchema.safeParse({
|
|
newOwner: { type: 'company', id: 'co1' },
|
|
effectiveDate: new Date(),
|
|
transferReason: 'sale',
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Company schemas ──────────────────────────────────────────────────────────
|
|
|
|
describe('createCompanySchema', () => {
|
|
it('rejects empty name', () => {
|
|
const result = createCompanySchema.safeParse({ name: '' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid billingEmail', () => {
|
|
const result = createCompanySchema.safeParse({
|
|
name: 'Aegean Holdings',
|
|
billingEmail: 'not-an-email',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('accepts minimal valid input', () => {
|
|
const result = createCompanySchema.safeParse({ name: 'Aegean Holdings' });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('accepts full valid input', () => {
|
|
const result = createCompanySchema.safeParse({
|
|
name: 'Aegean Holdings',
|
|
legalName: 'Aegean Holdings Ltd.',
|
|
taxId: 'GR123456789',
|
|
registrationNumber: 'REG-001',
|
|
incorporationCountry: 'GR',
|
|
incorporationDate: '2010-04-15',
|
|
status: 'active',
|
|
billingEmail: 'billing@aegean.example',
|
|
notes: 'Longtime customer',
|
|
tagIds: ['tag-1', 'tag-2'],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Company membership schemas ──────────────────────────────────────────────
|
|
|
|
describe('addMembershipSchema', () => {
|
|
const validInput = {
|
|
clientId: 'client-uuid-1',
|
|
role: 'director' as const,
|
|
startDate: '2026-01-01',
|
|
};
|
|
|
|
it('rejects missing clientId', () => {
|
|
const result = addMembershipSchema.safeParse({
|
|
role: 'director',
|
|
startDate: '2026-01-01',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid role', () => {
|
|
const result = addMembershipSchema.safeParse({ ...validInput, role: 'janitor' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('accepts minimal valid input', () => {
|
|
const result = addMembershipSchema.safeParse(validInput);
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Reservation schemas ─────────────────────────────────────────────────────
|
|
|
|
describe('createPendingSchema', () => {
|
|
const validInput = {
|
|
berthId: 'berth-1',
|
|
clientId: 'client-1',
|
|
yachtId: 'yacht-1',
|
|
startDate: '2026-05-01',
|
|
};
|
|
|
|
it('rejects missing berthId', () => {
|
|
const result = createPendingSchema.safeParse({
|
|
clientId: validInput.clientId,
|
|
yachtId: validInput.yachtId,
|
|
startDate: validInput.startDate,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects missing clientId', () => {
|
|
const result = createPendingSchema.safeParse({
|
|
berthId: validInput.berthId,
|
|
yachtId: validInput.yachtId,
|
|
startDate: validInput.startDate,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects missing yachtId', () => {
|
|
const result = createPendingSchema.safeParse({
|
|
berthId: validInput.berthId,
|
|
clientId: validInput.clientId,
|
|
startDate: validInput.startDate,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('accepts minimal valid input with default tenureType', () => {
|
|
const result = createPendingSchema.safeParse(validInput);
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.tenureType).toBe('permanent');
|
|
}
|
|
});
|
|
});
|