Server/project/tests/services/RagfairPriceService.test.ts
Refringe 3d77ed8595
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.
2024-04-10 12:23:28 -04:00

502 lines
23 KiB
TypeScript

/* 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);
});
});
});