feat(tenancies-p4): public-map status flip via active permanent tenancy
- derivePublicStatus gains optional hasActivePermanentTenancy flag; precedence updated to "sold > under_offer > available" where Sold can come from EITHER berths.status='sold' (admin set) OR an active permanent-class tenancy (only when module enabled). - Permanent-class tenure types defined in one place (isPermanentTenureType): permanent | fee_simple | strata_lot. Seasonal / fixed_term tenancies do NOT flip — they fall through to the existing under_offer / available precedence. - /api/public/berths (list) + /api/public/berths/[mooringNumber] (single) both gate the lookup on isTenanciesModuleEnabled(portId). Disabled module = lookup skipped entirely, preserving pre-module behaviour for ports that haven't opted in. - 8 new unit tests covering: flip from available, flip from under_offer, explicit sold idempotency, false-flag fallthrough, default-omit pre- module behaviour, permanent-class membership for each tenure type, and null/undefined/unknown rejection. Verified: tsc clean, 1493/1493 vitest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,11 @@ import { db } from '@/lib/db';
|
||||
import { ports } from '@/lib/db/schema/ports';
|
||||
import { berths, berthMapData } from '@/lib/db/schema/berths';
|
||||
import { interestBerths, interests } from '@/lib/db/schema/interests';
|
||||
import { berthTenancies } from '@/lib/db/schema/tenancies';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { toPublicBerth } from '@/lib/services/public-berths';
|
||||
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
|
||||
import { isPermanentTenureType, toPublicBerth } from '@/lib/services/public-berths';
|
||||
|
||||
/**
|
||||
* GET /api/public/berths/[mooringNumber]
|
||||
@@ -105,7 +107,32 @@ export async function GET(
|
||||
.limit(1),
|
||||
]);
|
||||
|
||||
const out = toPublicBerth(berth, mapData[0] ?? null, specificInterestRows.length > 0);
|
||||
// Tenancies-module status flip: an active permanent-class tenancy
|
||||
// pushes the berth to "Sold" when the module is enabled for this port.
|
||||
let hasActivePermanentTenancy = false;
|
||||
if (await isTenanciesModuleEnabled(port.id)) {
|
||||
const activeTenancy = await db
|
||||
.select({ tenureType: berthTenancies.tenureType })
|
||||
.from(berthTenancies)
|
||||
.where(
|
||||
and(
|
||||
eq(berthTenancies.portId, port.id),
|
||||
eq(berthTenancies.berthId, berth.id),
|
||||
eq(berthTenancies.status, 'active'),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
hasActivePermanentTenancy = activeTenancy.some((row) =>
|
||||
isPermanentTenureType(row.tenureType),
|
||||
);
|
||||
}
|
||||
|
||||
const out = toPublicBerth(
|
||||
berth,
|
||||
mapData[0] ?? null,
|
||||
specificInterestRows.length > 0,
|
||||
hasActivePermanentTenancy,
|
||||
);
|
||||
|
||||
if (out.Status !== 'Available' && out.Status !== 'Under Offer' && out.Status !== 'Sold') {
|
||||
logger.error({ berthId: berth.id, status: out.Status }, 'Public berth status out of range');
|
||||
|
||||
@@ -5,9 +5,15 @@ import { db } from '@/lib/db';
|
||||
import { ports } from '@/lib/db/schema/ports';
|
||||
import { berths, berthMapData } from '@/lib/db/schema/berths';
|
||||
import { interestBerths, interests } from '@/lib/db/schema/interests';
|
||||
import { berthTenancies } from '@/lib/db/schema/tenancies';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { toPublicBerth, type PublicBerth } from '@/lib/services/public-berths';
|
||||
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
|
||||
import {
|
||||
isPermanentTenureType,
|
||||
toPublicBerth,
|
||||
type PublicBerth,
|
||||
} from '@/lib/services/public-berths';
|
||||
|
||||
/**
|
||||
* GET /api/public/berths
|
||||
@@ -109,8 +115,35 @@ export async function GET(request: Request): Promise<Response> {
|
||||
const mapByBerth = new Map(mapRows.map((m) => [m.berthId, m]));
|
||||
const specificInterestSet = new Set(specificInterestRows.map((r) => r.berthId));
|
||||
|
||||
// Tenancies module: per-port flag. When enabled, an active tenancy
|
||||
// with a permanent-class tenure type pushes a berth to "Sold" in the
|
||||
// public feed (per docs/tenancies-design.md §"Public map status flip").
|
||||
// When disabled, the lookup is skipped entirely — preserves the
|
||||
// pre-module behaviour for ports that haven't opted in.
|
||||
const permanentTenancyBerthIds = new Set<string>();
|
||||
if (await isTenanciesModuleEnabled(port.id)) {
|
||||
const activeTenancyRows = await db
|
||||
.select({ berthId: berthTenancies.berthId, tenureType: berthTenancies.tenureType })
|
||||
.from(berthTenancies)
|
||||
.where(
|
||||
and(
|
||||
eq(berthTenancies.portId, port.id),
|
||||
eq(berthTenancies.status, 'active'),
|
||||
inArray(berthTenancies.berthId, berthIds),
|
||||
),
|
||||
);
|
||||
for (const row of activeTenancyRows) {
|
||||
if (isPermanentTenureType(row.tenureType)) permanentTenancyBerthIds.add(row.berthId);
|
||||
}
|
||||
}
|
||||
|
||||
const list = berthRows.map((b) =>
|
||||
toPublicBerth(b, mapByBerth.get(b.id) ?? null, specificInterestSet.has(b.id)),
|
||||
toPublicBerth(
|
||||
b,
|
||||
mapByBerth.get(b.id) ?? null,
|
||||
specificInterestSet.has(b.id),
|
||||
permanentTenancyBerthIds.has(b.id),
|
||||
),
|
||||
);
|
||||
|
||||
// Validate the response enum before returning - any unknown status
|
||||
|
||||
Reference in New Issue
Block a user