feat(schema): berths.archived_at + clients.source_inquiry_id + email_bounces

Step 3 schema additions per PRE-DEPLOY-PLAN § 1.4.

berths.archived_at (+ archived_by, archive_reason) — soft-delete column
so retired moorings can be hidden from the public feed and admin lists
without losing historical interest joins. Partial index `idx_berths_active`
on (port_id) WHERE archived_at IS NULL keeps the active-only list path
fast. Already wired:
- /api/public/berths and /api/public/berths/[mooringNumber] now filter
  out archived rows.
- berths.service.listBerths defaults to active-only with an
  ?includeArchived=true escape hatch for the archive bin.

clients.source_inquiry_id — text column with ON DELETE SET NULL FK to
website_submissions(id). Preserves the linkage from a website inquiry
to the client that came out of the "Convert to client" triage flow
(P-4.5). Drives the conversion-funnel-by-source chart (Step 6). The
Drizzle column ships without `.references()` to avoid the cross-file
circular import; the FK lives in the migration SQL.

email_bounces table — bounce-monitoring storage. The DSN poller worker
(forthcoming, depends on this table existing) writes one row per parsed
bounce; consumers join via (original_send_type, original_send_id).
Three secondary indexes cover the expected access patterns (port +
recent bounces; lookup by bounced address; lookup by original send).

Schema additions plus the migration SQL are ready for `pnpm db:push`
(or the migration runner once its journal is backfilled — separate
concern, journal currently stops at 0042 despite migrations through
0065 existing on disk).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 15:33:20 +02:00
parent fd2c7d6b12
commit e933e32dbd
9 changed files with 153 additions and 10 deletions

View File

@@ -69,7 +69,13 @@ export async function GET(
const [berth] = await db
.select()
.from(berths)
.where(and(eq(berths.portId, port.id), eq(berths.mooringNumber, mooringNumber)))
.where(
and(
eq(berths.portId, port.id),
eq(berths.mooringNumber, mooringNumber),
isNull(berths.archivedAt),
),
)
.limit(1);
if (!berth) {

View File

@@ -72,13 +72,12 @@ export async function GET(request: Request): Promise<Response> {
);
}
// 1. Active berths for the port (archived would be an explicit field
// once we add one - today we don't have an archived_at on berths,
// so we surface every row except those marked status='sold' on
// request? No: §4.5 says "filters out berths archived in CRM".
// The current schema has no archived flag for berths, so this is
// a no-op today; future archive flag plugs in here.
const berthRows = await db.select().from(berths).where(eq(berths.portId, port.id));
// 1. Active berths for the port — retired moorings are hidden via
// the archived_at soft-delete column (migration 0065).
const berthRows = await db
.select()
.from(berths)
.where(and(eq(berths.portId, port.id), isNull(berths.archivedAt)));
if (berthRows.length === 0) {
return jsonResponse({ list: [], pageInfo: emptyPageInfo() });