Extend the global search service to include yacht and company results using ILIKE matching on name, hull number, registration, legal name, and tax ID. Results are tenant-scoped and exclude archived rows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
197 lines
7.0 KiB
TypeScript
197 lines
7.0 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { eq } from 'drizzle-orm';
|
|
|
|
import { search } from '@/lib/services/search.service';
|
|
import { db } from '@/lib/db';
|
|
import { yachts, companies } from '@/lib/db/schema';
|
|
|
|
import { makePort, makeClient, makeYacht, makeCompany } from '../../helpers/factories';
|
|
|
|
// ─── Yachts ──────────────────────────────────────────────────────────────────
|
|
|
|
describe('search.service — yachts', () => {
|
|
it('matches yachts by name (case-insensitive)', async () => {
|
|
const port = await makePort();
|
|
const owner = await makeClient({ portId: port.id });
|
|
await makeYacht({
|
|
portId: port.id,
|
|
ownerType: 'client',
|
|
ownerId: owner.id,
|
|
name: 'Sea Breeze',
|
|
});
|
|
await makeYacht({
|
|
portId: port.id,
|
|
ownerType: 'client',
|
|
ownerId: owner.id,
|
|
name: 'Wind Dancer',
|
|
});
|
|
|
|
const results = await search(port.id, 'BREEZE');
|
|
expect(results.yachts.some((y) => y.name === 'Sea Breeze')).toBe(true);
|
|
expect(results.yachts.some((y) => y.name === 'Wind Dancer')).toBe(false);
|
|
});
|
|
|
|
it('matches yachts by hull number', async () => {
|
|
const port = await makePort();
|
|
const owner = await makeClient({ portId: port.id });
|
|
await makeYacht({
|
|
portId: port.id,
|
|
ownerType: 'client',
|
|
ownerId: owner.id,
|
|
name: 'Nomad',
|
|
hullNumber: 'HULL-XYZ-999',
|
|
});
|
|
|
|
const results = await search(port.id, 'hull-xyz');
|
|
expect(results.yachts.some((y) => y.hullNumber === 'HULL-XYZ-999')).toBe(true);
|
|
});
|
|
|
|
it('matches yachts by registration', async () => {
|
|
const port = await makePort();
|
|
const owner = await makeClient({ portId: port.id });
|
|
await makeYacht({
|
|
portId: port.id,
|
|
ownerType: 'client',
|
|
ownerId: owner.id,
|
|
name: 'Registered One',
|
|
registration: 'REG-ABC-123',
|
|
});
|
|
|
|
const results = await search(port.id, 'reg-abc');
|
|
expect(results.yachts.some((y) => y.registration === 'REG-ABC-123')).toBe(true);
|
|
});
|
|
|
|
it('excludes archived yachts', async () => {
|
|
const port = await makePort();
|
|
const owner = await makeClient({ portId: port.id });
|
|
const yacht = await makeYacht({
|
|
portId: port.id,
|
|
ownerType: 'client',
|
|
ownerId: owner.id,
|
|
name: 'Ghost Ship',
|
|
});
|
|
await db.update(yachts).set({ archivedAt: new Date() }).where(eq(yachts.id, yacht.id));
|
|
|
|
const results = await search(port.id, 'ghost ship');
|
|
expect(results.yachts.some((y) => y.id === yacht.id)).toBe(false);
|
|
});
|
|
|
|
it('is tenant-scoped', async () => {
|
|
const portA = await makePort();
|
|
const portB = await makePort();
|
|
const ownerA = await makeClient({ portId: portA.id });
|
|
const ownerB = await makeClient({ portId: portB.id });
|
|
await makeYacht({
|
|
portId: portA.id,
|
|
ownerType: 'client',
|
|
ownerId: ownerA.id,
|
|
name: 'UniqueYachtNameA',
|
|
});
|
|
await makeYacht({
|
|
portId: portB.id,
|
|
ownerType: 'client',
|
|
ownerId: ownerB.id,
|
|
name: 'UniqueYachtNameB',
|
|
});
|
|
|
|
const resultsA = await search(portA.id, 'UniqueYachtName');
|
|
expect(resultsA.yachts.some((y) => y.name === 'UniqueYachtNameA')).toBe(true);
|
|
expect(resultsA.yachts.some((y) => y.name === 'UniqueYachtNameB')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Companies ───────────────────────────────────────────────────────────────
|
|
|
|
describe('search.service — companies', () => {
|
|
it('matches companies by name', async () => {
|
|
const port = await makePort();
|
|
await makeCompany({ portId: port.id, overrides: { name: 'Poseidon Maritime Ltd' } });
|
|
await makeCompany({ portId: port.id, overrides: { name: 'Neptune Holdings' } });
|
|
|
|
const results = await search(port.id, 'poseidon');
|
|
expect(results.companies.some((c) => c.name === 'Poseidon Maritime Ltd')).toBe(true);
|
|
expect(results.companies.some((c) => c.name === 'Neptune Holdings')).toBe(false);
|
|
});
|
|
|
|
it('matches companies by legal name', async () => {
|
|
const port = await makePort();
|
|
await makeCompany({
|
|
portId: port.id,
|
|
overrides: { name: 'AcmeShort', legalName: 'Acme Legal Holdings Inc.' },
|
|
});
|
|
|
|
const results = await search(port.id, 'Acme Legal');
|
|
expect(results.companies.some((c) => c.legalName === 'Acme Legal Holdings Inc.')).toBe(true);
|
|
});
|
|
|
|
it('matches companies by tax ID', async () => {
|
|
const port = await makePort();
|
|
await makeCompany({
|
|
portId: port.id,
|
|
overrides: { name: 'TaxyCo', taxId: 'VAT-112233445' },
|
|
});
|
|
|
|
const results = await search(port.id, 'vat-112233');
|
|
expect(results.companies.some((c) => c.taxId === 'VAT-112233445')).toBe(true);
|
|
});
|
|
|
|
it('excludes archived companies', async () => {
|
|
const port = await makePort();
|
|
const company = await makeCompany({
|
|
portId: port.id,
|
|
overrides: { name: 'ArchivedCompanyXyz' },
|
|
});
|
|
await db.update(companies).set({ archivedAt: new Date() }).where(eq(companies.id, company.id));
|
|
|
|
const results = await search(port.id, 'ArchivedCompanyXyz');
|
|
expect(results.companies.some((c) => c.id === company.id)).toBe(false);
|
|
});
|
|
|
|
it('is tenant-scoped', async () => {
|
|
const portA = await makePort();
|
|
const portB = await makePort();
|
|
await makeCompany({ portId: portA.id, overrides: { name: 'UniqueCompanyA' } });
|
|
await makeCompany({ portId: portB.id, overrides: { name: 'UniqueCompanyB' } });
|
|
|
|
const resultsA = await search(portA.id, 'UniqueCompany');
|
|
expect(resultsA.companies.some((c) => c.name === 'UniqueCompanyA')).toBe(true);
|
|
expect(resultsA.companies.some((c) => c.name === 'UniqueCompanyB')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Combined ────────────────────────────────────────────────────────────────
|
|
|
|
describe('search.service — combined', () => {
|
|
it('returns clients, yachts, and companies for a query that matches multiple', async () => {
|
|
const port = await makePort();
|
|
const client = await makeClient({
|
|
portId: port.id,
|
|
overrides: { fullName: 'Alpha Person' },
|
|
});
|
|
await makeYacht({
|
|
portId: port.id,
|
|
ownerType: 'client',
|
|
ownerId: client.id,
|
|
name: 'Alpha Yacht',
|
|
});
|
|
await makeCompany({ portId: port.id, overrides: { name: 'Alpha Holdings' } });
|
|
|
|
const results = await search(port.id, 'alpha');
|
|
expect(results.clients.some((c) => c.fullName === 'Alpha Person')).toBe(true);
|
|
expect(results.yachts.some((y) => y.name === 'Alpha Yacht')).toBe(true);
|
|
expect(results.companies.some((c) => c.name === 'Alpha Holdings')).toBe(true);
|
|
});
|
|
|
|
it('returns all result keys even when no matches exist', async () => {
|
|
const port = await makePort();
|
|
const results = await search(port.id, 'zzz-nothing-matches-zzz');
|
|
expect(results).toEqual({
|
|
clients: [],
|
|
interests: [],
|
|
berths: [],
|
|
yachts: [],
|
|
companies: [],
|
|
});
|
|
});
|
|
});
|