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

@@ -126,7 +126,7 @@ describe('resolveTemplate — EOI scope tokens', () => {
const port = await makePort();
const client = await makeClient({
portId: port.id,
overrides: { fullName: 'Alice Client', nationality: 'US', source: 'referral' },
overrides: { fullName: 'Alice Client', nationalityIso: 'US', source: 'referral' },
});
await db.insert(clientContacts).values([
{ clientId: client.id, channel: 'email', value: 'alice@example.com', isPrimary: true },
@@ -137,7 +137,7 @@ describe('resolveTemplate — EOI scope tokens', () => {
portId: port.id,
streetAddress: '1 Main St',
city: 'Town',
country: 'US',
countryIso: 'US',
isPrimary: true,
});
@@ -321,7 +321,7 @@ describe('resolveTemplate — legacy fallback (no interestId)', () => {
const port = await makePort();
const client = await makeClient({
portId: port.id,
overrides: { fullName: 'Carol NoInterest', nationality: 'UK', source: 'website' },
overrides: { fullName: 'Carol NoInterest', nationalityIso: 'GB', source: 'website' },
});
await db.insert(clientContacts).values({
clientId: client.id,
@@ -349,7 +349,8 @@ describe('resolveTemplate — legacy fallback (no interestId)', () => {
expect(resolved).toContain('Hello Carol NoInterest');
expect(resolved).toContain('carol@example.com');
expect(resolved).toContain('from UK');
// Nationality renders the localized name from the ISO code (GB -> United Kingdom).
expect(resolved).toContain('from United Kingdom');
expect(resolved).toContain('src=website');
});

View File

@@ -108,7 +108,7 @@ beforeAll(async () => {
const port = await makePort();
const client = await makeClient({
portId: port.id,
overrides: { fullName: 'Dual Path Client', nationality: 'US' },
overrides: { fullName: 'Dual Path Client', nationalityIso: 'US' },
});
await db.insert(clientContacts).values({
clientId: client.id,
@@ -121,7 +121,7 @@ beforeAll(async () => {
portId: port.id,
streetAddress: '1 Wharf Rd',
city: 'Harbor',
country: 'US',
countryIso: 'US',
isPrimary: true,
});

View File

@@ -161,9 +161,9 @@ describe('invoices.service — billing entity', () => {
label: 'Primary',
streetAddress: '1 Pier Road',
city: 'Harbor City',
stateProvince: 'CA',
subdivisionIso: 'US-CA',
postalCode: '90000',
country: 'USA',
countryIso: 'US',
isPrimary: true,
});
@@ -180,7 +180,10 @@ describe('invoices.service — billing entity', () => {
);
expect(invoice.billingEmail).toBe('bob@example.com');
expect(invoice.billingAddress).toBe('1 Pier Road, Harbor City, CA, 90000, USA');
// Address is rendered using ISO->name lookup (US-CA -> California, US -> United States).
expect(invoice.billingAddress).toBe(
'1 Pier Road, Harbor City, California, 90000, United States',
);
});
it('allows caller to override billingEmail and billingAddress', async () => {
@@ -196,9 +199,9 @@ describe('invoices.service — billing entity', () => {
label: 'Primary',
streetAddress: '2 Ocean Blvd',
city: 'Portville',
stateProvince: 'FL',
subdivisionIso: 'US-FL',
postalCode: '33101',
country: 'USA',
countryIso: 'US',
isPrimary: true,
});

View File

@@ -35,7 +35,7 @@ describe('buildEoiContext', () => {
const port = await makePort();
const client = await makeClient({
portId: port.id,
overrides: { fullName: 'Alice Test', nationality: 'US' },
overrides: { fullName: 'Alice Test', nationalityIso: 'US' },
});
// Insert contacts.
@@ -60,7 +60,7 @@ describe('buildEoiContext', () => {
portId: port.id,
streetAddress: '1 Harbour Way',
city: 'Anguilla',
country: 'AI',
countryIso: 'AI',
isPrimary: true,
});
@@ -94,15 +94,16 @@ describe('buildEoiContext', () => {
const ctx = await buildEoiContext(interest.id, port.id);
// Client assertions.
// Client assertions. Nationality + address country are rendered as
// localized names (Intl.DisplayNames) from the ISO codes.
expect(ctx.client.fullName).toBe('Alice Test');
expect(ctx.client.nationality).toBe('US');
expect(ctx.client.nationality).toBe('United States');
expect(ctx.client.primaryEmail).toBe('alice@example.com');
expect(ctx.client.primaryPhone).toBe('+1-555-1234');
expect(ctx.client.address).toEqual({
street: '1 Harbour Way',
city: 'Anguilla',
country: 'AI',
country: 'Anguilla',
});
// Yacht assertions.
@@ -181,7 +182,7 @@ describe('buildEoiContext', () => {
portId: port.id,
streetAddress: '99 Commerce St',
city: 'Valley',
country: 'AI',
countryIso: 'AI',
isPrimary: true,
});
@@ -206,7 +207,8 @@ describe('buildEoiContext', () => {
expect(ctx.company!.billingAddress).not.toBeNull();
expect(ctx.company!.billingAddress).toContain('99 Commerce St');
expect(ctx.company!.billingAddress).toContain('Valley');
expect(ctx.company!.billingAddress).toContain('AI');
// Country is rendered as the localized name (AI -> Anguilla).
expect(ctx.company!.billingAddress).toContain('Anguilla');
});
it('throws ValidationError when interest has no yacht', async () => {