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:
@@ -39,7 +39,9 @@ export async function listClients(portId: string, query: ListClientsInput) {
|
||||
filters.push(eq(clients.source, source));
|
||||
}
|
||||
if (nationality) {
|
||||
filters.push(ilike(clients.nationality, `%${nationality}%`));
|
||||
// Filter accepts an ISO-3166-1 alpha-2 code; legacy free-text matching is
|
||||
// gone after the i18n column drop.
|
||||
filters.push(eq(clients.nationalityIso, nationality.toUpperCase()));
|
||||
}
|
||||
if (tagIds && tagIds.length > 0) {
|
||||
const clientsWithTags = await db
|
||||
|
||||
@@ -64,7 +64,8 @@ export async function createCompany(portId: string, data: CreateCompanyInput, me
|
||||
legalName: data.legalName ?? null,
|
||||
taxId: data.taxId ?? null,
|
||||
registrationNumber: data.registrationNumber ?? null,
|
||||
incorporationCountry: data.incorporationCountry ?? null,
|
||||
incorporationCountryIso: data.incorporationCountryIso ?? null,
|
||||
incorporationSubdivisionIso: data.incorporationSubdivisionIso ?? null,
|
||||
incorporationDate: data.incorporationDate ?? null,
|
||||
status: data.status ?? 'active',
|
||||
billingEmail: data.billingEmail ?? null,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { minioClient, buildStoragePath } from '@/lib/minio';
|
||||
import { env } from '@/lib/env';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { generatePdf } from '@/lib/pdf/generate';
|
||||
import { getCountryName } from '@/lib/i18n/countries';
|
||||
import {
|
||||
createDocument as documensoCreate,
|
||||
sendDocument as documensoSend,
|
||||
@@ -344,7 +345,9 @@ export async function resolveTemplate(
|
||||
tokenMap['{{client.fullName}}'] = client.fullName ?? '';
|
||||
tokenMap['{{client.email}}'] = emailContact?.value ?? '';
|
||||
tokenMap['{{client.phone}}'] = phoneContact?.value ?? '';
|
||||
tokenMap['{{client.nationality}}'] = client.nationality ?? '';
|
||||
tokenMap['{{client.nationality}}'] = client.nationalityIso
|
||||
? getCountryName(client.nationalityIso, 'en')
|
||||
: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,6 +13,8 @@ import { createAuditLog } from '@/lib/audit';
|
||||
import { diffEntity } from '@/lib/entity-diff';
|
||||
import { withTransaction } from '@/lib/db/utils';
|
||||
import { NotFoundError, ConflictError, ValidationError } from '@/lib/errors';
|
||||
import { getCountryName } from '@/lib/i18n/countries';
|
||||
import { getSubdivisionName } from '@/lib/i18n/subdivisions';
|
||||
import { emitToRoom } from '@/lib/socket/server';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { generatePdf } from '@/lib/pdf/generate';
|
||||
@@ -99,9 +101,9 @@ async function resolveBillingEntity(
|
||||
? [
|
||||
addressRow.streetAddress,
|
||||
addressRow.city,
|
||||
addressRow.stateProvince,
|
||||
addressRow.subdivisionIso ? getSubdivisionName(addressRow.subdivisionIso) : null,
|
||||
addressRow.postalCode,
|
||||
addressRow.country,
|
||||
addressRow.countryIso ? getCountryName(addressRow.countryIso, 'en') : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(', ')
|
||||
@@ -126,9 +128,9 @@ async function resolveBillingEntity(
|
||||
? [
|
||||
addressRow.streetAddress,
|
||||
addressRow.city,
|
||||
addressRow.stateProvince,
|
||||
addressRow.subdivisionIso ? getSubdivisionName(addressRow.subdivisionIso) : null,
|
||||
addressRow.postalCode,
|
||||
addressRow.country,
|
||||
addressRow.countryIso ? getCountryName(addressRow.countryIso, 'en') : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(', ')
|
||||
|
||||
@@ -11,6 +11,7 @@ import { yachts } from '@/lib/db/schema/yachts';
|
||||
import { companies, companyMemberships } from '@/lib/db/schema/companies';
|
||||
import { berthReservations } from '@/lib/db/schema/reservations';
|
||||
import { getPresignedUrl } from '@/lib/minio';
|
||||
import { getCountryName } from '@/lib/i18n/countries';
|
||||
|
||||
// ─── Dashboard ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -82,7 +83,7 @@ export async function getPortalDashboard(
|
||||
client: {
|
||||
id: client.id,
|
||||
fullName: client.fullName,
|
||||
nationality: client.nationality ?? null,
|
||||
nationality: client.nationalityIso ? getCountryName(client.nationalityIso, 'en') : null,
|
||||
},
|
||||
port: {
|
||||
name: port.name,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { berthReservations } from '@/lib/db/schema/reservations';
|
||||
import { clients } from '@/lib/db/schema/clients';
|
||||
import { ports } from '@/lib/db/schema/ports';
|
||||
import { yachts } from '@/lib/db/schema/yachts';
|
||||
import { getCountryName } from '@/lib/i18n/countries';
|
||||
import { NotFoundError } from '@/lib/errors';
|
||||
|
||||
export type ReservationAgreementContext = {
|
||||
@@ -100,7 +101,7 @@ export async function buildReservationAgreementContext(
|
||||
client: {
|
||||
id: client.id,
|
||||
fullName: client.fullName,
|
||||
nationality: client.nationality,
|
||||
nationality: client.nationalityIso ? getCountryName(client.nationalityIso, 'en') : null,
|
||||
},
|
||||
yacht: {
|
||||
id: yacht.id,
|
||||
|
||||
Reference in New Issue
Block a user