hyapp-admin-platform/ops-center/src/configForm.test.js

291 lines
11 KiB
JavaScript

import { describe, expect, test } from "vitest";
import { applyDynamicStrategyDefaults, configToForm, formToConfig, validateLuckyGiftForm } from "./configForm.js";
describe("ops center lucky gift dynamic_v3 form", () => {
test("loads safe dynamic defaults without inventing app-specific monetary caps", () => {
const form = configToForm({ app_code: "lalu", pool_id: "default", strategy_version: "dynamic_v3" });
expect(form).toMatchObject({
anchor_rate_percent: "1",
enabled: false,
initial_pool_coins: "0",
jackpot_multipliers: "200, 500, 1000",
max_single_payout: "0",
pool_rate_percent: "98",
profit_rate_percent: "1",
strategy_version: "dynamic_v3",
target_rtp_percent: "98",
});
expect(form.stages.map((stage) => [stage.min_recharge_7d_coins, stage.min_recharge_30d_coins])).toEqual([
["0", "0"],
["0", "0"],
["0", "0"],
]);
expect(form.stages.map((stage) => stage.tiers.map((tier) => Number(tier.probabilityPercent)))).toEqual([
[5, 4, 86, 5],
[5, 4, 86, 5],
[5, 4, 86, 5],
]);
});
test("round-trips every dynamic strategy field and canonical stage keys", () => {
const config = completeDynamicConfig();
// 使用空原对象可证明每个字段都真正经过 formToConfig 写回;若沿用 config 作为 spread 基底,
// 漏掉的动态字段也会被旧值掩盖,测试无法发现发布 payload 的字段丢失。
const output = formToConfig({}, configToForm(config));
expect(output).toEqual(config);
expect(output.stages[1]).toMatchObject({
min_recharge_30d_coins: 20_000,
min_recharge_7d_coins: 10_000,
stage: "normal",
});
expect(output.stages[1].tiers[0]).toEqual({
enabled: true,
high_water_only: false,
multiplier: 0,
probability_percent: 5,
tier_id: "normal_none",
});
});
test("drops legacy dynamic initial seed when loading and publishing a new version", () => {
const legacy = completeDynamicConfig();
legacy.initial_pool_coins = 1_000_000;
legacy.stages[0].min_recharge_7d_coins = 12_345;
legacy.stages[0].min_recharge_30d_coins = 67_890;
const form = configToForm(legacy);
expect(form.initial_pool_coins).toBe("0");
expect(form.stages[0]).toMatchObject({ min_recharge_7d_coins: "0", min_recharge_30d_coins: "0" });
const output = formToConfig(legacy, form);
expect(output.initial_pool_coins).toBe(0);
expect(output.stages[0]).toMatchObject({ min_recharge_7d_coins: 0, min_recharge_30d_coins: 0 });
});
test("allows disabled drafts but blocks enabled dynamic rules until app monetary limits are filled", () => {
const draft = configToForm({ app_code: "lalu", pool_id: "default", strategy_version: "dynamic_v3" });
expect(validateLuckyGiftForm(draft)).toEqual([]);
const enabled = { ...draft, enabled: true };
const errors = validateLuckyGiftForm(enabled);
expect(errors).toContain("启用前普通阶段至少一个最低充值门槛必须大于 0");
expect(errors).toContain("高阶充值门槛必须逐维不低于正常阶段且至少一项更高");
expect(errors).toContain("启用前必须填写单次返奖上限");
expect(errors).toContain("启用前必须填写主播每日上限");
});
test("keeps fixed_v2 readable and does not apply dynamic-only validation", () => {
const form = configToForm({
app_code: "yumi",
control_band_percent: 1,
enabled: true,
gift_price_reference: 100,
normal_max_equivalent_draws: 20_000,
novice_max_equivalent_draws: 2_000,
pool_id: "legacy",
pool_rate_percent: 96,
settlement_window_wager: 1_000_000,
stages: fixedStages(),
strategy_version: "fixed_v2",
target_rtp_percent: 95,
});
expect(form.strategy_version).toBe("fixed_v2");
expect(validateLuckyGiftForm(form)).toEqual([]);
});
test("switching a legacy form to dynamic creates a disabled safe draft", () => {
const legacy = configToForm({
app_code: "yumi",
control_band_percent: 1,
enabled: true,
gift_price_reference: 100,
normal_max_equivalent_draws: 20_000,
novice_max_equivalent_draws: 2_000,
pool_id: "legacy",
pool_rate_percent: 96,
settlement_window_wager: 1_000_000,
stages: fixedStages(),
strategy_version: "fixed_v2",
target_rtp_percent: 95,
});
const dynamic = applyDynamicStrategyDefaults(legacy);
expect(dynamic).toMatchObject({
enabled: false,
initial_pool_coins: "0",
pool_rate_percent: "98",
strategy_version: "dynamic_v3",
});
expect(dynamic.stages.map((stage) => stage.tiers.map((tier) => Number(tier.multiplier)))).toEqual([
[0, 0.5, 1, 2],
[0, 0.5, 1, 2],
[0, 0.5, 1, 2],
]);
expect(dynamic.stages.map((stage) => stage.tiers.map((tier) => Number(tier.probabilityPercent)))).toEqual([
[5, 4, 86, 5],
[5, 4, 86, 5],
[5, 4, 86, 5],
]);
expect(validateLuckyGiftForm(dynamic)).toEqual([]);
});
test("rejects invalid disabled draft values that the owner never accepts", () => {
const draft = configToForm({ app_code: "lalu", pool_id: "default", strategy_version: "dynamic_v3" });
const errors = validateLuckyGiftForm({
...draft,
jackpot_spend_threshold_coins: "-1",
max_single_payout: "-1",
profit_rate_percent: "-1",
anchor_rate_percent: "2",
stages: draft.stages.map((stage) =>
stage.stage === "normal" ? { ...stage, min_recharge_7d_coins: "-1" } : stage,
),
});
expect(errors).toContain("平台盈利和主播收益比例不能小于 0");
expect(errors).toContain("大奖消费门槛不能小于 0");
expect(errors).toContain("单次返奖上限不能小于 0");
expect(errors).toContain("充值阶段门槛不能小于 0");
});
test("keeps ordinary and jackpot paths independent when they use the same multiplier", () => {
const form = configToForm(completeDynamicConfig());
const overlapping = {
...form,
stages: form.stages.map((stage) =>
stage.stage === "normal"
? {
...stage,
tiers: [
...stage.tiers.map((tier, index) =>
index === 0 ? { ...tier, probabilityPercent: "4.99" } : tier,
),
{
enabled: true,
highWaterOnly: true,
multiplier: "200",
probabilityPercent: "0.01",
tierId: "normal_200x",
},
],
}
: stage,
),
};
expect(validateLuckyGiftForm(overlapping)).toEqual([]);
const output = formToConfig({}, overlapping);
expect(output.jackpot_multipliers).toEqual([200, 500, 1000]);
expect(output.stages.find((stage) => stage.stage === "normal")?.tiers).toContainEqual({
enabled: true,
high_water_only: true,
multiplier: 200,
probability_percent: 0.01,
tier_id: "normal_200x",
});
// 放开同倍率不等于放开高倍率安全要求;普通 200x 未确认安全标记时仍必须阻断。
const unsafe = {
...overlapping,
stages: overlapping.stages.map((stage) =>
stage.stage === "normal"
? {
...stage,
tiers: stage.tiers.map((tier) =>
Number(tier.multiplier) === 200 ? { ...tier, highWaterOnly: false } : tier,
),
}
: stage,
),
};
expect(validateLuckyGiftForm(unsafe)).toContain("正常阶段10x 及以上奖档必须限制为高水位");
});
});
function completeDynamicConfig() {
return {
anchor_daily_payout_cap: 6_000_000,
anchor_rate_percent: 1,
app_code: "lalu",
control_band_percent: 3,
device_daily_payout_cap: 4_000_000,
effective_from_ms: 0,
enabled: true,
gift_price_reference: 100,
high_water_nonzero_factor_percent: 130,
high_watermark_coins: 20_000_000,
initial_pool_coins: 0,
jackpot_global_rtp_max_percent: 98,
jackpot_multipliers: [200, 500, 1000],
jackpot_spend_threshold_coins: 50_000,
jackpot_user_72h_rtp_max_percent: 96,
jackpot_user_day_rtp_max_percent: 96,
loss_streak_guarantee: 5,
low_water_nonzero_factor_percent: 70,
low_watermark_coins: 10_000_000,
max_jackpot_hits_per_user_day: 5,
max_single_payout: 1_000_000,
normal_max_equivalent_draws: 20_000,
novice_max_equivalent_draws: 2_000,
pool_id: "default",
pool_rate_percent: 98,
profit_rate_percent: 1,
recharge_boost_factor_percent: 110,
recharge_boost_window_ms: 300_000,
room_hourly_payout_cap: 5_000_000,
settlement_window_wager: 1_000_000,
stages: dynamicStages(),
strategy_version: "dynamic_v3",
target_rtp_percent: 98,
user_daily_payout_cap: 3_000_000,
user_hourly_payout_cap: 2_000_000,
};
}
function dynamicStages() {
return [
dynamicStage("novice", 0, 0),
dynamicStage("normal", 10_000, 20_000),
dynamicStage("advanced", 20_000, 30_000),
];
}
function dynamicStage(stage, min7d, min30d) {
return {
min_recharge_30d_coins: min30d,
min_recharge_7d_coins: min7d,
stage,
tiers: [
tier(`${stage}_none`, 0, 5),
tier(`${stage}_0_5x`, 0.5, 4),
tier(`${stage}_1x`, 1, 86),
tier(`${stage}_2x`, 2, 5),
],
};
}
function fixedStages() {
return ["novice", "normal", "advanced"].map((stage) => ({
stage,
tiers: [
tier(`${stage}_none`, 0, 10),
tier(`${stage}_0_5x`, 0.5, 20),
tier(`${stage}_1x`, 1, 55),
tier(`${stage}_2x`, 2, 15),
],
}));
}
function tier(tierId, multiplier, probabilityPercent) {
return {
enabled: true,
high_water_only: false,
multiplier,
probability_percent: probabilityPercent,
tier_id: tierId,
};
}