feat(recommendations): read yacht dimensions from yachts table
Switch recommendations engine to read yacht dimensions (lengthFt, widthFt, draftFt) from the yachts table via interest.yachtId instead of from the deprecated client fields. Cross-tenant safety is maintained by scoping the yacht lookup to the same portId. Falls back gracefully to null dimensions when interest.yachtId is null or yacht is not found. - Modified: src/lib/services/recommendations.ts — replaced client.yacht*Ft fields with yacht table lookups via interest.yachtId - Created: tests/integration/recommendations-yacht-dims.test.ts — 4 tests covering happy path, null-yacht fallback, cross-tenant safety, and dimension-based scoring All 594 tests passing, tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { and, eq } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { interests } from '@/lib/db/schema/interests';
|
||||
import { clients } from '@/lib/db/schema/clients';
|
||||
import { yachts } from '@/lib/db/schema/yachts';
|
||||
import { berths, berthRecommendations } from '@/lib/db/schema/berths';
|
||||
import { NotFoundError } from '@/lib/errors';
|
||||
import { createAuditLog } from '@/lib/audit';
|
||||
@@ -29,7 +29,10 @@ function scoreBerth(
|
||||
const berthLen = parseFloat(berth.lengthFt);
|
||||
if (berthLen >= yachtLengthFt) {
|
||||
// Prefer berths that are not too oversized (within 20% extra is ideal)
|
||||
const score = berthLen <= yachtLengthFt * 1.2 ? 100 : Math.max(50, 100 - (berthLen / yachtLengthFt - 1.2) * 100);
|
||||
const score =
|
||||
berthLen <= yachtLengthFt * 1.2
|
||||
? 100
|
||||
: Math.max(50, 100 - (berthLen / yachtLengthFt - 1.2) * 100);
|
||||
reasons['length_fit'] = Math.round(score);
|
||||
weights.push(score);
|
||||
} else {
|
||||
@@ -42,7 +45,10 @@ function scoreBerth(
|
||||
if (yachtWidthFt && berth.widthFt) {
|
||||
const berthWidth = parseFloat(berth.widthFt);
|
||||
if (berthWidth >= yachtWidthFt) {
|
||||
const score = berthWidth <= yachtWidthFt * 1.3 ? 100 : Math.max(40, 100 - (berthWidth / yachtWidthFt - 1.3) * 80);
|
||||
const score =
|
||||
berthWidth <= yachtWidthFt * 1.3
|
||||
? 100
|
||||
: Math.max(40, 100 - (berthWidth / yachtWidthFt - 1.3) * 80);
|
||||
reasons['beam_fit'] = Math.round(score);
|
||||
weights.push(score);
|
||||
} else {
|
||||
@@ -73,23 +79,27 @@ function scoreBerth(
|
||||
|
||||
// ─── Generate Recommendations ─────────────────────────────────────────────────
|
||||
|
||||
export async function generateRecommendations(
|
||||
interestId: string,
|
||||
portId: string,
|
||||
meta: AuditMeta,
|
||||
) {
|
||||
export async function generateRecommendations(interestId: string, portId: string, meta: AuditMeta) {
|
||||
const interest = await db.query.interests.findFirst({
|
||||
where: and(eq(interests.id, interestId), eq(interests.portId, portId)),
|
||||
});
|
||||
if (!interest) throw new NotFoundError('Interest');
|
||||
|
||||
const client = await db.query.clients.findFirst({
|
||||
where: eq(clients.id, interest.clientId),
|
||||
});
|
||||
// Read yacht dimensions from the yachts table via interest.yachtId (PR 9)
|
||||
let yachtLengthFt: number | null = null;
|
||||
let yachtWidthFt: number | null = null;
|
||||
let yachtDraftFt: number | null = null;
|
||||
|
||||
const yachtLengthFt = client?.yachtLengthFt ? parseFloat(client.yachtLengthFt) : null;
|
||||
const yachtWidthFt = client?.yachtWidthFt ? parseFloat(client.yachtWidthFt) : null;
|
||||
const yachtDraftFt = client?.yachtDraftFt ? parseFloat(client.yachtDraftFt) : null;
|
||||
if (interest.yachtId) {
|
||||
const yacht = await db.query.yachts.findFirst({
|
||||
where: and(eq(yachts.id, interest.yachtId), eq(yachts.portId, portId)),
|
||||
});
|
||||
if (yacht) {
|
||||
yachtLengthFt = yacht.lengthFt ? parseFloat(yacht.lengthFt) : null;
|
||||
yachtWidthFt = yacht.widthFt ? parseFloat(yacht.widthFt) : null;
|
||||
yachtDraftFt = yacht.draftFt ? parseFloat(yacht.draftFt) : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all available berths for the port
|
||||
const availableBerths = await db
|
||||
@@ -111,10 +121,7 @@ export async function generateRecommendations(
|
||||
await db
|
||||
.delete(berthRecommendations)
|
||||
.where(
|
||||
and(
|
||||
eq(berthRecommendations.interestId, interestId),
|
||||
eq(berthRecommendations.source, 'ai'),
|
||||
),
|
||||
and(eq(berthRecommendations.interestId, interestId), eq(berthRecommendations.source, 'ai')),
|
||||
);
|
||||
|
||||
// Insert new recommendations
|
||||
|
||||
Reference in New Issue
Block a user