Updated tenancy-auto-create integration test to assert M29 (explicit disable respected) instead of the old re-enable behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
176 lines
6.0 KiB
TypeScript
176 lines
6.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { and, eq } from 'drizzle-orm';
|
|
|
|
import { db } from '@/lib/db';
|
|
import { interests, interestBerths } from '@/lib/db/schema/interests';
|
|
import { berthTenancies } from '@/lib/db/schema/tenancies';
|
|
import { systemSettings } from '@/lib/db/schema/system';
|
|
import { autoCreatePendingTenancies } from '@/lib/services/berth-tenancies.service';
|
|
import {
|
|
enableTenanciesModule,
|
|
isTenanciesModuleEnabled,
|
|
} from '@/lib/services/tenancies-module.service';
|
|
import { makeBerth, makeClient, makePort, makeYacht } from '../helpers/factories';
|
|
|
|
async function seedInterestWithBundleBerths(
|
|
portId: string,
|
|
bundleCount: number,
|
|
): Promise<{ interestId: string; clientId: string; yachtId: string; berthIds: string[] }> {
|
|
const client = await makeClient({ portId });
|
|
const yacht = await makeYacht({ portId, ownerType: 'client', ownerId: client.id });
|
|
const [interest] = await db
|
|
.insert(interests)
|
|
.values({
|
|
portId,
|
|
clientId: client.id,
|
|
yachtId: yacht.id,
|
|
pipelineStage: 'reservation',
|
|
outcome: 'open',
|
|
})
|
|
.returning();
|
|
|
|
const berthIds: string[] = [];
|
|
for (let i = 0; i < bundleCount; i++) {
|
|
const b = await makeBerth({ portId });
|
|
berthIds.push(b.id);
|
|
await db.insert(interestBerths).values({
|
|
interestId: interest!.id,
|
|
berthId: b.id,
|
|
isInEoiBundle: true,
|
|
isPrimary: i === 0,
|
|
isSpecificInterest: true,
|
|
});
|
|
}
|
|
return { interestId: interest!.id, clientId: client.id, yachtId: yacht.id, berthIds };
|
|
}
|
|
|
|
async function disableModule(portId: string): Promise<void> {
|
|
await db
|
|
.insert(systemSettings)
|
|
.values({ key: 'tenancies_module_enabled', portId, value: false })
|
|
.onConflictDoUpdate({
|
|
target: [systemSettings.key, systemSettings.portId],
|
|
set: { value: false, updatedAt: new Date() },
|
|
});
|
|
}
|
|
|
|
describe('autoCreatePendingTenancies', () => {
|
|
let portId: string;
|
|
beforeEach(async () => {
|
|
const port = await makePort();
|
|
portId = port.id;
|
|
// Start each test with the module explicitly disabled so the lazy
|
|
// auto-enable path doesn't taint the next test's port.
|
|
await disableModule(portId);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Make sure no stale advisory locks from a failed test linger.
|
|
});
|
|
|
|
it('mints one pending tenancy per in-bundle berth', async () => {
|
|
await enableTenanciesModule(portId);
|
|
const { interestId, berthIds } = await seedInterestWithBundleBerths(portId, 3);
|
|
|
|
const result = await autoCreatePendingTenancies(portId, interestId, {
|
|
signedAt: new Date('2026-01-15'),
|
|
sourceDocumentId: 'doc-fixture-123',
|
|
signedFileId: null,
|
|
});
|
|
|
|
expect(result).toHaveLength(3);
|
|
expect(result.every((r) => r.status === 'pending')).toBe(true);
|
|
expect(result.every((r) => r.interestId === interestId)).toBe(true);
|
|
expect(result.every((r) => r.contractFileId === null)).toBe(true);
|
|
expect(result.map((r) => r.berthId).sort()).toEqual(berthIds.sort());
|
|
});
|
|
|
|
it('skips berths that already have a live tenancy (idempotent under retry)', async () => {
|
|
await enableTenanciesModule(portId);
|
|
const { interestId, berthIds } = await seedInterestWithBundleBerths(portId, 2);
|
|
|
|
const first = await autoCreatePendingTenancies(portId, interestId, {
|
|
signedAt: new Date(),
|
|
sourceDocumentId: 'doc-1',
|
|
signedFileId: null,
|
|
});
|
|
expect(first).toHaveLength(2);
|
|
|
|
const replay = await autoCreatePendingTenancies(portId, interestId, {
|
|
signedAt: new Date(),
|
|
sourceDocumentId: 'doc-1',
|
|
signedFileId: null,
|
|
});
|
|
expect(replay).toHaveLength(0);
|
|
|
|
const allRows = await db
|
|
.select()
|
|
.from(berthTenancies)
|
|
.where(and(eq(berthTenancies.portId, portId), eq(berthTenancies.interestId, interestId)));
|
|
expect(allRows).toHaveLength(2);
|
|
expect(allRows.map((r) => r.berthId).sort()).toEqual(berthIds.sort());
|
|
});
|
|
|
|
it('returns empty when the interest has no in-bundle berths', async () => {
|
|
await enableTenanciesModule(portId);
|
|
const client = await makeClient({ portId });
|
|
const yacht = await makeYacht({ portId, ownerType: 'client', ownerId: client.id });
|
|
const [interest] = await db
|
|
.insert(interests)
|
|
.values({
|
|
portId,
|
|
clientId: client.id,
|
|
yachtId: yacht.id,
|
|
pipelineStage: 'reservation',
|
|
outcome: 'open',
|
|
})
|
|
.returning();
|
|
|
|
const result = await autoCreatePendingTenancies(portId, interest!.id, {
|
|
signedAt: new Date(),
|
|
sourceDocumentId: 'doc-empty',
|
|
signedFileId: null,
|
|
});
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('does not mint when the interest is missing (deleted before webhook fires)', async () => {
|
|
await enableTenanciesModule(portId);
|
|
const result = await autoCreatePendingTenancies(portId, 'nonexistent-interest', {
|
|
signedAt: new Date(),
|
|
sourceDocumentId: 'doc-x',
|
|
signedFileId: null,
|
|
});
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('Tenancies module enable-on-createPending (audit M29: explicit-disable wins)', () => {
|
|
it('does NOT re-enable a module an admin explicitly disabled (M29)', async () => {
|
|
const port = await makePort();
|
|
const portId = port.id;
|
|
await disableModule(portId);
|
|
expect(await isTenanciesModuleEnabled(portId)).toBe(false);
|
|
|
|
const client = await makeClient({ portId });
|
|
const yacht = await makeYacht({ portId, ownerType: 'client', ownerId: client.id });
|
|
const berth = await makeBerth({ portId });
|
|
const { createPending } = await import('@/lib/services/berth-tenancies.service');
|
|
await createPending(
|
|
portId,
|
|
{
|
|
berthId: berth.id,
|
|
clientId: client.id,
|
|
yachtId: yacht.id,
|
|
startDate: new Date(),
|
|
},
|
|
{ userId: 'system', portId, ipAddress: '0.0.0.0', userAgent: 'test' },
|
|
);
|
|
|
|
// Audit M29: a manual createPending (or a Reservation-Agreement webhook)
|
|
// must respect an EXPLICIT `false` and not silently flip the module back
|
|
// on. The lazy auto-surface applies only when the setting is UNSET.
|
|
expect(await isTenanciesModuleEnabled(portId)).toBe(false);
|
|
});
|
|
});
|