feat(tenancies-p3): webhook auto-create on signed Reservation Agreement + first-insert flip
- berth-tenancies.service.ts: autoCreatePendingTenancies(portId, interestId, opts) loops over interest_berths WHERE is_in_eoi_bundle=true and mints ONE pending tenancy per in-bundle berth. Wrapped in pg_advisory_xact_lock per port + idempotent skip when a (pending|active) tenancy already exists for the berth (webhook retry-safe). Each insert audit-logged + emits berth_tenancy:created socket event. - createPending: same advisory-lock + tx pattern, additionally calls enableTenanciesModule(portId) so the FIRST manual tenancy in a port lazily flips tenancies_module_enabled=true (idempotent UPSERT, no-op on subsequent inserts). - handleDocumentCompleted: branch on reservation_agreement completion gates on isTenanciesModuleEnabled, then calls autoCreatePendingTenancies with the just-committed signedFileId. Per design §"When disabled": stage advance + reservationDocStatus flip still fire when the module is off; only the tenancy mint is skipped. - 5-case integration test covering bundle expansion, idempotent retry, empty-bundle no-op, missing-interest no-op, and the first-insert module-enable side effect. Verified: tsc clean, 1485/1485 vitest (5 new cases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1660,6 +1660,36 @@ export async function handleDocumentCompleted(eventData: { documentId: string; p
|
||||
void import('@/lib/services/berth-rules-engine').then(({ evaluateRule }) =>
|
||||
evaluateRule('contract_signed', doc.interestId!, doc.portId, systemMeta),
|
||||
);
|
||||
|
||||
// Tenancies P3 — auto-create pending tenancies (one per in-bundle berth)
|
||||
// when the module is enabled for this port. Gating is at the call site:
|
||||
// disabled module = stage + docStatus updates still fire, only the
|
||||
// tenancy mint is skipped (per docs/tenancies-design.md §"When disabled").
|
||||
void (async () => {
|
||||
try {
|
||||
const { isTenanciesModuleEnabled } =
|
||||
await import('@/lib/services/tenancies-module.service');
|
||||
if (!(await isTenanciesModuleEnabled(doc.portId))) return;
|
||||
const { autoCreatePendingTenancies } =
|
||||
await import('@/lib/services/berth-tenancies.service');
|
||||
// Re-read signedFileId from the post-commit row; the in-tx update
|
||||
// above sets it, but doc was loaded before completion.
|
||||
const fresh = await db.query.documents.findFirst({
|
||||
where: eq(documents.id, doc.id),
|
||||
columns: { signedFileId: true },
|
||||
});
|
||||
await autoCreatePendingTenancies(doc.portId, doc.interestId!, {
|
||||
signedAt: new Date(),
|
||||
sourceDocumentId: doc.id,
|
||||
signedFileId: fresh?.signedFileId ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err, documentId: doc.id, interestId: doc.interestId, portId: doc.portId },
|
||||
'autoCreatePendingTenancies failed during reservation_agreement completion',
|
||||
);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// Update interest if contract type. Outcome flip to 'won' is a separate
|
||||
|
||||
Reference in New Issue
Block a user