add api/objectives/v1/updateobjective endpoint even though it does not work right now

This commit is contained in:
Devin Zuczek
2026-08-09 22:51:13 -04:00
parent 3d846ef2ad
commit c6ec993e2d
3 changed files with 90 additions and 0 deletions
+33
View File
@@ -69,6 +69,8 @@ import {
SaveOutfitV4Response,
SubscriptionResponse,
UNAUTHORIZED_RESPONSE,
UpdateObjectiveRequest,
UpdateObjectiveResponse,
} from './openapi'
import { getOutfits, setOutfit } from './outfit-db'
@@ -500,6 +502,37 @@ const app = new Hono<App>({ strict: false })
(c) => c.json([])
)
// Report one objective's progress. The client posts the whole objective as it now
// sees it (Index/Group identify it within `myprogress`) and reads back the state of
// the GROUP that objective belongs to — camelCase here, unlike the PascalCase body it
// posted. Stubbed: with no objectives store yet we persist nothing, echo the group
// back and never complete it, so the reward-claim flow isn't triggered. `clearedAt`
// is the clear time, which for a group we didn't clear is just now.
.post(
'/api/objectives/v1/updateobjective',
describeRoute({
tags: ['Econ'],
summary: 'Report objective progress',
description: [
'Stubbed: with no objectives store we persist nothing and never complete a group.',
'Echoes `Group` back as camelCase `group` with `isCompleted: false` so the client',
'gets a well-formed body.',
].join(' '),
requestBody: jsonBody(UpdateObjectiveRequest, 'The objective as the client now sees it'),
responses: { 200: json(UpdateObjectiveResponse, 'The echoed group, never completed') },
}),
async (c) => {
const body = await c.req
.json<{ Group?: string | number }>()
.catch(() => ({}) as Record<string, never>)
return c.json({
group: Number(body.Group) || 0,
isCompleted: false,
clearedAt: new Date().toISOString(),
})
}
)
// The player's avatar, stored as a JSON blob on their account row. Falls back
// to the default outfit when they haven't saved one — the client's parser NREs
// on an empty OutfitSelections (real RecNet never returns one).
+25
View File
@@ -112,6 +112,17 @@ export const ChallengeProgressResponse = z.object({
Complete: z.boolean().describe('Always false — no challenge-progress store yet'),
})
/**
* `POST /api/objectives/v1/updateobjective` — the group the objective belongs to, after
* the update. camelCase, unlike the PascalCase body the client posts and the PascalCase
* `ObjectiveGroups` entries `myprogress` serves — three spellings of the same group.
*/
export const UpdateObjectiveResponse = z.object({
group: z.int().describe('Echoed back from the request'),
isCompleted: z.boolean().describe('Always false — no objectives store yet'),
clearedAt: z.string().describe('When the group was cleared — now, since nothing persists'),
})
/**
* `POST /api/storefronts/v2/buyItem` — the purchase result. `Balance` is the CHANGE
* applied (the negated price), not the resulting total; the client reads its new total
@@ -198,6 +209,20 @@ export const ChallengeProgressRequest = z.object({
Config: z.string().optional().describe('The client-evaluated rule tree'),
})
/**
* `POST /api/objectives/v1/updateobjective` JSON body — one objective's state as the
* client now sees it. `Index`/`Group` identify it within `myprogress`; the rest is the
* progress it wants persisted.
*/
export const UpdateObjectiveRequest = z.object({
Index: z.int().describe('Which objective within the group'),
Group: z.int().describe('Which objective group'),
Progress: z.int().optional(),
VisualProgress: z.int().optional().describe('What the client animates towards'),
IsCompleted: z.boolean().optional(),
HasClaimedReward: z.boolean().optional(),
})
/** `POST /api/avatar/v3/saved/set` JSON body — an outfit with a target `Slot`. */
export const SaveOutfitRequest = z
.object({ Slot: z.int().describe('Which slot to overwrite; a non-integer is 400') })
@@ -342,6 +342,37 @@ describe('econ endpoints', () => {
}
})
test('POST /api/objectives/v1/updateobjective echoes the group, never completed', async () => {
const res = await exports.default.fetch(`${ORIGIN}/api/objectives/v1/updateobjective`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
Index: 2,
Group: 3,
Progress: 1,
VisualProgress: 0,
IsCompleted: true,
HasClaimedReward: false,
}),
})
expect(res.status).toBe(200)
const body = (await res.json()) as { group: number; isCompleted: boolean; clearedAt: string }
expect(body.group).toBe(3)
expect(body.isCompleted).toBe(false)
expect(Number.isNaN(Date.parse(body.clearedAt))).toBe(false)
})
test('POST /api/objectives/v1/updateobjective tolerates a non-JSON body', async () => {
const res = await exports.default.fetch(`${ORIGIN}/api/objectives/v1/updateobjective`, {
method: 'POST',
body: 'not json',
})
expect(res.status).toBe(200)
const body = (await res.json()) as { group: number; isCompleted: boolean }
expect(body.group).toBe(0)
expect(body.isCompleted).toBe(false)
})
test('GET /api/checklist/v1/current 401s without a token, returns [] with one', async () => {
const anon = await exports.default.fetch(`${ORIGIN}/api/checklist/v1/current`)
expect(anon.status).toBe(401)
@@ -1425,6 +1456,7 @@ describe('econ endpoints', () => {
'POST /api/consumables/v1/consume',
'POST /api/gamerewards/v1/request',
'POST /api/objectives/v1/cleargroup',
'POST /api/objectives/v1/updateobjective',
'POST /api/storefronts/v2/buyItem',
'PUT /api/equipment/v1/update',
])