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:
6
src/lib/db/migrations/0016_magical_spyke.sql
Normal file
6
src/lib/db/migrations/0016_magical_spyke.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "client_addresses" DROP COLUMN "state_province";--> statement-breakpoint
|
||||
ALTER TABLE "client_addresses" DROP COLUMN "country";--> statement-breakpoint
|
||||
ALTER TABLE "clients" DROP COLUMN "nationality";--> statement-breakpoint
|
||||
ALTER TABLE "companies" DROP COLUMN "incorporation_country";--> statement-breakpoint
|
||||
ALTER TABLE "company_addresses" DROP COLUMN "state_province";--> statement-breakpoint
|
||||
ALTER TABLE "company_addresses" DROP COLUMN "country";
|
||||
9849
src/lib/db/migrations/meta/0016_snapshot.json
Normal file
9849
src/lib/db/migrations/meta/0016_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -113,6 +113,13 @@
|
||||
"when": 1777391373291,
|
||||
"tag": "0015_i18n_columns",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 16,
|
||||
"version": "7",
|
||||
"when": 1777395538988,
|
||||
"tag": "0016_magical_spyke",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,9 +21,7 @@ export const clients = pgTable(
|
||||
.notNull()
|
||||
.references(() => ports.id),
|
||||
fullName: text('full_name').notNull(),
|
||||
nationality: text('nationality'),
|
||||
/** ISO-3166-1 alpha-2 nationality code. Supersedes `nationality`
|
||||
* after the i18n backfill (PR10) drops the legacy column. */
|
||||
/** ISO-3166-1 alpha-2 nationality code. */
|
||||
nationalityIso: text('nationality_iso'),
|
||||
preferredContactMethod: text('preferred_contact_method'), // email, phone, whatsapp
|
||||
preferredLanguage: text('preferred_language'),
|
||||
@@ -162,12 +160,10 @@ export const clientAddresses = pgTable(
|
||||
label: text('label').notNull().default('Primary'),
|
||||
streetAddress: text('street_address'),
|
||||
city: text('city'),
|
||||
stateProvince: text('state_province'),
|
||||
/** ISO 3166-2 subdivision code (e.g. 'PL-MZ', 'US-CA'). Optional. */
|
||||
subdivisionIso: text('subdivision_iso'),
|
||||
postalCode: text('postal_code'),
|
||||
country: text('country'),
|
||||
/** ISO-3166-1 alpha-2 country code. Supersedes `country` after backfill. */
|
||||
/** ISO-3166-1 alpha-2 country code. */
|
||||
countryIso: text('country_iso'),
|
||||
isPrimary: boolean('is_primary').notNull().default(true),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
@@ -24,9 +24,7 @@ export const companies = pgTable(
|
||||
legalName: text('legal_name'),
|
||||
taxId: text('tax_id'),
|
||||
registrationNumber: text('registration_number'),
|
||||
incorporationCountry: text('incorporation_country'),
|
||||
/** ISO-3166-1 alpha-2 country of incorporation. Replaces the
|
||||
* free-text `incorporation_country` after the i18n backfill. */
|
||||
/** ISO-3166-1 alpha-2 country of incorporation. */
|
||||
incorporationCountryIso: text('incorporation_country_iso'),
|
||||
/** ISO 3166-2 subdivision (state/province) of incorporation. Optional. */
|
||||
incorporationSubdivisionIso: text('incorporation_subdivision_iso'),
|
||||
@@ -93,12 +91,10 @@ export const companyAddresses = pgTable(
|
||||
label: text('label').notNull().default('Primary'),
|
||||
streetAddress: text('street_address'),
|
||||
city: text('city'),
|
||||
stateProvince: text('state_province'),
|
||||
/** ISO 3166-2 subdivision code. Optional. */
|
||||
subdivisionIso: text('subdivision_iso'),
|
||||
postalCode: text('postal_code'),
|
||||
country: text('country'),
|
||||
/** ISO-3166-1 alpha-2 country code. Supersedes `country` after backfill. */
|
||||
/** ISO-3166-1 alpha-2 country code. */
|
||||
countryIso: text('country_iso'),
|
||||
isPrimary: boolean('is_primary').notNull().default(true),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
@@ -230,7 +230,7 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
legalName: 'Aegean Holdings Ltd.',
|
||||
taxId: `AH-${portSlug}-001`,
|
||||
registrationNumber: 'AH-2019-8842',
|
||||
incorporationCountry: 'Greece',
|
||||
incorporationCountryIso: 'GR',
|
||||
incorporationDate: new Date('2019-03-14'),
|
||||
status: 'active',
|
||||
billingEmail: `billing@aegean-holdings.example`,
|
||||
@@ -242,7 +242,7 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
legalName: 'Blue Seas Marine S.A.',
|
||||
taxId: `BSM-${portSlug}-002`,
|
||||
registrationNumber: 'BSM-2021-3310',
|
||||
incorporationCountry: 'Monaco',
|
||||
incorporationCountryIso: 'MC',
|
||||
incorporationDate: new Date('2021-07-02'),
|
||||
status: 'active',
|
||||
billingEmail: `accounts@blueseas-marine.example`,
|
||||
@@ -254,7 +254,7 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
legalName: 'Phantom Maritime SA',
|
||||
taxId: `PHT-${portSlug}-003`,
|
||||
registrationNumber: 'PHT-2017-7001',
|
||||
incorporationCountry: 'Panama',
|
||||
incorporationCountryIso: 'PA',
|
||||
incorporationDate: new Date('2017-11-20'),
|
||||
status: 'dissolved',
|
||||
billingEmail: null,
|
||||
@@ -276,9 +276,9 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
label: 'Head Office',
|
||||
streetAddress: '14 Mikonou Avenue',
|
||||
city: 'Athens',
|
||||
stateProvince: 'Attica',
|
||||
subdivisionIso: 'GR-A',
|
||||
postalCode: '10558',
|
||||
country: 'Greece',
|
||||
countryIso: 'GR',
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
@@ -287,9 +287,9 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
label: 'Registered Office',
|
||||
streetAddress: '3 Boulevard des Moulins',
|
||||
city: 'Monte Carlo',
|
||||
stateProvince: null,
|
||||
subdivisionIso: null,
|
||||
postalCode: 'MC-98000',
|
||||
country: 'Monaco',
|
||||
countryIso: 'MC',
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
@@ -298,9 +298,9 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
label: 'Former Office',
|
||||
streetAddress: 'Calle 50, Torre Global, Piso 20',
|
||||
city: 'Panama City',
|
||||
stateProvince: null,
|
||||
subdivisionIso: null,
|
||||
postalCode: '0801',
|
||||
country: 'Panama',
|
||||
countryIso: 'PA',
|
||||
isPrimary: true,
|
||||
},
|
||||
]);
|
||||
@@ -313,96 +313,96 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
// 7 → Phantom SA (ended membership)
|
||||
const CLIENT_SPECS: Array<{
|
||||
fullName: string;
|
||||
nationality: string;
|
||||
nationalityIso: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
whatsapp?: string;
|
||||
city: string;
|
||||
country: string;
|
||||
countryIso: string;
|
||||
postalCode: string;
|
||||
street: string;
|
||||
}> = [
|
||||
{
|
||||
fullName: 'Helena Marsh',
|
||||
nationality: 'British',
|
||||
nationalityIso: 'GB',
|
||||
email: 'helena.marsh@example.com',
|
||||
phone: '+44 20 7946 0001',
|
||||
whatsapp: '+44 7700 900001',
|
||||
city: 'London',
|
||||
country: 'United Kingdom',
|
||||
countryIso: 'GB',
|
||||
postalCode: 'SW1A 1AA',
|
||||
street: '22 Belgrave Square',
|
||||
},
|
||||
{
|
||||
fullName: 'Marcus Laurent',
|
||||
nationality: 'French',
|
||||
nationalityIso: 'FR',
|
||||
email: 'marcus.laurent@example.com',
|
||||
phone: '+33 4 93 00 0002',
|
||||
city: 'Nice',
|
||||
country: 'France',
|
||||
countryIso: 'FR',
|
||||
postalCode: '06300',
|
||||
street: '8 Promenade des Anglais',
|
||||
},
|
||||
{
|
||||
fullName: 'Sofia Reyes',
|
||||
nationality: 'Spanish',
|
||||
nationalityIso: 'ES',
|
||||
email: 'sofia.reyes@example.com',
|
||||
phone: '+34 971 000 003',
|
||||
whatsapp: '+34 666 000 003',
|
||||
city: 'Palma',
|
||||
country: 'Spain',
|
||||
countryIso: 'ES',
|
||||
postalCode: '07012',
|
||||
street: 'Passeig Marítim 12',
|
||||
},
|
||||
{
|
||||
fullName: 'Dimitrios Andreadis',
|
||||
nationality: 'Greek',
|
||||
nationalityIso: 'GR',
|
||||
email: 'd.andreadis@aegean-holdings.example',
|
||||
phone: '+30 210 000 0004',
|
||||
city: 'Athens',
|
||||
country: 'Greece',
|
||||
countryIso: 'GR',
|
||||
postalCode: '10558',
|
||||
street: '14 Mikonou Avenue',
|
||||
},
|
||||
{
|
||||
fullName: 'Katerina Papadakis',
|
||||
nationality: 'Greek',
|
||||
nationalityIso: 'GR',
|
||||
email: 'k.papadakis@aegean-holdings.example',
|
||||
phone: '+30 210 000 0005',
|
||||
whatsapp: '+30 694 000 0005',
|
||||
city: 'Athens',
|
||||
country: 'Greece',
|
||||
countryIso: 'GR',
|
||||
postalCode: '10558',
|
||||
street: '14 Mikonou Avenue',
|
||||
},
|
||||
{
|
||||
fullName: 'Jonas Lindqvist',
|
||||
nationality: 'Swedish',
|
||||
nationalityIso: 'SE',
|
||||
email: 'jonas.lindqvist@example.com',
|
||||
phone: '+46 8 000 0006',
|
||||
city: 'Stockholm',
|
||||
country: 'Sweden',
|
||||
countryIso: 'SE',
|
||||
postalCode: '11129',
|
||||
street: 'Strandvägen 47',
|
||||
},
|
||||
{
|
||||
fullName: 'Isabella Conti',
|
||||
nationality: 'Italian',
|
||||
nationalityIso: 'IT',
|
||||
email: 'isabella.conti@example.com',
|
||||
phone: '+39 010 000 0007',
|
||||
whatsapp: '+39 333 000 0007',
|
||||
city: 'Genoa',
|
||||
country: 'Italy',
|
||||
countryIso: 'IT',
|
||||
postalCode: '16124',
|
||||
street: 'Via Garibaldi 9',
|
||||
},
|
||||
{
|
||||
fullName: 'Raymond Osei',
|
||||
nationality: 'Ghanaian',
|
||||
nationalityIso: 'GH',
|
||||
email: 'raymond.osei@example.com',
|
||||
phone: '+233 30 000 0008',
|
||||
city: 'Accra',
|
||||
country: 'Ghana',
|
||||
countryIso: 'GH',
|
||||
postalCode: 'GA-183-1090',
|
||||
street: '21 Independence Ave',
|
||||
},
|
||||
@@ -414,7 +414,7 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
CLIENT_SPECS.map((c) => ({
|
||||
portId,
|
||||
fullName: c.fullName,
|
||||
nationality: c.nationality,
|
||||
nationalityIso: c.nationalityIso,
|
||||
preferredContactMethod: 'email' as const,
|
||||
preferredLanguage: 'en',
|
||||
source: 'referral' as const,
|
||||
@@ -462,9 +462,9 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
label: 'Primary',
|
||||
streetAddress: c.street,
|
||||
city: c.city,
|
||||
stateProvince: null,
|
||||
subdivisionIso: null,
|
||||
postalCode: c.postalCode,
|
||||
country: c.country,
|
||||
countryIso: c.countryIso,
|
||||
isPrimary: true,
|
||||
})),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user