From 2222a545ff423e1ae46d33ae3f8c1f091ff5a226 Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Thu, 22 Oct 2026 12:32:54 -0400 Subject: [PATCH] [econ] owner currency 0 amount purchase --- apps/econ/src/econ.app.ts | 15 +++++++++++++-- apps/econ/src/test/integration/api.test.ts | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/econ/src/econ.app.ts b/apps/econ/src/econ.app.ts index 25fd92c..26561d3 100644 --- a/apps/econ/src/econ.app.ts +++ b/apps/econ/src/econ.app.ts @@ -6651,7 +6651,10 @@ const app = new Hono({ strict: false }) 'NOBODY IS PAID — unlike the token sale, the price does not reach the room’s owner.', '', '`RequestedPurchaseCurrencyId` must be the key’s own `PurchaseCurrencyId` and', - '`RequestedPrice` its `Price`. A key priced in TOKENS (a null `PurchaseCurrencyId`)', + '`RequestedPrice` its `Price` — except that the room’s creator or a CO-OWNER gets', + 'their own room’s keys free: the client asks them `RequestedPrice=0` (the stored', + 'price is accepted too) and nothing is subtracted.', + 'A key priced in TOKENS (a null `PurchaseCurrencyId`)', 'is not sold here — that is `GET /api/storefronts/v1/buyRoomKey`.', '', 'Answers the client’s `RoomKeyPurchaseWithCurrencyResponseDTO`, a bare object with no', @@ -6710,13 +6713,21 @@ const app = new Hono({ strict: false }) if (requestedCurrencyId.toLowerCase() !== key.PurchaseCurrencyId.toLowerCase()) { return answer(false) } - if (int(body.RequestedPrice) !== key.Price) return answer(false) + // The room's creator and co-owners get their own room's keys free, as with the token + // sale — the client asks them `RequestedPrice=0` — and are never charged. + const canManage = await canManageRoomById(c.env.DB, key.RoomId, id) + if (canManage === null) return answer(false) + const requestedPrice = int(body.RequestedPrice) + if (requestedPrice !== key.Price && !(canManage && requestedPrice === 0)) { + return answer(false) + } // Claim the key BEFORE charging, as the token sale does: the insert is atomic, so of // two concurrent buys only one is charged. A buyer who cannot pay gives it back. if (!(await claimRoomKey(c.env.DB, id, key.RoomKeyId))) return answer(false) if ( key.Price > 0 && + !canManage && !(await spendRoomCurrency(c.env.DB, key.PurchaseCurrencyId, id, key.Price)) ) { await releaseRoomKey(c.env.DB, id, key.RoomKeyId) diff --git a/apps/econ/src/test/integration/api.test.ts b/apps/econ/src/test/integration/api.test.ts index 8a30138..ea8872e 100644 --- a/apps/econ/src/test/integration/api.test.ts +++ b/apps/econ/src/test/integration/api.test.ts @@ -3238,6 +3238,24 @@ describe('econ endpoints', () => { ) expect(await held(coin, 7501)).toBe(500) + // A non-manager cannot buy it for 0; the room's managers can — the co-owner at the 0 + // the client asks them, the owner at the stored price — and nothing is subtracted. + expect(await (await purchase('7502', key.RoomKeyId, 0, coin)).json()).toEqual( + refused(7502, coin, 999) + ) + await give(coin, 2, 10) + const coOwner = (await (await purchase('2', key.RoomKeyId, 0, coin)).json()) as { + Balance: { Balance: number } + RoomKeyResponse: { Status: number } + } + expect(coOwner.RoomKeyResponse.Status).toBe(0) + expect(coOwner.Balance.Balance).toBe(10) + const own = (await (await purchase('1', key.RoomKeyId, 1000, coin)).json()) as typeof coOwner + expect(own.RoomKeyResponse.Status).toBe(0) + expect(own.Balance.Balance).toBe(0) + expect(await holders(key.RoomKeyId)).toEqual([1, 2, 7501]) + expect([await held(coin, 1), await held(coin, 2)]).toEqual([0, 10]) + await env.DB.prepare('DELETE FROM room_key_player').run() await env.DB.prepare('DELETE FROM room_key').run() await env.DB.prepare('DELETE FROM room_balance WHERE currency_id IN (?1, ?2)')