Files
serfaandClaude Opus 5 cb148c3dc8 Panel: an architecture diagram, store costs, and Spanish that reads as Spanish
The right-hand panel now answers the question the stack list provoked — what
does a client actually install? Capacitor, iOS and Android lead "Built with",
and a new SVG diagram shows the two ways in: customers through the installed
app, staff through the admin dashboard behind Cloudflare Access.

Costs were half-quoted. The stack named Mapbox, Twilio, Resend and Sentry and
priced none of them, so each now states the shape of its bill rather than a
figure that would be wrong at every scale. App store accounts are listed too,
in the client's own name for the same reason the hosting is.

Currency was the serious one. Every figure is US dollars and the panel wrote
them as "$59", but this launches in Mexico, where "$" is the peso — the Spanish
column understated the running cost roughly seventeenfold to the only reader
who matters. money() now prints "$59" in English and "59 USD" in Spanish.

The rest of the Spanish was a literal translation of dense English: peninsular
vocabulary in a Mexican market, two stray vosotros forms among the tuteo, and
calques that were not grammatical — "aplicada en ninguna parte", "todo lo
discontinuo" for dashed lines. Reviewed all 126 strings. The architecture note
was 120 words in one block covering five topics; it is now two paragraphs, one
idea each, and the PostGIS sentence went because "What it does" already makes
that promise in language a client can act on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:57:05 -04:00

118 lines
4.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Integration test — runs against a live seeded database.
*
* pnpm services:up && pnpm db:migrate && pnpm db:seed
* pnpm --filter @linkdr/db test
*
* getShowcaseDeck feeds the entry screen, which is the one deck an anonymous
* visitor sees. Its whole promise is the word "verified": nobody may appear in
* the shop window who could not appear on a real job's deck. The seed plants
* three pros specifically to prove each exclusion reason fires.
*/
import { config } from 'dotenv';
import { sql } from 'drizzle-orm';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
config({ path: '../../.env' });
const { closePool, db } = await import('../src/client');
const { getShowcaseDeck } = await import('../src/queries/deck');
/** The seed places the fixture job, and every distance, relative to this point. */
const CENTRE = {
lat: Number(process.env.NEXT_PUBLIC_CITY_LAT ?? 19.4326),
lng: Number(process.env.NEXT_PUBLIC_CITY_LNG ?? -99.1332),
};
let names: (string | null)[];
beforeAll(async () => {
const cards = await getShowcaseDeck(db, { ...CENTRE, limit: 100 });
names = cards.map((c) => c.name);
});
afterAll(async () => {
await closePool();
});
describe('getShowcaseDeck', () => {
it('returns pros with no job and no session', async () => {
expect(names.length).toBeGreaterThan(0);
});
it('excludes a pro who will not travel this far', () => {
// Pablo Rivas is seeded 22 km out with a 5 km service radius.
expect(names).not.toContain('Pablo Rivas');
});
it('excludes a pro whose verification has not passed', () => {
// Unverified Ulises is 1 km away — close enough to prove distance is not
// what is keeping her out.
expect(names).not.toContain('Unverified Ulises');
});
it('excludes a verified pro who is not accepting work', () => {
// Away Arturo is verified and nearby, but on holiday mode.
expect(names).not.toContain('Away Arturo');
});
it('includes the nearest eligible pro', () => {
expect(names).toContain('Sergio Fabela');
});
it('honours the limit', async () => {
const three = await getShowcaseDeck(db, { ...CENTRE, limit: 3 });
expect(three).toHaveLength(3);
});
it('honours the searcher own range, not just the pro one', async () => {
// Mario Villanueva is seeded 9.1 km out with a 30 km radius: they would travel
// here happily, but someone who said "within 3 km" did not ask for her.
const near = await getShowcaseDeck(db, { ...CENTRE, maxDistanceM: 3_000, limit: 100 });
const nearNames = near.map((c) => c.name);
expect(nearNames).not.toContain('Mario Villanueva');
expect(nearNames).toContain('Sergio Fabela'); // 1.1 km away
expect(near.every((c) => c.distanceM <= 3_000)).toBe(true);
});
it('narrows to a single trade when given a category', async () => {
const [plumber] = await db.execute<{ id: string }>(
sql`SELECT id FROM categories WHERE slug = 'plumber' LIMIT 1`,
);
if (!plumber) throw new Error('plumber category missing from seed');
const cards = await getShowcaseDeck(db, { ...CENTRE, categoryId: plumber.id, limit: 100 });
expect(cards.length).toBeGreaterThan(0);
// Every card carries its trade names, so the filter is checkable per card.
expect(cards.every((c) => c.categories.includes('Plumber'))).toBe(true);
// And it must be a strict subset — otherwise the filter did nothing.
expect(cards.length).toBeLessThan(names.length);
});
it('still excludes the ineligible when a category is given', async () => {
const [plumber] = await db.execute<{ id: string }>(
sql`SELECT id FROM categories WHERE slug = 'plumber' LIMIT 1`,
);
if (!plumber) throw new Error('plumber category missing from seed');
const cards = await getShowcaseDeck(db, { ...CENTRE, categoryId: plumber.id, limit: 100 });
const filtered = cards.map((c) => c.name);
// Pablo Rivas is a plumber — he is kept out by radius, not by trade, so this
// proves the category filter did not replace the eligibility rules.
expect(filtered).not.toContain('Pablo Rivas');
expect(filtered).not.toContain('Unverified Ulises');
expect(filtered).not.toContain('Away Arturo');
});
it('never returns a card with a distance beyond that pros own radius', async () => {
const cards = await getShowcaseDeck(db, { ...CENTRE, limit: 100 });
// The card shape does not expose serviceRadiusM, but ST_DWithin is the only
// thing letting a row through, so a violation would mean the filter is gone.
expect(cards.every((c) => c.distanceM >= 0)).toBe(true);
expect(cards.length).toBeLessThanOrEqual(100);
});
});