14 KiB
Feature Research
Domain: Profit/Loss analytics functions — platform-perspective P&L aggregation for a game/e-commerce platform Researched: 2026-03-21 Confidence: HIGH (based on direct codebase analysis of existing analytics + domain patterns)
Feature Landscape
Table Stakes (Users Expect These)
These are the non-negotiable capabilities that any reusable P&L service function must have. Operators calling these functions expect all of the following to "just work."
| Feature | Why Expected | Complexity | Notes |
|---|---|---|---|
| Revenue calculation (actual_amount + discount_amount) | Core platform-perspective income; existing Dashboard already does this | LOW | Coupon discount must be added back: it's real value received |
| Game-pass order classification | Orders with source_type=4, order_no LIKE 'GP%', or remark containing 'use_game_pass' need separate treatment | LOW | Logic already exists in finance.IsGamePassOrder — must be reused, not reimplemented |
| Game-pass value derivation (draw_count × activity_price) | Zero-cash orders have economic value; existing logic computes it correctly | LOW | finance.ComputeGamePassValue exists; new functions must call it |
| Prize cost calculation with item-card multiplier | Item cards double/triple prize value output; omitting multiplier understates cost | MEDIUM | finance.ComputePrizeCostWithMultiplier exists; value comes from system_item_cards.reward_multiplier_x1000 |
| Profit = spending - prize_cost | Core formula; operators see profit and profit_rate | LOW | finance.ComputeProfit exists and returns (int64, float64) |
| Time-range filter (optional) | All existing dashboard analytics support time scoping | LOW | Must accept *time.Time for start/end; nil = all-time |
| User-dimension aggregation (one or many user IDs) | Operators look up whale users; existing GetUserSpendingDashboard does single-user only |
MEDIUM | New function must accept []int64; empty = all users |
| Activity-dimension aggregation (one activity ID) | Per-activity P&L is the primary ops view; DashboardActivityProfitLoss does this at handler level |
MEDIUM | New function wraps the same logic as a reusable service method |
| "All asset types" as default (nil asset type = all) | PROJECT.md requires all params optional | LOW | Asset-type filter is additive; absence means no filter |
| Summary + per-asset-type breakdown in return value | Operators need total AND split by asset class | MEDIUM | Return struct must carry both Summary and []AssetBreakdown |
| Refund/cancelled order exclusion | Orders in status 3 (cancelled) or 4 (refunded) must NOT count as revenue | LOW | Already enforced in existing Dashboard SQL; must be replicated |
| Voided inventory exclusion | Inventory with remark LIKE '%void%' or status=2 represents decomposed assets; must be excluded from prize cost | LOW | Pattern already established in existing queries |
Differentiators (Competitive Advantage)
Features that go beyond what the existing dashboard provides, making the new service layer genuinely more reusable.
| Feature | Value Proposition | Complexity | Notes |
|---|---|---|---|
Multi-user batch support ([]int64 user IDs) |
Existing dashboard only handles single user at a time; batch enables cross-user analytics (e.g., cohort P&L) | MEDIUM | Accept empty slice as "all users"; pass through as SQL IN clause |
| Composable filter struct (asset type, dimension ID, time range all optional) | Callers can mix and match filters without writing bespoke queries | MEDIUM | Use a ProfitLossFilter options struct with pointer fields for optionality |
Canonical AssetType enum covering all 5 types |
Points, coupon, item-card, physical-good, fragment — each type maps to different source tables | MEDIUM | Defining the enum properly prevents future callers guessing string/int values |
| Per-asset-type cost tracking (not just total) | Operators want to see "how much did item-card prizes cost vs physical goods" — the Dashboard conflates them | HIGH | Requires separate GROUP BY legs or CASE-based aggregation per type |
| Canonical spending classification reuse | New functions must call finance.ClassifyOrderSpending — not re-derive the rule — so calculation stays consistent everywhere |
LOW | This is a correctness feature; prevents drift from the Dashboard numbers |
Read-only DB enforcement (DbR) |
Statistics queries must route to the read replica; new functions must accept a *gorm.DB injected from the caller (already DbR-aware) |
LOW | Function signature should accept db *gorm.DB so callers can pass h.repo.GetDbR() |
Anti-Features (Commonly Requested, Often Problematic)
| Feature | Why Requested | Why Problematic | Alternative |
|---|---|---|---|
| Caching / memoization inside the service function | "Stats queries are slow" | The service layer is not the right place for caching; it would break test isolation and caller control over staleness | Let the HTTP handler or a future cache layer wrap the call; the function stays pure |
| Real-time streaming / push notifications for P&L changes | "Alert me when profit drops" | Out of scope for v1 per PROJECT.md; adds event infrastructure complexity | Defer to a future monitoring milestone |
| Automatic pagination inside the aggregate function | "Return page X of users by profit" | Pagination belongs at the API layer; the service function returning a flat result set is more composable | Callers receive the full aggregated slice and paginate themselves |
Reusing DashboardActivityProfitLoss handler logic directly |
"Don't duplicate code" | The handler is tightly coupled to HTTP context, request parsing, and response formatting; pulling it into service layer would invert the dependency | New functions in internal/service/finance/ are fresh implementations using shared finance.* primitives |
| Storing computed P&L in a materialized table | "Pre-compute for speed" | Requires write access and schema migration; risks stale data bugs | Query on demand from DbR; optimize with indexes if needed later |
| Returning string-formatted amounts (e.g. "¥12.50") | "UI-ready output" | Formatting belongs in the presentation layer; service functions should return raw int64 cents | Callers convert cents to display strings |
Feature Dependencies
[Time-range filter]
└──requires──> [Optional *time.Time parameters]
[Multi-user aggregation]
└──requires──> [Revenue calculation]
└──requires──> [Game-pass classification]
└──requires──> [Prize cost with multiplier]
└──requires──> [Refund/void exclusion]
[Activity-dimension aggregation]
└──requires──> [Revenue calculation]
└──requires──> [Game-pass classification]
└──requires──> [Prize cost with multiplier]
└──requires──> [Refund/void exclusion]
[Per-asset-type breakdown]
└──requires──> [Canonical AssetType enum]
└──enhances──> [User-dimension aggregation]
└──enhances──> [Activity-dimension aggregation]
[Composable filter struct]
└──enhances──> [User-dimension aggregation]
└──enhances──> [Activity-dimension aggregation]
[Canonical spending classification reuse]
└──requires──> [finance.ClassifyOrderSpending (existing)]
└──prevents-conflict──> [Game-pass classification (must not re-derive)]
[Read-only DB enforcement]
└──requires──> [Caller passes *gorm.DB from DbR]
Dependency Notes
- Per-asset-type breakdown requires AssetType enum: Without a canonical type definition, callers and implementations will use ad-hoc int/string values that drift.
- Multi-user aggregation requires all revenue/cost sub-features: The aggregation is just a GROUP BY wrapper around the same revenue and cost logic.
- Canonical spending classification must reuse existing
finance.*functions: The existing Dashboard and the new service functions must produce identical numbers for the same data. Any divergence in classification logic breaks operator trust in the analytics. - Composable filter struct enhances both dimension functions: A
ProfitLossFilterstruct with optional fields (asset types, IDs, time range) is shared between the user-dimension and activity-dimension functions — same struct, different dimension-ID field used.
MVP Definition
Launch With (v1)
The minimum that makes both service functions useful and correct.
ProfitLossFilterstruct — optional asset types, optional user/activity IDs, optional time rangeQueryUserProfitLoss(db, filter) (ProfitLossResult, error)— aggregates across specified user IDsQueryActivityProfitLoss(db, filter) (ProfitLossResult, error)— aggregates for a single activity IDProfitLossResultstruct — total revenue, total cost, profit, profit_rate, plus[]AssetBreakdown- Canonical
AssetTypeconstants: Points, Coupon, ItemCard, PhysicalGood, Fragment - Revenue calculation reusing
finance.ClassifyOrderSpending(existing) - Prize cost calculation reusing
finance.ComputePrizeCostWithMultiplier(existing) - Refund (status 3/4) and voided inventory exclusion
- Time-range filter applied consistently to both orders and inventory tables
- Unit tests covering: normal order, game-pass order, mixed, empty result, nil filter
Add After Validation (v1.x)
- Per-asset-type breakdown populated (requires extending SQL GROUP BY or running separate legs per type)
- Trigger: ops team requests drill-down beyond total numbers
- Fragment asset type cost integration via
fragment_synthesis_logs- Trigger: fragment economy becomes significant in platform revenue reports
- Batch activity IDs support (
[]int64activity IDs, not just one)- Trigger: ops needs cross-activity comparison in a single call
Future Consideration (v2+)
- Caching wrapper (Redis TTL-based) around the query functions
- Defer: not needed until query latency becomes user-visible (>2s)
- Incremental / time-bucketed aggregation (daily snapshots stored in a stats table)
- Defer: requires schema additions and migration planning
- Douyin (livestream) order integration into the user-dimension function
- Defer: currently only in the HTTP-layer spending leaderboard; integrating it requires joining
douyin_orderswhich adds complexity and is outside the 5 declared asset types
- Defer: currently only in the HTTP-layer spending leaderboard; integrating it requires joining
Feature Prioritization Matrix
| Feature | Operator Value | Implementation Cost | Priority |
|---|---|---|---|
Revenue calculation (reuse existing finance.*) |
HIGH | LOW | P1 |
| Game-pass classification (reuse existing) | HIGH | LOW | P1 |
| Prize cost with multiplier (reuse existing) | HIGH | LOW | P1 |
| Refund/void exclusion | HIGH | LOW | P1 |
| Time-range filter | HIGH | LOW | P1 |
| User-dimension aggregation | HIGH | MEDIUM | P1 |
| Activity-dimension aggregation | HIGH | MEDIUM | P1 |
ProfitLossFilter composable struct |
HIGH | LOW | P1 |
ProfitLossResult with Summary + Breakdown |
HIGH | LOW | P1 |
Canonical AssetType enum |
MEDIUM | LOW | P1 |
| Multi-user batch ([]int64) | MEDIUM | LOW | P1 |
| Per-asset-type breakdown (5 types) | MEDIUM | HIGH | P2 |
| Fragment synthesis cost integration | LOW | MEDIUM | P2 |
| Batch activity IDs support | LOW | LOW | P2 |
| Read-only DB routing enforcement | HIGH | LOW | P1 (design constraint, not optional) |
Priority key:
- P1: Must have for launch — without these the functions are not useful or correct
- P2: Should have — adds analytical depth, add when P1 is proven
- P3: Nice to have — future milestone
Competitor Feature Analysis
This is an internal platform analytics function, not a user-facing product. The relevant "competition" is the existing Dashboard code that this service layer must be consistent with and eventually replace as the canonical source of truth.
| Feature | Existing Dashboard (DashboardActivityProfitLoss) | Existing Dashboard (GetUserSpendingDashboard) | New Service Functions |
|---|---|---|---|
| Reusability | None — HTTP handler only | None — HTTP handler only | Core goal: callable from anywhere |
| Multi-user support | No — activity-scoped | No — single user ID only | Yes — []int64 user IDs |
| Asset-type breakdown | Implicit (physical goods via inventory) | Implicit | Explicit enum + breakdown slice |
| Time-range | Not supported | Supported | Supported (optional) |
| Spending classification | Inline SQL CASE | Inline SQL CASE | Calls finance.ClassifyOrderSpending |
| Douyin/livestream | Not included | Included (separate leg) | Out of scope for v1 |
| Calculation consistency | Source of truth today | Source of truth today | Must match exactly |
| Fragment asset type | Not supported | Not supported | Enum defined; cost TBD in v1.x |
Sources
- Direct analysis of
/internal/service/finance/profit_metrics.go— existing shared primitives - Direct analysis of
/internal/api/admin/dashboard_activity.go— activity P&L implementation - Direct analysis of
/internal/api/admin/dashboard_spending.go— user spending leaderboard - Direct analysis of
/internal/api/admin/dashboard_user_spending.go— per-user spending drill-down - Direct analysis of GORM models:
orders,user_inventory,user_points_ledger,user_coupon_ledger,fragment_synthesis_logs - PROJECT.md requirements (validated requirements section)
Feature research for: Bindbox Game profit/loss analytics service layer Researched: 2026-03-21