195 lines
14 KiB
Markdown
195 lines
14 KiB
Markdown
# 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*
|