Seperates ragfair pricing into seperate method

This changes the `RagfairPriceService.getDynamicOfferPriceForOffer()` method to handle the logic surrounding collecting prices for items in offers, while offloading the individual price generation to a new method. The new method, `RagfairPriceService.getDynamicItemPrice()`, is responsible for generating a price for either an item template, or optionally an offerItems collection. This change also allows `getDynamicItemPrice()` to be used elsewhere in the code-base to gather more "realistic" pricing for specific item templates.

Fixes an edge-case where unreasonable prices would only be adjusted on the first item within an offer.

Includes some tests.

Related to #618. This will allow the insurance system to use this method to get better pricing for items.
This commit is contained in:
Refringe 2024-04-10 12:23:19 -04:00
parent 834a2e3ef5
commit 3d77ed8595
No known key found for this signature in database
GPG Key ID: 7715B85B4A6306ED
3 changed files with 608 additions and 76 deletions

View File

@ -85,6 +85,17 @@ export class PresetHelper
return id in this.databaseServer.getTables().globals.ItemPresets;
}
/**
* Checks to see if the preset is of the given base class.
* @param id The id of the preset
* @param baseClass The BaseClasses enum to check against
* @returns True if the preset is of the given base class, false otherwise
*/
public isPresetBaseClass(id: string, baseClass: BaseClasses): boolean
{
return this.isPreset(id) && this.itemHelper.isOfBaseclass(this.getPreset(id)._encyclopedia, baseClass);
}
public hasPreset(templateId: string): boolean
{
return templateId in this.lookup;

View File

@ -223,109 +223,129 @@ export class RagfairPriceService implements OnLoad
*/
public getDynamicOfferPriceForOffer(offerItems: Item[], desiredCurrency: string, isPackOffer: boolean): number
{
const rootItem = offerItems[0];
// Price to return
// Price to return.
let price = 0;
let endLoop = false;
let isPreset = false;
let manuallyAdjusted = false;
// Iterate over each item in the offer.
for (const item of offerItems)
{
// Armor insert, skip - we dont factor these into an items price
// Skip over armour inserts as those are not factored into item prices.
if (this.itemHelper.isOfBaseclass(item._tpl, BaseClasses.BUILT_IN_INSERTS))
{
continue;
}
// Get dynamic price, fallback to handbook price if value of 1 found
let itemPrice = this.getFleaPriceForItem(item._tpl);
price += this.getDynamicItemPrice(item._tpl, desiredCurrency, item, offerItems, isPackOffer);
if (this.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice)
{
itemPrice = this.adjustPriceIfBelowHandbook(itemPrice, item._tpl);
}
if (this.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher)
{
// Get highest trader price for item, if greater than value found so far, use it
const traderPrice = this.traderHelper.getHighestSellToTraderPrice(item._tpl);
if (traderPrice > itemPrice)
{
itemPrice = traderPrice;
}
}
// Check if item type is weapon preset, handle differently
const itemDetails = this.itemHelper.getItem(item._tpl);
if (this.presetHelper.isPreset(item.upd?.sptPresetId) && itemDetails[1]._props.weapFireType)
{
itemPrice = this.getWeaponPresetPrice(item, offerItems, itemPrice);
endLoop = true;
isPreset = true;
}
// Check for existance of manual price adjustment multiplier
const manualPriceMultipler = this.ragfairConfig.dynamic.itemPriceMultiplier[item._tpl];
if (manualPriceMultipler)
{
manuallyAdjusted = true;
itemPrice *= manualPriceMultipler;
}
// Multiply dynamic price by quality modifier
const itemQualityModifier = this.itemHelper.getItemQualityModifier(item);
price += itemPrice * itemQualityModifier;
// Stop loop if weapon preset price function has been run
if (endLoop)
// Check if the item is a weapon preset.
if (item?.upd?.sptPresetId && this.presetHelper.isPresetBaseClass(item.upd.sptPresetId, BaseClasses.WEAPON))
{
// This is a weapon preset, which has it's own price calculation that takes into account the mods in the
// preset. Since we've already calculated the price for the preset entire preset in
// `getDynamicItemPrice`, we can skip the rest of the items in the offer.
break;
}
}
// Check for unreasonable price on singular items
if (offerItems.length === 1 && !manuallyAdjusted)
{
const rootItemDb = this.itemHelper.getItem(rootItem._tpl)[1];
let unreasonableItemPriceChange: IUnreasonableModPrices;
for (const key of Object.keys(this.ragfairConfig.dynamic.unreasonableModPrices))
{
if (this.itemHelper.isOfBaseclass(rootItemDb._id, key))
{
unreasonableItemPriceChange = this.ragfairConfig.dynamic.unreasonableModPrices[key];
return Math.round(price);
}
break;
}
}
if (unreasonableItemPriceChange?.enabled)
/**
* @param itemTemplateId
* @param desiredCurrency
* @param item
* @param offerItems
* @param isPackOffer
* @returns
*/
public getDynamicItemPrice(
itemTemplateId: string,
desiredCurrency: string,
item?: Item,
offerItems?: Item[],
isPackOffer?: boolean,
): number
{
let isPreset = false;
let price = this.getFleaPriceForItem(itemTemplateId);
// Adjust price if below handbook price, based on config.
if (this.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice)
{
price = this.adjustPriceIfBelowHandbook(price, itemTemplateId);
}
// Use trader price if higher, based on config.
if (this.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher)
{
const traderPrice = this.traderHelper.getHighestSellToTraderPrice(itemTemplateId);
if (traderPrice > price)
{
price = this.adjustUnreasonablePrice(
this.databaseServer.getTables().templates.handbook.Items,
unreasonableItemPriceChange,
rootItem._tpl,
price,
);
price = traderPrice;
}
}
// Get price multiplier min/max to vary price
const rangeValues = this.getOfferTypeRangeValues(isPreset, isPackOffer);
price = this.randomiseOfferPrice(price, rangeValues);
// Prices for weapon presets are handled differently.
if (
item?.upd?.sptPresetId
&& offerItems
&& this.presetHelper.isPresetBaseClass(item.upd.sptPresetId, BaseClasses.WEAPON)
)
{
price = this.getWeaponPresetPrice(item, offerItems, price);
isPreset = true;
}
// Convert to different currency if desiredCurrency param is not roubles
if (desiredCurrency !== Money.ROUBLES)
// Check for existence of manual price adjustment multiplier
const multiplier = this.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId];
if (multiplier)
{
price *= multiplier;
}
// The quality of the item affects the price.
if (item)
{
const qualityModifier = this.itemHelper.getItemQualityModifier(item);
price *= qualityModifier;
}
// Make adjustments for unreasonably priced items.
for (const baseClassTemplateId of Object.keys(this.ragfairConfig.dynamic.unreasonableModPrices))
{
if (this.itemHelper.isOfBaseclass(itemTemplateId, baseClassTemplateId))
{
// Found an unreasonable price type.
const unreasonableModifier: IUnreasonableModPrices =
this.ragfairConfig.dynamic.unreasonableModPrices[baseClassTemplateId];
if (unreasonableModifier.enabled)
{
price = this.adjustUnreasonablePrice(
this.databaseServer.getTables().templates.handbook.Items,
unreasonableModifier,
itemTemplateId,
price,
);
}
}
}
// Vary the price based on the type of offer.
const range = this.getOfferTypeRangeValues(isPreset, isPackOffer);
price = this.randomiseOfferPrice(price, range);
// Convert to different currency if required.
const roublesId = Money.ROUBLES;
if (desiredCurrency !== roublesId)
{
price = this.handbookHelper.fromRUB(price, desiredCurrency);
}
// Guard against weird prices
if (price < 1)
{
price = 1;
return 1;
}
return price;
}
@ -400,7 +420,7 @@ export class RagfairPriceService implements OnLoad
const itemHandbookPrice = this.getStaticPriceForItem(itemTpl);
const priceDifferencePercent = this.getPriceDifference(itemHandbookPrice, itemPrice);
// Only adjust price if difference is > a percent AND item price passes threshhold set in config
// Only adjust price if difference is > a percent AND item price passes threshold set in config
if (
priceDifferencePercent > this.ragfairConfig.dynamic.offerAdjustment.maxPriceDifferenceBelowHandbookPercent
&& itemPrice >= this.ragfairConfig.dynamic.offerAdjustment.priceThreshholdRub

View File

@ -0,0 +1,501 @@
/* eslint-disable @typescript-eslint/naming-convention */
import "reflect-metadata";
import { container } from "tsyringe";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService";
import { MinMax } from "@spt-aki/models/common/MinMax";
import { Money } from "@spt-aki/models/enums/Money";
describe("RagfairPriceService", () =>
{
let ragfairPriceService: any; // Using "any" to access private/protected methods without type errors.
beforeEach(() =>
{
ragfairPriceService = container.resolve<RagfairPriceService>("RagfairPriceService");
});
afterEach(() =>
{
vi.restoreAllMocks();
});
describe("getDynamicOfferPriceForOffer", () =>
{
it("should return zero when empty offerItems array is passed", () =>
{
const offerItems = [];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toEqual(0);
});
it("should return non-zero number when valid item is passed", () =>
{
const offerItems = [{
_id: "d445ea263cdfc5f278334264",
_tpl: "57e3dba62459770f0c32322b",
parentId: "631abbff398cc0170cbd3089",
slotId: "mod_pistol_grip",
}];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
const expectedPrice = 42069;
// Mock the getDynamicItemPrice method to return a static price.
vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(expectedPrice);
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toBe(expectedPrice);
});
it("should always return a whole number", () =>
{
const offerItems = [{
_id: "d445ea263cdfc5f278334264",
_tpl: "57e3dba62459770f0c32322b",
parentId: "631abbff398cc0170cbd3089",
slotId: "mod_pistol_grip",
}];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
const originalPrice = 42069.999999999;
// Mock the getDynamicItemPrice method to return a static price.
vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(originalPrice);
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toBeGreaterThan(originalPrice);
expect(price).toBe(Math.round(originalPrice));
});
it("should skip prices for soft armour inserts", () =>
{
const offerItems = [{
_id: "d445ea263cdfc5f278334264",
_tpl: "657080a212755ae0d907ad04",
parentId: "631abbff398cc0170cbd3089",
slotId: "Soft_armor_front",
}];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
// Mock the getDynamicItemPrice method.
const getDynamicItemPriceSpy = vi.spyOn(ragfairPriceService, "getDynamicItemPrice");
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toBe(0);
expect(getDynamicItemPriceSpy).not.toHaveBeenCalled();
});
it("should not add value of mods to weapon preset", () =>
{
const offerItems = [{
_id: "344d02bbf2102ce4e145bf35",
_tpl: "579204f224597773d619e051",
upd: {
StackObjectsCount: 1,
UnlimitedCount: true,
sptPresetId: "5841499024597759f825ff3e",
Repairable: { Durability: 90, MaxDurability: 90 },
},
}, {
_id: "59c6897a59ed48f1ca02f659",
_tpl: "5448c12b4bdc2d02308b456f",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_magazine",
}, {
_id: "7e8062d4bc57b56927c2d117",
_tpl: "6374a822e629013b9c0645c8",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_reciever",
}, {
_id: "3b09149e8b7833dc5fdd32a4",
_tpl: "63c6adcfb4ba094317063742",
parentId: "7e8062d4bc57b56927c2d117",
slotId: "mod_sight_rear",
}, {
_id: "e833a5c26af29870df9cdd2e",
_tpl: "6374a7e7417239a7bf00f042",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_pistolgrip",
}];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
const expectedPrice = 10000;
// Mock the getDynamicItemPrice method to return a static price.
const getDynamicItemPriceSpy = vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(
expectedPrice,
);
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toBe(expectedPrice);
expect(getDynamicItemPriceSpy).toHaveBeenCalledTimes(1);
});
it("should sum value of all offer items", () =>
{
const offerItems = [{
_id: "59c6897a59ed48f1ca02f659",
_tpl: "5448c12b4bdc2d02308b456f",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_magazine",
}, {
_id: "7e8062d4bc57b56927c2d117",
_tpl: "6374a822e629013b9c0645c8",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_reciever",
}, {
_id: "3b09149e8b7833dc5fdd32a4",
_tpl: "63c6adcfb4ba094317063742",
parentId: "7e8062d4bc57b56927c2d117",
slotId: "mod_sight_rear",
}, {
_id: "e833a5c26af29870df9cdd2e",
_tpl: "6374a7e7417239a7bf00f042",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_pistolgrip",
}];
const desiredCurrency = Money.ROUBLES;
const isPackOffer = false;
const expectedPrice = 10000;
// Mock the getDynamicItemPrice method to return a static price.
const getDynamicItemPriceSpy = vi.spyOn(ragfairPriceService, "getDynamicItemPrice").mockReturnValue(
expectedPrice,
);
const price = ragfairPriceService.getDynamicOfferPriceForOffer(offerItems, desiredCurrency, isPackOffer);
expect(price).toBe(expectedPrice * offerItems.length);
expect(getDynamicItemPriceSpy).toHaveBeenCalledTimes(offerItems.length);
});
});
describe("getDynamicItemPrice", () =>
{
it("should not return zero for a valid template ID", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.ROUBLES;
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(price).not.toBe(0);
});
it("should use trader price if it is higher than flea price and configuration allows it", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.ROUBLES;
const mockTraderPrice = 20000;
const mockFleaPrice = 15000;
const getOfferTypeRangeValues = { max: 1, min: 1 };
// Mock the configs to allow using trader price if higher. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = true;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock the getFleaPriceForItem method to return a static price.
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
// Mock the getHighestSellToTraderPrice method to return a higher static price.
vi.spyOn((ragfairPriceService as any).traderHelper, "getHighestSellToTraderPrice").mockReturnValue(
mockTraderPrice,
);
// Mock the getOfferTypeRangeValues method to return a static minMax.
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(price).toBe(mockTraderPrice);
});
it("should adjust flea price when below handbook price and configuration allows it", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.ROUBLES;
const mockFleaPrice = 1;
const handbookPrice = 10000;
const adjustedPrice = 9000;
const getOfferTypeRangeValues = { max: 1, min: 1 };
// Enable adjustment for prices below handbook price. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = true;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock the getFleaPriceForItem method to return a static price below the handbook price.
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
// Mock the adjustPriceIfBelowHandbook method to simulate price adjustment.
vi.spyOn(ragfairPriceService, "adjustPriceIfBelowHandbook").mockImplementation(
(price: number, templateId) =>
{
return price < handbookPrice ? adjustedPrice : price;
},
);
// Mock the getOfferTypeRangeValues method to return a static minMax.
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
// Verify the price is adjusted correctly according to the mocked handbook price adjustment logic.
expect(price).toBe(adjustedPrice);
});
it("should handle weapon preset prices correctly", () =>
{
const itemTemplateId = "579204f224597773d619e051";
const desiredCurrency = Money.ROUBLES;
const mockPresetPrice = 25000;
const getOfferTypeRangeValues = { max: 1, min: 1 };
const offerItems = [{
_id: "344d02bbf2102ce4e145bf35",
_tpl: "579204f224597773d619e051",
upd: {
StackObjectsCount: 1,
UnlimitedCount: true,
sptPresetId: "5841499024597759f825ff3e",
Repairable: { Durability: 90, MaxDurability: 90 },
},
}, {
_id: "7e8062d4bc57b56927c2d117",
_tpl: "6374a822e629013b9c0645c8",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_reciever",
}];
const item = offerItems[0];
// Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock getFleaPriceForItem to bypass initial flea price fetch
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(0);
// Mock the isPresetBaseClass method to return true for the item
vi.spyOn((ragfairPriceService as any).presetHelper, "isPresetBaseClass").mockReturnValue(true);
// Mock the getWeaponPresetPrice method to return a specific preset price
const getWeaponPresetPriceSpy = vi.spyOn(ragfairPriceService, "getWeaponPresetPrice").mockReturnValue(
mockPresetPrice,
);
// Mock the getOfferTypeRangeValues method to return a static minMax.
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Mock the getItemQualityModifier method to return 1 (no change)
vi.spyOn((ragfairPriceService as any).itemHelper, "getItemQualityModifier").mockReturnValue(1);
// Call the method with the mock item and offer items
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency, item, offerItems);
// Call the method.
expect(price).toBe(mockPresetPrice);
// Additionally, you can verify that getWeaponPresetPrice was called with the correct parameters
expect(getWeaponPresetPriceSpy).toHaveBeenCalledWith(item, offerItems, expect.any(Number));
});
it("should update price based on the ragfair config item price multiplier values", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.ROUBLES;
const mockFleaPrice = 20000;
const itemPriceMultiplier = 2;
const getOfferTypeRangeValues = { max: 1, min: 1 };
// Mock the ragfair config to have a price multiplier of 2. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = itemPriceMultiplier;
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
// Mock the getFleaPriceForItem method to return a static price.
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
// Mock the getOfferTypeRangeValues method to return a static minMax.
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(price).toBe(mockFleaPrice * itemPriceMultiplier);
});
it("should adjust price when durability is not perfect", () =>
{
const itemTemplateId = "579204f224597773d619e051";
const desiredCurrency = Money.ROUBLES;
const mockPrice = 25000;
const mockDurabilityMulti = 0.5;
const getOfferTypeRangeValues = { max: 1, min: 1 };
const offerItems = [{
_id: "344d02bbf2102ce4e145bf35",
_tpl: "579204f224597773d619e051",
upd: {
StackObjectsCount: 1,
UnlimitedCount: true,
sptPresetId: "5841499024597759f825ff3e",
Repairable: { Durability: 40, MaxDurability: 90 },
},
}, {
_id: "7e8062d4bc57b56927c2d117",
_tpl: "6374a822e629013b9c0645c8",
parentId: "344d02bbf2102ce4e145bf35",
slotId: "mod_reciever",
}];
const item = offerItems[0];
// Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock getFleaPriceForItem to bypass initial flea price fetch
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(0);
// Mock the isPresetBaseClass method to return true for the item
vi.spyOn((ragfairPriceService as any).presetHelper, "isPresetBaseClass").mockReturnValue(true);
// Mock the getWeaponPresetPrice method to return a specific preset price
vi.spyOn(ragfairPriceService, "getWeaponPresetPrice").mockReturnValue(mockPrice);
// Mock the getOfferTypeRangeValues method to return a static minMax.
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Mock the getItemQualityModifier method to return 1 (no change)
const getItemQualityModifierSpy = vi.spyOn(
(ragfairPriceService as any).itemHelper,
"getItemQualityModifier",
).mockReturnValue(mockDurabilityMulti);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency, item, offerItems);
expect(getItemQualityModifierSpy).toHaveBeenCalled();
expect(price).toBe(mockPrice * mockDurabilityMulti);
});
it("should adjust unreasonable prices based on ragfair config unreasonable price values", () =>
{
const itemTemplateId = "5c052f6886f7746b1e3db148";
const desiredCurrency = Money.ROUBLES;
const mockFleaPrice = 9999999;
const getOfferTypeRangeValues = { max: 1, min: 1 };
const mockBaseClassTemplateId = "57864a66245977548f04a81f";
const mockUnreasonableModPrices = {
itemType: "Electronics",
enabled: true,
handbookPriceOverMultiplier: 11,
newPriceHandbookMultiplier: 11,
};
// Mock the Disable unreasonableModPrices config. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.unreasonableModPrices[mockBaseClassTemplateId] =
mockUnreasonableModPrices;
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock getFleaPriceForItem to bypass initial flea price fetch
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
// Mock isOfBaseclass to ensure that the item is always of the base class
const isOfBaseclassSpy = vi.spyOn((ragfairPriceService as any).itemHelper, "isOfBaseclass").mockReturnValue(
true,
);
// Mock the adjustUnreasonablePrice method to ensure it was called
const adjustUnreasonablePriceSpy = vi.spyOn(ragfairPriceService, "adjustUnreasonablePrice");
// Mock the getOfferTypeRangeValues method to return a static minMax
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(isOfBaseclassSpy).toHaveBeenCalled();
expect(adjustUnreasonablePriceSpy).toHaveBeenCalled();
expect(price).toBeLessThan(mockFleaPrice);
});
it("should vary the price within a random range", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.ROUBLES;
const mockFleaPrice = 10000;
const mockRandomiseOfferPrice = 9500;
// Mock the configs to allow using trader price if higher. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock the getFleaPriceForItem method to return a static price
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockFleaPrice);
// Mock the isPresetBaseClass method to return false
vi.spyOn((ragfairPriceService as any).presetHelper, "isPresetBaseClass").mockReturnValue(false);
// Mock the randomiseOfferPrice method to have a simplified implementation
const randomiseOfferPriceSpy = vi.spyOn(ragfairPriceService, "randomiseOfferPrice").mockReturnValue(
mockRandomiseOfferPrice,
);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(randomiseOfferPriceSpy).toHaveBeenCalled();
expect(price).toBe(mockRandomiseOfferPrice);
});
it("should convert currency", () =>
{
const itemTemplateId = "5e54f6af86f7742199090bf3";
const desiredCurrency = Money.DOLLARS;
const mockRoublePrice = 10000;
const mockDollarPrice = 500;
const getOfferTypeRangeValues = { max: 1, min: 1 };
// Mock the configs to allow using trader price if higher. Disable other adjustments for isolation.
ragfairPriceService.ragfairConfig.dynamic.offerAdjustment.adjustPriceWhenBelowHandbookPrice = false;
ragfairPriceService.ragfairConfig.dynamic.useTraderPriceForOffersIfHigher = false;
ragfairPriceService.ragfairConfig.dynamic.itemPriceMultiplier[itemTemplateId] = null;
// Mock the getFleaPriceForItem method to return a static price.
vi.spyOn(ragfairPriceService, "getFleaPriceForItem").mockReturnValue(mockRoublePrice);
// Mock the getOfferTypeRangeValues method to return a static minMax
vi.spyOn(ragfairPriceService, "getOfferTypeRangeValues").mockReturnValue(getOfferTypeRangeValues);
// Mock the fromRUB method to convert the price to a different currency
const fromRUBSpy = vi.spyOn((ragfairPriceService as any).handbookHelper, "fromRUB").mockReturnValue(
mockDollarPrice,
);
// Call the method.
const price = ragfairPriceService.getDynamicItemPrice(itemTemplateId, desiredCurrency);
expect(fromRUBSpy).toHaveBeenCalledWith(mockRoublePrice, desiredCurrency);
expect(price).not.toBe(mockRoublePrice);
expect(price).toBe(mockDollarPrice);
});
});
});