assessment-model/src/app/db/schema/bulk_address_uploads.ts
2026-06-08 09:38:48 +00:00

68 lines
2.9 KiB
TypeScript

import { pgTable, uuid, text, timestamp, jsonb, boolean } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
// Shape of the multi_entry_summary jsonb (ADR-0004). Co-located with the column
// so the schema is self-contained; the detection logic in
// src/lib/bulkUpload/multiEntry.ts imports these.
export interface MultiEntryEntry {
raw: string;
description: string;
}
export interface MultiEntryColumn {
field: string;
header: string;
entries: MultiEntryEntry[];
}
export interface MultiEntrySample {
address: string;
count: number;
columns: MultiEntryColumn[];
}
export interface MultiEntrySummary {
multiValuedFields: string[];
countDistribution: Record<string, number>;
largestCount: number;
// Step 1 (verify) sample: the largest-count row when multi-entry, else the
// first classified row. `null` ⇒ nothing to verify.
sample: MultiEntrySample | null;
// Step 2 (order): one sample per distinct entry-count ≥ 2 present in the file,
// keyed by count. Each count needs its OWN confirmed permutation — a smaller
// count's ordering can't be derived from a larger one (ADR-0004, amended
// 2026-06-05). Absent on uploads detected before that amendment.
samplesByCount?: Record<string, MultiEntrySample>;
}
// User-confirmed building-part ordering (ADR-0004, amended 2026-06-05). Keyed by
// entry-count: a permutation is captured for EVERY distinct count ≥ 2 in the
// file (the v2 fact layer can't derive one count's order from another).
// permutations[count][k] = the 0-based file position holding building part k,
// where 0 = Main building, 1..N-1 = Extension 1..N-1.
// e.g. { "2": [1, 0] } => for 2-part rows the main building is file position 1.
export interface MultiEntryOrdering {
permutations: Record<string, number[]>;
// True once EVERY detected count ≥ 2 has a permutation; gates Finalise when the
// upload is multi-entry.
confirmed: boolean;
}
export const bulkAddressUploads = pgTable("bulk_address_uploads", {
id: uuid("id").defaultRandom().primaryKey(),
portfolioId: text("portfolio_id").notNull(),
userId: text("user_id").notNull(),
s3Bucket: text("s3_bucket").notNull(),
s3Key: text("s3_key").notNull(),
filename: text("filename").notNull(),
status: text("status").notNull().default("ready_for_processing"),
sourceHeaders: text("source_headers").array().notNull().default(sql`'{}'`),
columnMapping: jsonb("column_mapping").$type<Record<string, string>>(),
multiEntrySummary: jsonb("multi_entry_summary").$type<MultiEntrySummary>(),
multiEntryOrdering: jsonb("multi_entry_ordering").$type<MultiEntryOrdering>(),
verifyAck: boolean("verify_ack").notNull().default(false),
taskId: uuid("task_id"),
combinedOutputS3Uri: text("combined_output_s3_uri"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
});