Greenfield scaffold for Linkder, a swipe-to-hire marketplace for local
professional services.
- pnpm/turbo monorepo: apps/web, packages/{shared,db}
- Postgres 16 + PostGIS via docker compose (ports 5442/6389 to avoid
clashing with other local stacks)
- Drizzle schema, 23 tables, geography(Point,4326) with GiST indexes
- Domain core in packages/shared: integer-cent money, status transition
graphs, deck ranking weights, cancellation policy — 46 unit tests
- Deck query: filtering in Postgres on the GiST index, ranking in JS so
the weights stay tunable — 18 integration tests against a seeded DB
- Deterministic seed placing pros at known distances, including three
that must NOT appear on a deck (out of radius, unverified, away)
- Next.js 15 app shell with a working swipe deck
- CI: typecheck, lint, test, build against live postgres+redis
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
UNRATED_BASELINE,
|
|
newProBoost,
|
|
proximityScore,
|
|
recencyScore,
|
|
score,
|
|
smoothedRating,
|
|
} from '../src/ranking';
|
|
|
|
const NOW = new Date('2026-06-01T12:00:00Z');
|
|
const daysAgo = (n: number) => new Date(NOW.getTime() - n * 86_400_000);
|
|
|
|
describe('smoothedRating', () => {
|
|
it('falls back to the baseline for an unrated pro', () => {
|
|
expect(smoothedRating(null, 0)).toBe(UNRATED_BASELINE);
|
|
});
|
|
|
|
it('does not let one 5-star review top the deck', () => {
|
|
const oneReview = smoothedRating(5, 1);
|
|
const manyReviews = smoothedRating(4.8, 50);
|
|
expect(oneReview).toBeLessThan(manyReviews);
|
|
});
|
|
|
|
it('converges on the true rating as reviews accumulate', () => {
|
|
expect(smoothedRating(4.8, 500)).toBeCloseTo(4.8, 1);
|
|
});
|
|
|
|
it('pulls a single bad review up toward the baseline too', () => {
|
|
expect(smoothedRating(1, 1)).toBeGreaterThan(1);
|
|
});
|
|
});
|
|
|
|
describe('proximityScore', () => {
|
|
it('is 1 next door and 0 at the radius edge', () => {
|
|
expect(proximityScore(0, 10_000)).toBe(1);
|
|
expect(proximityScore(10_000, 10_000)).toBe(0);
|
|
});
|
|
|
|
it('clamps beyond the radius rather than going negative', () => {
|
|
expect(proximityScore(50_000, 10_000)).toBe(0);
|
|
});
|
|
|
|
it('handles a zero radius without dividing by zero', () => {
|
|
expect(proximityScore(100, 0)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('recencyScore', () => {
|
|
it('rewards active pros and decays dormant ones to zero', () => {
|
|
expect(recencyScore(NOW, NOW)).toBe(1);
|
|
expect(recencyScore(daysAgo(7), NOW)).toBeCloseTo(0.5, 1);
|
|
expect(recencyScore(daysAgo(30), NOW)).toBe(0);
|
|
});
|
|
|
|
it('scores a pro who has never been active at zero', () => {
|
|
expect(recencyScore(null, NOW)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('newProBoost', () => {
|
|
it('gives fresh supply a decaying head start', () => {
|
|
expect(newProBoost(NOW, NOW)).toBe(1);
|
|
expect(newProBoost(daysAgo(15), NOW)).toBeCloseTo(0.5, 1);
|
|
expect(newProBoost(daysAgo(60), NOW)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('score', () => {
|
|
const base = {
|
|
ratingAvg: 4.5,
|
|
ratingCount: 20,
|
|
responseRate: 0.9,
|
|
distanceM: 2_000,
|
|
serviceRadiusM: 10_000,
|
|
lastActiveAt: NOW,
|
|
createdAt: daysAgo(200),
|
|
now: NOW,
|
|
};
|
|
|
|
it('always lands in 0..1', () => {
|
|
expect(score(base)).toBeGreaterThan(0);
|
|
expect(score(base)).toBeLessThanOrEqual(1);
|
|
});
|
|
|
|
it('ranks the closer of two identical pros higher', () => {
|
|
const near = score({ ...base, distanceM: 500 });
|
|
const far = score({ ...base, distanceM: 9_000 });
|
|
expect(near).toBeGreaterThan(far);
|
|
});
|
|
|
|
it('ranks the more responsive of two identical pros higher', () => {
|
|
expect(score({ ...base, responseRate: 0.95 })).toBeGreaterThan(
|
|
score({ ...base, responseRate: 0.2 }),
|
|
);
|
|
});
|
|
|
|
it('does not bury a brand-new pro beneath an established one', () => {
|
|
const newbie = score({ ...base, ratingAvg: null, ratingCount: 0, responseRate: null, createdAt: NOW });
|
|
expect(newbie).toBeGreaterThan(0.3);
|
|
});
|
|
|
|
it('assumes an unknown response rate is average rather than terrible', () => {
|
|
const unknown = score({ ...base, responseRate: null });
|
|
const terrible = score({ ...base, responseRate: 0 });
|
|
expect(unknown).toBeGreaterThan(terrible);
|
|
});
|
|
});
|