diff --git a/project/src/helpers/PresetHelper.ts b/project/src/helpers/PresetHelper.ts index b7fc1eb4..d7149fc1 100644 --- a/project/src/helpers/PresetHelper.ts +++ b/project/src/helpers/PresetHelper.ts @@ -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; diff --git a/project/src/services/RagfairPriceService.ts b/project/src/services/RagfairPriceService.ts index 6762bcec..2c7288ea 100644 --- a/project/src/services/RagfairPriceService.ts +++ b/project/src/services/RagfairPriceService.ts @@ -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 diff --git a/project/tests/services/RagfairPriceService.test.ts b/project/tests/services/RagfairPriceService.test.ts new file mode 100644 index 00000000..8ae6fee2 --- /dev/null +++ b/project/tests/services/RagfairPriceService.test.ts @@ -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"); + }); + + 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); + }); + }); +});