feat(migration): old-LOI EOI recovery, folded berth-links, contactless flag
Three polish items so the legacy seed is one-shot and complete: - backfill-documents: recover the ~10 pre-Documenso "LOI process" EOIs whose signed PDF lives only as a NocoDB attachment in the `database` MinIO bucket (the pipeline keys EOI-doc creation off documensoID, so it never created rows for them). Reads EOI_Document attachment metadata from the local nocodb_legacy dump, pulls the PDF (read-only) from the `database` bucket, and CREATES the document + file + folder, linking the signed PDF. Idempotent via a `nocodb_eoi_document` ledger entry. - connect-berth-links: refactored into an exported connectBerthLinks() and folded into migrate-from-nocodb --apply (best-effort; skips with a warning if the local dump isn't restored) so the multi-berth junction is reconnected as part of the one-shot seed, not a separate manual step. - migration-apply: contactless legacy clients (no email/phone across the whole dedup cluster) get a per-port "Needs contact info" tag so staff can filter + chase them, instead of being dropped. The current dev DB's 29 contactless clients were tagged via a one-off mirroring the pipeline logic. EOI recovery code is ready but the actual run needs LEGACY_MINIO_* read creds supplied at the command line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,13 +19,14 @@
|
||||
import { and, eq, inArray } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { clients, clientContacts, clientAddresses } from '@/lib/db/schema/clients';
|
||||
import { clients, clientContacts, clientAddresses, clientTags } from '@/lib/db/schema/clients';
|
||||
import { interests, interestBerths } from '@/lib/db/schema/interests';
|
||||
import { yachts } from '@/lib/db/schema/yachts';
|
||||
import { berths } from '@/lib/db/schema/berths';
|
||||
import { documents, documentSigners } from '@/lib/db/schema/documents';
|
||||
import { residentialClients, residentialInterests } from '@/lib/db/schema/residential';
|
||||
import { expenses } from '@/lib/db/schema/financial';
|
||||
import { tags } from '@/lib/db/schema/system';
|
||||
import { migrationSourceLinks } from '@/lib/db/schema/migration';
|
||||
import type {
|
||||
MigrationPlan,
|
||||
@@ -38,6 +39,29 @@ import type {
|
||||
|
||||
const SOURCE_SYSTEM = 'nocodb_interests';
|
||||
|
||||
/** Tag applied to legacy clients that arrived with no email/phone, so staff
|
||||
* can filter + chase them. Kept here (not in the transform) because it's an
|
||||
* apply-side side effect, not part of the canonical planned shape. */
|
||||
const CONTACTLESS_TAG_NAME = 'Needs contact info';
|
||||
const contactlessTagByPort = new Map<string, string>();
|
||||
|
||||
async function ensureContactlessTag(portId: string): Promise<string> {
|
||||
const cached = contactlessTagByPort.get(portId);
|
||||
if (cached) return cached;
|
||||
await db
|
||||
.insert(tags)
|
||||
.values({ portId, name: CONTACTLESS_TAG_NAME, color: '#F59E0B' })
|
||||
.onConflictDoNothing();
|
||||
const [row] = await db
|
||||
.select({ id: tags.id })
|
||||
.from(tags)
|
||||
.where(and(eq(tags.portId, portId), eq(tags.name, CONTACTLESS_TAG_NAME)))
|
||||
.limit(1);
|
||||
if (!row) throw new Error('failed to ensure contactless tag');
|
||||
contactlessTagByPort.set(portId, row.id);
|
||||
return row.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonicalize a mooring string to the unified form ("A1", "D32", "E18")
|
||||
* used by the new berths schema after the Phase 0 mooring-number sweep.
|
||||
@@ -223,6 +247,14 @@ async function applyClient(
|
||||
result.addressesInserted += addressRows.length;
|
||||
}
|
||||
|
||||
// Flag legacy records that arrived with no contact info so staff can chase
|
||||
// them. The dedup cluster aggregates all source rows, so an empty contacts
|
||||
// list means truly no email/phone for this person across every merged row.
|
||||
if (planned.contacts.length === 0) {
|
||||
const tagId = await ensureContactlessTag(opts.port.id);
|
||||
await db.insert(clientTags).values({ clientId, tagId }).onConflictDoNothing();
|
||||
}
|
||||
|
||||
result.clientsInserted += 1;
|
||||
return { clientId, inserted: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user