import { describe, expect, test } from "vitest"; import { addTierToStage, correctStageTierProbabilities, emptyLuckyGiftForm, payloadFromLuckyGiftForm, stageExpectedRTP, stageProbabilityTotal, updateStageTier, } from "@/features/lucky-gift/configModel.js"; describe("lucky gift config payload", () => { test("submits business units required by the v2 admin contract", () => { const payload = payloadFromLuckyGiftForm(emptyLuckyGiftForm()); expect(payload.target_rtp_percent).toBe(95); expect(payload.pool_rate_percent).toBe(96); expect(payload.control_band_percent).toBe(1); expect(payload.gift_price_reference).toBe(100); expect(payload.novice_max_equivalent_draws).toBe(2000); expect(payload.normal_max_equivalent_draws).toBe(20000); expect(payload.max_single_payout).toBe(50000); expect(payload.user_hourly_payout_cap).toBe(34200); expect(payload.user_daily_payout_cap).toBe(615600); expect(payload.device_daily_payout_cap).toBe(1026000); expect(payload.room_hourly_payout_cap).toBe(684000); expect(payload.anchor_daily_payout_cap).toBe(12312000); expect(payload).not.toHaveProperty("target_rtp_ppm"); expect(payload).not.toHaveProperty("multiplier_ppms"); expect(payload.stages.map((stage) => stage.stage)).toEqual(["novice", "normal", "advanced"]); }); test("keeps each default stage probability at 100 percent", () => { const form = emptyLuckyGiftForm(); expect(form.stages.map(stageProbabilityTotal)).toEqual([100, 100, 100]); }); test("keeps each default stage expected RTP at target percent", () => { const form = emptyLuckyGiftForm(); expect(form.stages.map(stageExpectedRTP)).toEqual([95, 95, 95]); }); test("updates a single tier without mutating other stages", () => { const form = emptyLuckyGiftForm(); const nextStages = updateStageTier(form.stages, "normal", 0, { probabilityPercent: "39.5" }); expect(nextStages.find((stage) => stage.stage === "normal").tiers[0].probabilityPercent).toBe("39.5"); expect(nextStages.find((stage) => stage.stage === "novice").tiers[0].probabilityPercent).toBe(10); }); test("adds explicit editable tier to requested stage", () => { const form = emptyLuckyGiftForm(); const nextStages = addTierToStage(form.stages, "advanced"); const advanced = nextStages.find((stage) => stage.stage === "advanced"); expect(advanced.tiers).toHaveLength(5); expect(advanced.tiers[4]).toMatchObject({ broadcastLevel: "none", rewardSource: "base_rtp" }); }); test("corrects a stage probability to requested RTP without zeroing enabled tiers", () => { const form = emptyLuckyGiftForm(); const nextStages = correctStageTierProbabilities(form.stages, "normal", 95); const normal = nextStages.find((stage) => stage.stage === "normal"); expect(stageProbabilityTotal(normal)).toBeCloseTo(100, 6); expect(stageExpectedRTP(normal)).toBeCloseTo(95, 6); expect(enabledProbabilityPPMTotal(normal)).toBe(1_000_000); expect(normal.tiers.map((tier) => Number(tier.multiplier))).toEqual([0, 0.5, 1, 2]); expect(normal.tiers.every((tier) => Number(tier.probabilityPercent) > 0)).toBe(true); }); test("clamps correction to closest reachable RTP when multiplier range cannot cover target", () => { const form = emptyLuckyGiftForm(); const nextStages = correctStageTierProbabilities(form.stages, "novice", 250); const novice = nextStages.find((stage) => stage.stage === "novice"); expect(stageProbabilityTotal(novice)).toBeCloseTo(100, 6); expect(stageExpectedRTP(novice)).toBeCloseTo(199.955, 6); expect(novice.tiers.every((tier) => Number(tier.probabilityPercent) > 0)).toBe(true); }); test("balances RTP contribution across high multipliers including 100x", () => { const form = emptyLuckyGiftForm(); const advanced = form.stages.find((stage) => stage.stage === "advanced"); advanced.tiers = [ luckyTier("advanced_none", 0), luckyTier("advanced_1x", 1), luckyTier("advanced_2x", 2), luckyTier("advanced_5x", 5, { highWaterOnly: true }), luckyTier("advanced_10x", 10, { highWaterOnly: true }), luckyTier("advanced_100x", 100, { highWaterOnly: true }), ]; const nextStages = correctStageTierProbabilities(form.stages, "advanced", 95); const corrected = nextStages.find((stage) => stage.stage === "advanced"); const positiveTiers = corrected.tiers.filter((tier) => Number(tier.multiplier) > 0); const contributions = positiveTiers.map((tier) => Number(tier.multiplier) * Number(tier.probabilityPercent)); const tier100x = corrected.tiers.find((tier) => tier.tierId === "advanced_100x"); expect(stageProbabilityTotal(corrected)).toBeCloseTo(100, 6); expect(stageExpectedRTP(corrected)).toBeCloseTo(95, 6); expect(enabledProbabilityPPMTotal(corrected)).toBe(1_000_000); expect(corrected.tiers.every((tier) => Number(tier.probabilityPercent) > 0)).toBe(true); expect(Number(tier100x.probabilityPercent)).toBeGreaterThan(0.01); expect(Math.max(...contributions) - Math.min(...contributions)).toBeLessThan(1.1); }); }); function luckyTier(tierId, multiplier, options = {}) { return { broadcastLevel: "none", enabled: true, highWaterOnly: Boolean(options.highWaterOnly), multiplier, probabilityPercent: 1, rewardSource: "base_rtp", tierId, }; } function enabledProbabilityPPMTotal(stage) { return stage.tiers.reduce((sum, tier) => { if (tier.enabled === false) { return sum; } return sum + Math.round(Number(tier.probabilityPercent) * 10_000); }, 0); }