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 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-22 21:07:05 +02:00
parent ab8bbeae5d
commit f0edf89ad3

View File

@@ -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;