# 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 `ProfitLossFilter` struct 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. - [x] `ProfitLossFilter` struct — optional asset types, optional user/activity IDs, optional time range - [x] `QueryUserProfitLoss(db, filter) (ProfitLossResult, error)` — aggregates across specified user IDs - [x] `QueryActivityProfitLoss(db, filter) (ProfitLossResult, error)` — aggregates for a single activity ID - [x] `ProfitLossResult` struct — total revenue, total cost, profit, profit_rate, plus `[]AssetBreakdown` - [x] Canonical `AssetType` constants: Points, Coupon, ItemCard, PhysicalGood, Fragment - [x] Revenue calculation reusing `finance.ClassifyOrderSpending` (existing) - [x] Prize cost calculation reusing `finance.ComputePrizeCostWithMultiplier` (existing) - [x] Refund (status 3/4) and voided inventory exclusion - [x] Time-range filter applied consistently to both orders and inventory tables - [x] 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 (`[]int64` activity 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_orders` which adds complexity and is outside the 5 declared asset types --- ## 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*