218 lines
11 KiB
Go
218 lines
11 KiB
Go
package databi
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"hyapp-admin-server/internal/modules/payment"
|
|
)
|
|
|
|
type fakeFinanceRechargeOverviewSource struct {
|
|
calls []payment.FinanceRechargeOverviewQuery
|
|
}
|
|
|
|
func (f *fakeFinanceRechargeOverviewSource) FinanceRechargeOverview(_ context.Context, query payment.FinanceRechargeOverviewQuery) (payment.FinanceRechargeOverview, error) {
|
|
f.calls = append(f.calls, query)
|
|
if query.EndAtMS == 200 {
|
|
return payment.FinanceRechargeOverview{Daily: []payment.FinanceRechargeDailyBucket{{
|
|
Date: "1970-01-01", GoogleUSDMinor: 50, ThirdPartyUSDMinor: 100, CoinSellerUSDMinor: 50,
|
|
}}}, nil
|
|
}
|
|
region := payment.FinanceRechargeRegionOverview{RegionID: 7, TotalUSDMinor: 150}
|
|
if len(query.ScopeRegionIDs) > 0 {
|
|
region.DailyAvailable = true
|
|
region.Daily = []payment.FinanceRechargeDailyBucket{{
|
|
Date: "2026-07-10", GoogleUSDMinor: 40, ThirdPartyUSDMinor: 50, CoinSellerUSDMinor: 60,
|
|
}}
|
|
}
|
|
return payment.FinanceRechargeOverview{
|
|
Daily: []payment.FinanceRechargeDailyBucket{
|
|
{Date: "2026-07-10", GoogleUSDMinor: 100, ThirdPartyUSDMinor: 200, CoinSellerUSDMinor: 300},
|
|
{Date: "2026-07-11", GoogleUSDMinor: 10, ThirdPartyUSDMinor: 20, CoinSellerUSDMinor: 30},
|
|
},
|
|
Regions: []payment.FinanceRechargeRegionOverview{region},
|
|
}, nil
|
|
}
|
|
|
|
func TestApplyFinanceRechargeOverviewReplacesDashboardRechargeMetrics(t *testing.T) {
|
|
finance := &fakeFinanceRechargeOverviewSource{}
|
|
service := &Service{financeRecharge: finance}
|
|
out := AppOverview{
|
|
AppCode: "aslan",
|
|
Total: map[string]any{
|
|
"active_users": int64(10), "paid_users": int64(2), "recharge_usd_minor": int64(9999),
|
|
"coin_seller_recharge_usd_minor": int64(8888), "arpu_delta_rate": 0.9, "arppu_delta_rate": 0.8,
|
|
},
|
|
DailySeries: []map[string]any{{
|
|
"stat_day": "2026-07-10", "active_users": int64(5), "paid_users": int64(1),
|
|
"recharge_usd_minor": int64(9999), "coin_seller_recharge_usd_minor": int64(8888),
|
|
}},
|
|
RegionBreakdown: []map[string]any{{
|
|
"region_id": int64(7), "active_users": int64(5), "paid_users": int64(1),
|
|
"recharge_usd_minor": int64(9999), "coin_seller_recharge_usd_minor": int64(8888),
|
|
}, {
|
|
"region_id": int64(8), "active_users": int64(3), "paid_users": int64(1),
|
|
"recharge_usd_minor": int64(7777), "coin_seller_recharge_usd_minor": int64(6666),
|
|
}},
|
|
DailyRegionBreakdown: []map[string]any{{
|
|
"region_id": int64(7), "stat_day": "2026-07-10", "active_users": int64(5), "paid_users": int64(1),
|
|
"recharge_usd_minor": int64(9999), "coin_seller_recharge_usd_minor": int64(8888),
|
|
}, {
|
|
"region_id": int64(8), "stat_day": "2026-07-10", "active_users": int64(3), "paid_users": int64(1),
|
|
"recharge_usd_minor": int64(7777), "coin_seller_recharge_usd_minor": int64(6666),
|
|
}},
|
|
CountryBreakdown: []map[string]any{{
|
|
"country_code": "SA", "recharge_usd_minor": int64(9999), "coin_seller_recharge_usd_minor": int64(8888),
|
|
}},
|
|
DailyCountryBreakdown: []map[string]any{{
|
|
"country_code": "SA", "stat_day": "2026-07-10", "recharge_usd_minor": int64(9999), "coin_seller_recharge_usd_minor": int64(8888),
|
|
}},
|
|
}
|
|
query := OverviewQuery{StartMS: 200, EndMS: 300}
|
|
regions := []RegionInfo{
|
|
{AppCode: "aslan", RegionID: 7, RegionCode: "SA", RegionName: "沙特"},
|
|
{AppCode: "aslan", RegionID: 8, RegionCode: "AE", RegionName: "阿联酋"},
|
|
}
|
|
|
|
if err := service.applyFinanceRechargeOverview(context.Background(), &out, query, regions, false, nil); err != nil {
|
|
t.Fatalf("apply Finance recharge overview failed: %v", err)
|
|
}
|
|
|
|
if got := mustMetric(t, out.Total, "recharge_usd_minor"); got != 660 {
|
|
t.Fatalf("total recharge should use Finance channels: got %d", got)
|
|
}
|
|
if got := mustMetric(t, out.Total, "coin_seller_recharge_usd_minor"); got != 330 {
|
|
t.Fatalf("coin seller recharge should use Finance verified/H5 amount: got %d", got)
|
|
}
|
|
if got := mustMetric(t, out.Total, "mifapay_recharge_usd_minor"); got != 220 {
|
|
t.Fatalf("third-party recharge should use Finance amount: got %d", got)
|
|
}
|
|
if delta, ok := out.Total["recharge_delta_rate"].(float64); !ok || delta != 2.3 {
|
|
t.Fatalf("recharge delta should compare the previous Finance range: %#v", out.Total["recharge_delta_rate"])
|
|
}
|
|
if out.Total["arpu_delta_rate"] != nil || out.Total["arppu_delta_rate"] != nil {
|
|
t.Fatalf("amount-derived deltas must not retain the old Dashboard numerator: %#v", out.Total)
|
|
}
|
|
if out.Total["arpu_usd_minor"] != nil || out.Total["arppu_usd_minor"] != nil || out.Total["finance_recharge_aligned"] != true {
|
|
t.Fatalf("Finance recharge must not be divided by old Dashboard users: %#v", out.Total)
|
|
}
|
|
if len(out.DailySeries) != 2 || mustMetric(t, out.DailySeries[0], "recharge_usd_minor") != 600 || mustMetric(t, out.DailySeries[1], "recharge_usd_minor") != 60 {
|
|
t.Fatalf("daily series should be replaced and missing Finance days appended: %#v", out.DailySeries)
|
|
}
|
|
if mustMetric(t, out.RegionBreakdown[0], "recharge_usd_minor") != 150 || out.RegionBreakdown[0]["coin_seller_recharge_usd_minor"] != nil {
|
|
t.Fatalf("global region rows should use Finance totals without inventing channel splits: %#v", out.RegionBreakdown)
|
|
}
|
|
if mustMetric(t, out.RegionBreakdown[1], "recharge_usd_minor") != 0 || out.DailyRegionBreakdown[0]["recharge_usd_minor"] != nil || out.DailyRegionBreakdown[1]["recharge_usd_minor"] != nil {
|
|
t.Fatalf("regions/days missing from Finance must not retain Dashboard recharge: region=%#v daily=%#v", out.RegionBreakdown, out.DailyRegionBreakdown)
|
|
}
|
|
if out.CountryBreakdown[0]["recharge_usd_minor"] != nil || out.CountryBreakdown[0]["coin_seller_recharge_usd_minor"] != nil || out.DailyCountryBreakdown[0]["recharge_usd_minor"] != nil {
|
|
t.Fatalf("country recharge must be unavailable because Finance has no country dimension: %#v %#v", out.CountryBreakdown, out.DailyCountryBreakdown)
|
|
}
|
|
if len(finance.calls) != 2 || !finance.calls[0].IncludeRegions || finance.calls[1].IncludeRegions || finance.calls[0].TZOffsetMinutes != 480 {
|
|
t.Fatalf("current/previous Finance queries mismatch: %#v", finance.calls)
|
|
}
|
|
}
|
|
|
|
func TestApplyFinanceRechargeOverviewLoadsDailyChannelsOnlyForExplicitRegions(t *testing.T) {
|
|
finance := &fakeFinanceRechargeOverviewSource{}
|
|
service := &Service{financeRecharge: finance}
|
|
out := AppOverview{
|
|
AppCode: "aslan",
|
|
Total: map[string]any{},
|
|
RegionBreakdown: []map[string]any{{"region_id": int64(7)}},
|
|
DailyRegionBreakdown: []map[string]any{{"region_id": int64(7), "stat_day": "2026-07-10"}},
|
|
}
|
|
query := OverviewQuery{StartMS: 200, EndMS: 300, RegionIDs: []int64{7}}
|
|
regions := []RegionInfo{{AppCode: "aslan", RegionID: 7, RegionCode: "SA", RegionName: "沙特"}}
|
|
|
|
if err := service.applyFinanceRechargeOverview(context.Background(), &out, query, regions, false, nil); err != nil {
|
|
t.Fatalf("apply scoped Finance recharge overview failed: %v", err)
|
|
}
|
|
if mustMetric(t, out.RegionBreakdown[0], "recharge_usd_minor") != 150 || mustMetric(t, out.DailyRegionBreakdown[0], "coin_seller_recharge_usd_minor") != 60 {
|
|
t.Fatalf("explicit regions should receive Finance daily channels: region=%#v daily=%#v", out.RegionBreakdown, out.DailyRegionBreakdown)
|
|
}
|
|
if len(finance.calls) != 2 || len(finance.calls[0].ScopeRegionIDs) != 1 || finance.calls[0].ScopeRegionIDs[0] != 7 || finance.calls[1].ScopeRegionIDs[0] != 7 {
|
|
t.Fatalf("current and previous windows must keep the explicit region scope: %#v", finance.calls)
|
|
}
|
|
}
|
|
|
|
func TestApplyFinanceRechargeKpiWindowUsesFinanceTotalsAndRegions(t *testing.T) {
|
|
finance := &fakeFinanceRechargeOverviewSource{}
|
|
service := &Service{financeRecharge: finance}
|
|
total := map[string]any{"recharge_usd_minor": int64(9999), "coin_seller_recharge_usd_minor": int64(8888)}
|
|
regions := map[int64]map[string]any{7: {
|
|
"region_id": int64(7), "recharge_usd_minor": int64(9999), "coin_seller_recharge_usd_minor": int64(8888),
|
|
}}
|
|
|
|
if err := service.applyFinanceRechargeKpiWindow(context.Background(), "aslan", 200, 300, total, regions); err != nil {
|
|
t.Fatalf("apply Finance KPI window failed: %v", err)
|
|
}
|
|
if mustMetric(t, total, "recharge_usd_minor") != 660 || mustMetric(t, regions[7], "recharge_usd_minor") != 150 {
|
|
t.Fatalf("KPI window should use Finance total/region values: total=%#v regions=%#v", total, regions)
|
|
}
|
|
if len(finance.calls) != 1 || !finance.calls[0].IncludeRegions {
|
|
t.Fatalf("KPI window should issue one Finance query with regions: %#v", finance.calls)
|
|
}
|
|
}
|
|
|
|
func TestOverlayFinanceRechargeRequirementsUsesFinanceAmounts(t *testing.T) {
|
|
sections := []map[string]any{{
|
|
"key": "revenue",
|
|
"total": map[string]any{"recharge_usd_minor": int64(9999), "recharge_users": int64(4), "arppu_usd_minor": int64(123)},
|
|
"average": map[string]any{"recharge_usd_minor": float64(9999)},
|
|
"daily_series": []any{
|
|
map[string]any{"stat_day": "2026-07-10", "recharge_usd_minor": int64(9999), "recharge_users": int64(3)},
|
|
map[string]any{"stat_day": "2026-07-11", "recharge_usd_minor": int64(9999), "recharge_users": int64(1)},
|
|
},
|
|
"columns": []any{map[string]any{"key": "coin_seller_recharge_usd_minor"}},
|
|
"metric_definitions": []any{map[string]any{"metric": "coin_seller_recharge_usd_minor", "definition": "old"}},
|
|
}}
|
|
overlayFinanceRechargeRequirements(sections, payment.FinanceRechargeOverview{Daily: []payment.FinanceRechargeDailyBucket{
|
|
{Date: "2026-07-10", GoogleUSDMinor: 100, ThirdPartyUSDMinor: 200, CoinSellerUSDMinor: 300},
|
|
{Date: "2026-07-11", GoogleUSDMinor: 10, ThirdPartyUSDMinor: 20, CoinSellerUSDMinor: 30},
|
|
}})
|
|
|
|
total := sections[0]["total"].(map[string]any)
|
|
daily := anyRowSlice(sections[0]["daily_series"])
|
|
average := sections[0]["average"].(map[string]any)
|
|
if mustMetric(t, total, "recharge_usd_minor") != 660 || mustMetric(t, total, "coin_seller_recharge_usd_minor") != 330 {
|
|
t.Fatalf("requirements total should use Finance channels: %#v", total)
|
|
}
|
|
if mustMetric(t, daily[0], "recharge_usd_minor") != 600 || mustMetric(t, daily[1], "recharge_usd_minor") != 60 {
|
|
t.Fatalf("requirements daily should use Finance channels: %#v", daily)
|
|
}
|
|
if average["recharge_usd_minor"] != float64(330) || total["recharge_users"] != int64(4) || total["arppu_usd_minor"] != nil {
|
|
t.Fatalf("requirements average/non-amount fields mismatch: total=%#v average=%#v", total, average)
|
|
}
|
|
if sections[0]["recharge_source"] != "finance" || sections[0]["finance_recharge_available"] != true {
|
|
t.Fatalf("requirements must expose Finance source metadata: %#v", sections[0])
|
|
}
|
|
}
|
|
|
|
func TestFinanceRechargeRequirementsCohortAmountsAreUnavailable(t *testing.T) {
|
|
query := RequirementsQuery{UserRole: "host", PayerType: "all", NewUserType: "all"}
|
|
if !requirementsHasCohortFilter(query) || !requirementsIncludesRevenue("revenue") {
|
|
t.Fatalf("cohort/revenue detection mismatch")
|
|
}
|
|
sections := []map[string]any{{
|
|
"key": "revenue",
|
|
"total": map[string]any{"recharge_usd_minor": int64(9999), "recharge_users": int64(2)},
|
|
"average": map[string]any{"recharge_usd_minor": float64(9999)},
|
|
"daily_series": []any{map[string]any{"stat_day": "2026-07-10", "recharge_usd_minor": int64(9999)}},
|
|
}}
|
|
markFinanceRechargeRequirementsUnavailable(sections)
|
|
total := sections[0]["total"].(map[string]any)
|
|
if total["recharge_usd_minor"] != nil || total["recharge_users"] != int64(2) || sections[0]["finance_recharge_available"] != false {
|
|
t.Fatalf("cohort should retain counts but hide non-Finance amounts: %#v", sections[0])
|
|
}
|
|
}
|
|
|
|
func mustMetric(t *testing.T, row map[string]any, key string) int64 {
|
|
t.Helper()
|
|
value, ok := metricInt64(row[key])
|
|
if !ok {
|
|
t.Fatalf("metric %s missing from %#v", key, row)
|
|
}
|
|
return value
|
|
}
|