mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[econ] add getBalance endpoint
This commit is contained in:
@@ -139,6 +139,7 @@ import {
|
||||
RemoveInfluencerBody,
|
||||
RoomConsumableDto,
|
||||
RoomConsumableEnvelope,
|
||||
RoomCurrencyBalance,
|
||||
RoomCurrencyDto,
|
||||
RoomCurrencyEnvelope,
|
||||
RoomCurrencyPurchaseOfferDto,
|
||||
@@ -4586,6 +4587,59 @@ const app = new Hono<App>({ strict: false })
|
||||
c.json([])
|
||||
)
|
||||
|
||||
// What one player holds of one room currency. Any signed-in caller may read any player's,
|
||||
// as the query names the player rather than taking the caller.
|
||||
.get(
|
||||
'/api/roomcurrencies/v1/getBalance',
|
||||
describeRoute({
|
||||
tags: ['Econ'],
|
||||
summary: 'A player’s balance of one room currency',
|
||||
description: [
|
||||
'What the player named by `accountId` holds of the room currency `currencyId`',
|
||||
'(`room_balance`). A player who has never earned any — and a currency that does not',
|
||||
'exist — is a `Balance` of 0, not a 404.',
|
||||
'',
|
||||
'`ModifiedAt` is the time of the response: `room_balance` keeps no modification time.',
|
||||
'A missing `currencyId` or a non-integer `accountId` is a 400.',
|
||||
].join('\n'),
|
||||
security: AUTHED,
|
||||
parameters: [
|
||||
{
|
||||
name: 'currencyId',
|
||||
in: 'query',
|
||||
required: true,
|
||||
description: 'The room currency',
|
||||
schema: { type: 'string' },
|
||||
},
|
||||
{
|
||||
name: 'accountId',
|
||||
in: 'query',
|
||||
required: true,
|
||||
description: 'The player whose balance to read',
|
||||
schema: { type: 'integer' },
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: json(RoomCurrencyBalance, 'The player’s balance'),
|
||||
400: { description: 'A missing `currencyId` or a non-integer `accountId`' },
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
if ((await authedId(c)) === null) return unauthorized(c)
|
||||
const currencyId = c.req.query('currencyId')?.trim() ?? ''
|
||||
const rawAccountId = c.req.query('accountId')?.trim() ?? ''
|
||||
if (currencyId === '' || !/^\d+$/.test(rawAccountId)) return c.body(null, 400)
|
||||
const accountId = Number(rawAccountId)
|
||||
return c.json({
|
||||
AccountId: accountId,
|
||||
CurrencyId: currencyId,
|
||||
Balance: await getRoomBalance(c.env.DB, currencyId, accountId),
|
||||
ModifiedAt: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Buy room currency with tokens, through one of the currency's purchase offers. The body
|
||||
// names only the OFFER — which currency it sells is whichever one lists it — and the
|
||||
// amount and price the client rendered, both of which must still match the offer.
|
||||
|
||||
@@ -1187,6 +1187,17 @@ export const PurchaseRoomCurrencyRequest = z.object({
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
* What a player holds of one room currency, as `GET /api/roomcurrencies/v1/getBalance` answers
|
||||
* it. `ModifiedAt` is the time of the response: `room_balance` keeps no modification time.
|
||||
*/
|
||||
export const RoomCurrencyBalance = z.object({
|
||||
AccountId: z.int().describe('The player the balance belongs to'),
|
||||
CurrencyId: z.string().describe('The room currency'),
|
||||
Balance: z.int().describe('What the player holds of it; 0 when they have never earned any'),
|
||||
ModifiedAt: z.string().describe('ISO-8601 UTC; the time of the response'),
|
||||
})
|
||||
|
||||
/**
|
||||
* What `POST /api/roomCurrencies/v2/purchase` answers: both balances the purchase moved,
|
||||
* inside the `{ Value, Success, Error, error_id }` envelope the room-currency writes use.
|
||||
|
||||
@@ -1888,6 +1888,31 @@ describe('econ endpoints', () => {
|
||||
expect(await resultsOf(res)).toEqual([])
|
||||
}
|
||||
expect(await resultsOf(await award([], await bearer('1')))).toEqual([])
|
||||
|
||||
// getBalance reads back what the awards left, for any player the query names.
|
||||
const balance = async (query: string, headers?: Record<string, string>) =>
|
||||
exports.default.fetch(`${ORIGIN}/api/roomcurrencies/v1/getBalance?${query}`, { headers })
|
||||
expect((await balance(`currencyId=${coin.CurrencyId}&accountId=205`)).status).toBe(401)
|
||||
const read = await balance(`currencyId=${coin.CurrencyId}&accountId=205`, await bearer('999'))
|
||||
expect(read.status).toBe(200)
|
||||
const body205 = (await read.json()) as Record<string, unknown>
|
||||
expect(Object.keys(body205)).toEqual(['AccountId', 'CurrencyId', 'Balance', 'ModifiedAt'])
|
||||
expect(body205).toMatchObject({ AccountId: 205, CurrencyId: coin.CurrencyId, Balance: 6 })
|
||||
expect(Date.parse(body205.ModifiedAt as string)).not.toBeNaN()
|
||||
// Never paid, and an unknown currency, are both a balance of 0.
|
||||
for (const query of [
|
||||
`currencyId=${coin.CurrencyId}&accountId=207`,
|
||||
'currencyId=a3f1e8d2-0000-4000-8000-000000000000&accountId=205',
|
||||
]) {
|
||||
expect(await (await balance(query, await bearer('1'))).json()).toMatchObject({ Balance: 0 })
|
||||
}
|
||||
for (const query of [
|
||||
`currencyId=${coin.CurrencyId}`,
|
||||
'accountId=205',
|
||||
`currencyId=${coin.CurrencyId}&accountId=x`,
|
||||
]) {
|
||||
expect((await balance(query, await bearer('1'))).status).toBe(400)
|
||||
}
|
||||
})
|
||||
|
||||
test('POST awardCurrency/bulk floors at zero and ignores the Limit', async () => {
|
||||
@@ -3784,7 +3809,9 @@ describe('econ endpoints', () => {
|
||||
})
|
||||
// ConfigurationData is JSON escaped into a string (or null), never a nested object.
|
||||
for (const item of body) {
|
||||
expect(item.ConfigurationData === null || typeof item.ConfigurationData === 'string').toBe(true)
|
||||
expect(item.ConfigurationData === null || typeof item.ConfigurationData === 'string').toBe(
|
||||
true
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6613,9 +6640,7 @@ describe('econ endpoints', () => {
|
||||
// first activity of the day, so another activity doesn't claim it either, and writes
|
||||
// no row of its own.
|
||||
expect((await request('rewardType=FirstActivityOfDay&Message=again')).status).toBe(200)
|
||||
expect(
|
||||
(await request('rewardType=FirstActivityOfDay&giftContext=Soccer')).status
|
||||
).toBe(200)
|
||||
expect((await request('rewardType=FirstActivityOfDay&giftContext=Soccer')).status).toBe(200)
|
||||
expect(await statusOf('FirstActivityOfDay')).toEqual(claimed)
|
||||
expect(await statusOf('FirstActivityOfDay', 'Soccer')).toBeNull()
|
||||
|
||||
@@ -7021,9 +7046,9 @@ describe('econ endpoints', () => {
|
||||
)
|
||||
|
||||
// An activity with a prize still hands it over, in a box carrying no XP.
|
||||
expect(
|
||||
(await request('rewardType=PostQuestActivity&giftContext=Quest_Goblin_S')).status
|
||||
).toBe(200)
|
||||
expect((await request('rewardType=PostQuestActivity&giftContext=Quest_Goblin_S')).status).toBe(
|
||||
200
|
||||
)
|
||||
const quest = (await rewardBoxes()).at(-1)
|
||||
expect(quest).toMatchObject({ GiftContext: 4003, Xp: 0 })
|
||||
expect(quest?.AvatarItemDesc).not.toBe('')
|
||||
@@ -7491,6 +7516,7 @@ describe('econ endpoints', () => {
|
||||
'GET /api/roomconsumables/v1/roomConsumable/room/{roomId}/me',
|
||||
'GET /api/roomcurrencies/v1/currencies',
|
||||
'GET /api/roomcurrencies/v1/getAllBalances',
|
||||
'GET /api/roomcurrencies/v1/getBalance',
|
||||
'GET /api/roomcurrencies/v1/getPurchaseOffersBatch',
|
||||
'GET /api/roomkeys/v1/mine',
|
||||
'GET /api/roomkeys/v1/room',
|
||||
|
||||
Reference in New Issue
Block a user