Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
/**
|
||
* i18n PR9–10 - public residential inquiry endpoint.
|
||
*
|
||
* Validates the server-side phone normalization that the public inquiry
|
||
* route runs when the website posts a raw international format (older
|
||
* Nuxt builds), and that pre-normalized payloads pass through unchanged.
|
||
*/
|
||
|
||
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
||
import { eq } from 'drizzle-orm';
|
||
|
||
import { db } from '@/lib/db';
|
||
import { residentialClients } from '@/lib/db/schema/residential';
|
||
import { makePort } from '../helpers/factories';
|
||
import { makeMockRequest } from '../helpers/route-tester';
|
||
|
||
vi.mock('@/lib/socket/server', () => ({ emitToRoom: vi.fn() }));
|
||
vi.mock('@/lib/email', () => ({ sendEmail: vi.fn().mockResolvedValue(undefined) }));
|
||
|
||
// Randomize per-run prefix so leftover redis sliding-window entries from a
|
||
// previous run don't 429 the new run (publicForm limiter pexpires after 1h).
|
||
const IP_PREFIX = `10.${Math.floor(Math.random() * 200) + 10}`;
|
||
let ipCounter = 1;
|
||
function uniqueIp(): string {
|
||
ipCounter += 1;
|
||
return `${IP_PREFIX}.${Math.floor(ipCounter / 255) % 255}.${ipCounter % 255}`;
|
||
}
|
||
|
||
describe('POST /api/public/residential-inquiries', () => {
|
||
let POST: typeof import('@/app/api/public/residential-inquiries/route').POST;
|
||
|
||
beforeAll(async () => {
|
||
const mod = await import('@/app/api/public/residential-inquiries/route');
|
||
POST = mod.POST;
|
||
});
|
||
|
||
it('parses a raw international phone string into E.164 + country', async () => {
|
||
const port = await makePort();
|
||
const email = `res-${Math.random().toString(36).slice(2, 8)}@test.local`;
|
||
|
||
const req = makeMockRequest(
|
||
'POST',
|
||
`http://localhost/api/public/residential-inquiries?portId=${port.id}`,
|
||
{
|
||
headers: { 'x-forwarded-for': uniqueIp() },
|
||
body: {
|
||
firstName: 'Anna',
|
||
lastName: 'Nowak',
|
||
email,
|
||
// Raw international format - server should normalize.
|
||
phone: '+44 20 7946 0958',
|
||
placeOfResidence: 'Warsaw',
|
||
},
|
||
},
|
||
);
|
||
|
||
const res = await POST(req);
|
||
expect(res.status).toBe(201);
|
||
|
||
const [row] = await db
|
||
.select()
|
||
.from(residentialClients)
|
||
.where(eq(residentialClients.email, email));
|
||
|
||
expect(row).toBeDefined();
|
||
expect(row?.phoneE164).toBe('+442079460958');
|
||
expect(row?.phoneCountry).toBe('GB');
|
||
// Free-text legacy column preserved verbatim for backfill.
|
||
expect(row?.phone).toBe('+44 20 7946 0958');
|
||
});
|
||
|
||
it('passes pre-normalized E.164 + country through unchanged', async () => {
|
||
const port = await makePort();
|
||
const email = `res-${Math.random().toString(36).slice(2, 8)}@test.local`;
|
||
|
||
const req = makeMockRequest(
|
||
'POST',
|
||
`http://localhost/api/public/residential-inquiries?portId=${port.id}`,
|
||
{
|
||
headers: { 'x-forwarded-for': uniqueIp() },
|
||
body: {
|
||
firstName: 'Jan',
|
||
lastName: 'Kowalski',
|
||
email,
|
||
phone: '+48 22 555 0100',
|
||
phoneE164: '+48225550100',
|
||
phoneCountry: 'PL',
|
||
nationalityIso: 'PL',
|
||
timezone: 'Europe/Warsaw',
|
||
placeOfResidence: 'Warsaw',
|
||
placeOfResidenceCountryIso: 'PL',
|
||
},
|
||
},
|
||
);
|
||
|
||
const res = await POST(req);
|
||
expect(res.status).toBe(201);
|
||
|
||
const [row] = await db
|
||
.select()
|
||
.from(residentialClients)
|
||
.where(eq(residentialClients.email, email));
|
||
|
||
expect(row?.phoneE164).toBe('+48225550100');
|
||
expect(row?.phoneCountry).toBe('PL');
|
||
expect(row?.nationalityIso).toBe('PL');
|
||
expect(row?.timezone).toBe('Europe/Warsaw');
|
||
expect(row?.placeOfResidenceCountryIso).toBe('PL');
|
||
});
|
||
|
||
it('persists a national-format phone when the website only sends a country hint', async () => {
|
||
const port = await makePort();
|
||
const email = `res-${Math.random().toString(36).slice(2, 8)}@test.local`;
|
||
|
||
const req = makeMockRequest(
|
||
'POST',
|
||
`http://localhost/api/public/residential-inquiries?portId=${port.id}`,
|
||
{
|
||
headers: { 'x-forwarded-for': uniqueIp() },
|
||
body: {
|
||
firstName: 'Marta',
|
||
lastName: 'Lewandowska',
|
||
email,
|
||
phone: '22 555 0200', // National-format
|
||
phoneCountry: 'PL', // Hint only - no E.164 yet.
|
||
},
|
||
},
|
||
);
|
||
|
||
const res = await POST(req);
|
||
expect(res.status).toBe(201);
|
||
|
||
const [row] = await db
|
||
.select()
|
||
.from(residentialClients)
|
||
.where(eq(residentialClients.email, email));
|
||
|
||
expect(row?.phoneE164).toBe('+48225550200');
|
||
expect(row?.phoneCountry).toBe('PL');
|
||
});
|
||
});
|