mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 15:09:18 -07:00
[econ] add getBalance endpoint
This commit is contained in:
@@ -139,6 +139,7 @@ import {
|
|||||||
RemoveInfluencerBody,
|
RemoveInfluencerBody,
|
||||||
RoomConsumableDto,
|
RoomConsumableDto,
|
||||||
RoomConsumableEnvelope,
|
RoomConsumableEnvelope,
|
||||||
|
RoomCurrencyBalance,
|
||||||
RoomCurrencyDto,
|
RoomCurrencyDto,
|
||||||
RoomCurrencyEnvelope,
|
RoomCurrencyEnvelope,
|
||||||
RoomCurrencyPurchaseOfferDto,
|
RoomCurrencyPurchaseOfferDto,
|
||||||
@@ -4586,6 +4587,59 @@ const app = new Hono<App>({ strict: false })
|
|||||||
c.json([])
|
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
|
// 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
|
// 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.
|
// 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,
|
* 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.
|
* 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(res)).toEqual([])
|
||||||
}
|
}
|
||||||
expect(await resultsOf(await award([], await bearer('1')))).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 () => {
|
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.
|
// ConfigurationData is JSON escaped into a string (or null), never a nested object.
|
||||||
for (const item of body) {
|
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
|
// first activity of the day, so another activity doesn't claim it either, and writes
|
||||||
// no row of its own.
|
// no row of its own.
|
||||||
expect((await request('rewardType=FirstActivityOfDay&Message=again')).status).toBe(200)
|
expect((await request('rewardType=FirstActivityOfDay&Message=again')).status).toBe(200)
|
||||||
expect(
|
expect((await request('rewardType=FirstActivityOfDay&giftContext=Soccer')).status).toBe(200)
|
||||||
(await request('rewardType=FirstActivityOfDay&giftContext=Soccer')).status
|
|
||||||
).toBe(200)
|
|
||||||
expect(await statusOf('FirstActivityOfDay')).toEqual(claimed)
|
expect(await statusOf('FirstActivityOfDay')).toEqual(claimed)
|
||||||
expect(await statusOf('FirstActivityOfDay', 'Soccer')).toBeNull()
|
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.
|
// An activity with a prize still hands it over, in a box carrying no XP.
|
||||||
expect(
|
expect((await request('rewardType=PostQuestActivity&giftContext=Quest_Goblin_S')).status).toBe(
|
||||||
(await request('rewardType=PostQuestActivity&giftContext=Quest_Goblin_S')).status
|
200
|
||||||
).toBe(200)
|
)
|
||||||
const quest = (await rewardBoxes()).at(-1)
|
const quest = (await rewardBoxes()).at(-1)
|
||||||
expect(quest).toMatchObject({ GiftContext: 4003, Xp: 0 })
|
expect(quest).toMatchObject({ GiftContext: 4003, Xp: 0 })
|
||||||
expect(quest?.AvatarItemDesc).not.toBe('')
|
expect(quest?.AvatarItemDesc).not.toBe('')
|
||||||
@@ -7491,6 +7516,7 @@ describe('econ endpoints', () => {
|
|||||||
'GET /api/roomconsumables/v1/roomConsumable/room/{roomId}/me',
|
'GET /api/roomconsumables/v1/roomConsumable/room/{roomId}/me',
|
||||||
'GET /api/roomcurrencies/v1/currencies',
|
'GET /api/roomcurrencies/v1/currencies',
|
||||||
'GET /api/roomcurrencies/v1/getAllBalances',
|
'GET /api/roomcurrencies/v1/getAllBalances',
|
||||||
|
'GET /api/roomcurrencies/v1/getBalance',
|
||||||
'GET /api/roomcurrencies/v1/getPurchaseOffersBatch',
|
'GET /api/roomcurrencies/v1/getPurchaseOffersBatch',
|
||||||
'GET /api/roomkeys/v1/mine',
|
'GET /api/roomkeys/v1/mine',
|
||||||
'GET /api/roomkeys/v1/room',
|
'GET /api/roomkeys/v1/room',
|
||||||
|
|||||||
Reference in New Issue
Block a user