From 18e5c124b0cb49fe58d0fc650ff9ad7f790acded Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Sun, 3 May 2026 14:50:01 +0200 Subject: [PATCH] feat(dedup): NocoDB migration script + tables (P3 dry-run) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lands the one-shot migration pipeline from the legacy NocoDB Interests base into the new client/interest schema. Dry-run mode is fully operational: pulls the live snapshot, runs the dedup library, and writes a CSV + Markdown report under .migration//. The --apply phase is stubbed for a follow-up PR per the design's P3 implementation sequence. Schema additions ================ - `client_merge_candidates` — pairs flagged by the background scoring job for the /admin/duplicates review queue. Status enum: pending / dismissed / merged. Unique-(portId, clientAId, clientBId) so the same pair can't surface twice. Empty until P2 lands the cron. - `migration_source_links` — idempotency ledger. Maps source-system rows (NocoDB Interest #624 → new client UUID) so re-running --apply against the same dry-run report skips already-imported entities. Both tables ship with the migration `0020_unusual_azazel.sql` — already applied to the local dev DB during this commit's preparation. Library ======= src/lib/dedup/nocodb-source.ts Read-only adapter for the legacy NocoDB v2 API. xc-token auth, auto-paginates until isLastPage, captures the table IDs from the 2026-05-03 audit. `fetchSnapshot()` pulls every relevant table in parallel into one in-memory object the transform layer consumes. src/lib/dedup/migration-transform.ts Pure function: NocoDB snapshot in, MigrationPlan out. Per row: - normalizes name / email / phone / country via the dedup library - parses the legacy DD-MM-YYYY / DD/MM/YYYY / ISO date formats - maps the 8-stage `Sales Process Level` enum to the new 9-stage pipelineStage - filters yacht-name placeholders ('TBC', 'Na', etc.) - merges Internal Notes + Extra Comments + Berth Size Desired into a single notes blob Then runs `findClientMatches` pairwise (with blocking) and union-finds clusters of rows whose score crosses the auto-link threshold (90). Lower-scoring pairs (50–89) become 'needs review'. Each cluster's "lead" row is picked by completeness score with recency tie-break. src/lib/dedup/migration-report.ts Writes three artifacts to .migration//: - report.csv — one row per planned op, RFC-4180 escaped - summary.md — human-skimmable overview - plan.json — full structured plan for the --apply phase CSV cells with comma / quote / newline are quoted; internal quotes are doubled. No external CSV dep. src/lib/dedup/phone-parse.ts Script-safe wrapper around libphonenumber-js's `core` entry that loads `metadata.min.json` directly. The default `index.cjs.js` bundled by libphonenumber hits a metadata-shape interop bug under Node 25 + tsx (`{ default }` wrapping); core+JSON sidesteps it. The dedup `normalizePhone` and `find-matches` both use this wrapper now so the same code path runs in vitest, Next.js, and the migration CLI without surprises. src/lib/dedup/normalize.ts Tightened country resolution: added Caribbean short-form aliases ('antigua' → AG, 'st kitts' → KN, etc.) and a city map covering the US locations seen in the NocoDB dump (Boston, Tampa, Fort Lauderdale, Port Jefferson, Nantucket). Also relaxed phone parsing to drop the `isValid()` strict check — the libphonenumber min build rejects many real NANP-territory numbers, and dedup only needs a canonical E.164 to compare. CLI === scripts/migrate-from-nocodb.ts pnpm tsx scripts/migrate-from-nocodb.ts --dry-run → Pulls the live NocoDB base (NOCODB_URL + NOCODB_TOKEN env vars), runs the transform, writes report. No DB writes. pnpm tsx scripts/migrate-from-nocodb.ts --apply --report .migration// → Stubbed; exits with `not yet implemented` and a pointer to the design doc. Apply phase ships in a follow-up. Tests ===== tests/unit/dedup/migration-transform.test.ts (7 cases) Fixture-based regression. A frozen 12-row NocoDB snapshot covers every duplicate pattern in the design (§1.2). The test asserts: - 12 input rows → 7 unique clients (cluster math is right) - Patterns A / B / C / E auto-link - Pattern F (Etiennette Clamouze) does NOT auto-link - Every interest preserved as its own row even when clients merge - 8-stage → 9-stage enum mapping is correct per spec - Multi-yacht merge (Constanzo CALYPSO + Costanzo GEMINI under one client) — the design's signature win - Output is deterministic (run twice, identical) Validation against real data ============================ Ran `pnpm tsx scripts/migrate-from-nocodb.ts --dry-run` against the live NocoDB. Result on 252 Interests rows: - 237 clients (15 merged into 13 clusters) - 252 interests (one per source row) - 406 contacts, 52 addresses - 13 auto-linked clusters (every confirmed cluster from §1.2 audit) - 3 pairs flagged for review (Camazou, Zasso, one new) - 1 phone placeholder flagged Total dedup test count: 57 (50 from P1 + 7 fixture tests). Lint: clean. Tsc: clean for new files. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + scripts/migrate-from-nocodb.ts | 144 + src/lib/db/migrations/0020_unusual_azazel.sql | 30 + src/lib/db/migrations/meta/0020_snapshot.json | 10482 ++++++++++++++++ src/lib/db/migrations/meta/_journal.json | 7 + src/lib/db/schema/clients.ts | 51 + src/lib/db/schema/index.ts | 3 + src/lib/db/schema/migration.ts | 48 + src/lib/dedup/find-matches.ts | 2 +- src/lib/dedup/migration-report.ts | 274 + src/lib/dedup/migration-transform.ts | 576 + src/lib/dedup/nocodb-source.ts | 152 + src/lib/dedup/normalize.ts | 33 +- src/lib/dedup/phone-parse.ts | 66 + tests/unit/dedup/migration-transform.test.ts | 213 + 15 files changed, 12079 insertions(+), 3 deletions(-) create mode 100644 scripts/migrate-from-nocodb.ts create mode 100644 src/lib/db/migrations/0020_unusual_azazel.sql create mode 100644 src/lib/db/migrations/meta/0020_snapshot.json create mode 100644 src/lib/db/schema/migration.ts create mode 100644 src/lib/dedup/migration-report.ts create mode 100644 src/lib/dedup/migration-transform.ts create mode 100644 src/lib/dedup/nocodb-source.ts create mode 100644 src/lib/dedup/phone-parse.ts create mode 100644 tests/unit/dedup/migration-transform.test.ts diff --git a/.gitignore b/.gitignore index 49f2dd2..b073cf7 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ docker-compose.override.yml # Mobile audit screenshots — generated locally, regenerable /.audit/ +.migration/ diff --git a/scripts/migrate-from-nocodb.ts b/scripts/migrate-from-nocodb.ts new file mode 100644 index 0000000..7c0bbeb --- /dev/null +++ b/scripts/migrate-from-nocodb.ts @@ -0,0 +1,144 @@ +/** + * One-shot migration: legacy NocoDB Interests → new client/interest split. + * + * Usage: + * + * pnpm tsx scripts/migrate-from-nocodb.ts --dry-run + * Pulls the live NocoDB base, runs the transform + dedup pipeline, + * writes a report to .migration//. NO database writes. + * + * pnpm tsx scripts/migrate-from-nocodb.ts --dry-run --port-slug harbor-royale + * Same, but tags the planned writes with the named port (matters for + * the apply phase — every client/interest belongs to one port). + * + * pnpm tsx scripts/migrate-from-nocodb.ts --apply --report .migration// + * [Not yet implemented — apply phase comes in a follow-up PR.] + * + * Design reference: docs/superpowers/specs/2026-05-03-dedup-and-migration-design.md §9. + */ + +import 'dotenv/config'; + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { fetchSnapshot, loadNocoDbConfig } from '@/lib/dedup/nocodb-source'; +import { transformSnapshot } from '@/lib/dedup/migration-transform'; +import { resolveReportPaths, writeReport } from '@/lib/dedup/migration-report'; + +interface CliArgs { + dryRun: boolean; + apply: boolean; + portSlug: string | null; + reportDir: string | null; +} + +function parseArgs(argv: string[]): CliArgs { + const args: CliArgs = { + dryRun: false, + apply: false, + portSlug: null, + reportDir: null, + }; + for (let i = 0; i < argv.length; i += 1) { + const a = argv[i]!; + if (a === '--dry-run') args.dryRun = true; + else if (a === '--apply') args.apply = true; + else if (a === '--port-slug') args.portSlug = argv[++i] ?? null; + else if (a === '--report') args.reportDir = argv[++i] ?? null; + else if (a === '-h' || a === '--help') { + printHelp(); + process.exit(0); + } else { + console.error(`Unknown argument: ${a}`); + printHelp(); + process.exit(1); + } + } + return args; +} + +function printHelp(): void { + console.log(`Usage: + pnpm tsx scripts/migrate-from-nocodb.ts --dry-run [--port-slug ] + Pulls NocoDB → transforms → writes report to .migration//. + No database writes. + + pnpm tsx scripts/migrate-from-nocodb.ts --apply --report .migration// + Apply phase. (Not yet implemented.) + +Flags: + --dry-run Read NocoDB, write report only. + --apply Actually write to the new DB. (Not yet supported.) + --port-slug Port slug to attach to all imported entities. + Defaults to the first available port if omitted. + --report Path to a previously-generated report dir + (only used by --apply). + -h, --help Show this help. +`); +} + +async function main(): Promise { + const args = parseArgs(process.argv.slice(2)); + + if (!args.dryRun && !args.apply) { + console.error('Must specify --dry-run or --apply'); + printHelp(); + process.exit(1); + } + + if (args.apply) { + console.error('--apply is not yet implemented in this version. P3 ships dry-run first.'); + console.error('See docs/superpowers/specs/2026-05-03-dedup-and-migration-design.md §9.2.'); + process.exit(2); + } + + // ── Dry-run path ─────────────────────────────────────────────────────────── + + console.log('[migrate] Loading NocoDB config…'); + const config = loadNocoDbConfig(); + console.log(`[migrate] Source: ${config.url}`); + + console.log('[migrate] Fetching snapshot from NocoDB…'); + const start = Date.now(); + const snapshot = await fetchSnapshot(config); + const elapsed = ((Date.now() - start) / 1000).toFixed(1); + console.log( + `[migrate] Snapshot fetched in ${elapsed}s — ${snapshot.interests.length} interests, ${snapshot.residentialInterests.length} residential, ${snapshot.berths.length} berths.`, + ); + + console.log('[migrate] Running transform + dedup pipeline…'); + const plan = transformSnapshot(snapshot); + + // Resolve output paths relative to the worktree root (the script itself + // lives in scripts/; we want the .migration dir at the repo root). + const scriptDir = path.dirname(fileURLToPath(import.meta.url)); + const repoRoot = path.resolve(scriptDir, '..'); + const generatedAt = new Date().toISOString(); + const paths = resolveReportPaths(repoRoot); + + console.log(`[migrate] Writing report to ${paths.rootDir}…`); + await writeReport(paths, plan, generatedAt); + + // ── Console summary ────────────────────────────────────────────────────── + const s = plan.stats; + console.log(''); + console.log('=== Migration Plan Summary ==='); + console.log( + ` Input: ${s.inputInterestRows} interests, ${s.inputResidentialRows} residential interests`, + ); + console.log(` Output: ${s.outputClients} clients, ${s.outputInterests} interests`); + console.log(` ${s.outputContacts} contacts, ${s.outputAddresses} addresses`); + console.log( + ` Dedup: ${s.autoLinkedClusters} auto-linked clusters, ${s.needsReviewPairs} pairs flagged for review`, + ); + console.log(` Quality: ${s.flaggedRows} rows flagged (see report.csv)`); + console.log(''); + console.log(` Full report: ${paths.summaryPath}`); + console.log(''); +} + +main().catch((err) => { + console.error('[migrate] Fatal error:', err); + process.exit(1); +}); diff --git a/src/lib/db/migrations/0020_unusual_azazel.sql b/src/lib/db/migrations/0020_unusual_azazel.sql new file mode 100644 index 0000000..aab09cb --- /dev/null +++ b/src/lib/db/migrations/0020_unusual_azazel.sql @@ -0,0 +1,30 @@ +CREATE TABLE "client_merge_candidates" ( + "id" text PRIMARY KEY NOT NULL, + "port_id" text NOT NULL, + "client_a_id" text NOT NULL, + "client_b_id" text NOT NULL, + "score" integer NOT NULL, + "reasons" jsonb NOT NULL, + "status" text DEFAULT 'pending' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "resolved_at" timestamp with time zone, + "resolved_by" text +); +--> statement-breakpoint +CREATE TABLE "migration_source_links" ( + "id" text PRIMARY KEY NOT NULL, + "source_system" text NOT NULL, + "source_id" text NOT NULL, + "target_entity_type" text NOT NULL, + "target_entity_id" text NOT NULL, + "applied_id" text NOT NULL, + "applied_by" text, + "applied_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "client_merge_candidates" ADD CONSTRAINT "client_merge_candidates_port_id_ports_id_fk" FOREIGN KEY ("port_id") REFERENCES "public"."ports"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "client_merge_candidates" ADD CONSTRAINT "client_merge_candidates_client_a_id_clients_id_fk" FOREIGN KEY ("client_a_id") REFERENCES "public"."clients"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "client_merge_candidates" ADD CONSTRAINT "client_merge_candidates_client_b_id_clients_id_fk" FOREIGN KEY ("client_b_id") REFERENCES "public"."clients"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_cmc_port_status" ON "client_merge_candidates" USING btree ("port_id","status");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_cmc_pair" ON "client_merge_candidates" USING btree ("port_id","client_a_id","client_b_id");--> statement-breakpoint +CREATE UNIQUE INDEX "idx_msl_source_target" ON "migration_source_links" USING btree ("source_system","source_id","target_entity_type"); \ No newline at end of file diff --git a/src/lib/db/migrations/meta/0020_snapshot.json b/src/lib/db/migrations/meta/0020_snapshot.json new file mode 100644 index 0000000..d0017dc --- /dev/null +++ b/src/lib/db/migrations/meta/0020_snapshot.json @@ -0,0 +1,10482 @@ +{ + "id": "e9d830fc-ec81-42ab-bea6-232dd99d20d1", + "prevId": "6326a9a7-0b30-4647-bf86-b3d79e6a08bf", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_usage_ledger": { + "name": "ai_usage_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ai_usage_port_created": { + "name": "idx_ai_usage_port_created", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_port_feature_created": { + "name": "idx_ai_usage_port_feature_created", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_usage_ledger_port_id_ports_id_fk": { + "name": "ai_usage_ledger_port_id_ports_id_fk", + "tableFrom": "ai_usage_ledger", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_usage_ledger_user_id_user_id_fk": { + "name": "ai_usage_ledger_user_id_user_id_fk", + "tableFrom": "ai_usage_ledger", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.berth_maintenance_log": { + "name": "berth_maintenance_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "berth_id": { + "name": "berth_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "cost_currency": { + "name": "cost_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'USD'" + }, + "responsible_party": { + "name": "responsible_party", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "performed_date": { + "name": "performed_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "photo_file_ids": { + "name": "photo_file_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_bml_berth": { + "name": "idx_bml_berth", + "columns": [ + { + "expression": "berth_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bml_port": { + "name": "idx_bml_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "berth_maintenance_log_berth_id_berths_id_fk": { + "name": "berth_maintenance_log_berth_id_berths_id_fk", + "tableFrom": "berth_maintenance_log", + "tableTo": "berths", + "columnsFrom": ["berth_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "berth_maintenance_log_port_id_ports_id_fk": { + "name": "berth_maintenance_log_port_id_ports_id_fk", + "tableFrom": "berth_maintenance_log", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.berth_map_data": { + "name": "berth_map_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "berth_id": { + "name": "berth_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "svg_path": { + "name": "svg_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "x": { + "name": "x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "y": { + "name": "y", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "transform": { + "name": "transform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "extra_data": { + "name": "extra_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "berth_map_data_berth_id_idx": { + "name": "berth_map_data_berth_id_idx", + "columns": [ + { + "expression": "berth_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "berth_map_data_berth_id_berths_id_fk": { + "name": "berth_map_data_berth_id_berths_id_fk", + "tableFrom": "berth_map_data", + "tableTo": "berths", + "columnsFrom": ["berth_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "berth_map_data_berth_id_unique": { + "name": "berth_map_data_berth_id_unique", + "nullsNotDistinct": false, + "columns": ["berth_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.berth_recommendations": { + "name": "berth_recommendations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "interest_id": { + "name": "interest_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "berth_id": { + "name": "berth_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_score": { + "name": "match_score", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "match_reasons": { + "name": "match_reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ai'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "berth_rec_interest_berth_idx": { + "name": "berth_rec_interest_berth_idx", + "columns": [ + { + "expression": "interest_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "berth_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_br_interest": { + "name": "idx_br_interest", + "columns": [ + { + "expression": "interest_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "berth_recommendations_berth_id_berths_id_fk": { + "name": "berth_recommendations_berth_id_berths_id_fk", + "tableFrom": "berth_recommendations", + "tableTo": "berths", + "columnsFrom": ["berth_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.berth_tags": { + "name": "berth_tags", + "schema": "", + "columns": { + "berth_id": { + "name": "berth_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "berth_tags_berth_id_berths_id_fk": { + "name": "berth_tags_berth_id_berths_id_fk", + "tableFrom": "berth_tags", + "tableTo": "berths", + "columnsFrom": ["berth_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "berth_tags_berth_id_tag_id_pk": { + "name": "berth_tags_berth_id_tag_id_pk", + "columns": ["berth_id", "tag_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.berth_waiting_list": { + "name": "berth_waiting_list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "berth_id": { + "name": "berth_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "yacht_id": { + "name": "yacht_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "notify_pref": { + "name": "notify_pref", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'email'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "berth_waiting_list_berth_client_idx": { + "name": "berth_waiting_list_berth_client_idx", + "columns": [ + { + "expression": "berth_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bwl_berth": { + "name": "idx_bwl_berth", + "columns": [ + { + "expression": "berth_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "berth_waiting_list_berth_id_berths_id_fk": { + "name": "berth_waiting_list_berth_id_berths_id_fk", + "tableFrom": "berth_waiting_list", + "tableTo": "berths", + "columnsFrom": ["berth_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "berth_waiting_list_client_id_clients_id_fk": { + "name": "berth_waiting_list_client_id_clients_id_fk", + "tableFrom": "berth_waiting_list", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.berths": { + "name": "berths", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mooring_number": { + "name": "mooring_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "area": { + "name": "area", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "length_ft": { + "name": "length_ft", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width_ft": { + "name": "width_ft", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "draft_ft": { + "name": "draft_ft", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "length_m": { + "name": "length_m", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width_m": { + "name": "width_m", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "draft_m": { + "name": "draft_m", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width_is_minimum": { + "name": "width_is_minimum", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "nominal_boat_size": { + "name": "nominal_boat_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nominal_boat_size_m": { + "name": "nominal_boat_size_m", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "water_depth": { + "name": "water_depth", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "water_depth_m": { + "name": "water_depth_m", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "water_depth_is_minimum": { + "name": "water_depth_is_minimum", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "side_pontoon": { + "name": "side_pontoon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "power_capacity": { + "name": "power_capacity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "voltage": { + "name": "voltage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mooring_type": { + "name": "mooring_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleat_type": { + "name": "cleat_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleat_capacity": { + "name": "cleat_capacity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bollard_type": { + "name": "bollard_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bollard_capacity": { + "name": "bollard_capacity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access": { + "name": "access", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "price_currency": { + "name": "price_currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "bow_facing": { + "name": "bow_facing", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "berth_approved": { + "name": "berth_approved", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tenure_type": { + "name": "tenure_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'permanent'" + }, + "tenure_years": { + "name": "tenure_years", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tenure_start_date": { + "name": "tenure_start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "tenure_end_date": { + "name": "tenure_end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status_last_changed_by": { + "name": "status_last_changed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_last_changed_reason": { + "name": "status_last_changed_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_last_modified": { + "name": "status_last_modified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_berths_port": { + "name": "idx_berths_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_berths_status": { + "name": "idx_berths_status", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_berths_area": { + "name": "idx_berths_area", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "area", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_berths_mooring": { + "name": "idx_berths_mooring", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mooring_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "berths_port_id_ports_id_fk": { + "name": "berths_port_id_ports_id_fk", + "tableFrom": "berths", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_addresses": { + "name": "client_addresses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Primary'" + }, + "street_address": { + "name": "street_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subdivision_iso": { + "name": "subdivision_iso", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_iso": { + "name": "country_iso", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ca_client": { + "name": "idx_ca_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ca_port": { + "name": "idx_ca_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ca_primary": { + "name": "idx_ca_primary", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"client_addresses\".\"is_primary\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "client_addresses_client_id_clients_id_fk": { + "name": "client_addresses_client_id_clients_id_fk", + "tableFrom": "client_addresses", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "client_addresses_port_id_ports_id_fk": { + "name": "client_addresses_port_id_ports_id_fk", + "tableFrom": "client_addresses", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_contacts": { + "name": "client_contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_e164": { + "name": "value_e164", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value_country": { + "name": "value_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cc_client": { + "name": "idx_cc_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_email": { + "name": "idx_cc_email", + "columns": [ + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"client_contacts\".\"channel\" = 'email'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_phone": { + "name": "idx_cc_phone", + "columns": [ + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"client_contacts\".\"channel\" = 'phone'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "client_contacts_client_id_clients_id_fk": { + "name": "client_contacts_client_id_clients_id_fk", + "tableFrom": "client_contacts", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_merge_candidates": { + "name": "client_merge_candidates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_a_id": { + "name": "client_a_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_b_id": { + "name": "client_b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_cmc_port_status": { + "name": "idx_cmc_port_status", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cmc_pair": { + "name": "idx_cmc_pair", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_a_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_b_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "client_merge_candidates_port_id_ports_id_fk": { + "name": "client_merge_candidates_port_id_ports_id_fk", + "tableFrom": "client_merge_candidates", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "client_merge_candidates_client_a_id_clients_id_fk": { + "name": "client_merge_candidates_client_a_id_clients_id_fk", + "tableFrom": "client_merge_candidates", + "tableTo": "clients", + "columnsFrom": ["client_a_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "client_merge_candidates_client_b_id_clients_id_fk": { + "name": "client_merge_candidates_client_b_id_clients_id_fk", + "tableFrom": "client_merge_candidates", + "tableTo": "clients", + "columnsFrom": ["client_b_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_merge_log": { + "name": "client_merge_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surviving_client_id": { + "name": "surviving_client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "merged_client_id": { + "name": "merged_client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "merged_by": { + "name": "merged_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "merge_details": { + "name": "merge_details", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cml_port": { + "name": "idx_cml_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "client_merge_log_port_id_ports_id_fk": { + "name": "client_merge_log_port_id_ports_id_fk", + "tableFrom": "client_merge_log", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "client_merge_log_surviving_client_id_clients_id_fk": { + "name": "client_merge_log_surviving_client_id_clients_id_fk", + "tableFrom": "client_merge_log", + "tableTo": "clients", + "columnsFrom": ["surviving_client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_notes": { + "name": "client_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mentions": { + "name": "mentions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cn_client": { + "name": "idx_cn_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "client_notes_client_id_clients_id_fk": { + "name": "client_notes_client_id_clients_id_fk", + "tableFrom": "client_notes", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_relationships": { + "name": "client_relationships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_a_id": { + "name": "client_a_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_b_id": { + "name": "client_b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "relationship_type": { + "name": "relationship_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cr_port": { + "name": "idx_cr_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "client_relationships_port_id_ports_id_fk": { + "name": "client_relationships_port_id_ports_id_fk", + "tableFrom": "client_relationships", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "client_relationships_client_a_id_clients_id_fk": { + "name": "client_relationships_client_a_id_clients_id_fk", + "tableFrom": "client_relationships", + "tableTo": "clients", + "columnsFrom": ["client_a_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "client_relationships_client_b_id_clients_id_fk": { + "name": "client_relationships_client_b_id_clients_id_fk", + "tableFrom": "client_relationships", + "tableTo": "clients", + "columnsFrom": ["client_b_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_tags": { + "name": "client_tags", + "schema": "", + "columns": { + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "client_tags_client_id_clients_id_fk": { + "name": "client_tags_client_id_clients_id_fk", + "tableFrom": "client_tags", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "client_tags_client_id_tag_id_pk": { + "name": "client_tags_client_id_tag_id_pk", + "columns": ["client_id", "tag_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.clients": { + "name": "clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nationality_iso": { + "name": "nationality_iso", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferred_contact_method": { + "name": "preferred_contact_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferred_language": { + "name": "preferred_language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_details": { + "name": "source_details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_clients_port": { + "name": "idx_clients_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_clients_name": { + "name": "idx_clients_name", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_clients_archived": { + "name": "idx_clients_archived", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_clients_nationality_iso": { + "name": "idx_clients_nationality_iso", + "columns": [ + { + "expression": "nationality_iso", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "clients_port_id_ports_id_fk": { + "name": "clients_port_id_ports_id_fk", + "tableFrom": "clients", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "legal_name": { + "name": "legal_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_number": { + "name": "registration_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "incorporation_country_iso": { + "name": "incorporation_country_iso", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "incorporation_subdivision_iso": { + "name": "incorporation_subdivision_iso", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "incorporation_date": { + "name": "incorporation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "billing_email": { + "name": "billing_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_companies_port": { + "name": "idx_companies_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_companies_name_unique": { + "name": "idx_companies_name_unique", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"name\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_companies_taxid": { + "name": "idx_companies_taxid", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tax_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"companies\".\"tax_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "companies_port_id_ports_id_fk": { + "name": "companies_port_id_ports_id_fk", + "tableFrom": "companies", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_addresses": { + "name": "company_addresses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Primary'" + }, + "street_address": { + "name": "street_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subdivision_iso": { + "name": "subdivision_iso", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_iso": { + "name": "country_iso", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_compa_company": { + "name": "idx_compa_company", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_compa_port": { + "name": "idx_compa_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_compa_primary": { + "name": "idx_compa_primary", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_addresses\".\"is_primary\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_addresses_company_id_companies_id_fk": { + "name": "company_addresses_company_id_companies_id_fk", + "tableFrom": "company_addresses", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_addresses_port_id_ports_id_fk": { + "name": "company_addresses_port_id_ports_id_fk", + "tableFrom": "company_addresses", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_detail": { + "name": "role_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_cm_company": { + "name": "idx_cm_company", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cm_client": { + "name": "idx_cm_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cm_active": { + "name": "idx_cm_active", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"company_memberships\".\"end_date\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_cm_exact": { + "name": "unique_cm_exact", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_memberships_client_id_clients_id_fk": { + "name": "company_memberships_client_id_clients_id_fk", + "tableFrom": "company_memberships", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_notes": { + "name": "company_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mentions": { + "name": "mentions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_compn_company": { + "name": "idx_compn_company", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_notes_company_id_companies_id_fk": { + "name": "company_notes_company_id_companies_id_fk", + "tableFrom": "company_notes", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_tags": { + "name": "company_tags", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "company_tags_company_id_companies_id_fk": { + "name": "company_tags_company_id_companies_id_fk", + "tableFrom": "company_tags", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "company_tags_company_id_tag_id_pk": { + "name": "company_tags_company_id_tag_id_pk", + "columns": ["company_id", "tag_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.crm_user_invites": { + "name": "crm_user_invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_super_admin": { + "name": "is_super_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_crm_invites_token_hash": { + "name": "idx_crm_invites_token_hash", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_crm_invites_email": { + "name": "idx_crm_invites_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_events": { + "name": "document_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signer_id": { + "name": "signer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_data": { + "name": "event_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "signature_hash": { + "name": "signature_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_de_doc": { + "name": "idx_de_doc", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_de_dedup": { + "name": "idx_de_dedup", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "signature_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document_events\".\"signature_hash\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_events_document_id_documents_id_fk": { + "name": "document_events_document_id_documents_id_fk", + "tableFrom": "document_events", + "tableTo": "documents", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_events_signer_id_document_signers_id_fk": { + "name": "document_events_signer_id_document_signers_id_fk", + "tableFrom": "document_events", + "tableTo": "document_signers", + "columnsFrom": ["signer_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_signers": { + "name": "document_signers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signer_name": { + "name": "signer_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signer_email": { + "name": "signer_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signer_role": { + "name": "signer_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signing_order": { + "name": "signing_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "signed_at": { + "name": "signed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "signing_url": { + "name": "signing_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedded_url": { + "name": "embedded_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ds_doc": { + "name": "idx_ds_doc", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_signers_document_id_documents_id_fk": { + "name": "document_signers_document_id_documents_id_fk", + "tableFrom": "document_signers", + "tableTo": "documents", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_templates": { + "name": "document_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template_type": { + "name": "template_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merge_fields": { + "name": "merge_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "template_format": { + "name": "template_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'html'" + }, + "source_file_id": { + "name": "source_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "documenso_template_id": { + "name": "documenso_template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "field_mapping": { + "name": "field_mapping", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "overlay_positions": { + "name": "overlay_positions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "reminder_cadence_days": { + "name": "reminder_cadence_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_dt_port": { + "name": "idx_dt_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_dt_type": { + "name": "idx_dt_type", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_templates_port_id_ports_id_fk": { + "name": "document_templates_port_id_ports_id_fk", + "tableFrom": "document_templates", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_templates_source_file_id_files_id_fk": { + "name": "document_templates_source_file_id_files_id_fk", + "tableFrom": "document_templates", + "tableTo": "files", + "columnsFrom": ["source_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_watchers": { + "name": "document_watchers", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_doc_watchers_doc": { + "name": "idx_doc_watchers_doc", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_doc_watchers_user": { + "name": "idx_doc_watchers_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_watchers_document_id_documents_id_fk": { + "name": "document_watchers_document_id_documents_id_fk", + "tableFrom": "document_watchers", + "tableTo": "documents", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "document_watchers_document_id_user_id_pk": { + "name": "document_watchers_document_id_user_id_pk", + "columns": ["document_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interest_id": { + "name": "interest_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "yacht_id": { + "name": "yacht_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reservation_id": { + "name": "reservation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "document_type": { + "name": "document_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "documenso_id": { + "name": "documenso_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signed_file_id": { + "name": "signed_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_manual_upload": { + "name": "is_manual_upload", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reminders_disabled": { + "name": "reminders_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reminder_cadence_override": { + "name": "reminder_cadence_override", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_docs_port": { + "name": "idx_docs_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_docs_interest": { + "name": "idx_docs_interest", + "columns": [ + { + "expression": "interest_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_docs_client": { + "name": "idx_docs_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_documents_yacht": { + "name": "idx_documents_yacht", + "columns": [ + { + "expression": "yacht_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_documents_company": { + "name": "idx_documents_company", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_docs_reservation": { + "name": "idx_docs_reservation", + "columns": [ + { + "expression": "reservation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_docs_type": { + "name": "idx_docs_type", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_docs_status_port": { + "name": "idx_docs_status_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_port_id_ports_id_fk": { + "name": "documents_port_id_ports_id_fk", + "tableFrom": "documents", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_client_id_clients_id_fk": { + "name": "documents_client_id_clients_id_fk", + "tableFrom": "documents", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_file_id_files_id_fk": { + "name": "documents_file_id_files_id_fk", + "tableFrom": "documents", + "tableTo": "files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_signed_file_id_files_id_fk": { + "name": "documents_signed_file_id_files_id_fk", + "tableFrom": "documents", + "tableTo": "files", + "columnsFrom": ["signed_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "yacht_id": { + "name": "yacht_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_path": { + "name": "storage_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_bucket": { + "name": "storage_bucket", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'crm-files'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_files_port": { + "name": "idx_files_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_files_client": { + "name": "idx_files_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_files_yacht": { + "name": "idx_files_yacht", + "columns": [ + { + "expression": "yacht_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_files_company": { + "name": "idx_files_company", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "files_port_id_ports_id_fk": { + "name": "files_port_id_ports_id_fk", + "tableFrom": "files", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_client_id_clients_id_fk": { + "name": "files_client_id_clients_id_fk", + "tableFrom": "files", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_submissions": { + "name": "form_submissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "form_template_id": { + "name": "form_template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interest_id": { + "name": "interest_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefilled_data": { + "name": "prefilled_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "submitted_data": { + "name": "submitted_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_fs_token": { + "name": "idx_fs_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_submissions_form_template_id_form_templates_id_fk": { + "name": "form_submissions_form_template_id_form_templates_id_fk", + "tableFrom": "form_submissions", + "tableTo": "form_templates", + "columnsFrom": ["form_template_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "form_submissions_client_id_clients_id_fk": { + "name": "form_submissions_client_id_clients_id_fk", + "tableFrom": "form_submissions", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "form_submissions_token_unique": { + "name": "form_submissions_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_templates": { + "name": "form_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "branding": { + "name": "branding", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ft_port": { + "name": "idx_ft_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_templates_port_id_ports_id_fk": { + "name": "form_templates_port_id_ports_id_fk", + "tableFrom": "form_templates", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_accounts": { + "name": "email_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_address": { + "name": "email_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtp_host": { + "name": "smtp_host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtp_port": { + "name": "smtp_port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "imap_host": { + "name": "imap_host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imap_port": { + "name": "imap_port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credentials_enc": { + "name": "credentials_enc", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ea_user": { + "name": "idx_ea_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ea_port": { + "name": "idx_ea_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_accounts_port_id_ports_id_fk": { + "name": "email_accounts_port_id_ports_id_fk", + "tableFrom": "email_accounts", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_messages": { + "name": "email_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id_header": { + "name": "message_id_header", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_address": { + "name": "from_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_addresses": { + "name": "to_addresses", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "cc_addresses": { + "name": "cc_addresses", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attachment_file_ids": { + "name": "attachment_file_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "raw_file_id": { + "name": "raw_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_em_thread": { + "name": "idx_em_thread", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_em_message_id": { + "name": "idx_em_message_id", + "columns": [ + { + "expression": "message_id_header", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"email_messages\".\"message_id_header\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_messages_thread_id_email_threads_id_fk": { + "name": "email_messages_thread_id_email_threads_id_fk", + "tableFrom": "email_messages", + "tableTo": "email_threads", + "columnsFrom": ["thread_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_messages_raw_file_id_files_id_fk": { + "name": "email_messages_raw_file_id_files_id_fk", + "tableFrom": "email_messages", + "tableTo": "files", + "columnsFrom": ["raw_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_threads": { + "name": "email_threads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_et_client": { + "name": "idx_et_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_et_port": { + "name": "idx_et_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_threads_port_id_ports_id_fk": { + "name": "email_threads_port_id_ports_id_fk", + "tableFrom": "email_threads", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "email_threads_client_id_clients_id_fk": { + "name": "email_threads_client_id_clients_id_fk", + "tableFrom": "email_threads", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.expenses": { + "name": "expenses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "establishment_name": { + "name": "establishment_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "amount_usd": { + "name": "amount_usd", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "exchange_rate": { + "name": "exchange_rate", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "payment_method": { + "name": "payment_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payer": { + "name": "payer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expense_date": { + "name": "expense_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "receipt_file_ids": { + "name": "receipt_file_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "payment_status": { + "name": "payment_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'unpaid'" + }, + "payment_date": { + "name": "payment_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "payment_reference": { + "name": "payment_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_notes": { + "name": "payment_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duplicate_of": { + "name": "duplicate_of", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dedup_scanned_at": { + "name": "dedup_scanned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ocr_status": { + "name": "ocr_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "ocr_raw": { + "name": "ocr_raw", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ocr_confidence": { + "name": "ocr_confidence", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_expenses_port": { + "name": "idx_expenses_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_expenses_date": { + "name": "idx_expenses_date", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expense_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_expenses_category": { + "name": "idx_expenses_category", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_expenses_dedup": { + "name": "idx_expenses_dedup", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "establishment_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "amount", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expense_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "duplicate_of IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "expenses_port_id_ports_id_fk": { + "name": "expenses_port_id_ports_id_fk", + "tableFrom": "expenses", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "expenses_duplicate_of_expenses_id_fk": { + "name": "expenses_duplicate_of_expenses_id_fk", + "tableFrom": "expenses", + "tableTo": "expenses", + "columnsFrom": ["duplicate_of"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoice_expenses": { + "name": "invoice_expenses", + "schema": "", + "columns": { + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expense_id": { + "name": "expense_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "invoice_expenses_invoice_id_invoices_id_fk": { + "name": "invoice_expenses_invoice_id_invoices_id_fk", + "tableFrom": "invoice_expenses", + "tableTo": "invoices", + "columnsFrom": ["invoice_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invoice_expenses_expense_id_expenses_id_fk": { + "name": "invoice_expenses_expense_id_expenses_id_fk", + "tableFrom": "invoice_expenses", + "tableTo": "expenses", + "columnsFrom": ["expense_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "invoice_expenses_invoice_id_expense_id_pk": { + "name": "invoice_expenses_invoice_id_expense_id_pk", + "columns": ["invoice_id", "expense_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoice_line_items": { + "name": "invoice_line_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "unit_price": { + "name": "unit_price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "total": { + "name": "total", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ili_invoice": { + "name": "idx_ili_invoice", + "columns": [ + { + "expression": "invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoice_line_items_invoice_id_invoices_id_fk": { + "name": "invoice_line_items_invoice_id_invoices_id_fk", + "tableFrom": "invoice_line_items", + "tableTo": "invoices", + "columnsFrom": ["invoice_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoices": { + "name": "invoices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoice_number": { + "name": "invoice_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'client'" + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "billing_email": { + "name": "billing_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_address": { + "name": "billing_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "payment_terms": { + "name": "payment_terms", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'net30'" + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "subtotal": { + "name": "subtotal", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "discount_pct": { + "name": "discount_pct", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "discount_amount": { + "name": "discount_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "fee_pct": { + "name": "fee_pct", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "fee_amount": { + "name": "fee_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total": { + "name": "total", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "payment_status": { + "name": "payment_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'unpaid'" + }, + "payment_date": { + "name": "payment_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "payment_method": { + "name": "payment_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_reference": { + "name": "payment_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pdf_file_id": { + "name": "pdf_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interest_id": { + "name": "interest_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_invoices_number": { + "name": "idx_invoices_number", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invoice_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invoices_port": { + "name": "idx_invoices_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invoices_status": { + "name": "idx_invoices_status", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invoices_billing_entity": { + "name": "idx_invoices_billing_entity", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invoices_interest": { + "name": "idx_invoices_interest", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "interest_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoices_port_id_ports_id_fk": { + "name": "invoices_port_id_ports_id_fk", + "tableFrom": "invoices", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invoices_pdf_file_id_files_id_fk": { + "name": "invoices_pdf_file_id_files_id_fk", + "tableFrom": "invoices", + "tableTo": "files", + "columnsFrom": ["pdf_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "invoices_interest_id_interests_id_fk": { + "name": "invoices_interest_id_interests_id_fk", + "tableFrom": "invoices", + "tableTo": "interests", + "columnsFrom": ["interest_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gdpr_exports": { + "name": "gdpr_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_to": { + "name": "sent_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ready_at": { + "name": "ready_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_gdpr_exports_client": { + "name": "idx_gdpr_exports_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_gdpr_exports_port_created": { + "name": "idx_gdpr_exports_port_created", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gdpr_exports_port_id_ports_id_fk": { + "name": "gdpr_exports_port_id_ports_id_fk", + "tableFrom": "gdpr_exports", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gdpr_exports_client_id_clients_id_fk": { + "name": "gdpr_exports_client_id_clients_id_fk", + "tableFrom": "gdpr_exports", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gdpr_exports_requested_by_user_id_fk": { + "name": "gdpr_exports_requested_by_user_id_fk", + "tableFrom": "gdpr_exports", + "tableTo": "user", + "columnsFrom": ["requested_by"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ports": { + "name": "ports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_currency": { + "name": "default_currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'America/Anguilla'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ports_slug_idx": { + "name": "ports_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.port_role_overrides": { + "name": "port_role_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_overrides": { + "name": "permission_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "port_role_overrides_port_role_idx": { + "name": "port_role_overrides_port_role_idx", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "port_role_overrides_port_idx": { + "name": "port_role_overrides_port_idx", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "port_role_overrides_port_id_ports_id_fk": { + "name": "port_role_overrides_port_id_ports_id_fk", + "tableFrom": "port_role_overrides", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "port_role_overrides_role_id_roles_id_fk": { + "name": "port_role_overrides_role_id_roles_id_fk", + "tableFrom": "port_role_overrides", + "tableTo": "roles", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "is_global": { + "name": "is_global", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_token_idx": { + "name": "sessions_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_port_roles": { + "name": "user_port_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residential_access": { + "name": "residential_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_port_roles_user_port_role_idx": { + "name": "user_port_roles_user_port_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_upr_user": { + "name": "idx_upr_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_upr_port": { + "name": "idx_upr_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_port_roles_port_id_ports_id_fk": { + "name": "user_port_roles_port_id_ports_id_fk", + "tableFrom": "user_port_roles", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_port_roles_role_id_roles_id_fk": { + "name": "user_port_roles_role_id_roles_id_fk", + "tableFrom": "user_port_roles", + "tableTo": "roles", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_super_admin": { + "name": "is_super_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "preferences": { + "name": "preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_profiles_user_id_idx": { + "name": "user_profiles_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_profiles_user_id_unique": { + "name": "user_profiles_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.yacht_notes": { + "name": "yacht_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "yacht_id": { + "name": "yacht_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mentions": { + "name": "mentions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_yn_yacht": { + "name": "idx_yn_yacht", + "columns": [ + { + "expression": "yacht_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "yacht_notes_yacht_id_yachts_id_fk": { + "name": "yacht_notes_yacht_id_yachts_id_fk", + "tableFrom": "yacht_notes", + "tableTo": "yachts", + "columnsFrom": ["yacht_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.yacht_ownership_history": { + "name": "yacht_ownership_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "yacht_id": { + "name": "yacht_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "transfer_reason": { + "name": "transfer_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transfer_notes": { + "name": "transfer_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_yoh_yacht": { + "name": "idx_yoh_yacht", + "columns": [ + { + "expression": "yacht_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_yoh_active": { + "name": "idx_yoh_active", + "columns": [ + { + "expression": "yacht_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"yacht_ownership_history\".\"end_date\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "yacht_ownership_history_yacht_id_yachts_id_fk": { + "name": "yacht_ownership_history_yacht_id_yachts_id_fk", + "tableFrom": "yacht_ownership_history", + "tableTo": "yachts", + "columnsFrom": ["yacht_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.yacht_tags": { + "name": "yacht_tags", + "schema": "", + "columns": { + "yacht_id": { + "name": "yacht_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "yacht_tags_yacht_id_yachts_id_fk": { + "name": "yacht_tags_yacht_id_yachts_id_fk", + "tableFrom": "yacht_tags", + "tableTo": "yachts", + "columnsFrom": ["yacht_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "yacht_tags_yacht_id_tag_id_pk": { + "name": "yacht_tags_yacht_id_tag_id_pk", + "columns": ["yacht_id", "tag_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.yachts": { + "name": "yachts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hull_number": { + "name": "hull_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration": { + "name": "registration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "flag": { + "name": "flag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year_built": { + "name": "year_built", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "builder": { + "name": "builder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hull_material": { + "name": "hull_material", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "length_ft": { + "name": "length_ft", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width_ft": { + "name": "width_ft", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "draft_ft": { + "name": "draft_ft", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "length_m": { + "name": "length_m", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width_m": { + "name": "width_m", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "draft_m": { + "name": "draft_m", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "current_owner_type": { + "name": "current_owner_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_owner_id": { + "name": "current_owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_yachts_port": { + "name": "idx_yachts_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_yachts_current_owner": { + "name": "idx_yachts_current_owner", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_owner_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_yachts_name": { + "name": "idx_yachts_name", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_yachts_archived": { + "name": "idx_yachts_archived", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "yachts_port_id_ports_id_fk": { + "name": "yachts_port_id_ports_id_fk", + "tableFrom": "yachts", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interest_notes": { + "name": "interest_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "interest_id": { + "name": "interest_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mentions": { + "name": "mentions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_in_interest": { + "name": "idx_in_interest", + "columns": [ + { + "expression": "interest_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interest_notes_interest_id_interests_id_fk": { + "name": "interest_notes_interest_id_interests_id_fk", + "tableFrom": "interest_notes", + "tableTo": "interests", + "columnsFrom": ["interest_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interest_tags": { + "name": "interest_tags", + "schema": "", + "columns": { + "interest_id": { + "name": "interest_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "interest_tags_interest_id_interests_id_fk": { + "name": "interest_tags_interest_id_interests_id_fk", + "tableFrom": "interest_tags", + "tableTo": "interests", + "columnsFrom": ["interest_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "interest_tags_interest_id_tag_id_pk": { + "name": "interest_tags_interest_id_tag_id_pk", + "columns": ["interest_id", "tag_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interests": { + "name": "interests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "berth_id": { + "name": "berth_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "yacht_id": { + "name": "yacht_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pipeline_stage": { + "name": "pipeline_stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "lead_category": { + "name": "lead_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "eoi_status": { + "name": "eoi_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "documenso_id": { + "name": "documenso_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contract_status": { + "name": "contract_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deposit_status": { + "name": "deposit_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reservation_status": { + "name": "reservation_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date_first_contact": { + "name": "date_first_contact", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "date_last_contact": { + "name": "date_last_contact", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "date_eoi_sent": { + "name": "date_eoi_sent", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "date_eoi_signed": { + "name": "date_eoi_signed", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "date_contract_sent": { + "name": "date_contract_sent", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "date_contract_signed": { + "name": "date_contract_signed", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "date_deposit_received": { + "name": "date_deposit_received", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reminder_enabled": { + "name": "reminder_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reminder_days": { + "name": "reminder_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reminder_last_fired": { + "name": "reminder_last_fired", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_reason": { + "name": "outcome_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_at": { + "name": "outcome_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_interests_port": { + "name": "idx_interests_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_interests_client": { + "name": "idx_interests_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_interests_berth": { + "name": "idx_interests_berth", + "columns": [ + { + "expression": "berth_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_interests_yacht": { + "name": "idx_interests_yacht", + "columns": [ + { + "expression": "yacht_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_interests_stage": { + "name": "idx_interests_stage", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pipeline_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_interests_archived": { + "name": "idx_interests_archived", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_interests_outcome": { + "name": "idx_interests_outcome", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "outcome", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interests_port_id_ports_id_fk": { + "name": "interests_port_id_ports_id_fk", + "tableFrom": "interests", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "interests_client_id_clients_id_fk": { + "name": "interests_client_id_clients_id_fk", + "tableFrom": "interests", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.berth_reservations": { + "name": "berth_reservations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "berth_id": { + "name": "berth_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "yacht_id": { + "name": "yacht_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interest_id": { + "name": "interest_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tenure_type": { + "name": "tenure_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'permanent'" + }, + "contract_file_id": { + "name": "contract_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_br_berth": { + "name": "idx_br_berth", + "columns": [ + { + "expression": "berth_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_br_client": { + "name": "idx_br_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_br_yacht": { + "name": "idx_br_yacht", + "columns": [ + { + "expression": "yacht_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_br_port": { + "name": "idx_br_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_br_active": { + "name": "idx_br_active", + "columns": [ + { + "expression": "berth_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"berth_reservations\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "berth_reservations_berth_id_berths_id_fk": { + "name": "berth_reservations_berth_id_berths_id_fk", + "tableFrom": "berth_reservations", + "tableTo": "berths", + "columnsFrom": ["berth_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "berth_reservations_port_id_ports_id_fk": { + "name": "berth_reservations_port_id_ports_id_fk", + "tableFrom": "berth_reservations", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "berth_reservations_client_id_clients_id_fk": { + "name": "berth_reservations_client_id_clients_id_fk", + "tableFrom": "berth_reservations", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "berth_reservations_yacht_id_yachts_id_fk": { + "name": "berth_reservations_yacht_id_yachts_id_fk", + "tableFrom": "berth_reservations", + "tableTo": "yachts", + "columnsFrom": ["yacht_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "berth_reservations_interest_id_interests_id_fk": { + "name": "berth_reservations_interest_id_interests_id_fk", + "tableFrom": "berth_reservations", + "tableTo": "interests", + "columnsFrom": ["interest_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "berth_reservations_contract_file_id_files_id_fk": { + "name": "berth_reservations_contract_file_id_files_id_fk", + "tableFrom": "berth_reservations", + "tableTo": "files", + "columnsFrom": ["contract_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portal_auth_tokens": { + "name": "portal_auth_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "portal_user_id": { + "name": "portal_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_portal_tokens_hash_unique": { + "name": "idx_portal_tokens_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_portal_tokens_user": { + "name": "idx_portal_tokens_user", + "columns": [ + { + "expression": "portal_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "portal_auth_tokens_portal_user_id_portal_users_id_fk": { + "name": "portal_auth_tokens_portal_user_id_portal_users_id_fk", + "tableFrom": "portal_auth_tokens", + "tableTo": "portal_users", + "columnsFrom": ["portal_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portal_users": { + "name": "portal_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_portal_users_email_unique": { + "name": "idx_portal_users_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_portal_users_client": { + "name": "idx_portal_users_client", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_portal_users_port": { + "name": "idx_portal_users_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "portal_users_port_id_ports_id_fk": { + "name": "portal_users_port_id_ports_id_fk", + "tableFrom": "portal_users", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "portal_users_client_id_clients_id_fk": { + "name": "portal_users_client_id_clients_id_fk", + "tableFrom": "portal_users", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.residential_clients": { + "name": "residential_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_e164": { + "name": "phone_e164", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone_country": { + "name": "phone_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nationality_iso": { + "name": "nationality_iso", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "place_of_residence": { + "name": "place_of_residence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "place_of_residence_country_iso": { + "name": "place_of_residence_country_iso", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subdivision_iso": { + "name": "subdivision_iso", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferred_contact_method": { + "name": "preferred_contact_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'prospect'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_residential_clients_port": { + "name": "idx_residential_clients_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_residential_clients_email": { + "name": "idx_residential_clients_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_residential_clients_archived": { + "name": "idx_residential_clients_archived", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "residential_clients_port_id_ports_id_fk": { + "name": "residential_clients_port_id_ports_id_fk", + "tableFrom": "residential_clients", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.residential_interests": { + "name": "residential_interests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residential_client_id": { + "name": "residential_client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pipeline_stage": { + "name": "pipeline_stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferences": { + "name": "preferences", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_to": { + "name": "assigned_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date_first_contact": { + "name": "date_first_contact", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "date_last_contact": { + "name": "date_last_contact", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_residential_interests_port": { + "name": "idx_residential_interests_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_residential_interests_client": { + "name": "idx_residential_interests_client", + "columns": [ + { + "expression": "residential_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_residential_interests_stage": { + "name": "idx_residential_interests_stage", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pipeline_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_residential_interests_assigned": { + "name": "idx_residential_interests_assigned", + "columns": [ + { + "expression": "assigned_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_residential_interests_archived": { + "name": "idx_residential_interests_archived", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "residential_interests_port_id_ports_id_fk": { + "name": "residential_interests_port_id_ports_id_fk", + "tableFrom": "residential_interests", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "residential_interests_residential_client_id_residential_clients_id_fk": { + "name": "residential_interests_residential_client_id_residential_clients_id_fk", + "tableFrom": "residential_interests", + "tableTo": "residential_clients", + "columnsFrom": ["residential_client_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.generated_reports": { + "name": "generated_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_report_id": { + "name": "scheduled_report_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_gr_port_created": { + "name": "idx_gr_port_created", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_gr_port_status": { + "name": "idx_gr_port_status", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_gr_scheduled": { + "name": "idx_gr_scheduled", + "columns": [ + { + "expression": "scheduled_report_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"generated_reports\".\"scheduled_report_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "generated_reports_port_id_ports_id_fk": { + "name": "generated_reports_port_id_ports_id_fk", + "tableFrom": "generated_reports", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "generated_reports_scheduled_report_id_scheduled_reports_id_fk": { + "name": "generated_reports_scheduled_report_id_scheduled_reports_id_fk", + "tableFrom": "generated_reports", + "tableTo": "scheduled_reports", + "columnsFrom": ["scheduled_report_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "generated_reports_file_id_files_id_fk": { + "name": "generated_reports_file_id_files_id_fk", + "tableFrom": "generated_reports", + "tableTo": "files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_calendar_cache": { + "name": "google_calendar_cache", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_crm_pushed": { + "name": "is_crm_pushed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reminder_id": { + "name": "reminder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "gcal_cache_user_event_idx": { + "name": "gcal_cache_user_event_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_gcal_cache_user": { + "name": "idx_gcal_cache_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "google_calendar_cache_reminder_id_reminders_id_fk": { + "name": "google_calendar_cache_reminder_id_reminders_id_fk", + "tableFrom": "google_calendar_cache", + "tableTo": "reminders", + "columnsFrom": ["reminder_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_calendar_tokens": { + "name": "google_calendar_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_expiry": { + "name": "token_expiry", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "calendar_id": { + "name": "calendar_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_enabled": { + "name": "sync_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "gcal_tokens_user_id_idx": { + "name": "gcal_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "google_calendar_tokens_user_id_unique": { + "name": "google_calendar_tokens_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_sent": { + "name": "email_sent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_notif_user": { + "name": "idx_notif_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notif_port": { + "name": "idx_notif_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_notifications_user_type": { + "name": "idx_notifications_user_type", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_port_id_ports_id_fk": { + "name": "notifications_port_id_ports_id_fk", + "tableFrom": "notifications", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminders": { + "name": "reminders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "assigned_to": { + "name": "assigned_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interest_id": { + "name": "interest_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "berth_id": { + "name": "berth_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_generated": { + "name": "auto_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "google_calendar_event_id": { + "name": "google_calendar_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "google_calendar_synced": { + "name": "google_calendar_synced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_reminders_port": { + "name": "idx_reminders_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_reminders_assigned": { + "name": "idx_reminders_assigned", + "columns": [ + { + "expression": "assigned_to", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_reminders_due": { + "name": "idx_reminders_due", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reminders\".\"status\" IN ('pending', 'snoozed')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reminders_port_id_ports_id_fk": { + "name": "reminders_port_id_ports_id_fk", + "tableFrom": "reminders", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reminders_client_id_clients_id_fk": { + "name": "reminders_client_id_clients_id_fk", + "tableFrom": "reminders", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.report_recipients": { + "name": "report_recipients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "report_id": { + "name": "report_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "report_recipients_report_email_idx": { + "name": "report_recipients_report_email_idx", + "columns": [ + { + "expression": "report_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_rr_report": { + "name": "idx_rr_report", + "columns": [ + { + "expression": "report_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "report_recipients_report_id_scheduled_reports_id_fk": { + "name": "report_recipients_report_id_scheduled_reports_id_fk", + "tableFrom": "report_recipients", + "tableTo": "scheduled_reports", + "columnsFrom": ["report_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduled_reports": { + "name": "scheduled_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_sr_port": { + "name": "idx_sr_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduled_reports_port_id_ports_id_fk": { + "name": "scheduled_reports_port_id_ports_id_fk", + "tableFrom": "scheduled_reports", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "field_changed": { + "name": "field_changed", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "old_value": { + "name": "old_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "new_value": { + "name": "new_value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reverted_by": { + "name": "reverted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reverted_at": { + "name": "reverted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revert_of": { + "name": "revert_of", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "search_text": { + "name": "search_text", + "type": "tsvector", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_al_port": { + "name": "idx_al_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_al_entity": { + "name": "idx_al_entity", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_al_user": { + "name": "idx_al_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_al_created": { + "name": "idx_al_created", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_port_id_ports_id_fk": { + "name": "audit_logs_port_id_ports_id_fk", + "tableFrom": "audit_logs", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "audit_logs_revert_of_audit_logs_id_fk": { + "name": "audit_logs_revert_of_audit_logs_id_fk", + "tableFrom": "audit_logs", + "tableTo": "audit_logs", + "columnsFrom": ["revert_of"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.currency_rates": { + "name": "currency_rates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "base_currency": { + "name": "base_currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_currency": { + "name": "target_currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'frankfurter'" + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "currency_rates_base_target_idx": { + "name": "currency_rates_base_target_idx", + "columns": [ + { + "expression": "base_currency", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_currency", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_field_definitions": { + "name": "custom_field_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_name": { + "name": "field_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_label": { + "name": "field_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "select_options": { + "name": "select_options", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_required": { + "name": "is_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cfd_port_entity_name_idx": { + "name": "cfd_port_entity_name_idx", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "field_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cfd_port": { + "name": "idx_cfd_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_field_definitions_port_id_ports_id_fk": { + "name": "custom_field_definitions_port_id_ports_id_fk", + "tableFrom": "custom_field_definitions", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_field_values": { + "name": "custom_field_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "field_id": { + "name": "field_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cfv_field_entity_idx": { + "name": "cfv_field_entity_idx", + "columns": [ + { + "expression": "field_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cfv_entity": { + "name": "idx_cfv_entity", + "columns": [ + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_field_values_field_id_custom_field_definitions_id_fk": { + "name": "custom_field_values_field_id_custom_field_definitions_id_fk", + "tableFrom": "custom_field_values", + "tableTo": "custom_field_definitions", + "columnsFrom": ["field_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_views": { + "name": "saved_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filters": { + "name": "filters", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sort_config": { + "name": "sort_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "column_config": { + "name": "column_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_shared": { + "name": "is_shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_sv_user": { + "name": "idx_sv_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "saved_views_port_id_ports_id_fk": { + "name": "saved_views_port_id_ports_id_fk", + "tableFrom": "saved_views", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scratchpad_notes": { + "name": "scratchpad_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "linked_client_id": { + "name": "linked_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_sp_user": { + "name": "idx_sp_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scratchpad_notes_linked_client_id_clients_id_fk": { + "name": "scratchpad_notes_linked_client_id_clients_id_fk", + "tableFrom": "scratchpad_notes", + "tableTo": "clients", + "columnsFrom": ["linked_client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "system_settings_key_port_idx": { + "name": "system_settings_key_port_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "system_settings_port_id_ports_id_fk": { + "name": "system_settings_port_id_ports_id_fk", + "tableFrom": "system_settings", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#6B7280'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tags_port_name_idx": { + "name": "tags_port_name_idx", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_tags_port": { + "name": "idx_tags_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tags_port_id_ports_id_fk": { + "name": "tags_port_id_ports_id_fk", + "tableFrom": "tags", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_notification_preferences": { + "name": "user_notification_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "in_app": { + "name": "in_app", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email": { + "name": "email", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "unp_user_port_type_idx": { + "name": "unp_user_port_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_notification_preferences_port_id_ports_id_fk": { + "name": "user_notification_preferences_port_id_ports_id_fk", + "tableFrom": "user_notification_preferences", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_wd_webhook": { + "name": "idx_wd_webhook", + "columns": [ + { + "expression": "webhook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_deliveries_webhook_id_webhooks_id_fk": { + "name": "webhook_deliveries_webhook_id_webhooks_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "webhooks", + "columnsFrom": ["webhook_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhooks": { + "name": "webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "events": { + "name": "events", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_webhooks_port": { + "name": "idx_webhooks_port", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhooks_port_id_ports_id_fk": { + "name": "webhooks_port_id_ports_id_fk", + "tableFrom": "webhooks", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alerts": { + "name": "alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fired_at": { + "name": "fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_by": { + "name": "dismissed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "acknowledged_by": { + "name": "acknowledged_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "idx_alerts_fingerprint_open": { + "name": "idx_alerts_fingerprint_open", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "resolved_at IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_alerts_port_fired": { + "name": "idx_alerts_port_fired", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fired_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_alerts_port_severity_open": { + "name": "idx_alerts_port_severity_open", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "resolved_at IS NULL AND dismissed_at IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "alerts_port_id_ports_id_fk": { + "name": "alerts_port_id_ports_id_fk", + "tableFrom": "alerts", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alerts_dismissed_by_user_id_fk": { + "name": "alerts_dismissed_by_user_id_fk", + "tableFrom": "alerts", + "tableTo": "user", + "columnsFrom": ["dismissed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "alerts_acknowledged_by_user_id_fk": { + "name": "alerts_acknowledged_by_user_id_fk", + "tableFrom": "alerts", + "tableTo": "user", + "columnsFrom": ["acknowledged_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.analytics_snapshots": { + "name": "analytics_snapshots", + "schema": "", + "columns": { + "port_id": { + "name": "port_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metric_id": { + "name": "metric_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "computed_at": { + "name": "computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_analytics_pk": { + "name": "idx_analytics_pk", + "columns": [ + { + "expression": "port_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "analytics_snapshots_port_id_ports_id_fk": { + "name": "analytics_snapshots_port_id_ports_id_fk", + "tableFrom": "analytics_snapshots", + "tableTo": "ports", + "columnsFrom": ["port_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_source_links": { + "name": "migration_source_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_system": { + "name": "source_system", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_entity_type": { + "name": "target_entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_entity_id": { + "name": "target_entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_id": { + "name": "applied_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_by": { + "name": "applied_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_msl_source_target": { + "name": "idx_msl_source_target", + "columns": [ + { + "expression": "source_system", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/lib/db/migrations/meta/_journal.json b/src/lib/db/migrations/meta/_journal.json index b559df0..13894a2 100644 --- a/src/lib/db/migrations/meta/_journal.json +++ b/src/lib/db/migrations/meta/_journal.json @@ -141,6 +141,13 @@ "when": 1777671562738, "tag": "0019_lazy_vampiro", "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1777811835982, + "tag": "0020_unusual_azazel", + "breakpoints": true } ] } diff --git a/src/lib/db/schema/clients.ts b/src/lib/db/schema/clients.ts index 4fd50b0..c28885b 100644 --- a/src/lib/db/schema/clients.ts +++ b/src/lib/db/schema/clients.ts @@ -2,6 +2,7 @@ import { pgTable, text, boolean, + integer, timestamp, jsonb, index, @@ -145,6 +146,54 @@ export const clientMergeLog = pgTable( (table) => [index('idx_cml_port').on(table.portId)], ); +/** + * Pairs of clients flagged by the background scoring job as potential + * duplicates. The `/admin/duplicates` review queue reads from here. + * + * Lifecycle: + * - Background job inserts a row when a pair scores >= the + * `dedup_review_queue_threshold` system setting. + * - User reviews in the admin UI and either merges (status='merged') + * or dismisses (status='dismissed'). + * - Subsequent runs of the scoring job skip pairs already + * `dismissed` so the same false-positive doesn't keep reappearing. + * A future score increase recreates the row. + * + * Pairs are stored canonically with `clientAId < clientBId` (string + * comparison) so the same pair only generates one row regardless of + * scoring direction. + */ +export const clientMergeCandidates = pgTable( + 'client_merge_candidates', + { + id: text('id') + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + portId: text('port_id') + .notNull() + .references(() => ports.id), + clientAId: text('client_a_id') + .notNull() + .references(() => clients.id, { onDelete: 'cascade' }), + clientBId: text('client_b_id') + .notNull() + .references(() => clients.id, { onDelete: 'cascade' }), + score: integer('score').notNull(), + /** Human-readable rule list, e.g. ["email match", "phone match"]. */ + reasons: jsonb('reasons').notNull(), + status: text('status').notNull().default('pending'), // pending | dismissed | merged + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + resolvedAt: timestamp('resolved_at', { withTimezone: true }), + resolvedBy: text('resolved_by'), + }, + (table) => [ + index('idx_cmc_port_status').on(table.portId, table.status), + // Same pair shouldn't surface twice — enforce uniqueness on the + // canonical (a < b) ordering. + uniqueIndex('idx_cmc_pair').on(table.portId, table.clientAId, table.clientBId), + ], +); + export const clientAddresses = pgTable( 'client_addresses', { @@ -190,3 +239,5 @@ export type ClientMergeLog = typeof clientMergeLog.$inferSelect; export type NewClientMergeLog = typeof clientMergeLog.$inferInsert; export type ClientAddress = typeof clientAddresses.$inferSelect; export type NewClientAddress = typeof clientAddresses.$inferInsert; +export type ClientMergeCandidate = typeof clientMergeCandidates.$inferSelect; +export type NewClientMergeCandidate = typeof clientMergeCandidates.$inferInsert; diff --git a/src/lib/db/schema/index.ts b/src/lib/db/schema/index.ts index fe9378e..018d50a 100644 --- a/src/lib/db/schema/index.ts +++ b/src/lib/db/schema/index.ts @@ -56,5 +56,8 @@ export * from './ai-usage'; // GDPR export tracking (Phase 3d) export * from './gdpr'; +// Migration ledger (one-shot scripts — NocoDB import etc.) +export * from './migration'; + // Relations (must come last — references all tables) export * from './relations'; diff --git a/src/lib/db/schema/migration.ts b/src/lib/db/schema/migration.ts new file mode 100644 index 0000000..9cb1c30 --- /dev/null +++ b/src/lib/db/schema/migration.ts @@ -0,0 +1,48 @@ +import { pgTable, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; + +/** + * Idempotency ledger for one-shot data migrations from external sources + * (e.g. the legacy NocoDB Interests table). + * + * Every entity created during a migration script's `--apply` run gets a + * row here mapping the source-system row identifier to the new-system + * entity id. Re-running `--apply` against the same report skips rows + * already linked, so partial-failure resumption is just "run again." + * + * One source row can generate multiple new entities (e.g. one NocoDB + * Interests row → one client + one interest + one yacht), so the + * uniqueness constraint includes `target_entity_type`. + */ +export const migrationSourceLinks = pgTable( + 'migration_source_links', + { + id: text('id') + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + /** e.g. 'nocodb_interests', 'nocodb_residences', 'nocodb_website_submissions'. */ + sourceSystem: text('source_system').notNull(), + /** Source row identifier as a string (NocoDB IDs are integers; we keep + * text here for forward compat with other sources). */ + sourceId: text('source_id').notNull(), + /** e.g. 'client', 'interest', 'yacht', 'document'. */ + targetEntityType: text('target_entity_type').notNull(), + /** UUID of the new-system entity (clients.id, interests.id, etc.). */ + targetEntityId: text('target_entity_id').notNull(), + /** Apply-id from the migration run that created this link — pairs with + * the on-disk apply manifest so `--rollback --apply-id ` knows + * exactly which links to remove. */ + appliedId: text('applied_id').notNull(), + appliedBy: text('applied_by'), + appliedAt: timestamp('applied_at', { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [ + uniqueIndex('idx_msl_source_target').on( + table.sourceSystem, + table.sourceId, + table.targetEntityType, + ), + ], +); + +export type MigrationSourceLink = typeof migrationSourceLinks.$inferSelect; +export type NewMigrationSourceLink = typeof migrationSourceLinks.$inferInsert; diff --git a/src/lib/dedup/find-matches.ts b/src/lib/dedup/find-matches.ts index db2a421..8e279a5 100644 --- a/src/lib/dedup/find-matches.ts +++ b/src/lib/dedup/find-matches.ts @@ -15,7 +15,7 @@ * Design reference: docs/superpowers/specs/2026-05-03-dedup-and-migration-design.md §4. */ -import { parsePhone } from '@/lib/i18n/phone'; +import { parsePhoneScriptSafe as parsePhone } from './phone-parse'; import { levenshtein } from './normalize'; diff --git a/src/lib/dedup/migration-report.ts b/src/lib/dedup/migration-report.ts new file mode 100644 index 0000000..f3044a2 --- /dev/null +++ b/src/lib/dedup/migration-report.ts @@ -0,0 +1,274 @@ +/** + * Migration report writer — turns a `MigrationPlan` (from + * `migration-transform.ts`) into a CSV + a human-readable Markdown + * summary on disk under `.migration//`. + * + * The CSV format is intentionally machine-friendly (one row per + * planned operation) so it can be diffed across runs and inspected + * by hand. The summary is designed for "open this in your editor and + * eyeball it for 5 minutes before --apply." + */ + +import { promises as fs } from 'node:fs'; +import path from 'node:path'; + +import type { MigrationPlan } from './migration-transform'; + +// ─── Output directory ─────────────────────────────────────────────────────── + +export interface ReportPaths { + rootDir: string; + csvPath: string; + summaryPath: string; + planJsonPath: string; +} + +/** Resolve report paths relative to the worktree root. The timestamped + * directory is created lazily by `writeReport`. */ +export function resolveReportPaths( + rootDir: string, + timestamp: string = new Date().toISOString().replace(/[:.]/g, '-'), +): ReportPaths { + const dir = path.join(rootDir, '.migration', timestamp); + return { + rootDir: dir, + csvPath: path.join(dir, 'report.csv'), + summaryPath: path.join(dir, 'summary.md'), + planJsonPath: path.join(dir, 'plan.json'), + }; +} + +// ─── CSV row shape ────────────────────────────────────────────────────────── + +interface CsvRow { + op: string; // create_client / create_contact / create_interest / auto_link / flag / needs_review + reason: string; + source_id: string; + target_table: string; + target_value: string; + confidence: string; + manual_review: 'true' | 'false'; +} + +// Trivial CSV escape: quote any cell that contains comma / quote / newline, +// double up internal quotes per RFC 4180. No need for a dependency. +function csvEscape(s: string): string { + if (/[",\n\r]/.test(s)) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +function rowToCsvLine(r: CsvRow): string { + return [ + r.op, + r.reason, + r.source_id, + r.target_table, + r.target_value, + r.confidence, + r.manual_review, + ] + .map(csvEscape) + .join(','); +} + +// ─── Build CSV ────────────────────────────────────────────────────────────── + +export function buildCsv(plan: MigrationPlan): string { + const lines: string[] = []; + lines.push( + [ + 'op', + 'reason', + 'source_id', + 'target_table', + 'target_value', + 'confidence', + 'manual_review', + ].join(','), + ); + + for (const client of plan.clients) { + lines.push( + rowToCsvLine({ + op: 'create_client', + reason: client.sourceIds.length > 1 ? 'auto-merged cluster' : 'new', + source_id: client.sourceIds.join('|'), + target_table: 'clients.fullName', + target_value: client.fullName, + confidence: 'N/A', + manual_review: 'false', + }), + ); + for (const c of client.contacts) { + lines.push( + rowToCsvLine({ + op: 'create_contact', + reason: c.flagged ?? 'new', + source_id: client.sourceIds.join('|'), + target_table: `clientContacts.${c.channel}`, + target_value: c.value, + confidence: 'N/A', + manual_review: c.flagged ? 'true' : 'false', + }), + ); + } + for (const a of client.addresses) { + lines.push( + rowToCsvLine({ + op: 'create_address', + reason: 'address text present', + source_id: client.sourceIds.join('|'), + target_table: 'clientAddresses.countryIso', + target_value: a.countryIso ?? '(unresolved)', + confidence: a.countryConfidence ?? 'fallback', + manual_review: a.countryConfidence === 'fallback' || !a.countryIso ? 'true' : 'false', + }), + ); + } + } + + for (const interest of plan.interests) { + lines.push( + rowToCsvLine({ + op: 'create_interest', + reason: `pipelineStage=${interest.pipelineStage}`, + source_id: String(interest.sourceId), + target_table: 'interests', + target_value: `${interest.berthMooringNumber ?? '(no berth)'} / ${interest.yachtName ?? '(no yacht)'}`, + confidence: 'N/A', + manual_review: 'false', + }), + ); + } + + for (const link of plan.autoLinks) { + lines.push( + rowToCsvLine({ + op: 'auto_link', + reason: link.reasons.join(' + '), + source_id: `${link.leadSourceId}<-${link.mergedSourceIds.join(',')}`, + target_table: 'clients', + target_value: '(merged into lead)', + confidence: `score=${link.score}`, + manual_review: 'false', + }), + ); + } + + for (const pair of plan.needsReview) { + lines.push( + rowToCsvLine({ + op: 'needs_review', + reason: pair.reasons.join(' + '), + source_id: `${pair.aSourceId}<->${pair.bSourceId}`, + target_table: 'clients', + target_value: '(human review required)', + confidence: `score=${pair.score}`, + manual_review: 'true', + }), + ); + } + + for (const flag of plan.flags) { + lines.push( + rowToCsvLine({ + op: 'flag', + reason: flag.reason, + source_id: String(flag.sourceId), + target_table: flag.sourceTable, + target_value: JSON.stringify(flag.details ?? {}), + confidence: 'N/A', + manual_review: 'true', + }), + ); + } + + return lines.join('\n') + '\n'; +} + +// ─── Build summary markdown ───────────────────────────────────────────────── + +export function buildSummary(plan: MigrationPlan, generatedAt: string): string { + const s = plan.stats; + const lines: string[] = []; + lines.push(`# Migration Dry-Run — ${generatedAt}`); + lines.push(''); + lines.push('## Input'); + lines.push(`- ${s.inputInterestRows} NocoDB Interests`); + lines.push(`- ${s.inputResidentialRows} NocoDB Residential Interests`); + lines.push(''); + lines.push('## Outcome'); + lines.push(`- ${s.outputClients} clients`); + lines.push(`- ${s.outputInterests} interests (one per source row, linked to deduped client)`); + lines.push(`- ${s.outputContacts} client_contacts`); + lines.push(`- ${s.outputAddresses} client_addresses`); + lines.push(''); + lines.push('## Auto-linked clusters'); + if (plan.autoLinks.length === 0) { + lines.push('_None — every input row maps to a unique client._'); + } else { + for (const link of plan.autoLinks) { + const merged = link.mergedSourceIds.length; + lines.push( + `- Lead row \`${link.leadSourceId}\` ← merged ${merged} other row${merged === 1 ? '' : 's'} (\`${link.mergedSourceIds.join(', ')}\`) — score ${link.score} via ${link.reasons.join(' + ')}`, + ); + } + } + lines.push(''); + lines.push('## Pairs flagged for human review'); + if (plan.needsReview.length === 0) { + lines.push('_None._'); + } else { + for (const pair of plan.needsReview) { + lines.push( + `- Rows \`${pair.aSourceId}\` ↔ \`${pair.bSourceId}\` — score ${pair.score} (${pair.reasons.join(' + ')})`, + ); + } + } + lines.push(''); + lines.push('## Data quality flags'); + if (plan.flags.length === 0) { + lines.push('_No quality issues._'); + } else { + const byReason = new Map(); + for (const f of plan.flags) { + byReason.set(f.reason, (byReason.get(f.reason) ?? 0) + 1); + } + for (const [reason, count] of [...byReason].sort((a, b) => b[1] - a[1])) { + lines.push(`- **${count}× ${reason}**`); + } + lines.push(''); + lines.push('### Detail'); + for (const f of plan.flags.slice(0, 30)) { + lines.push( + `- \`${f.sourceTable}#${f.sourceId}\`: ${f.reason}${f.details ? ` — \`${JSON.stringify(f.details)}\`` : ''}`, + ); + } + if (plan.flags.length > 30) { + lines.push(`- _… and ${plan.flags.length - 30} more (see report.csv for full list)_`); + } + } + lines.push(''); + lines.push('## Next step'); + lines.push(''); + lines.push('Eyeball the auto-linked + flagged-for-review pairs above.'); + lines.push('When satisfied, re-run the script with `--apply --report .migration//`.'); + lines.push('Apply will refuse to run if the source NocoDB has changed since this dry-run.'); + + return lines.join('\n') + '\n'; +} + +// ─── Write to disk ────────────────────────────────────────────────────────── + +export async function writeReport( + paths: ReportPaths, + plan: MigrationPlan, + generatedAt: string, +): Promise { + await fs.mkdir(paths.rootDir, { recursive: true }); + await fs.writeFile(paths.csvPath, buildCsv(plan), 'utf-8'); + await fs.writeFile(paths.summaryPath, buildSummary(plan, generatedAt), 'utf-8'); + await fs.writeFile(paths.planJsonPath, JSON.stringify(plan, null, 2), 'utf-8'); +} diff --git a/src/lib/dedup/migration-transform.ts b/src/lib/dedup/migration-transform.ts new file mode 100644 index 0000000..5b46b4e --- /dev/null +++ b/src/lib/dedup/migration-transform.ts @@ -0,0 +1,576 @@ +/** + * Pure transform: NocoDB snapshot → planned new-system entities + dedup result. + * + * Used by the migration script's `--dry-run` (to produce the report) and + * `--apply` (to actually write). Keeping this pure means the same code + * runs in both modes, in tests against the frozen fixture, and in the + * one-off CLI run against the live base. + * + * No side effects, no DB calls, no external services. + */ + +import { + normalizeName, + normalizeEmail, + normalizePhone, + resolveCountry, + type NormalizedPhone, +} from './normalize'; +import { findClientMatches, type MatchCandidate } from './find-matches'; +import type { CountryCode } from '@/lib/i18n/countries'; +import type { NocoDbRow, NocoDbSnapshot } from './nocodb-source'; + +// ─── Plan output ──────────────────────────────────────────────────────────── + +export interface PlannedClient { + /** Stable id derived from the deduped cluster's lead row. Used by the + * apply phase to reference newly-created clients before they exist + * in the DB. */ + tempId: string; + /** Source row IDs that contributed to this client (one if no duplicates, + * many if dedup merged a cluster). */ + sourceIds: number[]; + fullName: string; + surnameToken?: string; + countryIso: CountryCode | null; + preferredContactMethod: string | null; + source: string | null; + contacts: PlannedContact[]; + addresses: PlannedAddress[]; +} + +export interface PlannedContact { + channel: 'email' | 'phone' | 'whatsapp' | 'other'; + value: string; + valueE164?: string | null; + valueCountry?: CountryCode | null; + isPrimary: boolean; + flagged?: string; +} + +export interface PlannedAddress { + streetAddress: string | null; + city: string | null; + countryIso: CountryCode | null; + /** When confidence is low, the migration script flags the row for + * human review. */ + countryConfidence: 'exact' | 'fuzzy' | 'city' | 'fallback' | null; +} + +export interface PlannedInterest { + /** NocoDB row id this interest came from. */ + sourceId: number; + /** tempId of the planned client this interest hangs off. */ + clientTempId: string; + pipelineStage: string; + leadCategory: string | null; + source: string | null; + notes: string | null; + /** Mooring number; the apply phase resolves this to a berthId via the + * new-system Berths table. */ + berthMooringNumber: string | null; + yachtName: string | null; + /** Date stamps for milestone columns. ISO strings if parseable. */ + dateEoiSent: string | null; + dateEoiSigned: string | null; + dateDepositReceived: string | null; + dateContractSent: string | null; + dateContractSigned: string | null; + dateLastContact: string | null; + /** Documenso linkage carried forward when present so the document + * record can be stitched up downstream. */ + documensoId: string | null; +} + +export interface MigrationFlag { + sourceTable: 'interests' | 'residential_interests' | 'website_interest_submissions'; + sourceId: number; + reason: string; + details?: Record; +} + +export interface MigrationPlan { + clients: PlannedClient[]; + interests: PlannedInterest[]; + flags: MigrationFlag[]; + /** Pairs that the migration would auto-link (high score). */ + autoLinks: Array<{ + leadSourceId: number; + mergedSourceIds: number[]; + score: number; + reasons: string[]; + }>; + /** Pairs that need human review (medium score). Each pair shows up + * in the migration report; the user resolves before --apply. */ + needsReview: Array<{ aSourceId: number; bSourceId: number; score: number; reasons: string[] }>; + stats: MigrationStats; +} + +export interface MigrationStats { + inputInterestRows: number; + inputResidentialRows: number; + outputClients: number; + outputInterests: number; + outputContacts: number; + outputAddresses: number; + flaggedRows: number; + autoLinkedClusters: number; + needsReviewPairs: number; +} + +export interface TransformOptions { + /** ISO country used when a phone has no prefix and the row has no + * Place of Residence. Defaults to AI (Anguilla / Port Nimara's home). */ + defaultPhoneCountry: CountryCode; + /** Score thresholds for auto-link vs human review. Should match the + * per-port `system_settings` values once the runtime UI is in place. */ + thresholds: { + autoLink: number; + needsReview: number; + }; +} + +const DEFAULT_OPTIONS: TransformOptions = { + defaultPhoneCountry: 'AI', + thresholds: { autoLink: 90, needsReview: 50 }, +}; + +// ─── Stage mapping ────────────────────────────────────────────────────────── + +const STAGE_MAP: Record = { + 'General Qualified Interest': 'open', + 'Specific Qualified Interest': 'details_sent', + 'EOI and NDA Sent': 'eoi_sent', + 'Signed EOI and NDA': 'eoi_signed', + 'Made Reservation': 'deposit_10pct', + 'Contract Negotiation': 'contract_sent', + 'Contract Negotiations Finalized': 'contract_sent', + 'Contract Signed': 'contract_signed', +}; + +const LEAD_CATEGORY_MAP: Record = { + General: 'general_interest', + 'Friends and Family': 'general_interest', +}; + +const SOURCE_MAP: Record = { + portal: 'website', + Form: 'website', + External: 'manual', +}; + +// ─── Date parsing ─────────────────────────────────────────────────────────── + +/** + * Parse a date the legacy NocoDB might have stored in DD-MM-YYYY, + * DD/MM/YYYY, YYYY-MM-DD, or ISO format. Returns ISO string or null. + */ +function parseFlexibleDate(input: unknown): string | null { + if (typeof input !== 'string' || input.trim() === '') return null; + const s = input.trim(); + + // Already ISO + if (/^\d{4}-\d{2}-\d{2}/.test(s)) { + const d = new Date(s); + return Number.isNaN(d.getTime()) ? null : d.toISOString(); + } + + // DD-MM-YYYY or DD/MM/YYYY + const m = s.match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})$/); + if (m) { + const [, day, month, year] = m; + const iso = `${year}-${month!.padStart(2, '0')}-${day!.padStart(2, '0')}`; + const d = new Date(iso); + return Number.isNaN(d.getTime()) ? null : d.toISOString(); + } + + // Anything else: try Date constructor as a last resort + const d = new Date(s); + return Number.isNaN(d.getTime()) ? null : d.toISOString(); +} + +// ─── Main transform ───────────────────────────────────────────────────────── + +/** + * Run the full transform pipeline against a NocoDB snapshot. Pure + * function — same input always produces the same plan. + */ +export function transformSnapshot( + snapshot: NocoDbSnapshot, + options: Partial = {}, +): MigrationPlan { + const opts = { ...DEFAULT_OPTIONS, ...options }; + + const flags: MigrationFlag[] = []; + // Build per-row candidates first so we can run dedup before assigning + // tempIds (clients with multiple source rows merge into one tempId). + const perRow = snapshot.interests.map((row) => rowToCandidate(row, 'interests', opts, flags)); + + // Dedup pass 1: every row scored against every other row (within the + // same pool). The blocking strategy in `findClientMatches` keeps this + // cheap even for the full 252-row dataset. + const clusters = clusterByDedup(perRow, opts); + + // Build the planned clients + interests from the clusters. + const clients: PlannedClient[] = []; + const interests: PlannedInterest[] = []; + const autoLinks: MigrationPlan['autoLinks'] = []; + const needsReview: MigrationPlan['needsReview'] = []; + + for (const cluster of clusters) { + const lead = cluster.leadCandidate; + const tempId = `client-${lead.row.Id}`; + + // Build the client record from the lead row, then merge in any + // contact info / address info from the other rows in the cluster. + const planned = buildPlannedClient(tempId, cluster, opts); + clients.push(planned); + + // Each row in the cluster becomes its own interest record. + for (const member of cluster.members) { + const interest = buildPlannedInterest(member.row, tempId); + interests.push(interest); + } + + if (cluster.members.length > 1) { + autoLinks.push({ + leadSourceId: lead.row.Id, + mergedSourceIds: cluster.members.filter((m) => m !== lead).map((m) => m.row.Id), + score: cluster.maxScore, + reasons: cluster.reasons, + }); + } + + for (const pair of cluster.reviewPairs) { + needsReview.push(pair); + } + } + + return { + clients, + interests, + flags, + autoLinks, + needsReview, + stats: { + inputInterestRows: snapshot.interests.length, + inputResidentialRows: snapshot.residentialInterests.length, + outputClients: clients.length, + outputInterests: interests.length, + outputContacts: clients.reduce((sum, c) => sum + c.contacts.length, 0), + outputAddresses: clients.reduce((sum, c) => sum + c.addresses.length, 0), + flaggedRows: flags.length, + autoLinkedClusters: autoLinks.length, + needsReviewPairs: needsReview.length, + }, + }; +} + +// ─── Helpers ──────────────────────────────────────────────────────────────── + +interface RowCandidate { + row: NocoDbRow; + candidate: MatchCandidate; + /** Phone normalize result for the row's primary phone; used downstream + * to attach valueE164 + country to the planned contact. */ + phoneResult: NormalizedPhone | null; + /** Country resolved from "Place of Residence". */ + countryIso: CountryCode | null; + countryConfidence: 'exact' | 'fuzzy' | 'city' | null; + /** Normalized email or null. */ + email: string | null; + /** Display name from `normalizeName`. */ + displayName: string; +} + +function rowToCandidate( + row: NocoDbRow, + sourceTable: MigrationFlag['sourceTable'], + opts: TransformOptions, + flags: MigrationFlag[], +): RowCandidate { + const rawName = (row['Full Name'] as string | undefined) ?? ''; + const rawEmail = (row['Email Address'] as string | undefined) ?? ''; + const rawPhone = (row['Phone Number'] as string | undefined) ?? ''; + const rawCountry = (row['Place of Residence'] as string | undefined) ?? ''; + + const normName = normalizeName(rawName); + const email = normalizeEmail(rawEmail); + const country = resolveCountry(rawCountry); + const phoneCountry = country.iso ?? opts.defaultPhoneCountry; + const phoneResult = normalizePhone(rawPhone, phoneCountry as CountryCode); + + // Surface anything weird so the report can show it. + if (rawPhone && !phoneResult?.e164) { + flags.push({ + sourceTable, + sourceId: row.Id, + reason: phoneResult?.flagged ? `phone ${phoneResult.flagged}` : 'phone unparseable', + details: { rawPhone }, + }); + } + if (rawEmail && !email) { + flags.push({ + sourceTable, + sourceId: row.Id, + reason: 'email invalid', + details: { rawEmail }, + }); + } + if (rawCountry && !country.iso) { + flags.push({ + sourceTable, + sourceId: row.Id, + reason: 'country unresolved', + details: { rawCountry }, + }); + } + + const candidate: MatchCandidate = { + id: String(row.Id), + fullName: normName.display || null, + surnameToken: normName.surnameToken ?? null, + emails: email ? [email] : [], + phonesE164: phoneResult?.e164 ? [phoneResult.e164] : [], + countryIso: country.iso ?? null, + }; + + return { + row, + candidate, + phoneResult, + countryIso: country.iso ?? null, + countryConfidence: country.confidence, + email, + displayName: normName.display, + }; +} + +interface Cluster { + /** The cluster's "lead" row (most complete + most recent). */ + leadCandidate: RowCandidate; + members: RowCandidate[]; + maxScore: number; + reasons: string[]; + /** Pairs in this cluster that scored medium (need review). */ + reviewPairs: Array<{ aSourceId: number; bSourceId: number; score: number; reasons: string[] }>; +} + +function clusterByDedup(rows: RowCandidate[], opts: TransformOptions): Cluster[] { + // Use a union-find structure indexed by row id. Every pair with a + // score >= autoLink threshold gets unioned. Pairs in [needsReview, + // autoLink) accumulate onto the cluster's reviewPairs list — they're + // surfaced for human triage but not auto-merged. + const parent = new Map(); + for (const r of rows) parent.set(r.candidate.id, r.candidate.id); + const find = (id: string): string => { + let cur = id; + while (parent.get(cur) !== cur) { + const next = parent.get(cur)!; + parent.set(cur, parent.get(next)!); // path compression + cur = parent.get(cur)!; + } + return cur; + }; + const union = (a: string, b: string) => { + const rootA = find(a); + const rootB = find(b); + if (rootA !== rootB) parent.set(rootA, rootB); + }; + + const clusterReasons = new Map(); + const clusterMaxScore = new Map(); + const clusterReviewPairs = new Map(); + + // Score every candidate against every other candidate. The find-matches + // function does its own blocking so this is cheap. + for (let i = 0; i < rows.length; i += 1) { + const left = rows[i]!; + const remainingPool = rows.slice(i + 1).map((r) => r.candidate); + if (remainingPool.length === 0) continue; + const matches = findClientMatches(left.candidate, remainingPool, { + highScore: opts.thresholds.autoLink, + mediumScore: opts.thresholds.needsReview, + }); + + for (const m of matches) { + if (m.score >= opts.thresholds.autoLink) { + union(left.candidate.id, m.candidate.id); + const root = find(left.candidate.id); + clusterMaxScore.set(root, Math.max(clusterMaxScore.get(root) ?? 0, m.score)); + const existing = clusterReasons.get(root) ?? []; + for (const reason of m.reasons) { + if (!existing.includes(reason)) existing.push(reason); + } + clusterReasons.set(root, existing); + } else if (m.score >= opts.thresholds.needsReview) { + // Medium — track on whichever cluster `left` belongs to. + const root = find(left.candidate.id); + const list = clusterReviewPairs.get(root) ?? []; + list.push({ + aSourceId: parseInt(left.candidate.id, 10), + bSourceId: parseInt(m.candidate.id, 10), + score: m.score, + reasons: m.reasons, + }); + clusterReviewPairs.set(root, list); + } + } + } + + // Group rows by their cluster root. + const byRoot = new Map(); + for (const r of rows) { + const root = find(r.candidate.id); + const list = byRoot.get(root) ?? []; + list.push(r); + byRoot.set(root, list); + } + + // Build cluster objects, choosing the most-complete row as the lead. + const clusters: Cluster[] = []; + for (const [root, members] of byRoot) { + const lead = pickLead(members); + clusters.push({ + leadCandidate: lead, + members, + maxScore: clusterMaxScore.get(root) ?? 0, + reasons: clusterReasons.get(root) ?? [], + reviewPairs: clusterReviewPairs.get(root) ?? [], + }); + } + return clusters; +} + +function pickLead(rows: RowCandidate[]): RowCandidate { + // Pick the row with the most populated fields, breaking ties by + // recency (highest Id, since NocoDB IDs are monotonic). + return rows.reduce((best, current) => { + const bestScore = completenessScore(best); + const currentScore = completenessScore(current); + if (currentScore > bestScore) return current; + if (currentScore === bestScore && current.row.Id > best.row.Id) return current; + return best; + }); +} + +function completenessScore(r: RowCandidate): number { + let score = 0; + if (r.email) score += 1; + if (r.phoneResult?.e164) score += 1; + if (r.row['Address']) score += 0.5; + if (r.row['Yacht Name']) score += 0.5; + if (r.row['Source']) score += 0.25; + if (r.row['Lead Category']) score += 0.25; + if (r.row['Internal Notes']) score += 0.25; + return score; +} + +function buildPlannedClient( + tempId: string, + cluster: Cluster, + opts: TransformOptions, +): PlannedClient { + const lead = cluster.leadCandidate; + + // Collect distinct emails + phones from across the cluster — duplicate + // submissions often come with different contact methods we want to + // preserve as multiple rows in `client_contacts`. + const seenEmails = new Set(); + const seenPhones = new Set(); + const contacts: PlannedContact[] = []; + + for (const member of cluster.members) { + if (member.email && !seenEmails.has(member.email)) { + seenEmails.add(member.email); + contacts.push({ + channel: 'email', + value: member.email, + isPrimary: contacts.length === 0, + }); + } + if (member.phoneResult?.e164 && !seenPhones.has(member.phoneResult.e164)) { + seenPhones.add(member.phoneResult.e164); + const isFirstPhone = !contacts.some((c) => c.channel === 'phone'); + contacts.push({ + channel: 'phone', + value: member.phoneResult.e164, + valueE164: member.phoneResult.e164, + valueCountry: member.phoneResult.country, + isPrimary: isFirstPhone && contacts.every((c) => !c.isPrimary || c.channel === 'email'), + flagged: member.phoneResult.flagged, + }); + } + } + + // Demote the email-primary if a more-completable phone exists. + // Simpler invariant: the first contact is primary unless the row + // explicitly preferred phone. + const preferredMethod = (lead.row['Contact Method Preferred'] as string | undefined) + ?.toLowerCase() + ?.trim(); + + // Address: only build if the lead row has a meaningful address text. + const rawAddress = (lead.row['Address'] as string | undefined)?.trim(); + const addresses: PlannedAddress[] = []; + if (rawAddress) { + addresses.push({ + streetAddress: rawAddress, + city: null, + countryIso: lead.countryIso ?? opts.defaultPhoneCountry, + countryConfidence: lead.countryConfidence ?? 'fallback', + }); + } + + const sourceFromRow = (lead.row['Source'] as string | undefined) ?? null; + const mappedSource = sourceFromRow ? (SOURCE_MAP[sourceFromRow] ?? 'manual') : null; + + return { + tempId, + sourceIds: cluster.members.map((m) => m.row.Id), + fullName: lead.displayName, + surnameToken: lead.candidate.surnameToken ?? undefined, + countryIso: lead.countryIso, + preferredContactMethod: preferredMethod ?? null, + source: mappedSource, + contacts, + addresses, + }; +} + +function buildPlannedInterest(row: NocoDbRow, clientTempId: string): PlannedInterest { + const stage = (row['Sales Process Level'] as string | undefined) ?? ''; + const cat = (row['Lead Category'] as string | undefined) ?? ''; + + const notesParts: string[] = []; + const internalNotes = row['Internal Notes'] as string | undefined; + const extraComments = row['Extra Comments'] as string | undefined; + if (internalNotes?.trim()) notesParts.push(internalNotes.trim()); + if (extraComments?.trim()) notesParts.push(`Extra Comments: ${extraComments.trim()}`); + const berthSize = row['Berth Size Desired'] as string | undefined; + if (berthSize?.trim()) notesParts.push(`Berth size desired: ${berthSize.trim()}`); + + return { + sourceId: row.Id, + clientTempId, + pipelineStage: STAGE_MAP[stage] ?? 'open', + leadCategory: LEAD_CATEGORY_MAP[cat] ?? null, + source: ((row['Source'] as string | undefined) ?? null) || null, + notes: notesParts.join('\n\n') || null, + berthMooringNumber: (row['Berth Number'] as string | undefined) ?? null, + yachtName: (() => { + const n = (row['Yacht Name'] as string | undefined)?.trim(); + // Filter placeholder values used by sales reps for "we don't know yet". + if (!n) return null; + if (['TBC', 'Na', 'NA', 'na', 'N/A', 'TBD', 'tbd'].includes(n)) return null; + return n; + })(), + dateEoiSent: parseFlexibleDate(row['EOI Time Sent']), + dateEoiSigned: parseFlexibleDate(row['all_signed_notified_at'] ?? row['developerSignTime']), + dateDepositReceived: null, // not directly tracked in legacy schema + dateContractSent: parseFlexibleDate(row['Time LOI Sent']), + dateContractSigned: parseFlexibleDate(row['developerSignTime']), + dateLastContact: parseFlexibleDate(row['Created At'] ?? row['Date Added']), + documensoId: (row['documensoID'] as string | undefined) ?? null, + }; +} diff --git a/src/lib/dedup/nocodb-source.ts b/src/lib/dedup/nocodb-source.ts new file mode 100644 index 0000000..dbecdab --- /dev/null +++ b/src/lib/dedup/nocodb-source.ts @@ -0,0 +1,152 @@ +/** + * Read-only adapter for the legacy NocoDB Port Nimara base. + * + * Used by the one-shot migration script (`scripts/migrate-from-nocodb.ts`) + * to pull every Interest, Residential Interest, and Website Submission + * row from the source-of-truth NocoDB tables. No mutations. + * + * Auth: `xc-token` header per NocoDB v2 API. + * + * The shape returned is a verbatim record of the row's fields — caller + * is responsible for mapping to the new schema via `nocodb-transform.ts`. + */ + +import { z } from 'zod'; + +// ─── Configuration ────────────────────────────────────────────────────────── + +const ConfigSchema = z.object({ + url: z.string().url(), + token: z.string().min(1), +}); + +export interface NocoDbConfig { + url: string; + token: string; +} + +export function loadNocoDbConfig(env: NodeJS.ProcessEnv = process.env): NocoDbConfig { + return ConfigSchema.parse({ + url: env.NOCODB_URL, + token: env.NOCODB_TOKEN, + }); +} + +// ─── Table identifiers ────────────────────────────────────────────────────── +// +// These IDs are stable per the NocoDB base — they were captured during the +// 2026-05-03 audit and won't change unless the base is rebuilt. If the +// base is reset, regenerate them from `getTablesList`. +export const NOCO_TABLES = { + interests: 'mbs9hjauug4eseo', + residentialInterests: 'mscfpwwwjuds4nt', + websiteInterestSubmissions: 'mevkpcih67c6jsm', + websiteContactFormSubmissions: 'mxk5cd0pmwnwlcl', + websiteBerthEoiSupplements: 'mglmioo0ku8zgqj', + berths: 'mczgos9hr3oa9qc', +} as const; + +// ─── HTTP shape ───────────────────────────────────────────────────────────── + +interface NocoDbListResponse { + list: T[]; + pageInfo: { + totalRows: number; + page: number; + pageSize: number; + isFirstPage: boolean; + isLastPage: boolean; + }; +} + +/** A row's `Id` is always present. The rest of the fields vary per table. */ +export type NocoDbRow = Record & { Id: number }; + +// ─── Public API ───────────────────────────────────────────────────────────── + +/** + * Fetch all rows from a NocoDB table. Auto-paginates until the API + * reports `isLastPage`. The legacy base is small (252 Interests rows + * being the largest table) so we keep this simple — no streaming. + */ +export async function fetchAllRows( + tableId: string, + config: NocoDbConfig, + pageSize = 250, +): Promise { + const all: NocoDbRow[] = []; + let page = 1; + // Hard cap to prevent infinite-loop bugs if pageInfo lies. Each page + // pulls up to `pageSize` rows, so 200 pages * 250 = 50k rows is the + // maximum we'll ever fetch from one table. + const MAX_PAGES = 200; + + while (page <= MAX_PAGES) { + const url = new URL(`${config.url}/api/v2/tables/${tableId}/records`); + url.searchParams.set('limit', String(pageSize)); + url.searchParams.set('offset', String((page - 1) * pageSize)); + + const res = await fetch(url, { + headers: { + 'xc-token': config.token, + accept: 'application/json', + }, + }); + + if (!res.ok) { + throw new Error( + `NocoDB fetch failed: ${res.status} ${res.statusText} — table ${tableId} page ${page}`, + ); + } + + const json = (await res.json()) as NocoDbListResponse; + all.push(...json.list); + + if (json.pageInfo.isLastPage || json.list.length === 0) break; + page += 1; + } + + return all; +} + +/** + * Convenience snapshot — pulls every table the migration cares about + * in parallel. Returned shape is the input the transform layer expects. + */ +export interface NocoDbSnapshot { + interests: NocoDbRow[]; + residentialInterests: NocoDbRow[]; + websiteInterestSubmissions: NocoDbRow[]; + websiteContactFormSubmissions: NocoDbRow[]; + websiteBerthEoiSupplements: NocoDbRow[]; + berths: NocoDbRow[]; + fetchedAt: string; +} + +export async function fetchSnapshot(config: NocoDbConfig): Promise { + const [ + interests, + residentialInterests, + websiteInterestSubmissions, + websiteContactFormSubmissions, + websiteBerthEoiSupplements, + berths, + ] = await Promise.all([ + fetchAllRows(NOCO_TABLES.interests, config), + fetchAllRows(NOCO_TABLES.residentialInterests, config), + fetchAllRows(NOCO_TABLES.websiteInterestSubmissions, config), + fetchAllRows(NOCO_TABLES.websiteContactFormSubmissions, config), + fetchAllRows(NOCO_TABLES.websiteBerthEoiSupplements, config), + fetchAllRows(NOCO_TABLES.berths, config), + ]); + + return { + interests, + residentialInterests, + websiteInterestSubmissions, + websiteContactFormSubmissions, + websiteBerthEoiSupplements, + berths, + fetchedAt: new Date().toISOString(), + }; +} diff --git a/src/lib/dedup/normalize.ts b/src/lib/dedup/normalize.ts index 76c274d..5fbe391 100644 --- a/src/lib/dedup/normalize.ts +++ b/src/lib/dedup/normalize.ts @@ -12,7 +12,7 @@ import { z } from 'zod'; import { ALL_COUNTRY_CODES, getCountryName, type CountryCode } from '@/lib/i18n/countries'; -import { parsePhone } from '@/lib/i18n/phone'; +import { parsePhoneScriptSafe as parsePhone } from './phone-parse'; // ─── Names ────────────────────────────────────────────────────────────────── @@ -228,10 +228,18 @@ export function normalizePhone( // 7. Parse via the existing i18n helper (libphonenumber-js under the hood). const parsed = parsePhone(cleaned, defaultCountry); - if (!parsed.e164 || !parsed.isValid) { + if (!parsed.e164) { + // Couldn't even produce a canonical form — genuinely garbage. return { e164: null, country: null, display: null, flagged: 'unparseable' }; } + // Note: we deliberately don't gate on `parsed.isValid`. The + // libphonenumber-js `min` build returns isValid=false for many real + // numbers (NANP territories share +1; some country metadata is + // truncated). For dedup we only need a canonical E.164 string to + // compare; strict validity is the form layer's problem, not ours. + // If a string-only test (e.g. \"abc-not-a-phone\") gets here, parse + // returns null e164 anyway and the branch above handles it. return { e164: parsed.e164, country: parsed.country, @@ -261,6 +269,13 @@ const COUNTRY_ALIASES: Record = { 'st barth': 'BL', 'st barths': 'BL', 'st barthelemy': 'BL', + // Caribbean short-forms whose canonical Intl names are awkward + // ("Antigua and Barbuda", "Saint Vincent and the Grenadines", etc.). + antigua: 'AG', + barbuda: 'AG', + 'st kitts': 'KN', + 'saint kitts': 'KN', + nevis: 'KN', }; /** @@ -275,6 +290,20 @@ const CITY_TO_COUNTRY: Record = { 'kansas city': 'US', 'sag harbor': 'US', 'new york': 'US', + // Cities that came out unresolved from the 2026-05-03 NocoDB dry-run. + // Using lowercase (post-normalize keys). + boston: 'US', + tampa: 'US', + 'fort lauderdale': 'US', + 'port jefferson': 'US', + nantucket: 'US', + // US state abbreviations that often appear standalone or as suffix: + ' fl': 'US', + ' ma': 'US', + ' ny': 'US', + ' tx': 'US', + ' ca': 'US', + // International london: 'GB', paris: 'FR', }; diff --git a/src/lib/dedup/phone-parse.ts b/src/lib/dedup/phone-parse.ts new file mode 100644 index 0000000..74624dc --- /dev/null +++ b/src/lib/dedup/phone-parse.ts @@ -0,0 +1,66 @@ +/** + * Script-safe phone parser. + * + * The project's existing `src/lib/i18n/phone.ts` imports from + * `libphonenumber-js`, which under Node 25 + tsx loader hits a + * metadata-shape interop bug (`{ default }` wrapping the JSON). It + * works fine in Next.js + vitest, but a `tsx scripts/...` invocation + * blows up. + * + * This wrapper bypasses the bundled `index.cjs.js` and calls + * `libphonenumber-js/core` directly with metadata loaded as raw JSON. + * Same surface as the i18n helper; usable from both runtimes. + * + * Used by the dedup library's `normalizePhone`. The runtime UI still + * imports `i18n/phone` directly — no reason to touch a working path. + */ + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const core: typeof import('libphonenumber-js/core') = require('libphonenumber-js/core'); +// Load the JSON directly. The bundled `index.cjs.js` does the same +// thing but its `require('../metadata.min.json')` hits a Node 25 ESM +// interop bug that wraps the JSON in `{ default }`. Importing the +// JSON file by absolute path through the package root sidesteps it. +// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any +const metadata: any = require('libphonenumber-js/metadata.min.json'); + +import type { CountryCode } from '@/lib/i18n/countries'; + +export interface ParsedPhone { + e164: string | null; + country: CountryCode | null; + national: string | null; + international: string | null; + isValid: boolean; +} + +const EMPTY: ParsedPhone = { + e164: null, + country: null, + national: null, + international: null, + isValid: false, +}; + +export function parsePhoneScriptSafe(raw: string, defaultCountry?: CountryCode): ParsedPhone { + const trimmed = raw.trim(); + if (!trimmed) return EMPTY; + try { + // The core entry expects its own `CountryCode` type from + // libphonenumber-js. Our `CountryCode` type is the same set of ISO + // alpha-2 codes (we re-derive from the same Intl source) so this + // cast is structural-equivalent, not lossy. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parsed = core.parsePhoneNumberFromString(trimmed, defaultCountry as any, metadata); + if (!parsed) return EMPTY; + return { + e164: parsed.number, + country: (parsed.country ?? null) as CountryCode | null, + national: parsed.formatNational(), + international: parsed.formatInternational(), + isValid: parsed.isValid(), + }; + } catch { + return EMPTY; + } +} diff --git a/tests/unit/dedup/migration-transform.test.ts b/tests/unit/dedup/migration-transform.test.ts new file mode 100644 index 0000000..42e369a --- /dev/null +++ b/tests/unit/dedup/migration-transform.test.ts @@ -0,0 +1,213 @@ +/** + * Migration transform — fixture-based regression test. + * + * Feeds the transform a small frozen NocoDB snapshot containing one + * representative row from each duplicate pattern documented in + * docs/superpowers/specs/2026-05-03-dedup-and-migration-design.md §1.2, + * and asserts the resulting plan matches the algorithm's expected + * behavior. If any future change starts merging Pattern F (Etiennette + * Clamouze) or stops merging Pattern A (Deepak Ramchandani), this + * test fails immediately. + */ +import { describe, expect, it } from 'vitest'; + +import { transformSnapshot } from '@/lib/dedup/migration-transform'; +import type { NocoDbRow, NocoDbSnapshot } from '@/lib/dedup/nocodb-source'; + +function row(fields: Partial & { Id: number }): NocoDbRow { + return fields as NocoDbRow; +} + +const FIXTURE: NocoDbSnapshot = { + fetchedAt: '2026-05-03T12:00:00.000Z', + berths: [], + residentialInterests: [], + websiteInterestSubmissions: [], + websiteContactFormSubmissions: [], + websiteBerthEoiSupplements: [], + interests: [ + // Pattern A: pure double-submit (Deepak Ramchandani #624/#625) + row({ + Id: 624, + 'Full Name': 'Deepak Ramchandani', + 'Email Address': 'dannyrams8888@gmail.com', + 'Phone Number': '+17215868888', + 'Sales Process Level': 'General Qualified Interest', + }), + row({ + Id: 625, + 'Full Name': 'Deepak Ramchandani', + 'Email Address': 'dannyrams8888@gmail.com', + 'Phone Number': '+17215868888', + 'Sales Process Level': 'General Qualified Interest', + }), + + // Pattern B: phone format variance (Howard Wiarda #236/#536) + row({ + Id: 236, + 'Full Name': 'Howard Wiarda', + 'Email Address': 'hwiarda@hotmail.com', + 'Phone Number': '574-274-0548', + 'Place of Residence': 'USA', + 'Sales Process Level': 'General Qualified Interest', + }), + row({ + Id: 536, + 'Full Name': 'Howard Wiarda', + 'Email Address': 'hwiarda@hotmail.com', + 'Phone Number': '+15742740548', + 'Sales Process Level': 'General Qualified Interest', + }), + + // Pattern C: name capitalization (Nicolas Ruiz #681/#682/#683 — three rows) + row({ + Id: 681, + 'Full Name': 'Nicolas Ruiz', + 'Email Address': 'ruiz.nicolas@ufl.edu', + 'Phone Number': '+17862006617', + 'Sales Process Level': 'General Qualified Interest', + }), + row({ + Id: 682, + 'Full Name': 'Nicolas Ruiz', + 'Email Address': 'ruiz.nicolas@ufl.edu', + 'Phone Number': '+17862006617', + 'Sales Process Level': 'Specific Qualified Interest', + }), + row({ + Id: 683, + 'Full Name': 'Nicolas Ruiz', + 'Email Address': 'Ruiz.Nicolas@ufl.edu', + 'Phone Number': '+17862006617', + 'Sales Process Level': 'General Qualified Interest', + }), + + // Pattern E: surname typo with same email + phone (Constanzo/Costanzo) + row({ + Id: 336, + 'Full Name': 'Gianfranco Di Costanzo', + 'Email Address': 'gdc@nauticall.com', + 'Phone Number': '+17542628669', + 'Yacht Name': 'GEMINI', + 'Sales Process Level': 'Contract Signed', + }), + row({ + Id: 585, + 'Full Name': 'Gianfranco Di Constanzo', + 'Email Address': 'gdc@nauticall.com', + 'Phone Number': '+17542628669', + 'Yacht Name': 'CALYPSO', + 'Sales Process Level': 'Signed EOI and NDA', + }), + + // Pattern F: same name, different country phones (Etiennette Clamouze) + row({ + Id: 188, + 'Full Name': 'Etiennette Clamouze', + 'Email Address': 'clamouze.etiennette@gmail.com', + 'Phone Number': '+33767780640', + 'Sales Process Level': 'General Qualified Interest', + }), + row({ + Id: 717, + 'Full Name': 'Etiennette Clamouze', + 'Email Address': 'Etiennette@the-manoah.com', + 'Phone Number': '+12645815607', + 'Sales Process Level': 'General Qualified Interest', + }), + + // Single isolated row to verify non-duplicates pass through + row({ + Id: 999, + 'Full Name': 'Lone Wolf', + 'Email Address': 'lone@example.com', + 'Phone Number': '+15551234567', + 'Sales Process Level': 'General Qualified Interest', + }), + ], +}; + +describe('transformSnapshot — fixture regression', () => { + it('produces the expected number of clients + interests', () => { + const plan = transformSnapshot(FIXTURE); + + // 12 input rows → 7 unique clients (Deepak: 1, Wiarda: 1, Ruiz: 1, + // Constanzo: 1, Etiennette x2: 2, Lone: 1). Etiennette stays as 2 + // because Pattern F is correctly NOT auto-merged. + expect(plan.stats.outputClients).toBe(7); + expect(plan.stats.outputInterests).toBe(12); // one per source row + }); + + it('auto-links every Pattern A–E cluster', () => { + const plan = transformSnapshot(FIXTURE); + const linkedSourceIds = new Set(); + for (const link of plan.autoLinks) { + linkedSourceIds.add(link.leadSourceId); + for (const merged of link.mergedSourceIds) { + linkedSourceIds.add(merged); + } + } + + // Pattern A: 624 + 625 + expect(linkedSourceIds.has(624) && linkedSourceIds.has(625)).toBe(true); + // Pattern B: 236 + 536 + expect(linkedSourceIds.has(236) && linkedSourceIds.has(536)).toBe(true); + // Pattern C: 681 + 682 + 683 (three-way) + expect(linkedSourceIds.has(681) && linkedSourceIds.has(682) && linkedSourceIds.has(683)).toBe( + true, + ); + // Pattern E: 336 + 585 + expect(linkedSourceIds.has(336) && linkedSourceIds.has(585)).toBe(true); + }); + + it('does NOT auto-link Pattern F (Etiennette Clamouze, different country)', () => { + const plan = transformSnapshot(FIXTURE); + const linkedSourceIds = new Set(); + for (const link of plan.autoLinks) { + linkedSourceIds.add(link.leadSourceId); + for (const merged of link.mergedSourceIds) { + linkedSourceIds.add(merged); + } + } + // Both Etiennette rows must remain as separate clients. + expect(linkedSourceIds.has(188)).toBe(false); + expect(linkedSourceIds.has(717)).toBe(false); + }); + + it('preserves every interest as its own row even when clients merge', () => { + const plan = transformSnapshot(FIXTURE); + const sourceIds = plan.interests.map((i) => i.sourceId).sort((a, b) => a - b); + expect(sourceIds).toEqual([188, 236, 336, 536, 585, 624, 625, 681, 682, 683, 717, 999]); + }); + + it('maps the legacy 8-stage enum to new pipeline stages', () => { + const plan = transformSnapshot(FIXTURE); + const stagesById = new Map(plan.interests.map((i) => [i.sourceId, i.pipelineStage])); + expect(stagesById.get(681)).toBe('open'); // General Qualified Interest + expect(stagesById.get(682)).toBe('details_sent'); // Specific Qualified Interest + expect(stagesById.get(336)).toBe('contract_signed'); // Contract Signed + expect(stagesById.get(585)).toBe('eoi_signed'); // Signed EOI and NDA + }); + + it('attaches different yachts to one merged Constanzo client', () => { + const plan = transformSnapshot(FIXTURE); + const constanzoClient = plan.clients.find( + (c) => c.sourceIds.includes(336) && c.sourceIds.includes(585), + ); + expect(constanzoClient).toBeDefined(); + const yachtsForConstanzo = plan.interests + .filter((i) => i.clientTempId === constanzoClient!.tempId) + .map((i) => i.yachtName) + .sort(); + expect(yachtsForConstanzo).toEqual(['CALYPSO', 'GEMINI']); + }); + + it('produces deterministic output (same input → same plan)', () => { + // The transform is pure — running it twice should yield bit-identical + // results. Catches order-dependent bugs in the dedup clustering. + const a = transformSnapshot(FIXTURE); + const b = transformSnapshot(FIXTURE); + expect(JSON.stringify(a.stats)).toBe(JSON.stringify(b.stats)); + expect(a.autoLinks.length).toBe(b.autoLinks.length); + }); +});