[econ] stub active consumable endpoint (?)

This commit is contained in:
Devin Zuczek
2026-10-06 14:00:25 -04:00
parent 19fe7ac7c3
commit 525e6ee24e
4 changed files with 88 additions and 0 deletions
+1
View File
@@ -43,6 +43,7 @@ missing/invalid). `~` = optional auth: served to anyone, personalised for a vali
| POST | `/api/settings/v2/set` | ✓ | Persist settings (accept-and-ack) | | POST | `/api/settings/v2/set` | ✓ | Persist settings (accept-and-ack) |
| GET | `/api/consumables/v2/getUnlocked` | ✓ | Unlocked consumables | | GET | `/api/consumables/v2/getUnlocked` | ✓ | Unlocked consumables |
| POST | `/api/consumables/v1/consume` | ✓ | Consume an owned consumable | | POST | `/api/consumables/v1/consume` | ✓ | Consume an owned consumable |
| POST | `/api/consumables/v1/updateActive` | ✓ | Mark a consumable active (stub, empty) |
| GET | `/api/storefronts/v4/balance/:currencyType` | ✓ | Currency balance | | GET | `/api/storefronts/v4/balance/:currencyType` | ✓ | Currency balance |
| GET | `/api/storefronts/v3/giftdropstore/:id` | | Gift-drop storefront catalog | | GET | `/api/storefronts/v3/giftdropstore/:id` | | Gift-drop storefront catalog |
| POST | `/api/storefronts/v2/buyItem` | ✓ | Buy a storefront item | | POST | `/api/storefronts/v2/buyItem` | ✓ | Buy a storefront item |
+49
View File
@@ -149,6 +149,7 @@ import {
UgcPurchasableBulkRequest, UgcPurchasableBulkRequest,
UgcPurchasableItemList, UgcPurchasableItemList,
UNAUTHORIZED_RESPONSE, UNAUTHORIZED_RESPONSE,
UpdateActiveConsumableRequest,
UpdateObjectiveRequest, UpdateObjectiveRequest,
UpdateObjectiveResponse, UpdateObjectiveResponse,
UpdateRoomCurrencyRequest, UpdateRoomCurrencyRequest,
@@ -4859,6 +4860,54 @@ const app = new Hono<App>({ strict: false })
} }
) )
// STUB. The client posts `{ Id, IsActive, ActivatedByRoomie }` when a consumable starts
// or stops being used — `Id` is the consumable row id, as on `/consume`. Nothing is
// stored: the body is read only to log it, and the client ignores the response entirely,
// so none is sent.
//
// What this most likely means: the consumable is IN USE, and marking it so is what keeps
// a player from reusing one before it is over (a second Confetti Cannon while the first
// is still firing), and `ActivatedByRoomie` says a roommate set it off rather than its
// owner. That is a guess from the field names, not observed behaviour, so no row flips
// and `/consume` is unchanged. When the client's reading of it is known, this is where
// an `is_active` flag on the consumable row (and a refusal on `/consume`) would go.
.post(
'/api/consumables/v1/updateActive',
describeRoute({
tags: ['Consumables'],
summary: 'Mark a consumable active or inactive (stub)',
description: [
'STUB. Accepts `{ Id, IsActive, ActivatedByRoomie }` — the client posting that one',
'of its consumables has started or stopped being used — and stores nothing. The',
'client ignores the response entirely, so this answers an empty 200.',
'',
'Believed to mark the consumable as IN USE so it cannot be reused before it is over,',
'with `ActivatedByRoomie` saying a roommate set it off; that is a guess from the field',
'names, and nothing acts on it yet. `/consume` is unaffected.',
].join(' '),
security: AUTHED,
requestBody: jsonBody(UpdateActiveConsumableRequest, 'The consumable and its new state'),
responses: {
200: { description: 'Empty body — the client ignores the response entirely' },
401: UNAUTHORIZED_RESPONSE,
},
}),
async (c) => {
const id = await authedId(c)
if (id === null) return unauthorized(c)
const body = await c.req
.json<Record<string, unknown>>()
.catch(() => ({}) as Record<string, unknown>)
logger.info('consumable updateActive (stub, not stored)', {
accountId: id,
consumableId: body.Id,
isActive: body.IsActive,
activatedByRoomie: body.ActivatedByRoomie,
})
return c.body(null, 200)
}
)
// Currency balance. [Authorize]. The trailing int is a CurrencyType — the client // Currency balance. [Authorize]. The trailing int is a CurrencyType — the client
// fetches `/balance/2` (RecCenterTokens) on load. Backed by the `balance` table; a // fetches `/balance/2` (RecCenterTokens) on load. Backed by the `balance` table; a
// player who has never been granted gets their starting balance on this first read. // player who has never been granted gets their starting balance on this first read.
+13
View File
@@ -1257,6 +1257,19 @@ export const ConsumeConsumableRequest = z.object({
DeltaCount: z.int().optional().describe('How many to spend; defaults to 1'), DeltaCount: z.int().optional().describe('How many to spend; defaults to 1'),
}) })
/**
* `POST /api/consumables/v1/updateActive` JSON body — the client marking one of its
* consumables active or not. Accepted and NOT acted on yet; see the route.
*/
export const UpdateActiveConsumableRequest = z.object({
Id: z.int().describe('The consumable row id'),
IsActive: z.boolean().describe('Whether the consumable is now in use'),
ActivatedByRoomie: z
.boolean()
.optional()
.describe('Whether a roommate, rather than the owner, activated it'),
})
/** `POST /api/avatar/v2/gifts/consume` form body (posted with a trailing slash). */ /** `POST /api/avatar/v2/gifts/consume` form body (posted with a trailing slash). */
export const ConsumeGiftRequest = z.object({ export const ConsumeGiftRequest = z.object({
Id: z.string().describe('The gift-box id to open'), Id: z.string().describe('The gift-box id to open'),
@@ -3578,6 +3578,30 @@ describe('econ endpoints', () => {
expect(await res.json()).toEqual([]) expect(await res.json()).toEqual([])
}) })
test('POST /api/consumables/v1/updateActive is a stub: auth-gated, stores nothing, answers nothing', async () => {
const before = await env.DB.prepare('SELECT * FROM consumable ORDER BY id').all()
const body = { Id: 7550, IsActive: true, ActivatedByRoomie: false }
const anon = await exports.default.fetch(`${ORIGIN}/api/consumables/v1/updateActive`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
expect(anon.status).toBe(401)
const res = await exports.default.fetch(`${ORIGIN}/api/consumables/v1/updateActive`, {
method: 'POST',
headers: { ...(await bearer('314')), 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
expect(res.status).toBe(200)
// No body at all: the client ignores the response entirely.
expect(await res.text()).toBe('')
// Nothing is stored yet.
const after = await env.DB.prepare('SELECT * FROM consumable ORDER BY id').all()
expect(after.results).toEqual(before.results)
})
test('POST /api/consumables/v1/consume reduces the count and deletes the row at zero', async () => { test('POST /api/consumables/v1/consume reduces the count and deletes the row at zero', async () => {
// Seed account 313 with two Supreme Pizza instances (counts 3 and 1). // Seed account 313 with two Supreme Pizza instances (counts 3 and 1).
await grantConsumable(env.DB, 313, 'Supreme Pizza', 3) await grantConsumable(env.DB, 313, 'Supreme Pizza', 3)
@@ -6991,6 +7015,7 @@ describe('econ endpoints', () => {
'POST /api/checklist/v1/complete', 'POST /api/checklist/v1/complete',
'POST /api/checklist/v2/complete', 'POST /api/checklist/v2/complete',
'POST /api/consumables/v1/consume', 'POST /api/consumables/v1/consume',
'POST /api/consumables/v1/updateActive',
'POST /api/equipment/v1/update', 'POST /api/equipment/v1/update',
'POST /api/gamerewards/v1/request', 'POST /api/gamerewards/v1/request',
'POST /api/itemWishlists/v1/isonwishlist/bulk', 'POST /api/itemWishlists/v1/isonwishlist/bulk',