chore(i18n): drop legacy free-text country/nationality columns

Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).

Migration 0016 drops:
  - clients.nationality
  - companies.incorporation_country
  - client_addresses.{state_province, country}
  - company_addresses.{state_province, country}

Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.

Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.

Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.

Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').

Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-28 19:00:57 +02:00
parent 31fa3d08ec
commit 27cdbcc695
30 changed files with 9959 additions and 104 deletions

View File

@@ -7,6 +7,7 @@ import { companies, companyAddresses } from '@/lib/db/schema/companies';
import { interests } from '@/lib/db/schema/interests';
import { ports } from '@/lib/db/schema/ports';
import { yachts } from '@/lib/db/schema/yachts';
import { getCountryName } from '@/lib/i18n/countries';
import { NotFoundError, ValidationError } from '@/lib/errors';
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -136,12 +137,13 @@ export async function buildEoiContext(interestId: string, portId: string): Promi
contactRows.find((c) => c.channel === 'phone') ??
contactRows.find((c) => c.channel === 'whatsapp');
// 6. Primary address.
// 6. Primary address. Country is rendered as the localized name (English by
// default for documents) from the ISO code.
const [primaryAddress] = await db
.select({
streetAddress: clientAddresses.streetAddress,
city: clientAddresses.city,
country: clientAddresses.country,
countryIso: clientAddresses.countryIso,
})
.from(clientAddresses)
.where(and(eq(clientAddresses.clientId, client.id), eq(clientAddresses.isPrimary, true)))
@@ -151,7 +153,7 @@ export async function buildEoiContext(interestId: string, portId: string): Promi
? {
street: primaryAddress.streetAddress ?? '',
city: primaryAddress.city ?? '',
country: primaryAddress.country ?? '',
country: primaryAddress.countryIso ? getCountryName(primaryAddress.countryIso, 'en') : '',
}
: null;
@@ -185,7 +187,7 @@ export async function buildEoiContext(interestId: string, portId: string): Promi
.select({
streetAddress: companyAddresses.streetAddress,
city: companyAddresses.city,
country: companyAddresses.country,
countryIso: companyAddresses.countryIso,
})
.from(companyAddresses)
.where(and(eq(companyAddresses.companyId, company.id), eq(companyAddresses.isPrimary, true)))
@@ -195,7 +197,9 @@ export async function buildEoiContext(interestId: string, portId: string): Promi
? [
companyPrimaryAddress.streetAddress,
companyPrimaryAddress.city,
companyPrimaryAddress.country,
companyPrimaryAddress.countryIso
? getCountryName(companyPrimaryAddress.countryIso, 'en')
: null,
]
.filter((s): s is string => Boolean(s))
.join(', ') || null
@@ -219,7 +223,7 @@ export async function buildEoiContext(interestId: string, portId: string): Promi
return {
client: {
fullName: client.fullName,
nationality: client.nationality,
nationality: client.nationalityIso ? getCountryName(client.nationalityIso, 'en') : null,
primaryEmail: firstEmail?.value ?? null,
primaryPhone: firstPhone?.value ?? null,
address: clientAddress,