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:
@@ -144,7 +144,6 @@ export async function POST(req: NextRequest) {
|
|||||||
name: data.company.name,
|
name: data.company.name,
|
||||||
legalName: data.company.legalName ?? null,
|
legalName: data.company.legalName ?? null,
|
||||||
taxId: data.company.taxId ?? null,
|
taxId: data.company.taxId ?? null,
|
||||||
incorporationCountry: data.company.incorporationCountry ?? null,
|
|
||||||
incorporationCountryIso: data.company.incorporationCountryIso ?? null,
|
incorporationCountryIso: data.company.incorporationCountryIso ?? null,
|
||||||
incorporationSubdivisionIso: data.company.incorporationSubdivisionIso ?? null,
|
incorporationSubdivisionIso: data.company.incorporationSubdivisionIso ?? null,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
@@ -216,10 +215,8 @@ export async function POST(req: NextRequest) {
|
|||||||
label: 'Primary',
|
label: 'Primary',
|
||||||
streetAddress: data.address.street ?? null,
|
streetAddress: data.address.street ?? null,
|
||||||
city: data.address.city ?? null,
|
city: data.address.city ?? null,
|
||||||
stateProvince: data.address.stateProvince ?? null,
|
|
||||||
subdivisionIso: data.address.subdivisionIso ?? null,
|
subdivisionIso: data.address.subdivisionIso ?? null,
|
||||||
postalCode: data.address.postalCode ?? null,
|
postalCode: data.address.postalCode ?? null,
|
||||||
country: data.address.country ?? null,
|
|
||||||
countryIso: data.address.countryIso ?? null,
|
countryIso: data.address.countryIso ?? null,
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { TagBadge } from '@/components/shared/tag-badge';
|
import { TagBadge } from '@/components/shared/tag-badge';
|
||||||
|
import { getCountryName } from '@/lib/i18n/countries';
|
||||||
|
|
||||||
export interface ClientRow {
|
export interface ClientRow {
|
||||||
id: string;
|
id: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
nationality: string | null;
|
nationalityIso: string | null;
|
||||||
source: string | null;
|
source: string | null;
|
||||||
archivedAt: string | null;
|
archivedAt: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -78,11 +79,14 @@ export function getClientColumns({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'nationality',
|
id: 'nationality',
|
||||||
accessorKey: 'nationality',
|
accessorKey: 'nationalityIso',
|
||||||
header: 'Nationality',
|
header: 'Nationality',
|
||||||
cell: ({ getValue }) => (
|
cell: ({ getValue }) => {
|
||||||
<span className="text-muted-foreground">{(getValue() as string | null) ?? '—'}</span>
|
const iso = getValue() as string | null;
|
||||||
),
|
return (
|
||||||
|
<span className="text-muted-foreground">{iso ? getCountryName(iso, 'en') : '—'}</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'source',
|
id: 'source',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface ClientData {
|
|||||||
id: string;
|
id: string;
|
||||||
portId: string;
|
portId: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
nationality: string | null;
|
nationalityIso: string | null;
|
||||||
preferredContactMethod: string | null;
|
preferredContactMethod: string | null;
|
||||||
preferredLanguage: string | null;
|
preferredLanguage: string | null;
|
||||||
timezone: string | null;
|
timezone: string | null;
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ interface ClientFormProps {
|
|||||||
client?: {
|
client?: {
|
||||||
id: string;
|
id: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
nationality?: string | null;
|
|
||||||
nationalityIso?: string | null;
|
nationalityIso?: string | null;
|
||||||
preferredContactMethod?: string | null;
|
preferredContactMethod?: string | null;
|
||||||
preferredLanguage?: string | null;
|
preferredLanguage?: string | null;
|
||||||
@@ -83,7 +82,6 @@ export function ClientForm({ open, onOpenChange, client }: ClientFormProps) {
|
|||||||
if (client && open) {
|
if (client && open) {
|
||||||
reset({
|
reset({
|
||||||
fullName: client.fullName,
|
fullName: client.fullName,
|
||||||
nationality: client.nationality ?? undefined,
|
|
||||||
nationalityIso: client.nationalityIso ?? undefined,
|
nationalityIso: client.nationalityIso ?? undefined,
|
||||||
preferredContactMethod:
|
preferredContactMethod:
|
||||||
(client.preferredContactMethod as CreateClientInput['preferredContactMethod']) ??
|
(client.preferredContactMethod as CreateClientInput['preferredContactMethod']) ??
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export interface CompanyRow {
|
|||||||
legalName: string | null;
|
legalName: string | null;
|
||||||
taxId: string | null;
|
taxId: string | null;
|
||||||
registrationNumber: string | null;
|
registrationNumber: string | null;
|
||||||
incorporationCountry: string | null;
|
|
||||||
incorporationCountryIso: string | null;
|
incorporationCountryIso: string | null;
|
||||||
incorporationSubdivisionIso: string | null;
|
incorporationSubdivisionIso: string | null;
|
||||||
incorporationDate: string | null;
|
incorporationDate: string | null;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ interface CompanyDetailHeaderCompany {
|
|||||||
legalName: string | null;
|
legalName: string | null;
|
||||||
taxId: string | null;
|
taxId: string | null;
|
||||||
registrationNumber: string | null;
|
registrationNumber: string | null;
|
||||||
incorporationCountry: string | null;
|
|
||||||
incorporationCountryIso: string | null;
|
incorporationCountryIso: string | null;
|
||||||
incorporationSubdivisionIso: string | null;
|
incorporationSubdivisionIso: string | null;
|
||||||
incorporationDate: string | null;
|
incorporationDate: string | null;
|
||||||
@@ -132,7 +131,6 @@ export function CompanyDetailHeader({ company }: CompanyDetailHeaderProps) {
|
|||||||
legalName: company.legalName,
|
legalName: company.legalName,
|
||||||
taxId: company.taxId,
|
taxId: company.taxId,
|
||||||
registrationNumber: company.registrationNumber,
|
registrationNumber: company.registrationNumber,
|
||||||
incorporationCountry: company.incorporationCountry,
|
|
||||||
incorporationCountryIso: company.incorporationCountryIso,
|
incorporationCountryIso: company.incorporationCountryIso,
|
||||||
incorporationSubdivisionIso: company.incorporationSubdivisionIso,
|
incorporationSubdivisionIso: company.incorporationSubdivisionIso,
|
||||||
incorporationDate: company.incorporationDate,
|
incorporationDate: company.incorporationDate,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export interface CompanyData {
|
|||||||
legalName: string | null;
|
legalName: string | null;
|
||||||
taxId: string | null;
|
taxId: string | null;
|
||||||
registrationNumber: string | null;
|
registrationNumber: string | null;
|
||||||
incorporationCountry: string | null;
|
|
||||||
incorporationCountryIso: string | null;
|
incorporationCountryIso: string | null;
|
||||||
incorporationSubdivisionIso: string | null;
|
incorporationSubdivisionIso: string | null;
|
||||||
incorporationDate: string | null;
|
incorporationDate: string | null;
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ interface CompanyFormProps {
|
|||||||
legalName: string | null;
|
legalName: string | null;
|
||||||
taxId: string | null;
|
taxId: string | null;
|
||||||
registrationNumber: string | null;
|
registrationNumber: string | null;
|
||||||
incorporationCountry: string | null;
|
|
||||||
incorporationCountryIso: string | null;
|
incorporationCountryIso: string | null;
|
||||||
incorporationSubdivisionIso: string | null;
|
incorporationSubdivisionIso: string | null;
|
||||||
incorporationDate: string | null;
|
incorporationDate: string | null;
|
||||||
@@ -83,7 +82,6 @@ export function CompanyForm({ open, onOpenChange, company }: CompanyFormProps) {
|
|||||||
legalName: company.legalName ?? undefined,
|
legalName: company.legalName ?? undefined,
|
||||||
taxId: company.taxId ?? undefined,
|
taxId: company.taxId ?? undefined,
|
||||||
registrationNumber: company.registrationNumber ?? undefined,
|
registrationNumber: company.registrationNumber ?? undefined,
|
||||||
incorporationCountry: company.incorporationCountry ?? undefined,
|
|
||||||
incorporationCountryIso: company.incorporationCountryIso ?? undefined,
|
incorporationCountryIso: company.incorporationCountryIso ?? undefined,
|
||||||
incorporationSubdivisionIso: company.incorporationSubdivisionIso ?? undefined,
|
incorporationSubdivisionIso: company.incorporationSubdivisionIso ?? undefined,
|
||||||
incorporationDate: company.incorporationDate
|
incorporationDate: company.incorporationDate
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ export function CompanyList() {
|
|||||||
legalName: editCompany.legalName,
|
legalName: editCompany.legalName,
|
||||||
taxId: editCompany.taxId,
|
taxId: editCompany.taxId,
|
||||||
registrationNumber: editCompany.registrationNumber,
|
registrationNumber: editCompany.registrationNumber,
|
||||||
incorporationCountry: editCompany.incorporationCountry,
|
|
||||||
incorporationCountryIso: editCompany.incorporationCountryIso,
|
incorporationCountryIso: editCompany.incorporationCountryIso,
|
||||||
incorporationSubdivisionIso: editCompany.incorporationSubdivisionIso,
|
incorporationSubdivisionIso: editCompany.incorporationSubdivisionIso,
|
||||||
incorporationDate: editCompany.incorporationDate,
|
incorporationDate: editCompany.incorporationDate,
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ interface CompanyTabsCompany {
|
|||||||
legalName: string | null;
|
legalName: string | null;
|
||||||
taxId: string | null;
|
taxId: string | null;
|
||||||
registrationNumber: string | null;
|
registrationNumber: string | null;
|
||||||
incorporationCountry: string | null;
|
|
||||||
incorporationCountryIso: string | null;
|
incorporationCountryIso: string | null;
|
||||||
incorporationSubdivisionIso: string | null;
|
incorporationSubdivisionIso: string | null;
|
||||||
incorporationDate: string | null;
|
incorporationDate: string | null;
|
||||||
|
|||||||
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,
|
"when": 1777391373291,
|
||||||
"tag": "0015_i18n_columns",
|
"tag": "0015_i18n_columns",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777395538988,
|
||||||
|
"tag": "0016_magical_spyke",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ export const clients = pgTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => ports.id),
|
.references(() => ports.id),
|
||||||
fullName: text('full_name').notNull(),
|
fullName: text('full_name').notNull(),
|
||||||
nationality: text('nationality'),
|
/** ISO-3166-1 alpha-2 nationality code. */
|
||||||
/** ISO-3166-1 alpha-2 nationality code. Supersedes `nationality`
|
|
||||||
* after the i18n backfill (PR10) drops the legacy column. */
|
|
||||||
nationalityIso: text('nationality_iso'),
|
nationalityIso: text('nationality_iso'),
|
||||||
preferredContactMethod: text('preferred_contact_method'), // email, phone, whatsapp
|
preferredContactMethod: text('preferred_contact_method'), // email, phone, whatsapp
|
||||||
preferredLanguage: text('preferred_language'),
|
preferredLanguage: text('preferred_language'),
|
||||||
@@ -162,12 +160,10 @@ export const clientAddresses = pgTable(
|
|||||||
label: text('label').notNull().default('Primary'),
|
label: text('label').notNull().default('Primary'),
|
||||||
streetAddress: text('street_address'),
|
streetAddress: text('street_address'),
|
||||||
city: text('city'),
|
city: text('city'),
|
||||||
stateProvince: text('state_province'),
|
|
||||||
/** ISO 3166-2 subdivision code (e.g. 'PL-MZ', 'US-CA'). Optional. */
|
/** ISO 3166-2 subdivision code (e.g. 'PL-MZ', 'US-CA'). Optional. */
|
||||||
subdivisionIso: text('subdivision_iso'),
|
subdivisionIso: text('subdivision_iso'),
|
||||||
postalCode: text('postal_code'),
|
postalCode: text('postal_code'),
|
||||||
country: text('country'),
|
/** ISO-3166-1 alpha-2 country code. */
|
||||||
/** ISO-3166-1 alpha-2 country code. Supersedes `country` after backfill. */
|
|
||||||
countryIso: text('country_iso'),
|
countryIso: text('country_iso'),
|
||||||
isPrimary: boolean('is_primary').notNull().default(true),
|
isPrimary: boolean('is_primary').notNull().default(true),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ export const companies = pgTable(
|
|||||||
legalName: text('legal_name'),
|
legalName: text('legal_name'),
|
||||||
taxId: text('tax_id'),
|
taxId: text('tax_id'),
|
||||||
registrationNumber: text('registration_number'),
|
registrationNumber: text('registration_number'),
|
||||||
incorporationCountry: text('incorporation_country'),
|
/** ISO-3166-1 alpha-2 country of incorporation. */
|
||||||
/** ISO-3166-1 alpha-2 country of incorporation. Replaces the
|
|
||||||
* free-text `incorporation_country` after the i18n backfill. */
|
|
||||||
incorporationCountryIso: text('incorporation_country_iso'),
|
incorporationCountryIso: text('incorporation_country_iso'),
|
||||||
/** ISO 3166-2 subdivision (state/province) of incorporation. Optional. */
|
/** ISO 3166-2 subdivision (state/province) of incorporation. Optional. */
|
||||||
incorporationSubdivisionIso: text('incorporation_subdivision_iso'),
|
incorporationSubdivisionIso: text('incorporation_subdivision_iso'),
|
||||||
@@ -93,12 +91,10 @@ export const companyAddresses = pgTable(
|
|||||||
label: text('label').notNull().default('Primary'),
|
label: text('label').notNull().default('Primary'),
|
||||||
streetAddress: text('street_address'),
|
streetAddress: text('street_address'),
|
||||||
city: text('city'),
|
city: text('city'),
|
||||||
stateProvince: text('state_province'),
|
|
||||||
/** ISO 3166-2 subdivision code. Optional. */
|
/** ISO 3166-2 subdivision code. Optional. */
|
||||||
subdivisionIso: text('subdivision_iso'),
|
subdivisionIso: text('subdivision_iso'),
|
||||||
postalCode: text('postal_code'),
|
postalCode: text('postal_code'),
|
||||||
country: text('country'),
|
/** ISO-3166-1 alpha-2 country code. */
|
||||||
/** ISO-3166-1 alpha-2 country code. Supersedes `country` after backfill. */
|
|
||||||
countryIso: text('country_iso'),
|
countryIso: text('country_iso'),
|
||||||
isPrimary: boolean('is_primary').notNull().default(true),
|
isPrimary: boolean('is_primary').notNull().default(true),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
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.',
|
legalName: 'Aegean Holdings Ltd.',
|
||||||
taxId: `AH-${portSlug}-001`,
|
taxId: `AH-${portSlug}-001`,
|
||||||
registrationNumber: 'AH-2019-8842',
|
registrationNumber: 'AH-2019-8842',
|
||||||
incorporationCountry: 'Greece',
|
incorporationCountryIso: 'GR',
|
||||||
incorporationDate: new Date('2019-03-14'),
|
incorporationDate: new Date('2019-03-14'),
|
||||||
status: 'active',
|
status: 'active',
|
||||||
billingEmail: `billing@aegean-holdings.example`,
|
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.',
|
legalName: 'Blue Seas Marine S.A.',
|
||||||
taxId: `BSM-${portSlug}-002`,
|
taxId: `BSM-${portSlug}-002`,
|
||||||
registrationNumber: 'BSM-2021-3310',
|
registrationNumber: 'BSM-2021-3310',
|
||||||
incorporationCountry: 'Monaco',
|
incorporationCountryIso: 'MC',
|
||||||
incorporationDate: new Date('2021-07-02'),
|
incorporationDate: new Date('2021-07-02'),
|
||||||
status: 'active',
|
status: 'active',
|
||||||
billingEmail: `accounts@blueseas-marine.example`,
|
billingEmail: `accounts@blueseas-marine.example`,
|
||||||
@@ -254,7 +254,7 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
|||||||
legalName: 'Phantom Maritime SA',
|
legalName: 'Phantom Maritime SA',
|
||||||
taxId: `PHT-${portSlug}-003`,
|
taxId: `PHT-${portSlug}-003`,
|
||||||
registrationNumber: 'PHT-2017-7001',
|
registrationNumber: 'PHT-2017-7001',
|
||||||
incorporationCountry: 'Panama',
|
incorporationCountryIso: 'PA',
|
||||||
incorporationDate: new Date('2017-11-20'),
|
incorporationDate: new Date('2017-11-20'),
|
||||||
status: 'dissolved',
|
status: 'dissolved',
|
||||||
billingEmail: null,
|
billingEmail: null,
|
||||||
@@ -276,9 +276,9 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
|||||||
label: 'Head Office',
|
label: 'Head Office',
|
||||||
streetAddress: '14 Mikonou Avenue',
|
streetAddress: '14 Mikonou Avenue',
|
||||||
city: 'Athens',
|
city: 'Athens',
|
||||||
stateProvince: 'Attica',
|
subdivisionIso: 'GR-A',
|
||||||
postalCode: '10558',
|
postalCode: '10558',
|
||||||
country: 'Greece',
|
countryIso: 'GR',
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -287,9 +287,9 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
|||||||
label: 'Registered Office',
|
label: 'Registered Office',
|
||||||
streetAddress: '3 Boulevard des Moulins',
|
streetAddress: '3 Boulevard des Moulins',
|
||||||
city: 'Monte Carlo',
|
city: 'Monte Carlo',
|
||||||
stateProvince: null,
|
subdivisionIso: null,
|
||||||
postalCode: 'MC-98000',
|
postalCode: 'MC-98000',
|
||||||
country: 'Monaco',
|
countryIso: 'MC',
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -298,9 +298,9 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
|||||||
label: 'Former Office',
|
label: 'Former Office',
|
||||||
streetAddress: 'Calle 50, Torre Global, Piso 20',
|
streetAddress: 'Calle 50, Torre Global, Piso 20',
|
||||||
city: 'Panama City',
|
city: 'Panama City',
|
||||||
stateProvince: null,
|
subdivisionIso: null,
|
||||||
postalCode: '0801',
|
postalCode: '0801',
|
||||||
country: 'Panama',
|
countryIso: 'PA',
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -313,96 +313,96 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
|||||||
// 7 → Phantom SA (ended membership)
|
// 7 → Phantom SA (ended membership)
|
||||||
const CLIENT_SPECS: Array<{
|
const CLIENT_SPECS: Array<{
|
||||||
fullName: string;
|
fullName: string;
|
||||||
nationality: string;
|
nationalityIso: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
whatsapp?: string;
|
whatsapp?: string;
|
||||||
city: string;
|
city: string;
|
||||||
country: string;
|
countryIso: string;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
street: string;
|
street: string;
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
fullName: 'Helena Marsh',
|
fullName: 'Helena Marsh',
|
||||||
nationality: 'British',
|
nationalityIso: 'GB',
|
||||||
email: 'helena.marsh@example.com',
|
email: 'helena.marsh@example.com',
|
||||||
phone: '+44 20 7946 0001',
|
phone: '+44 20 7946 0001',
|
||||||
whatsapp: '+44 7700 900001',
|
whatsapp: '+44 7700 900001',
|
||||||
city: 'London',
|
city: 'London',
|
||||||
country: 'United Kingdom',
|
countryIso: 'GB',
|
||||||
postalCode: 'SW1A 1AA',
|
postalCode: 'SW1A 1AA',
|
||||||
street: '22 Belgrave Square',
|
street: '22 Belgrave Square',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullName: 'Marcus Laurent',
|
fullName: 'Marcus Laurent',
|
||||||
nationality: 'French',
|
nationalityIso: 'FR',
|
||||||
email: 'marcus.laurent@example.com',
|
email: 'marcus.laurent@example.com',
|
||||||
phone: '+33 4 93 00 0002',
|
phone: '+33 4 93 00 0002',
|
||||||
city: 'Nice',
|
city: 'Nice',
|
||||||
country: 'France',
|
countryIso: 'FR',
|
||||||
postalCode: '06300',
|
postalCode: '06300',
|
||||||
street: '8 Promenade des Anglais',
|
street: '8 Promenade des Anglais',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullName: 'Sofia Reyes',
|
fullName: 'Sofia Reyes',
|
||||||
nationality: 'Spanish',
|
nationalityIso: 'ES',
|
||||||
email: 'sofia.reyes@example.com',
|
email: 'sofia.reyes@example.com',
|
||||||
phone: '+34 971 000 003',
|
phone: '+34 971 000 003',
|
||||||
whatsapp: '+34 666 000 003',
|
whatsapp: '+34 666 000 003',
|
||||||
city: 'Palma',
|
city: 'Palma',
|
||||||
country: 'Spain',
|
countryIso: 'ES',
|
||||||
postalCode: '07012',
|
postalCode: '07012',
|
||||||
street: 'Passeig Marítim 12',
|
street: 'Passeig Marítim 12',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullName: 'Dimitrios Andreadis',
|
fullName: 'Dimitrios Andreadis',
|
||||||
nationality: 'Greek',
|
nationalityIso: 'GR',
|
||||||
email: 'd.andreadis@aegean-holdings.example',
|
email: 'd.andreadis@aegean-holdings.example',
|
||||||
phone: '+30 210 000 0004',
|
phone: '+30 210 000 0004',
|
||||||
city: 'Athens',
|
city: 'Athens',
|
||||||
country: 'Greece',
|
countryIso: 'GR',
|
||||||
postalCode: '10558',
|
postalCode: '10558',
|
||||||
street: '14 Mikonou Avenue',
|
street: '14 Mikonou Avenue',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullName: 'Katerina Papadakis',
|
fullName: 'Katerina Papadakis',
|
||||||
nationality: 'Greek',
|
nationalityIso: 'GR',
|
||||||
email: 'k.papadakis@aegean-holdings.example',
|
email: 'k.papadakis@aegean-holdings.example',
|
||||||
phone: '+30 210 000 0005',
|
phone: '+30 210 000 0005',
|
||||||
whatsapp: '+30 694 000 0005',
|
whatsapp: '+30 694 000 0005',
|
||||||
city: 'Athens',
|
city: 'Athens',
|
||||||
country: 'Greece',
|
countryIso: 'GR',
|
||||||
postalCode: '10558',
|
postalCode: '10558',
|
||||||
street: '14 Mikonou Avenue',
|
street: '14 Mikonou Avenue',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullName: 'Jonas Lindqvist',
|
fullName: 'Jonas Lindqvist',
|
||||||
nationality: 'Swedish',
|
nationalityIso: 'SE',
|
||||||
email: 'jonas.lindqvist@example.com',
|
email: 'jonas.lindqvist@example.com',
|
||||||
phone: '+46 8 000 0006',
|
phone: '+46 8 000 0006',
|
||||||
city: 'Stockholm',
|
city: 'Stockholm',
|
||||||
country: 'Sweden',
|
countryIso: 'SE',
|
||||||
postalCode: '11129',
|
postalCode: '11129',
|
||||||
street: 'Strandvägen 47',
|
street: 'Strandvägen 47',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullName: 'Isabella Conti',
|
fullName: 'Isabella Conti',
|
||||||
nationality: 'Italian',
|
nationalityIso: 'IT',
|
||||||
email: 'isabella.conti@example.com',
|
email: 'isabella.conti@example.com',
|
||||||
phone: '+39 010 000 0007',
|
phone: '+39 010 000 0007',
|
||||||
whatsapp: '+39 333 000 0007',
|
whatsapp: '+39 333 000 0007',
|
||||||
city: 'Genoa',
|
city: 'Genoa',
|
||||||
country: 'Italy',
|
countryIso: 'IT',
|
||||||
postalCode: '16124',
|
postalCode: '16124',
|
||||||
street: 'Via Garibaldi 9',
|
street: 'Via Garibaldi 9',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fullName: 'Raymond Osei',
|
fullName: 'Raymond Osei',
|
||||||
nationality: 'Ghanaian',
|
nationalityIso: 'GH',
|
||||||
email: 'raymond.osei@example.com',
|
email: 'raymond.osei@example.com',
|
||||||
phone: '+233 30 000 0008',
|
phone: '+233 30 000 0008',
|
||||||
city: 'Accra',
|
city: 'Accra',
|
||||||
country: 'Ghana',
|
countryIso: 'GH',
|
||||||
postalCode: 'GA-183-1090',
|
postalCode: 'GA-183-1090',
|
||||||
street: '21 Independence Ave',
|
street: '21 Independence Ave',
|
||||||
},
|
},
|
||||||
@@ -414,7 +414,7 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
|||||||
CLIENT_SPECS.map((c) => ({
|
CLIENT_SPECS.map((c) => ({
|
||||||
portId,
|
portId,
|
||||||
fullName: c.fullName,
|
fullName: c.fullName,
|
||||||
nationality: c.nationality,
|
nationalityIso: c.nationalityIso,
|
||||||
preferredContactMethod: 'email' as const,
|
preferredContactMethod: 'email' as const,
|
||||||
preferredLanguage: 'en',
|
preferredLanguage: 'en',
|
||||||
source: 'referral' as const,
|
source: 'referral' as const,
|
||||||
@@ -462,9 +462,9 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
|||||||
label: 'Primary',
|
label: 'Primary',
|
||||||
streetAddress: c.street,
|
streetAddress: c.street,
|
||||||
city: c.city,
|
city: c.city,
|
||||||
stateProvince: null,
|
subdivisionIso: null,
|
||||||
postalCode: c.postalCode,
|
postalCode: c.postalCode,
|
||||||
country: c.country,
|
countryIso: c.countryIso,
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ export async function listClients(portId: string, query: ListClientsInput) {
|
|||||||
filters.push(eq(clients.source, source));
|
filters.push(eq(clients.source, source));
|
||||||
}
|
}
|
||||||
if (nationality) {
|
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) {
|
if (tagIds && tagIds.length > 0) {
|
||||||
const clientsWithTags = await db
|
const clientsWithTags = await db
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ export async function createCompany(portId: string, data: CreateCompanyInput, me
|
|||||||
legalName: data.legalName ?? null,
|
legalName: data.legalName ?? null,
|
||||||
taxId: data.taxId ?? null,
|
taxId: data.taxId ?? null,
|
||||||
registrationNumber: data.registrationNumber ?? null,
|
registrationNumber: data.registrationNumber ?? null,
|
||||||
incorporationCountry: data.incorporationCountry ?? null,
|
incorporationCountryIso: data.incorporationCountryIso ?? null,
|
||||||
|
incorporationSubdivisionIso: data.incorporationSubdivisionIso ?? null,
|
||||||
incorporationDate: data.incorporationDate ?? null,
|
incorporationDate: data.incorporationDate ?? null,
|
||||||
status: data.status ?? 'active',
|
status: data.status ?? 'active',
|
||||||
billingEmail: data.billingEmail ?? null,
|
billingEmail: data.billingEmail ?? null,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { minioClient, buildStoragePath } from '@/lib/minio';
|
|||||||
import { env } from '@/lib/env';
|
import { env } from '@/lib/env';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
import { generatePdf } from '@/lib/pdf/generate';
|
import { generatePdf } from '@/lib/pdf/generate';
|
||||||
|
import { getCountryName } from '@/lib/i18n/countries';
|
||||||
import {
|
import {
|
||||||
createDocument as documensoCreate,
|
createDocument as documensoCreate,
|
||||||
sendDocument as documensoSend,
|
sendDocument as documensoSend,
|
||||||
@@ -344,7 +345,9 @@ export async function resolveTemplate(
|
|||||||
tokenMap['{{client.fullName}}'] = client.fullName ?? '';
|
tokenMap['{{client.fullName}}'] = client.fullName ?? '';
|
||||||
tokenMap['{{client.email}}'] = emailContact?.value ?? '';
|
tokenMap['{{client.email}}'] = emailContact?.value ?? '';
|
||||||
tokenMap['{{client.phone}}'] = phoneContact?.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 { interests } from '@/lib/db/schema/interests';
|
||||||
import { ports } from '@/lib/db/schema/ports';
|
import { ports } from '@/lib/db/schema/ports';
|
||||||
import { yachts } from '@/lib/db/schema/yachts';
|
import { yachts } from '@/lib/db/schema/yachts';
|
||||||
|
import { getCountryName } from '@/lib/i18n/countries';
|
||||||
import { NotFoundError, ValidationError } from '@/lib/errors';
|
import { NotFoundError, ValidationError } from '@/lib/errors';
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── 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 === 'phone') ??
|
||||||
contactRows.find((c) => c.channel === 'whatsapp');
|
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
|
const [primaryAddress] = await db
|
||||||
.select({
|
.select({
|
||||||
streetAddress: clientAddresses.streetAddress,
|
streetAddress: clientAddresses.streetAddress,
|
||||||
city: clientAddresses.city,
|
city: clientAddresses.city,
|
||||||
country: clientAddresses.country,
|
countryIso: clientAddresses.countryIso,
|
||||||
})
|
})
|
||||||
.from(clientAddresses)
|
.from(clientAddresses)
|
||||||
.where(and(eq(clientAddresses.clientId, client.id), eq(clientAddresses.isPrimary, true)))
|
.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 ?? '',
|
street: primaryAddress.streetAddress ?? '',
|
||||||
city: primaryAddress.city ?? '',
|
city: primaryAddress.city ?? '',
|
||||||
country: primaryAddress.country ?? '',
|
country: primaryAddress.countryIso ? getCountryName(primaryAddress.countryIso, 'en') : '',
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -185,7 +187,7 @@ export async function buildEoiContext(interestId: string, portId: string): Promi
|
|||||||
.select({
|
.select({
|
||||||
streetAddress: companyAddresses.streetAddress,
|
streetAddress: companyAddresses.streetAddress,
|
||||||
city: companyAddresses.city,
|
city: companyAddresses.city,
|
||||||
country: companyAddresses.country,
|
countryIso: companyAddresses.countryIso,
|
||||||
})
|
})
|
||||||
.from(companyAddresses)
|
.from(companyAddresses)
|
||||||
.where(and(eq(companyAddresses.companyId, company.id), eq(companyAddresses.isPrimary, true)))
|
.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.streetAddress,
|
||||||
companyPrimaryAddress.city,
|
companyPrimaryAddress.city,
|
||||||
companyPrimaryAddress.country,
|
companyPrimaryAddress.countryIso
|
||||||
|
? getCountryName(companyPrimaryAddress.countryIso, 'en')
|
||||||
|
: null,
|
||||||
]
|
]
|
||||||
.filter((s): s is string => Boolean(s))
|
.filter((s): s is string => Boolean(s))
|
||||||
.join(', ') || null
|
.join(', ') || null
|
||||||
@@ -219,7 +223,7 @@ export async function buildEoiContext(interestId: string, portId: string): Promi
|
|||||||
return {
|
return {
|
||||||
client: {
|
client: {
|
||||||
fullName: client.fullName,
|
fullName: client.fullName,
|
||||||
nationality: client.nationality,
|
nationality: client.nationalityIso ? getCountryName(client.nationalityIso, 'en') : null,
|
||||||
primaryEmail: firstEmail?.value ?? null,
|
primaryEmail: firstEmail?.value ?? null,
|
||||||
primaryPhone: firstPhone?.value ?? null,
|
primaryPhone: firstPhone?.value ?? null,
|
||||||
address: clientAddress,
|
address: clientAddress,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { createAuditLog } from '@/lib/audit';
|
|||||||
import { diffEntity } from '@/lib/entity-diff';
|
import { diffEntity } from '@/lib/entity-diff';
|
||||||
import { withTransaction } from '@/lib/db/utils';
|
import { withTransaction } from '@/lib/db/utils';
|
||||||
import { NotFoundError, ConflictError, ValidationError } from '@/lib/errors';
|
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 { emitToRoom } from '@/lib/socket/server';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
import { generatePdf } from '@/lib/pdf/generate';
|
import { generatePdf } from '@/lib/pdf/generate';
|
||||||
@@ -99,9 +101,9 @@ async function resolveBillingEntity(
|
|||||||
? [
|
? [
|
||||||
addressRow.streetAddress,
|
addressRow.streetAddress,
|
||||||
addressRow.city,
|
addressRow.city,
|
||||||
addressRow.stateProvince,
|
addressRow.subdivisionIso ? getSubdivisionName(addressRow.subdivisionIso) : null,
|
||||||
addressRow.postalCode,
|
addressRow.postalCode,
|
||||||
addressRow.country,
|
addressRow.countryIso ? getCountryName(addressRow.countryIso, 'en') : null,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
@@ -126,9 +128,9 @@ async function resolveBillingEntity(
|
|||||||
? [
|
? [
|
||||||
addressRow.streetAddress,
|
addressRow.streetAddress,
|
||||||
addressRow.city,
|
addressRow.city,
|
||||||
addressRow.stateProvince,
|
addressRow.subdivisionIso ? getSubdivisionName(addressRow.subdivisionIso) : null,
|
||||||
addressRow.postalCode,
|
addressRow.postalCode,
|
||||||
addressRow.country,
|
addressRow.countryIso ? getCountryName(addressRow.countryIso, 'en') : null,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { yachts } from '@/lib/db/schema/yachts';
|
|||||||
import { companies, companyMemberships } from '@/lib/db/schema/companies';
|
import { companies, companyMemberships } from '@/lib/db/schema/companies';
|
||||||
import { berthReservations } from '@/lib/db/schema/reservations';
|
import { berthReservations } from '@/lib/db/schema/reservations';
|
||||||
import { getPresignedUrl } from '@/lib/minio';
|
import { getPresignedUrl } from '@/lib/minio';
|
||||||
|
import { getCountryName } from '@/lib/i18n/countries';
|
||||||
|
|
||||||
// ─── Dashboard ────────────────────────────────────────────────────────────────
|
// ─── Dashboard ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ export async function getPortalDashboard(
|
|||||||
client: {
|
client: {
|
||||||
id: client.id,
|
id: client.id,
|
||||||
fullName: client.fullName,
|
fullName: client.fullName,
|
||||||
nationality: client.nationality ?? null,
|
nationality: client.nationalityIso ? getCountryName(client.nationalityIso, 'en') : null,
|
||||||
},
|
},
|
||||||
port: {
|
port: {
|
||||||
name: port.name,
|
name: port.name,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { berthReservations } from '@/lib/db/schema/reservations';
|
|||||||
import { clients } from '@/lib/db/schema/clients';
|
import { clients } from '@/lib/db/schema/clients';
|
||||||
import { ports } from '@/lib/db/schema/ports';
|
import { ports } from '@/lib/db/schema/ports';
|
||||||
import { yachts } from '@/lib/db/schema/yachts';
|
import { yachts } from '@/lib/db/schema/yachts';
|
||||||
|
import { getCountryName } from '@/lib/i18n/countries';
|
||||||
import { NotFoundError } from '@/lib/errors';
|
import { NotFoundError } from '@/lib/errors';
|
||||||
|
|
||||||
export type ReservationAgreementContext = {
|
export type ReservationAgreementContext = {
|
||||||
@@ -100,7 +101,7 @@ export async function buildReservationAgreementContext(
|
|||||||
client: {
|
client: {
|
||||||
id: client.id,
|
id: client.id,
|
||||||
fullName: client.fullName,
|
fullName: client.fullName,
|
||||||
nationality: client.nationality,
|
nationality: client.nationalityIso ? getCountryName(client.nationalityIso, 'en') : null,
|
||||||
},
|
},
|
||||||
yacht: {
|
yacht: {
|
||||||
id: yacht.id,
|
id: yacht.id,
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ export const contactSchema = z.object({
|
|||||||
export const createClientSchema = z.object({
|
export const createClientSchema = z.object({
|
||||||
fullName: z.string().min(1).max(200),
|
fullName: z.string().min(1).max(200),
|
||||||
contacts: z.array(contactSchema).min(1, 'At least one contact is required'),
|
contacts: z.array(contactSchema).min(1, 'At least one contact is required'),
|
||||||
/** Legacy free-text nationality. Kept for backfill only — new edits write `nationalityIso`. */
|
|
||||||
nationality: z.string().optional(),
|
|
||||||
/** ISO-3166-1 alpha-2 nationality code. */
|
/** ISO-3166-1 alpha-2 nationality code. */
|
||||||
nationalityIso: optionalCountryIsoSchema.optional(),
|
nationalityIso: optionalCountryIsoSchema.optional(),
|
||||||
preferredContactMethod: z.enum(['email', 'phone', 'whatsapp']).optional(),
|
preferredContactMethod: z.enum(['email', 'phone', 'whatsapp']).optional(),
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ export const createCompanySchema = z.object({
|
|||||||
legalName: z.string().optional(),
|
legalName: z.string().optional(),
|
||||||
taxId: z.string().optional(),
|
taxId: z.string().optional(),
|
||||||
registrationNumber: z.string().optional(),
|
registrationNumber: z.string().optional(),
|
||||||
/** Legacy free-text. New writes use `incorporationCountryIso`. */
|
|
||||||
incorporationCountry: z.string().optional(),
|
|
||||||
/** ISO-3166-1 alpha-2 country of incorporation. */
|
/** ISO-3166-1 alpha-2 country of incorporation. */
|
||||||
incorporationCountryIso: optionalCountryIsoSchema.optional(),
|
incorporationCountryIso: optionalCountryIsoSchema.optional(),
|
||||||
/** ISO 3166-2 state/province of incorporation. */
|
/** ISO 3166-2 state/province of incorporation. */
|
||||||
|
|||||||
@@ -74,13 +74,9 @@ export const generateRecommendationsSchema = z.object({
|
|||||||
const addressSchema = z.object({
|
const addressSchema = z.object({
|
||||||
street: z.string().max(500).optional(),
|
street: z.string().max(500).optional(),
|
||||||
city: z.string().max(200).optional(),
|
city: z.string().max(200).optional(),
|
||||||
/** Legacy free-text. New writes use `subdivisionIso`. */
|
|
||||||
stateProvince: z.string().max(200).optional(),
|
|
||||||
/** ISO 3166-2 subdivision code (e.g. 'PL-MZ'). */
|
/** ISO 3166-2 subdivision code (e.g. 'PL-MZ'). */
|
||||||
subdivisionIso: optionalSubdivisionIsoSchema.optional(),
|
subdivisionIso: optionalSubdivisionIsoSchema.optional(),
|
||||||
postalCode: z.string().max(50).optional(),
|
postalCode: z.string().max(50).optional(),
|
||||||
/** Legacy free-text. New writes use `countryIso`. */
|
|
||||||
country: z.string().max(100).optional(),
|
|
||||||
/** ISO-3166-1 alpha-2 country code. */
|
/** ISO-3166-1 alpha-2 country code. */
|
||||||
countryIso: optionalCountryIsoSchema.optional(),
|
countryIso: optionalCountryIsoSchema.optional(),
|
||||||
});
|
});
|
||||||
@@ -105,8 +101,6 @@ const publicCompanySchema = z.object({
|
|||||||
name: z.string().min(1).max(200),
|
name: z.string().min(1).max(200),
|
||||||
legalName: z.string().max(200).optional(),
|
legalName: z.string().max(200).optional(),
|
||||||
taxId: z.string().max(100).optional(),
|
taxId: z.string().max(100).optional(),
|
||||||
/** Legacy free-text. New website builds should send `incorporationCountryIso`. */
|
|
||||||
incorporationCountry: z.string().max(100).optional(),
|
|
||||||
/** ISO-3166-1 alpha-2 country of incorporation. */
|
/** ISO-3166-1 alpha-2 country of incorporation. */
|
||||||
incorporationCountryIso: optionalCountryIsoSchema.optional(),
|
incorporationCountryIso: optionalCountryIsoSchema.optional(),
|
||||||
/** ISO 3166-2 state/province of incorporation. */
|
/** ISO 3166-2 state/province of incorporation. */
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ describe('resolveTemplate — EOI scope tokens', () => {
|
|||||||
const port = await makePort();
|
const port = await makePort();
|
||||||
const client = await makeClient({
|
const client = await makeClient({
|
||||||
portId: port.id,
|
portId: port.id,
|
||||||
overrides: { fullName: 'Alice Client', nationality: 'US', source: 'referral' },
|
overrides: { fullName: 'Alice Client', nationalityIso: 'US', source: 'referral' },
|
||||||
});
|
});
|
||||||
await db.insert(clientContacts).values([
|
await db.insert(clientContacts).values([
|
||||||
{ clientId: client.id, channel: 'email', value: 'alice@example.com', isPrimary: true },
|
{ clientId: client.id, channel: 'email', value: 'alice@example.com', isPrimary: true },
|
||||||
@@ -137,7 +137,7 @@ describe('resolveTemplate — EOI scope tokens', () => {
|
|||||||
portId: port.id,
|
portId: port.id,
|
||||||
streetAddress: '1 Main St',
|
streetAddress: '1 Main St',
|
||||||
city: 'Town',
|
city: 'Town',
|
||||||
country: 'US',
|
countryIso: 'US',
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -321,7 +321,7 @@ describe('resolveTemplate — legacy fallback (no interestId)', () => {
|
|||||||
const port = await makePort();
|
const port = await makePort();
|
||||||
const client = await makeClient({
|
const client = await makeClient({
|
||||||
portId: port.id,
|
portId: port.id,
|
||||||
overrides: { fullName: 'Carol NoInterest', nationality: 'UK', source: 'website' },
|
overrides: { fullName: 'Carol NoInterest', nationalityIso: 'GB', source: 'website' },
|
||||||
});
|
});
|
||||||
await db.insert(clientContacts).values({
|
await db.insert(clientContacts).values({
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
@@ -349,7 +349,8 @@ describe('resolveTemplate — legacy fallback (no interestId)', () => {
|
|||||||
|
|
||||||
expect(resolved).toContain('Hello Carol NoInterest');
|
expect(resolved).toContain('Hello Carol NoInterest');
|
||||||
expect(resolved).toContain('carol@example.com');
|
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');
|
expect(resolved).toContain('src=website');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ beforeAll(async () => {
|
|||||||
const port = await makePort();
|
const port = await makePort();
|
||||||
const client = await makeClient({
|
const client = await makeClient({
|
||||||
portId: port.id,
|
portId: port.id,
|
||||||
overrides: { fullName: 'Dual Path Client', nationality: 'US' },
|
overrides: { fullName: 'Dual Path Client', nationalityIso: 'US' },
|
||||||
});
|
});
|
||||||
await db.insert(clientContacts).values({
|
await db.insert(clientContacts).values({
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
@@ -121,7 +121,7 @@ beforeAll(async () => {
|
|||||||
portId: port.id,
|
portId: port.id,
|
||||||
streetAddress: '1 Wharf Rd',
|
streetAddress: '1 Wharf Rd',
|
||||||
city: 'Harbor',
|
city: 'Harbor',
|
||||||
country: 'US',
|
countryIso: 'US',
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -161,9 +161,9 @@ describe('invoices.service — billing entity', () => {
|
|||||||
label: 'Primary',
|
label: 'Primary',
|
||||||
streetAddress: '1 Pier Road',
|
streetAddress: '1 Pier Road',
|
||||||
city: 'Harbor City',
|
city: 'Harbor City',
|
||||||
stateProvince: 'CA',
|
subdivisionIso: 'US-CA',
|
||||||
postalCode: '90000',
|
postalCode: '90000',
|
||||||
country: 'USA',
|
countryIso: 'US',
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -180,7 +180,10 @@ describe('invoices.service — billing entity', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(invoice.billingEmail).toBe('bob@example.com');
|
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 () => {
|
it('allows caller to override billingEmail and billingAddress', async () => {
|
||||||
@@ -196,9 +199,9 @@ describe('invoices.service — billing entity', () => {
|
|||||||
label: 'Primary',
|
label: 'Primary',
|
||||||
streetAddress: '2 Ocean Blvd',
|
streetAddress: '2 Ocean Blvd',
|
||||||
city: 'Portville',
|
city: 'Portville',
|
||||||
stateProvince: 'FL',
|
subdivisionIso: 'US-FL',
|
||||||
postalCode: '33101',
|
postalCode: '33101',
|
||||||
country: 'USA',
|
countryIso: 'US',
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ describe('buildEoiContext', () => {
|
|||||||
const port = await makePort();
|
const port = await makePort();
|
||||||
const client = await makeClient({
|
const client = await makeClient({
|
||||||
portId: port.id,
|
portId: port.id,
|
||||||
overrides: { fullName: 'Alice Test', nationality: 'US' },
|
overrides: { fullName: 'Alice Test', nationalityIso: 'US' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Insert contacts.
|
// Insert contacts.
|
||||||
@@ -60,7 +60,7 @@ describe('buildEoiContext', () => {
|
|||||||
portId: port.id,
|
portId: port.id,
|
||||||
streetAddress: '1 Harbour Way',
|
streetAddress: '1 Harbour Way',
|
||||||
city: 'Anguilla',
|
city: 'Anguilla',
|
||||||
country: 'AI',
|
countryIso: 'AI',
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,15 +94,16 @@ describe('buildEoiContext', () => {
|
|||||||
|
|
||||||
const ctx = await buildEoiContext(interest.id, port.id);
|
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.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.primaryEmail).toBe('alice@example.com');
|
||||||
expect(ctx.client.primaryPhone).toBe('+1-555-1234');
|
expect(ctx.client.primaryPhone).toBe('+1-555-1234');
|
||||||
expect(ctx.client.address).toEqual({
|
expect(ctx.client.address).toEqual({
|
||||||
street: '1 Harbour Way',
|
street: '1 Harbour Way',
|
||||||
city: 'Anguilla',
|
city: 'Anguilla',
|
||||||
country: 'AI',
|
country: 'Anguilla',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Yacht assertions.
|
// Yacht assertions.
|
||||||
@@ -181,7 +182,7 @@ describe('buildEoiContext', () => {
|
|||||||
portId: port.id,
|
portId: port.id,
|
||||||
streetAddress: '99 Commerce St',
|
streetAddress: '99 Commerce St',
|
||||||
city: 'Valley',
|
city: 'Valley',
|
||||||
country: 'AI',
|
countryIso: 'AI',
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,7 +207,8 @@ describe('buildEoiContext', () => {
|
|||||||
expect(ctx.company!.billingAddress).not.toBeNull();
|
expect(ctx.company!.billingAddress).not.toBeNull();
|
||||||
expect(ctx.company!.billingAddress).toContain('99 Commerce St');
|
expect(ctx.company!.billingAddress).toContain('99 Commerce St');
|
||||||
expect(ctx.company!.billingAddress).toContain('Valley');
|
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 () => {
|
it('throws ValidationError when interest has no yacht', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user