557 lines
18 KiB
Go
557 lines
18 KiB
Go
package databi
|
||
|
||
import (
|
||
"context"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"hyapp-admin-server/internal/modules/payment"
|
||
)
|
||
|
||
const financeRechargeTZOffsetMinutes int32 = 480
|
||
|
||
type financeRechargeValues struct {
|
||
GoogleUSDMinor int64
|
||
ThirdPartyUSDMinor int64
|
||
CoinSellerUSDMinor int64
|
||
}
|
||
|
||
func (v financeRechargeValues) userUSDMinor() int64 {
|
||
return v.GoogleUSDMinor + v.ThirdPartyUSDMinor
|
||
}
|
||
|
||
func (v financeRechargeValues) totalUSDMinor() int64 {
|
||
return v.userUSDMinor() + v.CoinSellerUSDMinor
|
||
}
|
||
|
||
func (s *Service) applyFinanceRechargeOverview(
|
||
ctx context.Context,
|
||
out *AppOverview,
|
||
query OverviewQuery,
|
||
regions []RegionInfo,
|
||
restricted bool,
|
||
allowedRegionIDs []int64,
|
||
) error {
|
||
if s == nil || s.financeRecharge == nil || out == nil {
|
||
return nil
|
||
}
|
||
scopeRegionIDs := []int64(nil)
|
||
if query.RegionID > 0 {
|
||
scopeRegionIDs = []int64{normalizeRegionID(regions, query.RegionID)}
|
||
} else if len(query.RegionIDs) > 0 {
|
||
scopeRegionIDs = normalizeRegionIDs(regions, query.RegionIDs)
|
||
} else if restricted {
|
||
scopeRegionIDs = normalizeRegionIDs(regions, allowedRegionIDs)
|
||
}
|
||
|
||
current, err := s.financeRecharge.FinanceRechargeOverview(ctx, payment.FinanceRechargeOverviewQuery{
|
||
AppCode: out.AppCode,
|
||
StartAtMS: query.StartMS,
|
||
EndAtMS: query.EndMS,
|
||
ScopeRegionIDs: scopeRegionIDs,
|
||
IncludeRegions: true,
|
||
TZOffsetMinutes: financeRechargeTZOffsetMinutes,
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
var previous payment.FinanceRechargeOverview
|
||
if query.StartMS > 0 && query.EndMS > query.StartMS {
|
||
periodLength := query.EndMS - query.StartMS
|
||
previous, err = s.financeRecharge.FinanceRechargeOverview(ctx, payment.FinanceRechargeOverviewQuery{
|
||
AppCode: out.AppCode,
|
||
StartAtMS: query.StartMS - periodLength,
|
||
EndAtMS: query.StartMS,
|
||
ScopeRegionIDs: scopeRegionIDs,
|
||
IncludeRegions: false,
|
||
TZOffsetMinutes: financeRechargeTZOffsetMinutes,
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
currentTotal := financeRechargeOverviewTotal(current)
|
||
previousTotal := financeRechargeOverviewTotal(previous)
|
||
applyFinanceRechargeValues(out.Total, currentTotal)
|
||
if query.StartMS > 0 && query.EndMS > query.StartMS {
|
||
out.Total["recharge_delta_rate"] = financeRechargeDelta(currentTotal.totalUSDMinor(), previousTotal.totalUSDMinor())
|
||
out.Total["user_recharge_delta_rate"] = financeRechargeDelta(currentTotal.userUSDMinor(), previousTotal.userUSDMinor())
|
||
}
|
||
// Finance 没有付费用户人数口径;金额切源后旧 ARPU/ARPPU 环比不可继续展示,否则分子来自旧 Dashboard。
|
||
out.Total["arpu_delta_rate"] = nil
|
||
out.Total["arppu_delta_rate"] = nil
|
||
|
||
out.DailySeries = overlayFinanceRechargeDaily(out.DailySeries, current.Daily)
|
||
out.RegionBreakdown, out.DailyRegionBreakdown = overlayFinanceRechargeRegions(
|
||
out.RegionBreakdown,
|
||
out.DailyRegionBreakdown,
|
||
current.Regions,
|
||
regions,
|
||
)
|
||
// Finance 页面没有国家维度,国家充值必须整体标为不可用;保留任何旧 Dashboard 金额都会导致
|
||
// App/区域合计与国家合计无法对账,并与下发的 Finance 指标来源说明矛盾。
|
||
markFinanceRechargeUnavailable(out.CountryBreakdown)
|
||
markFinanceRechargeUnavailable(out.DailyCountryBreakdown)
|
||
out.ReportMetricSources = appendFinanceRechargeMetricSources(out.ReportMetricSources)
|
||
return nil
|
||
}
|
||
|
||
func (s *Service) applyFinanceRechargeKpiWindow(
|
||
ctx context.Context,
|
||
appCode string,
|
||
startMS int64,
|
||
endMS int64,
|
||
total map[string]any,
|
||
regionMetrics map[int64]map[string]any,
|
||
) error {
|
||
financeOverview, err := s.financeRecharge.FinanceRechargeOverview(ctx, payment.FinanceRechargeOverviewQuery{
|
||
AppCode: appCode,
|
||
StartAtMS: startMS,
|
||
EndAtMS: endMS,
|
||
IncludeRegions: true,
|
||
TZOffsetMinutes: financeRechargeTZOffsetMinutes,
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
applyFinanceRechargeValues(total, financeRechargeOverviewTotal(financeOverview))
|
||
for regionID, row := range regionMetrics {
|
||
zeroFinanceRecharge([]map[string]any{row})
|
||
regionMetrics[regionID] = row
|
||
}
|
||
for _, financeRegion := range financeOverview.Regions {
|
||
row := regionMetrics[financeRegion.RegionID]
|
||
if row == nil {
|
||
row = map[string]any{"region_id": financeRegion.RegionID}
|
||
regionMetrics[financeRegion.RegionID] = row
|
||
}
|
||
// KPI 只消费区域总充值;Finance 区域分布没有渠道拆分,不能用 0 伪造各渠道。
|
||
applyFinanceRechargeTotalOnly(row, financeRegion.TotalUSDMinor)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func financeRechargeOverviewTotal(overview payment.FinanceRechargeOverview) financeRechargeValues {
|
||
var total financeRechargeValues
|
||
for _, row := range overview.Daily {
|
||
total.GoogleUSDMinor += row.GoogleUSDMinor
|
||
total.ThirdPartyUSDMinor += row.ThirdPartyUSDMinor
|
||
total.CoinSellerUSDMinor += row.CoinSellerUSDMinor
|
||
}
|
||
return total
|
||
}
|
||
|
||
func financeRechargeValuesFromDaily(row payment.FinanceRechargeDailyBucket) financeRechargeValues {
|
||
return financeRechargeValues{
|
||
GoogleUSDMinor: row.GoogleUSDMinor,
|
||
ThirdPartyUSDMinor: row.ThirdPartyUSDMinor,
|
||
CoinSellerUSDMinor: row.CoinSellerUSDMinor,
|
||
}
|
||
}
|
||
|
||
func applyFinanceRechargeValues(row map[string]any, values financeRechargeValues) {
|
||
if row == nil {
|
||
return
|
||
}
|
||
row["google_recharge_usd_minor"] = values.GoogleUSDMinor
|
||
// Databi 历史字段名是 mifapay,但 Finance 的 thirdParty 包含全部非 Google 三方渠道。
|
||
row["mifapay_recharge_usd_minor"] = values.ThirdPartyUSDMinor
|
||
row["third_party_recharge_usd_minor"] = values.ThirdPartyUSDMinor
|
||
row["coin_seller_recharge_usd_minor"] = values.CoinSellerUSDMinor
|
||
row["user_recharge_usd_minor"] = values.userUSDMinor()
|
||
row["recharge_usd_minor"] = values.totalUSDMinor()
|
||
row["total_recharge_usd_minor"] = values.totalUSDMinor()
|
||
row["new_user_recharge_usd_minor"] = nil
|
||
markFinanceRechargeDerivedUnavailable(row)
|
||
}
|
||
|
||
func applyFinanceRechargeTotalOnly(row map[string]any, totalUSDMinor int64) {
|
||
if row == nil {
|
||
return
|
||
}
|
||
markFinanceRechargeUnavailable([]map[string]any{row})
|
||
row["recharge_usd_minor"] = totalUSDMinor
|
||
row["total_recharge_usd_minor"] = totalUSDMinor
|
||
}
|
||
|
||
func markFinanceRechargeDerivedUnavailable(row map[string]any) {
|
||
if row == nil {
|
||
return
|
||
}
|
||
// Finance 的币商/H5 金额没有与旧 Dashboard paid_users 一致的去重人数,
|
||
// 因此 ARPU/ARPPU 不做跨来源除法;前端通过该标记在跨 App 聚合时继续保持缺失。
|
||
row["finance_recharge_aligned"] = true
|
||
row["arpu_usd_minor"] = nil
|
||
row["arppu_usd_minor"] = nil
|
||
}
|
||
|
||
func overlayFinanceRechargeDaily(existing []map[string]any, finance []payment.FinanceRechargeDailyBucket) []map[string]any {
|
||
byDay := make(map[string]payment.FinanceRechargeDailyBucket, len(finance))
|
||
for _, row := range finance {
|
||
if row.Date != "" {
|
||
byDay[row.Date] = row
|
||
}
|
||
}
|
||
seen := map[string]struct{}{}
|
||
for _, row := range existing {
|
||
day := rowString(row, "stat_day", "label")
|
||
bucket := byDay[day]
|
||
applyFinanceRechargeValues(row, financeRechargeValuesFromDaily(bucket))
|
||
if day != "" {
|
||
seen[day] = struct{}{}
|
||
}
|
||
}
|
||
for day, bucket := range byDay {
|
||
if _, ok := seen[day]; ok {
|
||
continue
|
||
}
|
||
row := map[string]any{"label": day, "stat_day": day}
|
||
applyFinanceRechargeValues(row, financeRechargeValuesFromDaily(bucket))
|
||
existing = append(existing, row)
|
||
}
|
||
sort.Slice(existing, func(i, j int) bool {
|
||
return rowString(existing[i], "stat_day", "label") < rowString(existing[j], "stat_day", "label")
|
||
})
|
||
return existing
|
||
}
|
||
|
||
func overlayFinanceRechargeRegions(
|
||
regionRows []map[string]any,
|
||
dailyRegionRows []map[string]any,
|
||
financeRegions []payment.FinanceRechargeRegionOverview,
|
||
regions []RegionInfo,
|
||
) ([]map[string]any, []map[string]any) {
|
||
zeroFinanceRecharge(regionRows)
|
||
dailyAvailable := false
|
||
for _, financeRegion := range financeRegions {
|
||
if financeRegion.DailyAvailable {
|
||
dailyAvailable = true
|
||
break
|
||
}
|
||
}
|
||
if dailyAvailable {
|
||
zeroFinanceRecharge(dailyRegionRows)
|
||
} else {
|
||
// 默认全量查询只复用 Finance 已有的区域总额;逐日渠道仅在用户显式选择区域时查询。
|
||
// 这里用 null 清掉旧 Dashboard 值,避免“未查询”被展示成 0 或旧口径。
|
||
markFinanceRechargeUnavailable(dailyRegionRows)
|
||
}
|
||
regionIndex := map[int64]map[string]any{}
|
||
for _, row := range regionRows {
|
||
if regionID, ok := rowInt64(row, "region_id", "regionId"); ok {
|
||
regionIndex[normalizeRegionID(regions, regionID)] = row
|
||
}
|
||
}
|
||
dailyIndex := map[string]map[string]any{}
|
||
for _, row := range dailyRegionRows {
|
||
regionID, ok := rowInt64(row, "region_id", "regionId")
|
||
if !ok {
|
||
continue
|
||
}
|
||
day := rowString(row, "stat_day", "label")
|
||
dailyIndex[financeRegionDayKey(normalizeRegionID(regions, regionID), day)] = row
|
||
}
|
||
|
||
for _, financeRegion := range financeRegions {
|
||
regionID := normalizeRegionID(regions, financeRegion.RegionID)
|
||
row := regionIndex[regionID]
|
||
if row == nil {
|
||
code, name := regionDisplay(regions, regionID)
|
||
row = map[string]any{"region_id": regionID, "region_code": code, "region_name": name}
|
||
regionRows = append(regionRows, row)
|
||
regionIndex[regionID] = row
|
||
}
|
||
var total financeRechargeValues
|
||
for _, dayBucket := range financeRegion.Daily {
|
||
values := financeRechargeValuesFromDaily(dayBucket)
|
||
total.GoogleUSDMinor += values.GoogleUSDMinor
|
||
total.ThirdPartyUSDMinor += values.ThirdPartyUSDMinor
|
||
total.CoinSellerUSDMinor += values.CoinSellerUSDMinor
|
||
key := financeRegionDayKey(regionID, dayBucket.Date)
|
||
dailyRow := dailyIndex[key]
|
||
if dailyRow == nil {
|
||
code, name := regionDisplay(regions, regionID)
|
||
dailyRow = map[string]any{
|
||
"label": dayBucket.Date,
|
||
"stat_day": dayBucket.Date,
|
||
"region_id": regionID,
|
||
"region_code": code,
|
||
"region_name": name,
|
||
}
|
||
dailyRegionRows = append(dailyRegionRows, dailyRow)
|
||
dailyIndex[key] = dailyRow
|
||
}
|
||
applyFinanceRechargeValues(dailyRow, values)
|
||
}
|
||
if financeRegion.DailyAvailable {
|
||
applyFinanceRechargeValues(row, total)
|
||
} else {
|
||
applyFinanceRechargeTotalOnly(row, financeRegion.TotalUSDMinor)
|
||
}
|
||
}
|
||
sort.Slice(regionRows, func(i, j int) bool {
|
||
left, _ := rowInt64(regionRows[i], "region_id", "regionId")
|
||
right, _ := rowInt64(regionRows[j], "region_id", "regionId")
|
||
return left < right
|
||
})
|
||
sort.Slice(dailyRegionRows, func(i, j int) bool {
|
||
leftDay := rowString(dailyRegionRows[i], "stat_day", "label")
|
||
rightDay := rowString(dailyRegionRows[j], "stat_day", "label")
|
||
if leftDay != rightDay {
|
||
return leftDay < rightDay
|
||
}
|
||
left, _ := rowInt64(dailyRegionRows[i], "region_id", "regionId")
|
||
right, _ := rowInt64(dailyRegionRows[j], "region_id", "regionId")
|
||
return left < right
|
||
})
|
||
return regionRows, dailyRegionRows
|
||
}
|
||
|
||
func zeroFinanceRecharge(rows []map[string]any) {
|
||
for _, row := range rows {
|
||
applyFinanceRechargeValues(row, financeRechargeValues{})
|
||
}
|
||
}
|
||
|
||
func markFinanceRechargeUnavailable(rows []map[string]any) {
|
||
for _, row := range rows {
|
||
row["google_recharge_usd_minor"] = nil
|
||
row["mifapay_recharge_usd_minor"] = nil
|
||
row["third_party_recharge_usd_minor"] = nil
|
||
row["coin_seller_recharge_usd_minor"] = nil
|
||
row["user_recharge_usd_minor"] = nil
|
||
row["recharge_usd_minor"] = nil
|
||
row["total_recharge_usd_minor"] = nil
|
||
row["new_user_recharge_usd_minor"] = nil
|
||
markFinanceRechargeDerivedUnavailable(row)
|
||
}
|
||
}
|
||
|
||
func requirementsIncludesRevenue(section string) bool {
|
||
switch strings.ToLower(strings.TrimSpace(section)) {
|
||
case "", "all", "revenue":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func requirementsHasCohortFilter(query RequirementsQuery) bool {
|
||
for _, value := range []string{query.UserRole, query.PayerType, query.NewUserType} {
|
||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||
if normalized != "" && normalized != "all" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func overlayFinanceRechargeRequirements(sections []map[string]any, overview payment.FinanceRechargeOverview) {
|
||
byDay := make(map[string]financeRechargeValues, len(overview.Daily))
|
||
for _, bucket := range overview.Daily {
|
||
if bucket.Date != "" {
|
||
byDay[bucket.Date] = financeRechargeValuesFromDaily(bucket)
|
||
}
|
||
}
|
||
total := financeRechargeOverviewTotal(overview)
|
||
for _, section := range sections {
|
||
if rowString(section, "key") != "revenue" {
|
||
continue
|
||
}
|
||
section["recharge_source"] = "finance"
|
||
section["finance_recharge_available"] = true
|
||
updateFinanceRequirementMetadata(section, true)
|
||
if row, ok := section["total"].(map[string]any); ok {
|
||
applyFinanceRequirementAmounts(row, total, 1)
|
||
}
|
||
|
||
daily := anyRowSlice(section["daily_series"])
|
||
seen := map[string]struct{}{}
|
||
for _, row := range daily {
|
||
day := rowString(row, "stat_day", "label")
|
||
applyFinanceRequirementAmounts(row, byDay[day], 1)
|
||
if day != "" {
|
||
seen[day] = struct{}{}
|
||
}
|
||
}
|
||
for day, values := range byDay {
|
||
if _, ok := seen[day]; ok {
|
||
continue
|
||
}
|
||
row := map[string]any{"label": day, "stat_day": day}
|
||
applyFinanceRequirementAmounts(row, values, 1)
|
||
daily = append(daily, row)
|
||
}
|
||
sort.Slice(daily, func(i, j int) bool {
|
||
return rowString(daily[i], "stat_day", "label") < rowString(daily[j], "stat_day", "label")
|
||
})
|
||
section["daily_series"] = daily
|
||
if row, ok := section["average"].(map[string]any); ok {
|
||
applyFinanceRequirementAmounts(row, total, len(daily))
|
||
}
|
||
}
|
||
}
|
||
|
||
func markFinanceRechargeRequirementsUnavailable(sections []map[string]any) {
|
||
for _, section := range sections {
|
||
if rowString(section, "key") != "revenue" {
|
||
continue
|
||
}
|
||
section["recharge_source"] = "unavailable_for_cohort"
|
||
section["finance_recharge_available"] = false
|
||
updateFinanceRequirementMetadata(section, false)
|
||
if row, ok := section["total"].(map[string]any); ok {
|
||
markFinanceRequirementAmountsUnavailable(row)
|
||
}
|
||
if row, ok := section["average"].(map[string]any); ok {
|
||
markFinanceRequirementAmountsUnavailable(row)
|
||
}
|
||
for _, row := range anyRowSlice(section["daily_series"]) {
|
||
markFinanceRequirementAmountsUnavailable(row)
|
||
}
|
||
}
|
||
}
|
||
|
||
func applyFinanceRequirementAmounts(row map[string]any, values financeRechargeValues, dayCount int) {
|
||
if row == nil {
|
||
return
|
||
}
|
||
assign := func(key string, value int64) {
|
||
if dayCount > 1 {
|
||
row[key] = float64(value) / float64(dayCount)
|
||
return
|
||
}
|
||
row[key] = value
|
||
}
|
||
assign("recharge_usd_minor", values.totalUSDMinor())
|
||
assign("google_recharge_usd_minor", values.GoogleUSDMinor)
|
||
assign("third_party_recharge_usd_minor", values.ThirdPartyUSDMinor)
|
||
assign("coin_seller_recharge_usd_minor", values.CoinSellerUSDMinor)
|
||
// Finance 无首充/复购和去重付费人数维度,这些金额均值不能继续沿用宽表分子。
|
||
row["first_recharge_usd_minor"] = nil
|
||
row["repeat_recharge_usd_minor"] = nil
|
||
row["arpu_usd_minor"] = nil
|
||
row["arppu_usd_minor"] = nil
|
||
row["finance_recharge_aligned"] = true
|
||
}
|
||
|
||
func markFinanceRequirementAmountsUnavailable(row map[string]any) {
|
||
if row == nil {
|
||
return
|
||
}
|
||
for _, key := range []string{
|
||
"recharge_usd_minor",
|
||
"google_recharge_usd_minor",
|
||
"third_party_recharge_usd_minor",
|
||
"coin_seller_recharge_usd_minor",
|
||
"first_recharge_usd_minor",
|
||
"repeat_recharge_usd_minor",
|
||
"arpu_usd_minor",
|
||
"arppu_usd_minor",
|
||
} {
|
||
row[key] = nil
|
||
}
|
||
row["finance_recharge_aligned"] = true
|
||
}
|
||
|
||
func updateFinanceRequirementMetadata(section map[string]any, available bool) {
|
||
definitions := map[string]string{
|
||
"recharge_usd_minor": "Finance 成功充值总额 = Google + 非 Google 三方 + 币商充值。",
|
||
"google_recharge_usd_minor": "Finance Google 成功充值。",
|
||
"third_party_recharge_usd_minor": "Finance 非 Google 三方成功充值。",
|
||
"coin_seller_recharge_usd_minor": "Finance 已核验币商订单 + H5 币商身份成功充值。",
|
||
"first_recharge_usd_minor": "Finance 暂无首充金额拆分,本字段不可用。",
|
||
"repeat_recharge_usd_minor": "Finance 暂无复购金额拆分,本字段不可用。",
|
||
"arpu_usd_minor": "Finance 金额没有同口径活跃人数,本字段不可用。",
|
||
"arppu_usd_minor": "Finance 币商/H5 金额没有同口径付费人数,本字段不可用。",
|
||
}
|
||
if !available {
|
||
for _, key := range []string{"recharge_usd_minor", "google_recharge_usd_minor", "third_party_recharge_usd_minor", "coin_seller_recharge_usd_minor"} {
|
||
definitions[key] = "Finance 不支持当前角色/付费身份/新老用户筛选,本字段不可用。"
|
||
}
|
||
}
|
||
|
||
metricDefinitions := anyRowSlice(section["metric_definitions"])
|
||
definitionIndex := map[string]map[string]any{}
|
||
for _, row := range metricDefinitions {
|
||
definitionIndex[rowString(row, "metric", "field")] = row
|
||
}
|
||
for metric, definition := range definitions {
|
||
row := definitionIndex[metric]
|
||
if row == nil {
|
||
row = map[string]any{"metric": metric}
|
||
metricDefinitions = append(metricDefinitions, row)
|
||
}
|
||
row["definition"] = definition
|
||
}
|
||
section["metric_definitions"] = metricDefinitions
|
||
|
||
columns := anyRowSlice(section["columns"])
|
||
for _, column := range columns {
|
||
key := rowString(column, "key")
|
||
if definition, ok := definitions[key]; ok {
|
||
column["tooltip"] = definition
|
||
}
|
||
}
|
||
section["columns"] = columns
|
||
}
|
||
|
||
func financeRegionDayKey(regionID int64, day string) string {
|
||
return strconv.FormatInt(regionID, 10) + "|" + day
|
||
}
|
||
|
||
func financeRechargeDelta(current int64, previous int64) float64 {
|
||
if previous == 0 {
|
||
if current > 0 {
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|
||
denominator := previous
|
||
if denominator < 0 {
|
||
denominator = -denominator
|
||
}
|
||
return float64(current-previous) / float64(denominator)
|
||
}
|
||
|
||
func appendFinanceRechargeMetricSources(value any) []map[string]any {
|
||
out := anyRowSlice(value)
|
||
fields := []struct {
|
||
field string
|
||
definition string
|
||
}{
|
||
{"recharge_usd_minor", "Finance 总充值 = Google + 三方 + 币商充值。"},
|
||
{"google_recharge_usd_minor", "Finance 谷歌成功充值账单。"},
|
||
{"mifapay_recharge_usd_minor", "Finance 非 Google 三方成功充值账单。"},
|
||
{"coin_seller_recharge_usd_minor", "Finance 已核验币商订单 + H5 币商身份成功充值。"},
|
||
}
|
||
for _, item := range fields {
|
||
replaced := false
|
||
for index := range out {
|
||
if rowString(out[index], "field") != item.field {
|
||
continue
|
||
}
|
||
out[index] = map[string]any{
|
||
"available": true,
|
||
"definition": item.definition,
|
||
"field": item.field,
|
||
"source": "Finance recharge-bills overview",
|
||
}
|
||
replaced = true
|
||
break
|
||
}
|
||
if !replaced {
|
||
out = append(out, map[string]any{
|
||
"available": true,
|
||
"definition": item.definition,
|
||
"field": item.field,
|
||
"source": "Finance recharge-bills overview",
|
||
})
|
||
}
|
||
}
|
||
return out
|
||
}
|