[store] wip

This commit is contained in:
Devin Zuczek
2026-08-28 01:56:14 -04:00
parent f8ca01dd4d
commit c13f62bb91
13 changed files with 988 additions and 388 deletions
+13 -9
View File
@@ -643,10 +643,11 @@ interface GiftRequest {
* 404s as "no such storefront".
*/
const STOREFRONT_ALIASES: Record<string, string> = {
// Empty. 1704 was here, served sf3's catalog, until `static/storefronts/sf1704.json` was
// generated — see `runx storefront build`. Its line came out the moment the file appeared,
// exactly as the note above says it must: an alias silently beats a real file of that name,
// so leaving it would have kept serving sf3 from a storefront that now has its own catalog.
// Empty. 1704 was here for a while, standing in for a 2025 gift-drop storefront nobody had
// captured; the items it was meant to sell turned out to belong in the general store, so
// they are in `sf3-2025.json` and served as storefront 3 — see {@link STOREFRONT_BY_BUILD}.
// That is a per-BUILD variant of one storefront rather than an alias between two ids, which
// is why nothing is listed here.
}
/**
@@ -1470,12 +1471,15 @@ function toPurchaseMethodId(raw: Partial<PurchaseMethodId> | null | undefined):
* Catalog rows as STORE ITEMS, so a bag can be resolved against the `catalog` table the same
* way it is resolved against an `sf{N}.json` file.
*
* The generated storefront (sf1704) is built from these very rows with this very pricing, so an
* item bought here costs exactly what that file lists it at. That is not a nicety: `priceCheck`
* refuses a line whose posted `RequestedPrice` doesn't match, so two pricings would 409 every
* purchase the client made from the page it was shown.
* The generated storefront (`sf3-2025.json`) is built from these very rows with this very
* pricing, so an item bought here costs exactly what that file lists it at. That is not a
* nicety: `priceCheck` refuses a line whose posted `RequestedPrice` doesn't match, so two
* pricings would 409 every purchase the client made from the page it was shown.
*
* SKINS come through too, keyed the way a gift-drop keys equipment (`EquipmentPrefabName` +
* Mostly redundant now that the merged store carries every sellable AVATAR ITEM — a newer
* build's bag resolves those straight out of the file. What it still reaches that the file does
* not is SKINS, which the generator leaves out, keyed the way a gift-drop keys equipment
* (`EquipmentPrefabName` +
* `EquipmentModificationGuid`) rather than as an avatar item — which is what lets a skin be
* bought at all, since no generated storefront file lists one.
*
@@ -785,6 +785,74 @@ describe('econ endpoints', () => {
expect(anon.status).toBe(401)
})
test('bulkpurchase buys a merged-store catalog item from storefront 3', async () => {
// The exact request the client sends, verbatim: a catalog id under storefront 3, which is
// what the merged sf3-2025 lists it as. It resolves because `loadStorefront` picks the file
// by the caller's build, so what the store page offered is what the purchase is checked
// against.
const res = await exports.default.fetch(`${ORIGIN}/api/items/bulkpurchase`, {
method: 'POST',
headers: {
...((await bearer('4801', undefined, '20250718.01')) as Record<string, string>),
'Content-Type': 'application/json',
},
body: JSON.stringify({
PurchaseItemRequests: [
{
ItemPurchaseMethodId: { Type: 0, NumberId: 10222, Guid: null },
RequestedPrice: 3000,
Gift: null,
CouponConsumablePlayerMappingId: null,
DuplicateItemCount: 1,
},
],
StorefrontType: 3,
CurrencyType: 2,
BypassGiftPackages: false,
AllowPartialSuccess: true,
ShoppingBagId: null,
}),
})
expect(res.status).toBe(200)
const body = (await res.json()) as {
Success: boolean
Error: string
Value: { Balance: number } | null
}
// It used to answer `{ Success: false, Error: "Item not found" }` — storefront 3 resolved
// to the captured sf3, which has no id in the catalog range.
expect(body.Error).not.toBe('Item not found')
expect(body.Success).toBe(true)
// 10222 is "Maker Pen Shirt", rarity 50, which the shared pricing puts at 3000 — the same
// number the client posted, because the file it browsed was generated from that pricing.
const item = sf32025.StoreItems.find((i) => i.PurchasableItemId === 10222)
expect(item?.Prices[0]?.Price).toBe(3000)
// The SAME request from an old build still fails: its storefront 3 is the captured sf3,
// which does not list the item, and nothing offers it that id anyway.
const legacy = await exports.default.fetch(`${ORIGIN}/api/items/bulkpurchase`, {
method: 'POST',
headers: {
...((await bearer('4802', undefined, '20230414')) as Record<string, string>),
'Content-Type': 'application/json',
},
body: JSON.stringify({
PurchaseItemRequests: [
{
ItemPurchaseMethodId: { Type: 0, NumberId: 10222, Guid: null },
RequestedPrice: 3000,
DuplicateItemCount: 1,
},
],
StorefrontType: 3,
CurrencyType: 2,
AllowPartialSuccess: true,
}),
})
expect(((await legacy.json()) as { Success: boolean }).Success).toBe(false)
})
test('bulkpurchase resolves catalog ids for newer builds, at the storefronts price', async () => {
// A catalog row the generated storefront would list at 600 (rarity 10) and a skin the
// storefront lists nowhere at all — both bought straight off the `catalog` table.