From 972432d4d30d42d4951e9361cd242e083d6a5c86 Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Thu, 15 Oct 2026 15:01:21 -0400 Subject: [PATCH] [api] add xp boost stub endpoint --- apps/api/src/openapi.ts | 18 ++++++++++++++++ apps/api/src/routes/progression.ts | 25 +++++++++++++++++++++++ apps/api/src/test/integration/api.test.ts | 7 +++++++ 3 files changed, 50 insertions(+) diff --git a/apps/api/src/openapi.ts b/apps/api/src/openapi.ts index af68c0b..bf5b25c 100644 --- a/apps/api/src/openapi.ts +++ b/apps/api/src/openapi.ts @@ -326,6 +326,24 @@ export const ProgressionEventRecordDto = z.object({ LimitedRewardCollectionState: z.int(), }) +/** + * One XP boost a player bought and has running, as `GET /api/purchasableXpBoosts/activations` + * lists it. Documented from the reference shape; nothing here sells or stores boosts yet. + */ +export const XpBoostActivationDto = z.object({ + PurchasableXpBoostLedgerEntryId: z.int().describe('The purchase this activation came from'), + PurchasableXpBoostId: z.string().describe('GUID of the boost product bought'), + XpMultiplier: z.number().describe('XP is multiplied by this while the boost runs, e.g. 2.0'), + StartTime: z.string().describe('ISO-8601 UTC'), + EndTime: z.string().describe('ISO-8601 UTC'), +}) + +/** `GET /api/purchasableXpBoosts/activations` — a paged list of the caller's active boosts. */ +export const XpBoostActivationsResponse = z.object({ + Results: XpBoostActivationDto.array(), + TotalResults: z.int().describe('The number of entries in `Results`'), +}) + /** * The form body of `POST /api/PlayerCheer/v1/create`. Nothing here is stored beyond the * counter the cheer increments: `Anonymous` is spent on the notification it triggers and diff --git a/apps/api/src/routes/progression.ts b/apps/api/src/routes/progression.ts index 1539f18..2d9b76f 100644 --- a/apps/api/src/routes/progression.ts +++ b/apps/api/src/routes/progression.ts @@ -45,6 +45,7 @@ import { ReputationDto, SetSelectedCheerRequest, UNAUTHORIZED_RESPONSE, + XpBoostActivationsResponse, } from '../openapi' import { addCheer, @@ -672,3 +673,27 @@ export const progressionRoutes = new Hono({ strict: false }) }) } ) + + // The XP boosts the caller has bought and has running — the client multiplies the XP it + // shows by them and draws a timer for each. + // + // STUB: always the empty page. Nothing sells boosts yet, so nobody has one running. No + // auth while the answer is the same for everybody, like `xpEarnedToday` above; it will + // need the caller once it is real, since the path names no player. + .get( + '/api/purchasableXpBoosts/activations', + describeRoute({ + tags: ['Progression'], + summary: 'The caller’s active XP boosts (stub)', + description: + 'The XP boosts the caller has running, as `{ Results, TotalResults }`. Always the ' + + 'empty page (`{ "Results": [], "TotalResults": 0 }`) — nothing on this server sells ' + + 'boosts yet. Each entry, once there are any, names the purchase ' + + '(`PurchasableXpBoostLedgerEntryId`), the boost bought (`PurchasableXpBoostId`), its ' + + '`XpMultiplier`, and when it runs (`StartTime`/`EndTime`, ISO-8601 UTC).', + responses: { + 200: json(XpBoostActivationsResponse, 'Always `{ "Results": [], "TotalResults": 0 }`'), + }, + }), + (c) => c.json({ Results: [], TotalResults: 0 }) + ) diff --git a/apps/api/src/test/integration/api.test.ts b/apps/api/src/test/integration/api.test.ts index 6a9286b..55a2640 100644 --- a/apps/api/src/test/integration/api.test.ts +++ b/apps/api/src/test/integration/api.test.ts @@ -2144,6 +2144,12 @@ describe('public endpoints', () => { expect(await other.json()).toMatchObject({ ProgressionEventId: 7 }) }) + test('GET /api/purchasableXpBoosts/activations is the empty page', async () => { + const res = await exports.default.fetch(`${ORIGIN}/api/purchasableXpBoosts/activations`) + expect(res.status).toBe(200) + expect(await res.json()).toStrictEqual({ Results: [], TotalResults: 0 }) + }) + test('GET /api/rooms/v1/filters returns an object with filter arrays', async () => { const res = await exports.default.fetch(`${ORIGIN}/api/rooms/v1/filters`) expect(res.status).toBe(200) @@ -10247,6 +10253,7 @@ describe('openapi', () => { 'GET /api/progressionEvents/active', 'GET /api/progressionEvents/event/{id}', 'GET /api/progressionEvents/record/{id}', + 'GET /api/purchasableXpBoosts/activations', 'GET /api/quickPlay/v1/getandclear', 'GET /api/referee/files', 'GET /api/relationships/mutualfriends',