From f0edf89ad34ae9c8d5777318d29faca23574f9dc Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Sat, 22 Aug 2026 21:07:05 +0200 Subject: [PATCH] fix(shop): make purchase resolve offers the same way the view does `purchase` listed a shop's offers with no ordering while `getShopView` orders by `sortOrder`, so the two paths answered "which offer does this key mean" by different rules, one of them at the database's discretion. Harmless today because item and bag keys are disjoint, but not a difference worth keeping. The faction lookup behind requirement labels also read every faction while the condition engine only matches enabled ones, so a gate on a disabled faction would have shown that faction's name next to a requirement the engine treats as unmeetable. Co-Authored-By: Claude Opus 5 --- apps/api/src/shops/shop.service.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/api/src/shops/shop.service.ts b/apps/api/src/shops/shop.service.ts index 19e5a79..c8726ee 100644 --- a/apps/api/src/shops/shop.service.ts +++ b/apps/api/src/shops/shop.service.ts @@ -117,11 +117,16 @@ export class ShopService { }); // One read for the whole view: every reputation requirement needs a - // display name, and offers commonly gate on the same faction. + // display name, and offers commonly gate on the same faction. Restricted + // to enabled factions because that is what the condition engine evaluates + // against -- naming a faction the engine treats as absent would describe a + // requirement that can never be met. const factionNames = new Map( - (await this.dataSource.getRepository(ReputationFaction).find()).map( - (faction) => [faction.key, faction.name], - ), + ( + await this.dataSource + .getRepository(ReputationFaction) + .find({ where: { enabled: true } }) + ).map((faction) => [faction.key, faction.name]), ); const context = { characterId, npcId }; @@ -205,6 +210,9 @@ export class ShopService { const offers = await manager.getRepository(ShopOffer).find({ where: { shopId: shop.id, enabled: true }, relations: { itemDefinition: true, lootBagDefinition: true }, + // Same order as the view: both paths answer "which offer does this key + // mean", so neither may answer it from an arbitrary row order. + order: { sortOrder: 'ASC' }, }); let match: ShopOffer | undefined; let target: OfferTarget | undefined;