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:
@@ -21,7 +21,7 @@ import { and, eq, ne, sql } from 'drizzle-orm';
|
||||
import { db } from '@/lib/db';
|
||||
import type { Tx } from '@/lib/db/utils';
|
||||
import { clients } from '@/lib/db/schema/clients';
|
||||
import { interests, interestBerths } from '@/lib/db/schema/interests';
|
||||
import { interests } from '@/lib/db/schema/interests';
|
||||
import { berths } from '@/lib/db/schema/berths';
|
||||
import { yachts } from '@/lib/db/schema/yachts';
|
||||
import { portalUsers } from '@/lib/db/schema/portal';
|
||||
@@ -29,6 +29,7 @@ import { documents } from '@/lib/db/schema/documents';
|
||||
import { createAuditLog, type AuditMeta } from '@/lib/audit';
|
||||
import { activeInterestsWhere } from '@/lib/services/active-interest';
|
||||
import { transferOwnershipTx } from '@/lib/services/yachts.service';
|
||||
import { upsertInterestBerthTx } from '@/lib/services/interest-berths.service';
|
||||
import { ConflictError, NotFoundError } from '@/lib/errors';
|
||||
import type { ArchiveMetadata } from '@/lib/services/client-archive.service';
|
||||
|
||||
@@ -380,18 +381,17 @@ async function applyReversal(
|
||||
.limit(1);
|
||||
if (!iv || iv.archivedAt) break;
|
||||
|
||||
// Idempotent re-insert: the unique index on (interestId, berthId)
|
||||
// means a duplicate is a no-op via onConflictDoNothing.
|
||||
await tx
|
||||
.insert(interestBerths)
|
||||
.values({
|
||||
interestId,
|
||||
berthId: r.refId,
|
||||
isPrimary: false,
|
||||
isSpecificInterest: true,
|
||||
isInEoiBundle: false,
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
// Idempotent re-link via the canonical junction helper (audit L27):
|
||||
// routes through `upsertInterestBerthTx` so the cross-port guard runs
|
||||
// (the prior raw insert bypassed it) and the unique (interestId, berthId)
|
||||
// index keeps a duplicate a benign merge. This row is a non-primary
|
||||
// re-attach, so the primary↔bundle invariant doesn't force the bundle
|
||||
// flag on — it stays an EOI-only/legal link as before.
|
||||
await upsertInterestBerthTx(tx, interestId, r.refId, {
|
||||
isPrimary: false,
|
||||
isSpecificInterest: true,
|
||||
isInEoiBundle: false,
|
||||
});
|
||||
// Flip berth status back to under_offer so the public map reflects
|
||||
// the re-link. Only when berth is currently 'available' (sold
|
||||
// berths are immutable; under_offer to another client is handled
|
||||
|
||||
Reference in New Issue
Block a user