mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[econ] Fix three 2026 response contracts (#125)
* [econ] Wrap 2026 room offer purchase counts in a result envelope * [econ] Include the built-in identity flag on owned 2026 consumables * [econ] Return the 2026 empty subscription season DTO
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { describeRoute, openAPIRouteHandler } from 'hono-openapi'
|
import { describeRoute, openAPIRouteHandler } from 'hono-openapi'
|
||||||
import { useWorkersLogger } from 'workers-tagged-logger'
|
import { useWorkersLogger } from 'workers-tagged-logger'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addXp,
|
addXp,
|
||||||
@@ -4720,8 +4721,30 @@ const app = new Hono<App>({ strict: false })
|
|||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
'/econ/roomOffer/room/:roomId/purchaseCounts',
|
'/econ/roomOffer/room/:roomId/purchaseCounts',
|
||||||
listRoute('Per-offer purchase counts for a room', 'Empty stub'),
|
describeRoute({
|
||||||
(c) => c.json([])
|
summary: 'Per-offer purchase counts for a room',
|
||||||
|
responses: {
|
||||||
|
200: json(
|
||||||
|
z.union([
|
||||||
|
JsonArray,
|
||||||
|
z.object({
|
||||||
|
Value: JsonArray,
|
||||||
|
Success: z.boolean(),
|
||||||
|
Error: z.string().nullable(),
|
||||||
|
error_id: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
'2026: RecNet result envelope; older builds: a list'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
if (((await authedBuild(c)) ?? 0) >= 20260323) {
|
||||||
|
// The 2026 client decodes RecNetResultListObject<RoomOfferPurchaseCountDTO>.
|
||||||
|
return c.json({ Value: [], Success: true, Error: null, error_id: null })
|
||||||
|
}
|
||||||
|
return c.json([])
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
'/econ/roomGiftDropShops/room/:roomId',
|
'/econ/roomGiftDropShops/room/:roomId',
|
||||||
@@ -4901,7 +4924,14 @@ const app = new Hono<App>({ strict: false })
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const id = await authedId(c)
|
const id = await authedId(c)
|
||||||
if (id === null) return unauthorized(c)
|
if (id === null) return unauthorized(c)
|
||||||
return c.json(await getConsumables(c.env.DB, id))
|
const consumables = await getConsumables(c.env.DB, id)
|
||||||
|
// These rows are purchases of built-in store items. The 2026 client
|
||||||
|
// includes this flag in the instance key and verifies it against the definition.
|
||||||
|
return c.json(
|
||||||
|
((await authedBuild(c)) ?? 0) >= 20260323
|
||||||
|
? consumables.map((item) => ({ ...item, IsRRO: true }))
|
||||||
|
: consumables
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -6638,13 +6668,41 @@ const app = new Hono<App>({ strict: false })
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// The subscription seasons running right now (the RR+ seasonal reward tracks). Nothing
|
// The 2026 decoder requires an object; RecNet rejects JSON null as an empty response.
|
||||||
// here runs a season, so this is an empty-list stub — the client reads it as "no season
|
|
||||||
// in progress" and skips the seasonal UI, where a 404 stalls the RR+ page load.
|
|
||||||
.get(
|
.get(
|
||||||
'/api/subscriptionseasons/v1/seasons/current',
|
'/api/subscriptionseasons/v1/seasons/current',
|
||||||
listRoute('Current subscription seasons', 'Empty stub — no RR+ season is running'),
|
describeRoute({
|
||||||
(c) => c.json([])
|
summary: 'Current subscription season',
|
||||||
|
responses: {
|
||||||
|
200: json(
|
||||||
|
z.union([
|
||||||
|
JsonArray,
|
||||||
|
z.object({
|
||||||
|
SubscriptionSeasonId: z.string().uuid(),
|
||||||
|
Name: z.string(),
|
||||||
|
ImageName: z.string(),
|
||||||
|
StartAt: z.string(),
|
||||||
|
NextSeasonStart: z.string().nullable(),
|
||||||
|
Milestones: JsonArray,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
'2026: season object without milestones; older builds: []'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) =>
|
||||||
|
c.json(
|
||||||
|
((await authedBuild(c)) ?? 0) >= 20260323
|
||||||
|
? {
|
||||||
|
SubscriptionSeasonId: '00000000-0000-0000-0000-000000000000',
|
||||||
|
Name: '',
|
||||||
|
ImageName: '',
|
||||||
|
StartAt: '0001-01-01T00:00:00Z',
|
||||||
|
NextSeasonStart: null,
|
||||||
|
Milestones: [],
|
||||||
|
}
|
||||||
|
: []
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Whether the caller can start a Maker AI free trial. Always false, mirroring the
|
// Whether the caller can start a Maker AI free trial. Always false, mirroring the
|
||||||
|
|||||||
@@ -3338,6 +3338,14 @@ describe('econ endpoints', () => {
|
|||||||
expect(await shop(92)).toEqual([])
|
expect(await shop(92)).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('2026 room-offer purchase counts use the captured result envelope', async () => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/econ/roomOffer/room/92/purchaseCounts`, {
|
||||||
|
headers: await bearer('42', undefined, '20260323.14'),
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual({ Value: [], Success: true, Error: null, error_id: null })
|
||||||
|
})
|
||||||
|
|
||||||
test('POST /api/ugcPurchasables/v1/items/bulk resolves custom avatar items, echoing RoomId', async () => {
|
test('POST /api/ugcPurchasables/v1/items/bulk resolves custom avatar items, echoing RoomId', async () => {
|
||||||
const item = await createCustomAvatarItem(env.DB, {
|
const item = await createCustomAvatarItem(env.DB, {
|
||||||
customAvatarItemId: crypto.randomUUID(),
|
customAvatarItemId: crypto.randomUUID(),
|
||||||
@@ -3705,6 +3713,24 @@ describe('econ endpoints', () => {
|
|||||||
expect(bad.status).toBe(400)
|
expect(bad.status).toBe(400)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('2026 owned built-in consumables include the definition identity flag', async () => {
|
||||||
|
await grantConsumable(env.DB, 9731, 'test-built-in', 3)
|
||||||
|
try {
|
||||||
|
for (const version of ['20260323.14', '20250718.01']) {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/consumables/v2/getUnlocked`, {
|
||||||
|
headers: await bearer('9731', undefined, version),
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const [item] = (await res.json()) as Array<Record<string, unknown>>
|
||||||
|
expect(item.Count).toBe(3)
|
||||||
|
if (version === '20260323.14') expect(item.IsRRO).toBe(true)
|
||||||
|
else expect(item).not.toHaveProperty('IsRRO')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await env.DB.prepare('DELETE FROM consumable WHERE account_id = ?').bind(9731).run()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('GET /api/consumables/v2/getUnlocked 401s without a token, returns []', async () => {
|
test('GET /api/consumables/v2/getUnlocked 401s without a token, returns []', async () => {
|
||||||
const anon = await exports.default.fetch(`${ORIGIN}/api/consumables/v2/getUnlocked`)
|
const anon = await exports.default.fetch(`${ORIGIN}/api/consumables/v2/getUnlocked`)
|
||||||
expect(anon.status).toBe(401)
|
expect(anon.status).toBe(401)
|
||||||
@@ -8256,3 +8282,18 @@ describe('discord role gift', () => {
|
|||||||
expect(await getPendingGifts(env.DB, 9322)).toHaveLength(1)
|
expect(await getPendingGifts(env.DB, 9322)).toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('2026 current subscription season supplies a nonempty disabled-season DTO', async () => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/subscriptionseasons/v1/seasons/current`, {
|
||||||
|
headers: await bearer('42', undefined, '20260323.14'),
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual({
|
||||||
|
SubscriptionSeasonId: '00000000-0000-0000-0000-000000000000',
|
||||||
|
Name: '',
|
||||||
|
ImageName: '',
|
||||||
|
StartAt: '0001-01-01T00:00:00Z',
|
||||||
|
NextSeasonStart: null,
|
||||||
|
Milestones: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user