All routes live in apps/zooly-app/ and are thin routers — they parse params, handle auth/CORS/errors, and delegate to @zooly/team-leaderboard-srv or @zooly/db. They reuse the shared CORS helper apps/zooly-app/app/api/merch/cors.ts (getCorsHeaders / handleOptions) with origins from ALLOWED_DOMAINS_CORS.
Base: apps/zooly-app/app/api/merch/leaderboard/
GET /api/merch/leaderboard/:boardReturns one board (league) by slug: its chrome + its BoardTeams, ordered by count desc (count = seedCount + event COUNT).
{ boardTeamId, boardTeamSlug, name, flagImgUrl, mainColor, colors[], facePaintImageUrls[], count, pct, target }.pct is the share within this board.target is { kind: "store", url }, where url is the campaign-derived store URL (built at runtime from the linked campaign), or null when the BoardTeam has no live campaign.isDefault = world-cup). Slug matching is case-insensitive and slug-normalized (nba-finals ↔ "NBA Finals").404 ("Board not found") when the requested board does not exist or is inactive.Handler: apps/zooly-app/app/api/merch/leaderboard/[board]/route.ts → buildBoardResponse(slug).
GET /api/merch/leaderboard/search?q=Server-side autocomplete (min 3 chars) returning matching boards and BoardTeams (by name / searchTerms). Each team result carries its leaderboardSlug so the client can switch boards or jump to the team.
Handler: apps/zooly-app/app/api/merch/leaderboard/search/route.ts → searchLeaderboard(q).
POST /api/merch/leaderboard/linkBody { sessionId, boardTeamId }. Persists the credited BoardTeam on merch_session (called by the merch store landing when arriving with ?bt=).
Handler: apps/zooly-app/app/api/merch/leaderboard/link/route.ts → linkSessionToBoardTeam.
POST /api/merch/leaderboard/statCredits a completion for a session by inserting one stat event for its boardTeamId; idempotent via unique(merchSessionId, eventType).
As built, crediting is triggered server-side from
ai/generate(aftergenerationCompletedAt) viarecordCompletionFromSession, not from the client. This endpoint remains for manual/explicit crediting.
Handler: apps/zooly-app/app/api/merch/leaderboard/stat/route.ts.
requireAdmin + CORS)Base: apps/zooly-app/app/api/merch/admin/. Thin routers calling @zooly/db directly. All require the same zooly-auth SSO + requireAdmin middleware as merch-admin.
| Method + path | Purpose |
|---|---|
GET /leaderboards | List all boards |
POST /leaderboards | Create a board |
GET /leaderboards/[id] | Get one board |
PATCH /leaderboards/[id] | Update a board |
DELETE /leaderboards/[id] | Delete a board |
GET /leaderboards/[id]/teams | List a board's teams |
POST /leaderboards/[id]/teams | Create a team under a board |
GET /leaderboards/[id]/teams/[teamId] | Get one team |
PATCH /leaderboards/[id]/teams/[teamId] | Update a team |
DELETE /leaderboards/[id]/teams/[teamId] | Delete a team |
GET /leads | Paginated lead submissions (kind filter + search) |
Files:
apps/zooly-app/app/api/merch/admin/leaderboards/route.tsapps/zooly-app/app/api/merch/admin/leaderboards/[id]/route.tsapps/zooly-app/app/api/merch/admin/leaderboards/[id]/teams/route.tsapps/zooly-app/app/api/merch/admin/leaderboards/[id]/teams/[teamId]/route.tsapps/zooly-app/app/api/merch/admin/leads/route.ts@zooly/team-leaderboard-srv)packages/team-leaderboard/srv/src/
build-board-response.ts — buildBoardResponse(slug): resolves board (or default), composes chrome + BoardTeams, builds each target.url at runtime from the linked campaign (buildStoreUrl(accountSlug, campaignSlug), null when no live campaign), ordering, and stat formatting.search-leaderboard.ts — searchLeaderboard(q): board + BoardTeam autocomplete (min 3 chars).link-session.ts — linkSessionToBoardTeam(sessionId, boardTeamId): wraps setSessionBoardTeam.record-completion.ts — recordCompletionFromSession(sessionId): reads the session's boardTeamId and records the stat; wired into ai/generate after generationCompletedAt (no-op when no boardTeamId).index.ts — barrel export.packages/db/src/access/merch/merch-leaderboard.ts (exported from packages/db/src/index.ts; no table exports):
getBoardBySlug, getDefaultBoard, listBoardTeams, searchBoardsAndTeams.recordCompletion, setSessionBoardTeam, getSessionBoardTeamId.upsertBoard, upsertBoardTeam.listAllBoards, getBoardById, createBoard, updateBoard, deleteBoard (with isDefault transaction), listAllBoardTeamsAdmin, getBoardTeamById, createBoardTeam, updateBoardTeam, deleteBoardTeam.listLeadSubmissions (packages/db/src/access/leadSubmission.ts).