import { expect, test } from "vitest"; import { createDashboardModel } from "./createDashboardModel.js"; test("uses lucky gift pool breakdown to aggregate business metrics", () => { const model = createDashboardModel( { lucky_gift_pools: [ { payout_coin: 300, pool_id: "lucky_100", turnover_coin: 1_000 }, { payout_coin: 50, pool_id: "super_lucky", turnover_coin: 500 }, ], lucky_gift_payout: 1, lucky_gift_profit: 1, lucky_gift_turnover: 1, }, { appCode: "lalu", countryId: 0, range: { end: "2026-06-04", start: "2026-06-04" } }, ); expect(model.luckyGiftPools).toEqual([ expect.objectContaining({ payout: 300, pool_id: "lucky_100", profit: 700, turnover: 1_000 }), expect.objectContaining({ payout: 50, pool_id: "super_lucky", profit: 450, turnover: 500 }), ]); expect(metricValue(model, "幸运礼物流水")).toBe("1,500"); expect(metricValue(model, "幸运礼物返奖")).toBe("350"); expect(metricValue(model, "幸运礼物利润")).toBe("1,150"); expect(model.businessKpis.filter((item) => item.detailKey === "luckyGiftPools")).toHaveLength(3); }); test("includes coin seller stock recharge in USD total and keeps seller transfer as coin metric", () => { const model = createDashboardModel( { coin_seller_recharge_usd_minor: 200, coin_seller_stock_coin: 400_000, coin_seller_transfer_coin: 160_000, salary_transfer_coin: 88_000, country_breakdown: [ { coin_seller_recharge_usd_minor: 200, coin_seller_stock_coin: 400_000, coin_seller_transfer_coin: 160_000, country: "阿富汗", google_recharge_usd_minor: 150, recharge_usd_minor: 150, salary_transfer_coin: 88_000, }, ], daily_series: [ { coin_seller: 200, google: 150, label: "当前", recharge_usd_minor: 150, }, ], google_recharge_usd_minor: 150, recharge_usd_minor: 150, }, { appCode: "lalu", countryId: 0, range: { end: "2026-06-04", start: "2026-06-04" } }, ); expect(metricValue(model, "总充值")).toBe("$4"); expect(metricValue(model, "币商出货金币")).toBe("160,000"); expect(model.revenueSeries[0]).toEqual(expect.objectContaining({ coin_seller: 200, google: 150, lineTotal: 350, total: 350 })); expect(model.countryBreakdown[0]).toEqual( expect.objectContaining({ coin_seller_stock_coin: 400_000, coin_seller_transfer_coin: 160_000, recharge_usd_minor: 350, salary_transfer_coin: 88_000, }), ); }); test("exposes new user KPI and removes visitor stage from funnel", () => { const model = createDashboardModel( { active_users: 80, new_users: 12, new_users_delta_rate: 0.24, paid_users: 8, recharge_users: 7, }, { appCode: "lalu", countryId: 0, range: { end: "2026-06-04", start: "2026-06-04" } }, ); expect(metricValue(model, "新增用户数")).toBe("12"); expect(model.kpis.map((item) => item.label)).toContain("新增用户数"); expect(model.funnel.map((item) => item.name)).toEqual(["活跃用户", "付费用户", "充值用户"]); }); test("adds real-room robot gift metrics to a separate card row", () => { const model = createDashboardModel( { real_room_robot_gift_room_count: 2, real_room_robot_lucky_gift_coin: 200, real_room_robot_normal_gift_coin: 100, real_room_robot_super_gift_coin: 300, }, { appCode: "lalu", countryId: 0, range: { end: "2026-06-04", start: "2026-06-04" } }, ); expect(metricValue(model, "真人房机器人普通礼物")).toBe("100"); expect(metricValue(model, "真人房机器人幸运礼物")).toBe("200"); expect(metricValue(model, "真人房机器人超级幸运礼物")).toBe("300"); expect(metricValue(model, "真人房机器人房均礼物")).toBe("300"); expect(model.businessKpis.map((item) => item.label)).not.toContain("真人房机器人房均礼物"); expect(model.robotGiftKpis.find((item) => item.label === "真人房机器人房均礼物")?.caption).toBe("2 个房间"); }); test("uses active users as payer and recharge conversion denominator", () => { const model = createDashboardModel( { active_users: 80, country_breakdown: [ { active_users: 20, country: "巴西", paid_users: 8, recharge_users: 6, recharge_usd_minor: 500, }, ], new_users: 4, paid_users: 8, recharge_users: 6, }, { appCode: "lalu", countryId: 0, range: { end: "2026-06-04", start: "2026-06-04" } }, ); expect(model.sideMetrics.find((item) => item.label === "付费转化率")?.value).toBe("10.00%"); expect(model.countryBreakdown[0]).toEqual( expect.objectContaining({ payer_rate: 0.4, recharge_conversion_rate: 0.3, }), ); }); test("uses registration count instead of legacy visitors in country breakdown", () => { const model = createDashboardModel( { country_breakdown: [ { country: "菲律宾", new_users: 12, visitors: 99, }, ], }, { appCode: "lalu", countryId: 0, range: { end: "2026-06-04", start: "2026-06-04" } }, ); expect(model.countryBreakdown[0]).toEqual( expect.objectContaining({ registrations: 12, }), ); expect(model.countryBreakdown[0]).not.toHaveProperty("visitors"); }); test("builds report rows with range total, daily totals, and daily country rows", () => { const model = createDashboardModel( { country_breakdown: [{ country: "巴西", country_id: 86, active_users: 30, recharge_usd_minor: 300 }], daily_country_breakdown: [ { active_users: 10, coin_seller_stock_coin: 800, coin_seller_transfer_coin: 500, coin_total: 1_200, consumed_coin: 300, country: "巴西", country_id: 86, game_turnover: 300, lucky_gift_turnover: 200, manual_grant_coin: 20, mic_online_ms: 6_000, mic_online_users: 3, output_coin: 120, platform_grant_coin: 150, recharge_usd_minor: 100, salary_usd_minor: 900, salary_transfer_coin: 700, stat_day: "2026-06-05", super_lucky_gift_turnover: 80, super_lucky_gift_payout: 20, }, { active_users: 20, country: "巴西", country_id: 86, recharge_usd_minor: 200, stat_day: "2026-06-06", }, ], daily_series: [ { active_users: 10, coin_total: 1_200, consumed_coin: 300, label: "2026-06-05", manual_grant_coin: 20, mic_online_ms: 6_000, mic_online_users: 3, output_coin: 120, platform_grant_coin: 150, recharge_usd_minor: 100, salary_usd_minor: 900, stat_day: "2026-06-05", }, { active_users: 20, label: "2026-06-06", recharge_usd_minor: 200, stat_day: "2026-06-06" }, ], recharge_usd_minor: 300, }, { appCode: "lalu", countryId: 0, range: { end: "2026-06-06", start: "2026-06-05" } }, ); expect(model.reportCountryRows.map((row) => row.row_type)).toEqual(["total", "date_total", "date_total"]); expect(model.reportCountryRows[0]).toEqual(expect.objectContaining({ country: "总计", date_label: "全部" })); expect(model.reportCountryRows[1]).toEqual( expect.objectContaining({ avg_mic_online_ms: 2_000, coin_total: 1_200, consumed_coin: 300, consume_output_delta: 180, consume_output_ratio: 0.4, country: "总计", date_label: "2026-06-05", manual_grant_coin: 20, mic_online_ms: 6_000, output_coin: 120, platform_grant_coin: 150, salary_usd_minor: 900, }), ); expect(model.reportCountryRows[1].children).toHaveLength(1); expect(model.reportCountryRows[1].children[0]).toEqual( expect.objectContaining({ avg_mic_online_ms: 2_000, coin_seller_stock_coin: 800, coin_seller_transfer_coin: 500, coin_total: 1_200, consumed_coin: 300, consume_output_delta: 180, consume_output_ratio: 0.4, country: "巴西", game_turnover: 300, lucky_gift_turnover: 200, manual_grant_coin: 20, mic_online_ms: 6_000, output_coin: 120, platform_grant_coin: 150, salary_usd_minor: 900, salary_transfer_coin: 700, super_lucky_gift_profit: 60, }), ); }); test("keeps unavailable external report metrics as missing values", () => { const model = createDashboardModel( { country_breakdown: [ { active_users: 12, arppu_usd_minor: null, coin_seller_recharge_usd_minor: null, coin_seller_transfer_coin: null, country: "Saudi Arabia", game_profit_rate: null, game_turnover: null, gift_coin_spent: null, lucky_gift_profit_rate: null, lucky_gift_turnover: null, paid_users: null, recharge_conversion_rate: null, recharge_users: null, recharge_usd_minor: 10_025, }, ], recharge_usd_minor: 10_025, }, { appCode: "aslan", countryId: 0, range: { end: "2026-06-30", start: "2026-06-30" } }, ); expect(model.countryBreakdown[0]).toEqual( expect.objectContaining({ arppu_usd_minor: null, coin_seller_recharge_usd_minor: null, coin_seller_transfer_coin: null, game_profit_rate: null, game_turnover: null, gift_coin_spent: null, lucky_gift_profit_rate: null, lucky_gift_turnover: null, paid_users: null, payer_rate: null, recharge_conversion_rate: null, recharge_users: null, }), ); expect(model.reportCountryRows[1]).toEqual( expect.objectContaining({ arppu_usd_minor: null, game_turnover: null, paid_users: null, recharge_users: null, }), ); }); function metricValue(model, label) { return [...model.kpis, ...model.businessKpis, ...model.robotGiftKpis].find((item) => item.label === label)?.value; }