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>
This commit is contained in:
26
src/app/api/v1/tenancies/[id]/renew/route.ts
Normal file
26
src/app/api/v1/tenancies/[id]/renew/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withAuth, withPermission, type RouteHandler } from '@/lib/api/helpers';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { renewTenancy } from '@/lib/services/berth-tenancies.service';
|
||||
import { assertTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
|
||||
import { renewTenancySchema } from '@/lib/validators/tenancies';
|
||||
|
||||
const renewHandler: RouteHandler = async (req, ctx, params) => {
|
||||
try {
|
||||
await assertTenanciesModuleEnabled(ctx.portId);
|
||||
const body = await parseBody(req, renewTenancySchema);
|
||||
const result = await renewTenancy(params.id!, ctx.portId, body, {
|
||||
userId: ctx.userId,
|
||||
portId: ctx.portId,
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
});
|
||||
return NextResponse.json({ data: result });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const POST = withAuth(withPermission('tenancies', 'manage', renewHandler));
|
||||
26
src/app/api/v1/tenancies/[id]/transfer/route.ts
Normal file
26
src/app/api/v1/tenancies/[id]/transfer/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { withAuth, withPermission, type RouteHandler } from '@/lib/api/helpers';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { transferTenancy } from '@/lib/services/berth-tenancies.service';
|
||||
import { assertTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
|
||||
import { transferTenancySchema } from '@/lib/validators/tenancies';
|
||||
|
||||
const transferHandler: RouteHandler = async (req, ctx, params) => {
|
||||
try {
|
||||
await assertTenanciesModuleEnabled(ctx.portId);
|
||||
const body = await parseBody(req, transferTenancySchema);
|
||||
const result = await transferTenancy(params.id!, ctx.portId, body, {
|
||||
userId: ctx.userId,
|
||||
portId: ctx.portId,
|
||||
ipAddress: ctx.ipAddress,
|
||||
userAgent: ctx.userAgent,
|
||||
});
|
||||
return NextResponse.json({ data: result });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const POST = withAuth(withPermission('tenancies', 'manage', transferHandler));
|
||||
Reference in New Issue
Block a user