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; 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; } // 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; // 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>(), multiEntrySummary: jsonb("multi_entry_summary").$type(), multiEntryOrdering: jsonb("multi_entry_ordering").$type(), 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()), });