mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: Add tests for 2023 watch shop tab and room consumables; implement new storefront endpoint and update documentation
This commit is contained in:
@@ -176,6 +176,25 @@ public sealed class RoomConsumablesShopTests : IClassFixture<DorkNetServerFactor
|
|||||||
Assert.Equal(0, emptied.GetArrayLength());
|
Assert.Equal(0, emptied.GetArrayLength());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Unprefixed_2023_commerce_probes_respond()
|
||||||
|
{
|
||||||
|
// The 2023 client calls these WITHOUT the api/ prefix on startup;
|
||||||
|
// a 404 here surfaces as "CleanupPendingTransactions failed" +
|
||||||
|
// unobserved-HTTP-404 crash reports in Player.log.
|
||||||
|
using var client = ApiClient();
|
||||||
|
|
||||||
|
using var cleanup = await client.PostAsync("/purchase/v1/cleanuppending",
|
||||||
|
new StringContent("", Encoding.UTF8, "application/x-www-form-urlencoded"));
|
||||||
|
Assert.True(cleanup.IsSuccessStatusCode);
|
||||||
|
|
||||||
|
using var hasSpent = await client.GetAsync("/purchase/v1/hasspentmoney");
|
||||||
|
Assert.Equal("false", await hasSpent.Content.ReadAsStringAsync());
|
||||||
|
|
||||||
|
var bundles = await GetJsonAsync(client, "/reminder/currentTokenBundles/v2");
|
||||||
|
Assert.Equal(JsonValueKind.Array, bundles.ValueKind);
|
||||||
|
}
|
||||||
|
|
||||||
private HttpClient ApiClient(GameClientSession? session = null)
|
private HttpClient ApiClient(GameClientSession? session = null)
|
||||||
{
|
{
|
||||||
var client = _factory.CreateClient(new() { AllowAutoRedirect = false });
|
var client = _factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace DorkNet.Server.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The 2023 client's watch Shop tab pipeline: rows come from
|
||||||
|
/// <c>/api/storefronts/v3/giftdropstore/{id}</c> (each row MUST carry a
|
||||||
|
/// singular <c>GiftDrop</c> object — the 2023 POGBAGAHGIA formatter
|
||||||
|
/// ignores the 2020 <c>GiftDrops</c> array, and a null inner desc makes
|
||||||
|
/// StoreItemListModel's filter predicate NRE so the Shop shows nothing),
|
||||||
|
/// and the featured strip ids come from <c>/v1/toptoday</c> as a BARE
|
||||||
|
/// int list that must resolve against those rows.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WatchShop2023Tests : IClassFixture<DorkNetServerFactory>
|
||||||
|
{
|
||||||
|
private readonly DorkNetServerFactory _factory;
|
||||||
|
|
||||||
|
public WatchShop2023Tests(DorkNetServerFactory factory) => _factory = factory;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Giftdropstore_rows_carry_singular_giftdrop_and_toptoday_ids_resolve()
|
||||||
|
{
|
||||||
|
using var client = ApiClient();
|
||||||
|
|
||||||
|
var storefront = await GetJsonAsync(client, "/api/storefronts/v3/giftdropstore/3");
|
||||||
|
var rows = storefront.GetProperty("StoreItems");
|
||||||
|
Assert.True(rows.GetArrayLength() > 0, "watch storefront seeded no rows");
|
||||||
|
|
||||||
|
var rowIds = new HashSet<int>();
|
||||||
|
foreach (var row in rows.EnumerateArray())
|
||||||
|
{
|
||||||
|
rowIds.Add(row.GetProperty("PurchasableItemId").GetInt32());
|
||||||
|
Assert.Equal(0, row.GetProperty("Type").GetInt32());
|
||||||
|
Assert.True(row.GetProperty("Prices").GetArrayLength() > 0);
|
||||||
|
|
||||||
|
var giftDrop = row.GetProperty("GiftDrop");
|
||||||
|
Assert.Equal(JsonValueKind.Object, giftDrop.ValueKind);
|
||||||
|
Assert.Equal(row.GetProperty("PurchasableItemId").GetInt32(),
|
||||||
|
giftDrop.GetProperty("GiftDropId").GetInt32());
|
||||||
|
Assert.Equal(JsonValueKind.String, giftDrop.GetProperty("FriendlyName").ValueKind);
|
||||||
|
Assert.Equal(JsonValueKind.String, giftDrop.GetProperty("TagList").ValueKind);
|
||||||
|
var rarity = giftDrop.GetProperty("Rarity").GetInt32();
|
||||||
|
Assert.Contains(rarity, new[] { 0, 10, 20, 30, 50 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Featured strip: bare int ids, every one resolvable in the rows.
|
||||||
|
var topToday = await GetJsonAsync(client, "/api/storefronts/v1/toptoday");
|
||||||
|
Assert.Equal(JsonValueKind.Array, topToday.ValueKind);
|
||||||
|
Assert.True(topToday.GetArrayLength() > 0, "toptoday returned no featured ids");
|
||||||
|
foreach (var id in topToday.EnumerateArray())
|
||||||
|
{
|
||||||
|
Assert.Equal(JsonValueKind.Number, id.ValueKind);
|
||||||
|
Assert.Contains(id.GetInt32(), rowIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpClient ApiClient()
|
||||||
|
{
|
||||||
|
var client = _factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||||
|
client.BaseAddress = new Uri($"http://api.{_factory.ApexDomain}");
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
||||||
|
client.DefaultRequestHeaders.UserAgent.ParseAdd("RecRoom/2023.03.21");
|
||||||
|
client.DefaultRequestHeaders.TryAddWithoutValidation("X-DorkNet-Version", "march_2023_03_21");
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<JsonElement> GetJsonAsync(HttpClient client, string path)
|
||||||
|
{
|
||||||
|
using var response = await client.GetAsync(path);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
using var document = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
|
||||||
|
return document.RootElement.Clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,6 @@ public class StorefrontsController(
|
|||||||
[HttpGet("api/storefronts/v1/all")]
|
[HttpGet("api/storefronts/v1/all")]
|
||||||
[HttpGet("api/storefronts/v2/all")]
|
[HttpGet("api/storefronts/v2/all")]
|
||||||
[HttpGet("api/storefronts/v3/all")]
|
[HttpGet("api/storefronts/v3/all")]
|
||||||
[HttpGet("api/storefronts/v1/toptoday")]
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> All()
|
public async Task<IActionResult> All()
|
||||||
{
|
{
|
||||||
@@ -40,6 +39,24 @@ public class StorefrontsController(
|
|||||||
return Ok(items.Select(i => StoreService.ToItemDto(i, domain.Apex)).ToArray());
|
return Ok(items.Select(i => StoreService.ToItemDto(i, domain.Apex)).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>GET <c>api/storefronts/v1/toptoday</c> — the 2023 Shop
|
||||||
|
/// tab's "featured today" strip. The client (DCFKEFHJAGC.IDCIMNLBINC)
|
||||||
|
/// deserialises the body as a bare <c>List<int></c> of
|
||||||
|
/// PurchasableItemIds and resolves each against the gift-drop cache
|
||||||
|
/// it loaded from <c>/v3/giftdropstore/{id}</c> — so these ids MUST
|
||||||
|
/// be ids that appear on that storefront's rows, or the client logs
|
||||||
|
/// "Could not find purchasable giftdrop" and drops them. Returning
|
||||||
|
/// item OBJECTS here (the old /all alias) made the whole featured
|
||||||
|
/// fetch fail to parse.</summary>
|
||||||
|
[HttpGet("api/storefronts/v1/toptoday")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> TopToday()
|
||||||
|
{
|
||||||
|
var items = await store.GetRenderableGiftDropItemsAsync();
|
||||||
|
var max = Math.Max(1, config.GetValue("Store:MaxTopTodayItems", 12));
|
||||||
|
return Ok(items.Take(max).Select(StoreService.PurchasableItemIdFor).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>GET <c>api/storefronts/v1/adcarouselitems</c> — the rotating
|
/// <summary>GET <c>api/storefronts/v1/adcarouselitems</c> — the rotating
|
||||||
/// promo banner at the top of the Shop, NOT the main catalog. The client
|
/// promo banner at the top of the Shop, NOT the main catalog. The client
|
||||||
/// (AdCarouselItemListModel) keys these by <c>AdCarouselItemId</c> in a
|
/// (AdCarouselItemListModel) keys these by <c>AdCarouselItemId</c> in a
|
||||||
|
|||||||
@@ -973,6 +973,13 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
|
|||||||
// REQUIRED.
|
// REQUIRED.
|
||||||
GiftDropId = itemId,
|
GiftDropId = itemId,
|
||||||
Rarity = MapRarity(i.Category),
|
Rarity = MapRarity(i.Category),
|
||||||
|
// 2023 additions (EMBCEDNHFLB formatter): TagList is a CSV
|
||||||
|
// string, Currency is the CurrencyType enum repeated, ItemSet*
|
||||||
|
// group multi-part outfits (we don't sell sets).
|
||||||
|
TagList = "",
|
||||||
|
Currency = i.CurrencyType,
|
||||||
|
ItemSetId = (int?)null,
|
||||||
|
ItemSetFriendlyName = "",
|
||||||
// Optional / nullable string fields the watch reads.
|
// Optional / nullable string fields the watch reads.
|
||||||
FriendlyName = i.DisplayName,
|
FriendlyName = i.DisplayName,
|
||||||
Tooltip = i.Description,
|
Tooltip = i.Description,
|
||||||
@@ -1008,11 +1015,22 @@ public class StoreService(DorkNetDbContext db, LevelService level, IConfiguratio
|
|||||||
IsFeatured = positionalIndex < 12, // first dozen show in Featured tab
|
IsFeatured = positionalIndex < 12, // first dozen show in Featured tab
|
||||||
Prices = new[] { price },
|
Prices = new[] { price },
|
||||||
SubscriberPrices = Array.Empty<object>(),
|
SubscriberPrices = Array.Empty<object>(),
|
||||||
// PurchasableGiftDrop adds GiftDrops on top.
|
// 2020 PurchasableGiftDrop carries a GiftDrops LIST; the 2023
|
||||||
|
// row (POGBAGAHGIA formatter) reads a single "GiftDrop" object
|
||||||
|
// instead. Ship both: whichever formatter parses the row skips
|
||||||
|
// the key it doesn't know. Missing "GiftDrop" leaves the 2023
|
||||||
|
// row's inner desc null and StoreItemListModel's filter
|
||||||
|
// predicate NREs on every item — the Shop tab shows nothing.
|
||||||
|
GiftDrop = giftDrop,
|
||||||
GiftDrops = new[] { giftDrop },
|
GiftDrops = new[] { giftDrop },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>The wire PurchasableItemId for a store row — must match
|
||||||
|
/// what <see cref="BuildPurchasableGiftDrop"/> puts on the row so
|
||||||
|
/// id-lists (toptoday) resolve against the gift-drop cache.</summary>
|
||||||
|
public static int PurchasableItemIdFor(StoreItemEntity i) => (int)(i.Id & 0x7fffffff);
|
||||||
|
|
||||||
/// <summary>GiftManager.GiftRarity wire values:
|
/// <summary>GiftManager.GiftRarity wire values:
|
||||||
/// None=-1, Common=0, Uncommon=10, Rare=20, Epic=30, Legendary=50.</summary>
|
/// None=-1, Common=0, Uncommon=10, Rare=20, Epic=30, Legendary=50.</summary>
|
||||||
private static int MapRarity(string? category) => category?.ToLowerInvariant() switch
|
private static int MapRarity(string? category) => category?.ToLowerInvariant() switch
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
# Rec Room 2023.03.21 — shop wire contracts (watch Shop tab + room consumables)
|
||||||
|
|
||||||
|
How the March-2023 client's in-room shop works
|
||||||
|
(`RecRoom.Systems.RoomConsumablesManager`) and the exact wire shapes
|
||||||
|
DorkNet serves for it. Everything here was verified against the
|
||||||
|
2023.03.21 ISIL dump (`RecNet.Runtime` Utf8Json formatters) and the
|
||||||
|
Cpp2IL dummy DLLs — key names, field types, and enum values are read
|
||||||
|
from the binary, not guessed.
|
||||||
|
|
||||||
|
Server implementation: `DorkNet.Server/Controllers/API/RoomConsumables/RoomConsumablesController.cs`
|
||||||
|
backed by the `RoomConsumables` + `RoomConsumableOwnership` tables.
|
||||||
|
Integration coverage: `DorkNet.Server.Tests/RoomConsumablesShopTests.cs`.
|
||||||
|
|
||||||
|
## Why this exists (the crash)
|
||||||
|
|
||||||
|
On every room join the client fetches
|
||||||
|
`GET api/roomconsumables/v1/roomConsumable/room/{roomId}/me` and feeds
|
||||||
|
the rows to `RoomConsumablesManager`. The row-processing method
|
||||||
|
(`ODAIDLGODIK.FBDIGFOANBG`) dereferences the row's `Consumable` desc
|
||||||
|
**unconditionally**. If the server's JSON keys don't match, Utf8Json
|
||||||
|
leaves the field null and the client throws
|
||||||
|
`NullReferenceException` → unobserved task exception → "Uploading Crash
|
||||||
|
Report" in `Player.log`, and the shop is dead for the session. The old
|
||||||
|
DorkNet endpoint sent `Id`/`ConsumableItemDesc`/`CreatedAt` (keys
|
||||||
|
borrowed from the *gift* DTO, which really does use
|
||||||
|
`ConsumableItemDesc`) — the 2023 room-consumable DTO does not.
|
||||||
|
|
||||||
|
## Endpoints (client literals)
|
||||||
|
|
||||||
|
Base `{0}` = `api/roomconsumables`. Route literals are case-mixed on
|
||||||
|
the wire (`roomConsumable` vs `roomconsumable`); ASP.NET matches both.
|
||||||
|
|
||||||
|
| Verb | Path | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `{0}/v1/roomConsumable/room/{roomId}` | room catalog (shop shelf) |
|
||||||
|
| GET | `{0}/v1/roomConsumable/room/{roomId}/me` | caller's inventory, fetched on room join |
|
||||||
|
| GET | `{0}/v1/roomConsumable/{id}` | single desc |
|
||||||
|
| GET | `{0}/v1/roomConsumable/{id}/isOwned` | "do non-creators hold stock" (bare bool) |
|
||||||
|
| POST | `{0}/v1/roomConsumable` | create (no id in body) / update (id in body); body = desc DTO |
|
||||||
|
| DELETE | `{0}/v1/roomConsumable/{id}` | soft delete; response = bare status int |
|
||||||
|
| POST | `{0}/v1/roomconsumable/{id}/purchase/tokens` | buy one with tokens |
|
||||||
|
| POST | `{0}/v1/roomconsumable/{id}/purchase/currency` | buy one with a room currency |
|
||||||
|
| POST | `{0}/v1/roomConsumable/{id}/consume` | spend one unit |
|
||||||
|
|
||||||
|
## DTOs
|
||||||
|
|
||||||
|
Consumable desc (`MOONCMIECPL`, formatter `HJPEABLEEKL`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"RoomConsumableId": "guid",
|
||||||
|
"RoomId": 116,
|
||||||
|
"Name": "Cola",
|
||||||
|
"Description": "…",
|
||||||
|
"ImageName": "…",
|
||||||
|
"PriceAndCurrency": { "Price": 10, "CurrencyId": null }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`CurrencyId` null = token-priced; otherwise a room-currency PublicId.
|
||||||
|
The desc also has a client-side `CreatedAt` field but it is **not** a
|
||||||
|
wire key.
|
||||||
|
|
||||||
|
Inventory row (`HOMJKAOHGDG`, formatter `CICEHLNDLDE`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"RoomConsumableId": "guid",
|
||||||
|
"AccountId": 4,
|
||||||
|
"Count": 1,
|
||||||
|
"ConcurrencyCode": "guid",
|
||||||
|
"ModifiedAt": "2026-07-07T12:00:00Z",
|
||||||
|
"Consumable": { …desc… }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`AccountId` is int32 on the wire. `Consumable` must never be null (see
|
||||||
|
crash above).
|
||||||
|
|
||||||
|
## Concurrency model
|
||||||
|
|
||||||
|
Each (player, consumable) stack carries an opaque `ConcurrencyCode`
|
||||||
|
Guid. Purchase and consume requests send
|
||||||
|
`{ CurrentConcurrencyCode, NewConcurrencyCode }` — the **client
|
||||||
|
generates the new code** and adopts it locally on success (the purchase
|
||||||
|
response carries no inventory row), so the server must store
|
||||||
|
`NewConcurrencyCode` verbatim or the next consume fails with
|
||||||
|
`ConcurrencyCodeMismatch` (32). On a mismatch the server returns the
|
||||||
|
current row so the client can resync.
|
||||||
|
|
||||||
|
Purchase body (`ILNJDMFNOCD`, formatter `JEBCHNDADKK`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ConcurrencyCodes": { "CurrentConcurrencyCode": null, "NewConcurrencyCode": "guid" },
|
||||||
|
"ExpectedPriceAndCurrency": { "Price": 10, "CurrencyId": null }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Consume body is the bare code pair (`CFOBCEPBBHA`, formatter
|
||||||
|
`JKJHOAKNOCF`): `{ "CurrentConcurrencyCode": "guid", "NewConcurrencyCode": "guid" }`.
|
||||||
|
|
||||||
|
## Responses
|
||||||
|
|
||||||
|
Create/update (`GFKDAADENEE`): `{ "Status": 0, "Consumable": {…desc…} }`.
|
||||||
|
Delete: bare status int (`0`).
|
||||||
|
Consume (`BGHBAILNNJJ`, formatter `LPFAFIGAIOE`):
|
||||||
|
`{ "Status": 0, "InventoryItem": {…inventory row…} }`.
|
||||||
|
|
||||||
|
Token purchase (`MJKFEHPIDME`, formatter `HJOKFJKKEJM`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"OperationResult": 0,
|
||||||
|
"BalanceUpdateResult": 0,
|
||||||
|
"TokenBalanceResponse": { "CurrencyType": 2, "Balance": 4990, "Platform": 0 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Room-currency purchase (`DEILBLCDNEA`, formatter `IDBFKCFEAHM`) swaps
|
||||||
|
the last key for
|
||||||
|
`"CurrencyBalanceResponse": { "AccountId", "CurrencyId", "Balance", "ModifiedAt" }`.
|
||||||
|
|
||||||
|
## Enums
|
||||||
|
|
||||||
|
`RoomConsumableStatus` (`DLDNGEDFMOJ`) — used by create/update/delete/consume:
|
||||||
|
`0 Success, 4 RoomConsumableNotFound, 7 PlayerDoesntHavePermission,
|
||||||
|
13 MaxConsumablesInRoom, 15 RoomIdMissing, 17 PriceOrCurrencyMissing,
|
||||||
|
18 CurrencyNotFound, 21/22 PriceTooLow/HighTokens, 23/24 NameTooShort/Long,
|
||||||
|
27 DescriptionTooLong, 32 ConcurrencyCodeMismatch,
|
||||||
|
33 PlayerDoesNotOwnConsumable, 36 RoomNotFound` (full 0–42 list in the
|
||||||
|
dummy DLL).
|
||||||
|
|
||||||
|
Token-purchase `OperationResult` (`CABBDKFODEC`):
|
||||||
|
`0 OK, 1 TooManyRequests, 2 NotEnoughCredit, 3 AlreadyOwned,
|
||||||
|
4 NoItemAvailable, 5 CouponNotApplicable, 6 RequestedPriceDoesNotMatch,
|
||||||
|
7 RequestedAmountNotAllowed, 8 PlayerNotEligible,
|
||||||
|
9 RequestCannotBeRefunded, 10 PlayerNotApproved`.
|
||||||
|
|
||||||
|
Currency-purchase result (`GACBLALELBP`): `0 Success, 1 NotEnoughCredit`.
|
||||||
|
|
||||||
|
## Watch Shop tab (storefronts)
|
||||||
|
|
||||||
|
The 2023 Shop tab is fed by two calls, both with shapes that changed
|
||||||
|
since 2020:
|
||||||
|
|
||||||
|
1. `GET api/storefronts/v3/giftdropstore/{id}` → `HOEGLKNEIOF`
|
||||||
|
(formatter GNMDEJJAHCJ): `{ StorefrontType, StoreItems, NewUntil,
|
||||||
|
NextUpdate, SubscriberDiscountPercent }`. Each StoreItems row
|
||||||
|
(JCCCKDCHMLG, formatter POGBAGAHGIA) is
|
||||||
|
`{ PurchasableItemId, Type (0=GiftDrop enum MDCCOLGHBMN), Prices:
|
||||||
|
[{CurrencyType, Price, StorefrontSaleData}], SubscriberPrices,
|
||||||
|
IsFeatured, AvailableAt, AvailableUntil, NewUntil, GiftDrop: {…} }`.
|
||||||
|
**`GiftDrop` is SINGULAR** — the 2020 `GiftDrops` array key is
|
||||||
|
ignored by the 2023 formatter, and a missing/null `GiftDrop` makes
|
||||||
|
`RRUI.Data.StoreItemListModel`'s per-item filter predicate NRE on
|
||||||
|
every row (bools/Rarity/type reads on the null desc), so the Shop
|
||||||
|
tab renders empty. DorkNet ships both keys.
|
||||||
|
|
||||||
|
The inner `GiftDrop` (EFFIEFEFHHB, formatter EMBCEDNHFLB) keys:
|
||||||
|
`GiftDropId (int), FriendlyName, Tooltip, TagList (CSV string),
|
||||||
|
ConsumableItemDesc (string), AvatarItemDesc (string descriptor),
|
||||||
|
AvatarItemType (int?), EquipmentPrefabName, EquipmentModificationGuid,
|
||||||
|
IsQuery, Unique, SubscribersOnly, Rarity (0/10/20/30/50 enum
|
||||||
|
BGDEDNBFOCH), AvatarItemId, CurrencyType, Currency (enum EAFDEJBEFJB,
|
||||||
|
RecCenterTokens=2), Context (GiftContext enum IFKEEPDDNBC),
|
||||||
|
ItemSetId (int?), ItemSetFriendlyName`.
|
||||||
|
|
||||||
|
2. `GET api/storefronts/v1/toptoday` → a **bare `List<int>` of
|
||||||
|
PurchasableItemIds** (client DCFKEFHJAGC.IDCIMNLBINC), resolved
|
||||||
|
against the gift-drop cache with "Could not find purchasable
|
||||||
|
giftdrop with purchasableItemId {0}" warnings for unknown ids.
|
||||||
|
Serving item objects here (the old `/all` alias) breaks the featured
|
||||||
|
fetch entirely.
|
||||||
|
|
||||||
|
Coverage: `DorkNet.Server.Tests/WatchShop2023Tests.cs`.
|
||||||
|
|
||||||
|
## Related: unprefixed 2023 commerce probes
|
||||||
|
|
||||||
|
The 2023 client also calls these commerce paths **without** the `api/`
|
||||||
|
prefix (same host as `api/catalog/...`); missing routes surface as
|
||||||
|
"CleanupPendingTransactions failed" + unobserved `HTTP Error 404`
|
||||||
|
crash reports at startup:
|
||||||
|
|
||||||
|
- `POST purchase/v1/cleanuppending` — ack (no pending IAP on DorkNet)
|
||||||
|
- `GET/POST purchase/v1/hasspentmoney` — bare `false`
|
||||||
|
- `GET reminder/currentTokenBundles/v2` — `[]` (no real-money offers)
|
||||||
|
- `POST purchase/v1/{initiatepurchase,processpurchase,completepurchase,cancelpurchase}`
|
||||||
|
— aliased onto the existing `api/purchase/v1` handlers
|
||||||
|
|
||||||
|
All of these plus `/purchasecampaign` are owned by the Commerce service
|
||||||
|
in `DorkNetRouteOwnership`.
|
||||||
Reference in New Issue
Block a user