mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
[store] wip
This commit is contained in:
@@ -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 storefront’s 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.
|
||||
|
||||
Reference in New Issue
Block a user