fix(audit): berth rules/recommender — M4 (bundle-wide status), M5 (berth_unlinked target), M20/L27 (interest_berths invariant + cross-port guard), L3 (recommender stage-scale), L4 (dead branch)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:59:12 +02:00
parent 4084029962
commit 70bf26aea1
5 changed files with 163 additions and 58 deletions

View File

@@ -21,7 +21,8 @@ import { and, eq, isNull, sql } from 'drizzle-orm';
import { db } from '@/lib/db';
import { withTransaction } from '@/lib/db/utils';
import { interests, interestBerths } from '@/lib/db/schema/interests';
import { interests } from '@/lib/db/schema/interests';
import { upsertInterestBerthTx } from '@/lib/services/interest-berths.service';
import { clients, clientContacts, clientAddresses } from '@/lib/db/schema/clients';
import { berths } from '@/lib/db/schema/berths';
import { yachts, yachtOwnershipHistory } from '@/lib/db/schema/yachts';
@@ -235,12 +236,16 @@ export async function createPublicInterest(
.returning();
if (berthId) {
await tx.insert(interestBerths).values({
interestId: newInterest!.id,
berthId,
// Route through the canonical junction helper (audit M20 + L27) rather
// than a raw insert. The helper:
// • forces `is_in_eoi_bundle=true` for the primary berth, so the
// website-originated interest doesn't violate the primary↔bundle
// invariant that migration 0083 had to repair (audit M20);
// • applies the cross-port guard the raw insert bypassed (audit L27).
await upsertInterestBerthTx(tx, newInterest!.id, berthId, {
isPrimary: true,
isSpecificInterest: true,
isInEoiBundle: false,
addedBy: 'public-submission',
});
}