4 plans across 3 waves: - 01-01 (wave 1): package scaffold — types.go, service.go, service_test.go - 01-02 (wave 2): QueryUserProfitLoss — query_user.go + user integration tests - 01-03 (wave 2, parallel): QueryActivityProfitLoss — query_activity.go + activity tests - 01-04 (wave 3): phase verification — static checks + full test suite gate Covers all 20 Phase 1 requirements: PNL-01..08, DIM-01..04, RET-01/03, AST-01, QUA-01..05.
489 lines
20 KiB
Markdown
489 lines
20 KiB
Markdown
---
|
|
phase: 01-core-pnl-functions
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- internal/service/finance/types.go
|
|
- internal/service/finance/service.go
|
|
- internal/service/finance/service_test.go
|
|
autonomous: true
|
|
requirements:
|
|
- PNL-01
|
|
- RET-01
|
|
- RET-03
|
|
- AST-01
|
|
- DIM-01
|
|
- DIM-02
|
|
- DIM-03
|
|
- DIM-04
|
|
- QUA-01
|
|
- QUA-02
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Package internal/service/finance compiles successfully with the new files"
|
|
- "AssetType constants All=0, Points=1, Coupon=2, ItemCard=3, Product=4, Fragment=5 are exported"
|
|
- "UserProfitLossParams and ActivityProfitLossParams structs exist with all optional fields"
|
|
- "ProfitLossResult struct has int64 TotalRevenue/TotalCost/TotalProfit and float64 ProfitRate"
|
|
- "Service interface exposes QueryUserProfitLoss and QueryActivityProfitLoss methods"
|
|
- "New() constructor injects only DbR — no GetDbW() call anywhere in the package"
|
|
- "service_test.go contains SQLite test setup that compiles and all existing tests pass"
|
|
artifacts:
|
|
- path: "internal/service/finance/types.go"
|
|
provides: "AssetType enum, UserProfitLossParams, ActivityProfitLossParams, ProfitLossDetail, ProfitLossResult"
|
|
exports:
|
|
- AssetType
|
|
- AssetTypeAll
|
|
- AssetTypePoints
|
|
- AssetTypeCoupon
|
|
- AssetTypeItemCard
|
|
- AssetTypeProduct
|
|
- AssetTypeFragment
|
|
- UserProfitLossParams
|
|
- ActivityProfitLossParams
|
|
- ProfitLossDetail
|
|
- ProfitLossResult
|
|
- path: "internal/service/finance/service.go"
|
|
provides: "Service interface + New() constructor"
|
|
exports:
|
|
- Service
|
|
- New
|
|
- path: "internal/service/finance/service_test.go"
|
|
provides: "Test helper newTestSvc() and seed helpers for orders/inventory/points/coupons"
|
|
key_links:
|
|
- from: "internal/service/finance/service.go"
|
|
to: "internal/repository/mysql/mysql.go"
|
|
via: "New(l logger.CustomLogger, db mysql.Repo) — calls db.GetDbR() only"
|
|
pattern: "GetDbR\\(\\)"
|
|
- from: "internal/service/finance/types.go"
|
|
to: "internal/service/finance/query_user.go (Plan 02)"
|
|
via: "UserProfitLossParams consumed by QueryUserProfitLoss"
|
|
pattern: "UserProfitLossParams"
|
|
---
|
|
|
|
<objective>
|
|
Scaffold the internal/service/finance package with all shared contracts: AssetType enum, parameter structs, result types, the Service interface, and the read-only constructor. Also create the service_test.go file with SQLite test infrastructure that Plans 02 and 03 will extend.
|
|
|
|
Purpose: Plans 02 and 03 run in parallel and both depend on these type definitions. Creating them first eliminates any ambiguity about field names, types, and the constructor signature.
|
|
|
|
Output: types.go, service.go, service_test.go — all compiling, all tested.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@~/.claude/get-shit-done/workflows/execute-plan.md
|
|
@~/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/phases/01-core-pnl-functions/1-CONTEXT.md
|
|
@.planning/phases/01-core-pnl-functions/01-RESEARCH.md
|
|
</context>
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
|
|
|
From internal/repository/mysql/mysql.go:
|
|
```go
|
|
type Repo interface {
|
|
i()
|
|
GetDbR() *gorm.DB
|
|
GetDbW() *gorm.DB
|
|
DbRClose() error
|
|
DbWClose() error
|
|
}
|
|
|
|
func NewSQLiteRepoForTest() (Repo, error) // in testrepo_sqlite.go
|
|
```
|
|
|
|
From internal/pkg/logger/logger.go:
|
|
```go
|
|
type CustomLogger interface { /* zap-based */ }
|
|
func NewCustomLogger(w io.Writer, opts ...Option) CustomLogger
|
|
func WithOutputInConsole() Option
|
|
```
|
|
|
|
From internal/service/finance/profit_metrics.go (EXISTING — must not be redefined):
|
|
```go
|
|
type SpendingBreakdown struct { PaidCoupon, GamePass, Total int64; IsGamePass bool }
|
|
func ClassifyOrderSpending(sourceType int32, orderNo string, actualAmount, discountAmount int64, remark string, gamePassValue int64) SpendingBreakdown
|
|
func IsGamePassOrder(sourceType int32, orderNo string, actualAmount int64, remark string) bool
|
|
func ComputeGamePassValue(drawCount, activityPrice int64) int64
|
|
func NormalizeMultiplierX1000(multiplierX1000 int64) int64
|
|
func ComputePrizeCostWithMultiplier(baseCost, multiplierX1000 int64) int64
|
|
func ComputeProfit(spending, prizeCost int64) (int64, float64)
|
|
```
|
|
|
|
From internal/repository/mysql/model/user_inventory.gen.go:
|
|
```go
|
|
const TableNameUserInventory = "user_inventory"
|
|
// Fields used: user_id, activity_id, order_id, value_cents, status, remark, reward_id, product_id
|
|
```
|
|
|
|
From internal/repository/mysql/model/user_points_ledger.gen.go:
|
|
```go
|
|
const TableNameUserPointsLedger = "user_points_ledger"
|
|
// Fields used: user_id, action, points, ref_table, ref_id
|
|
```
|
|
|
|
From internal/repository/mysql/model/user_coupon_ledger.gen.go:
|
|
```go
|
|
const TableNameUserCouponLedger = "user_coupon_ledger"
|
|
// Fields used: user_id, change_amount, order_id, action
|
|
```
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Create types.go — AssetType enum and all struct contracts</name>
|
|
<read_first>
|
|
- internal/service/finance/profit_metrics.go (verify SpendingBreakdown is not redefined here)
|
|
- internal/repository/mysql/model/user_inventory.gen.go (confirm value_cents field name)
|
|
- .planning/phases/01-core-pnl-functions/1-CONTEXT.md (locked decisions D-04 through D-11)
|
|
</read_first>
|
|
<files>internal/service/finance/types.go</files>
|
|
<behavior>
|
|
- AssetTypeAll = 0, AssetTypePoints = 1, AssetTypeCoupon = 2, AssetTypeItemCard = 3, AssetTypeProduct = 4, AssetTypeFragment = 5
|
|
- UserProfitLossParams has: UserIDs []int64, AssetType AssetType, StartTime *time.Time, EndTime *time.Time
|
|
- ActivityProfitLossParams has: ActivityIDs []int64, AssetType AssetType, StartTime *time.Time, EndTime *time.Time
|
|
- ProfitLossDetail has: UserID int64, ActivityID int64, Revenue int64, Cost int64, Profit int64, ProfitRate float64
|
|
- ProfitLossResult has: TotalRevenue int64, TotalCost int64, TotalProfit int64, ProfitRate float64, Details []ProfitLossDetail, Breakdown []interface{}
|
|
- All monetary fields are int64 (fen); only ProfitRate and ProfitLossDetail.ProfitRate use float64
|
|
- Breakdown is []interface{} initialized as empty slice (Phase 2 placeholder per CONTEXT.md deferred section)
|
|
</behavior>
|
|
<action>
|
|
Create `internal/service/finance/types.go` with package `finance`. Import only `"time"`.
|
|
|
|
Define the AssetType and constants block:
|
|
```go
|
|
type AssetType int
|
|
|
|
const (
|
|
AssetTypeAll AssetType = 0 // zero value = all types (DIM-04)
|
|
AssetTypePoints AssetType = 1
|
|
AssetTypeCoupon AssetType = 2
|
|
AssetTypeItemCard AssetType = 3
|
|
AssetTypeProduct AssetType = 4
|
|
AssetTypeFragment AssetType = 5
|
|
)
|
|
```
|
|
|
|
Define param structs (per D-04 — two independent structs, not shared):
|
|
```go
|
|
// UserProfitLossParams — all fields optional (D-07)
|
|
type UserProfitLossParams struct {
|
|
UserIDs []int64 // empty = all users (DIM-01)
|
|
AssetType AssetType // 0 = all types (DIM-04)
|
|
StartTime *time.Time // nil = no lower bound (DIM-03)
|
|
EndTime *time.Time // nil = no upper bound (DIM-03)
|
|
}
|
|
|
|
// ActivityProfitLossParams — all fields optional (D-07)
|
|
type ActivityProfitLossParams struct {
|
|
ActivityIDs []int64 // empty = all activities (DIM-02)
|
|
AssetType AssetType // 0 = all types (DIM-04)
|
|
StartTime *time.Time // nil = no lower bound (DIM-03)
|
|
EndTime *time.Time // nil = no upper bound (DIM-03)
|
|
}
|
|
```
|
|
|
|
Define result structs (per D-05, D-06, RET-01, RET-03):
|
|
```go
|
|
// ProfitLossDetail — per-user or per-activity row (D-06)
|
|
type ProfitLossDetail struct {
|
|
UserID int64 // populated for user dimension queries
|
|
ActivityID int64 // populated for activity dimension queries
|
|
Revenue int64 // fen (RET-03: int64 only, no float64 for monetary)
|
|
Cost int64 // fen
|
|
Profit int64 // fen
|
|
ProfitRate float64 // ratio; only float64 field for monetary concept
|
|
}
|
|
|
|
// ProfitLossResult — aggregated P&L result (RET-01)
|
|
type ProfitLossResult struct {
|
|
TotalRevenue int64 // fen
|
|
TotalCost int64 // fen
|
|
TotalProfit int64 // fen
|
|
ProfitRate float64 // ratio
|
|
Details []ProfitLossDetail // per-user or per-activity breakdowns (D-06)
|
|
Breakdown []interface{} // Phase 2: per-asset-type breakdown (empty for Phase 1)
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>go build ./internal/service/finance/ 2>&1 | grep -v "^$" || echo "BUILD OK"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- internal/service/finance/types.go exists
|
|
- File contains `type AssetType int`
|
|
- File contains `AssetTypeAll AssetType = 0`
|
|
- File contains `AssetTypeFragment AssetType = 5`
|
|
- File contains `type UserProfitLossParams struct`
|
|
- File contains `type ActivityProfitLossParams struct`
|
|
- File contains `UserIDs []int64` inside UserProfitLossParams
|
|
- File contains `ActivityIDs []int64` inside ActivityProfitLossParams
|
|
- File contains `StartTime *time.Time` and `EndTime *time.Time` (pointer, not value)
|
|
- File contains `type ProfitLossResult struct`
|
|
- File contains `TotalRevenue int64`
|
|
- File contains `TotalCost int64`
|
|
- File contains `TotalProfit int64`
|
|
- File contains `ProfitRate float64`
|
|
- File contains `Details []ProfitLossDetail`
|
|
- File contains `Breakdown []interface{}`
|
|
- File contains `type ProfitLossDetail struct`
|
|
- File contains `Revenue int64` (not float64)
|
|
- File contains `Cost int64` (not float64)
|
|
- `go build ./internal/service/finance/` exits 0
|
|
</acceptance_criteria>
|
|
<done>types.go exists in internal/service/finance/, all types exported with correct field names and types, package builds without errors.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Create service.go — Service interface and read-only constructor</name>
|
|
<read_first>
|
|
- internal/service/finance/types.go (just created — verify param/result type names)
|
|
- internal/service/user/user.go (lines 93-102 — constructor pattern to replicate)
|
|
- internal/repository/mysql/mysql.go (Repo interface — confirm GetDbR() signature)
|
|
- .planning/phases/01-core-pnl-functions/1-CONTEXT.md (QUA-02: no GetDbW() in this package)
|
|
</read_first>
|
|
<files>internal/service/finance/service.go</files>
|
|
<behavior>
|
|
- Service interface declares exactly two methods: QueryUserProfitLoss and QueryActivityProfitLoss
|
|
- QueryUserProfitLoss signature: (ctx context.Context, params UserProfitLossParams) (*ProfitLossResult, error)
|
|
- QueryActivityProfitLoss signature: (ctx context.Context, params ActivityProfitLossParams) (*ProfitLossResult, error)
|
|
- service struct has logger field and dbR *gorm.DB — NO writeDB or GetDbW() call
|
|
- New() calls db.GetDbR() to populate dbR; no reference to GetDbW() anywhere in file
|
|
- Stub implementations return (nil, nil) — they will be replaced in Plans 02 and 03
|
|
</behavior>
|
|
<action>
|
|
Create `internal/service/finance/service.go` with package `finance`.
|
|
|
|
Imports:
|
|
```go
|
|
import (
|
|
"context"
|
|
|
|
"bindbox-game/internal/pkg/logger"
|
|
"bindbox-game/internal/repository/mysql"
|
|
"gorm.io/gorm"
|
|
)
|
|
```
|
|
|
|
Define Service interface and struct:
|
|
```go
|
|
type Service interface {
|
|
QueryUserProfitLoss(ctx context.Context, params UserProfitLossParams) (*ProfitLossResult, error)
|
|
QueryActivityProfitLoss(ctx context.Context, params ActivityProfitLossParams) (*ProfitLossResult, error)
|
|
}
|
|
|
|
type service struct {
|
|
logger logger.CustomLogger
|
|
dbR *gorm.DB // read replica only — QUA-02: no writes in this package
|
|
}
|
|
|
|
func New(l logger.CustomLogger, db mysql.Repo) Service {
|
|
return &service{
|
|
logger: l,
|
|
dbR: db.GetDbR(),
|
|
}
|
|
}
|
|
```
|
|
|
|
Add stub method bodies (Plans 02 and 03 will replace these):
|
|
```go
|
|
func (s *service) QueryUserProfitLoss(ctx context.Context, params UserProfitLossParams) (*ProfitLossResult, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *service) QueryActivityProfitLoss(ctx context.Context, params ActivityProfitLossParams) (*ProfitLossResult, error) {
|
|
return nil, nil
|
|
}
|
|
```
|
|
|
|
CRITICAL: Do NOT write `db.GetDbW()` or `GetDbW` anywhere in this file or any other file in the package.
|
|
</action>
|
|
<verify>
|
|
<automated>go build ./internal/service/finance/ && grep -r "GetDbW" ./internal/service/finance/ | wc -l | xargs test 0 -eq && echo "QUA-02 OK: no GetDbW in package"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- internal/service/finance/service.go exists
|
|
- File contains `type Service interface`
|
|
- File contains `QueryUserProfitLoss(ctx context.Context, params UserProfitLossParams) (*ProfitLossResult, error)`
|
|
- File contains `QueryActivityProfitLoss(ctx context.Context, params ActivityProfitLossParams) (*ProfitLossResult, error)`
|
|
- File contains `type service struct`
|
|
- File contains `dbR *gorm.DB`
|
|
- File does NOT contain `GetDbW`
|
|
- File contains `db.GetDbR()`
|
|
- File contains `func New(l logger.CustomLogger, db mysql.Repo) Service`
|
|
- `go build ./internal/service/finance/` exits 0
|
|
- `grep -r "GetDbW" ./internal/service/finance/` returns empty output (zero matches)
|
|
</acceptance_criteria>
|
|
<done>service.go compiles, Service interface declared with both method signatures, constructor injects GetDbR() only, no GetDbW() anywhere in the finance package.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 3: Create service_test.go — SQLite test infrastructure and contract tests</name>
|
|
<read_first>
|
|
- internal/service/finance/service.go (just created — verify New() signature)
|
|
- internal/service/finance/types.go (verify AssetType constant values and struct field names)
|
|
- internal/repository/mysql/testrepo_sqlite.go (NewSQLiteRepoForTest() — verify signature)
|
|
- internal/service/finance/profit_metrics_test.go (existing test style to replicate)
|
|
- .planning/phases/01-core-pnl-functions/01-RESEARCH.md (Pitfall 6: SQLite compat — CAST AS INTEGER not SIGNED)
|
|
</read_first>
|
|
<files>internal/service/finance/service_test.go</files>
|
|
<behavior>
|
|
- newTestSvc() helper creates SQLiteRepo and returns (Service, *gorm.DB, error)
|
|
- seedOrder() helper inserts a model.Orders row into the test DB
|
|
- seedInventory() helper inserts a model.UserInventory row
|
|
- seedPointsLedger() helper inserts a model.UserPointsLedger row
|
|
- seedCouponLedger() helper inserts a model.UserCouponLedger row
|
|
- TestAssetTypeConstants verifies All=0, Points=1, Coupon=2, ItemCard=3, Product=4, Fragment=5
|
|
- TestNew_ReturnsService verifies New() returns a non-nil Service
|
|
- TestQueryUserProfitLoss_EmptyParams_ReturnsNoError verifies stub returns (nil, nil) — will be updated in Plan 02
|
|
- TestQueryActivityProfitLoss_EmptyParams_ReturnsNoError same for activity function
|
|
- AutoMigrate runs for Orders, UserInventory, UserPointsLedger, UserCouponLedger tables
|
|
</behavior>
|
|
<action>
|
|
Create `internal/service/finance/service_test.go` with package `finance`.
|
|
|
|
Imports needed:
|
|
```go
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"bindbox-game/internal/pkg/logger"
|
|
"bindbox-game/internal/repository/mysql"
|
|
"bindbox-game/internal/repository/mysql/model"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
)
|
|
```
|
|
|
|
Test helper — newTestSvc creates an in-memory SQLite repo, auto-migrates tables, returns (Service, *gorm.DB):
|
|
```go
|
|
func newTestSvc(t *testing.T) (Service, *gorm.DB) {
|
|
t.Helper()
|
|
repo, err := mysql.NewSQLiteRepoForTest()
|
|
require.NoError(t, err)
|
|
db := repo.GetDbR()
|
|
err = db.AutoMigrate(
|
|
&model.Orders{},
|
|
&model.UserInventory{},
|
|
&model.UserPointsLedger{},
|
|
&model.UserCouponLedger{},
|
|
)
|
|
require.NoError(t, err)
|
|
svc := New(logger.NewCustomLogger(nil, logger.WithOutputInConsole()), repo)
|
|
return svc, db
|
|
}
|
|
```
|
|
|
|
Seed helpers (minimal fields — add more fields in Plans 02/03 tests as needed):
|
|
```go
|
|
func seedOrder(t *testing.T, db *gorm.DB, o model.Orders) {
|
|
t.Helper()
|
|
require.NoError(t, db.Create(&o).Error)
|
|
}
|
|
|
|
func seedInventory(t *testing.T, db *gorm.DB, inv model.UserInventory) {
|
|
t.Helper()
|
|
require.NoError(t, db.Create(&inv).Error)
|
|
}
|
|
|
|
func seedPointsLedger(t *testing.T, db *gorm.DB, row model.UserPointsLedger) {
|
|
t.Helper()
|
|
require.NoError(t, db.Create(&row).Error)
|
|
}
|
|
|
|
func seedCouponLedger(t *testing.T, db *gorm.DB, row model.UserCouponLedger) {
|
|
t.Helper()
|
|
require.NoError(t, db.Create(&row).Error)
|
|
}
|
|
```
|
|
|
|
Tests:
|
|
```go
|
|
func TestAssetTypeConstants(t *testing.T) {
|
|
require.Equal(t, AssetType(0), AssetTypeAll)
|
|
require.Equal(t, AssetType(1), AssetTypePoints)
|
|
require.Equal(t, AssetType(2), AssetTypeCoupon)
|
|
require.Equal(t, AssetType(3), AssetTypeItemCard)
|
|
require.Equal(t, AssetType(4), AssetTypeProduct)
|
|
require.Equal(t, AssetType(5), AssetTypeFragment)
|
|
}
|
|
|
|
func TestNew_ReturnsService(t *testing.T) {
|
|
svc, _ := newTestSvc(t)
|
|
require.NotNil(t, svc)
|
|
}
|
|
|
|
func TestQueryUserProfitLoss_EmptyParams_ReturnsNoError(t *testing.T) {
|
|
svc, _ := newTestSvc(t)
|
|
result, err := svc.QueryUserProfitLoss(context.Background(), UserProfitLossParams{})
|
|
require.NoError(t, err)
|
|
_ = result // stub returns nil — Plan 02 will make this return real data
|
|
}
|
|
|
|
func TestQueryActivityProfitLoss_EmptyParams_ReturnsNoError(t *testing.T) {
|
|
svc, _ := newTestSvc(t)
|
|
result, err := svc.QueryActivityProfitLoss(context.Background(), ActivityProfitLossParams{})
|
|
require.NoError(t, err)
|
|
_ = result // stub returns nil — Plan 03 will make this return real data
|
|
}
|
|
```
|
|
|
|
NOTE on SQLite compatibility (Pitfall 6 from RESEARCH.md):
|
|
- Do NOT use CAST(... AS SIGNED) in test SQL — SQLite requires CAST(... AS INTEGER)
|
|
- Do NOT use GREATEST() in SQL for tests — apply multiplier logic in Go instead
|
|
- Tests here are unit/compile-time tests only; integration tests added in Plans 02 and 03
|
|
</action>
|
|
<verify>
|
|
<automated>go test -v -run "TestAssetType|TestNew|TestQuery.*EmptyParams" ./internal/service/finance/</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- internal/service/finance/service_test.go exists
|
|
- File contains `func newTestSvc(t *testing.T) (Service, *gorm.DB)`
|
|
- File contains `func seedOrder(`
|
|
- File contains `func seedInventory(`
|
|
- File contains `func seedPointsLedger(`
|
|
- File contains `func seedCouponLedger(`
|
|
- File contains `func TestAssetTypeConstants(`
|
|
- File contains `mysql.NewSQLiteRepoForTest()`
|
|
- File contains `db.AutoMigrate`
|
|
- `go test -v -run "TestAssetType|TestNew|TestQuery.*EmptyParams" ./internal/service/finance/` exits 0
|
|
- All 4 tests pass: TestAssetTypeConstants, TestNew_ReturnsService, TestQueryUserProfitLoss_EmptyParams_ReturnsNoError, TestQueryActivityProfitLoss_EmptyParams_ReturnsNoError
|
|
- `go test -v ./internal/service/finance/` exits 0 (all existing profit_metrics tests still pass)
|
|
</acceptance_criteria>
|
|
<done>service_test.go compiles and all tests pass including the existing profit_metrics tests. The newTestSvc and seed helpers are ready for Plans 02 and 03 to extend.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
After all tasks complete:
|
|
|
|
1. Package compiles: `go build ./internal/service/finance/` exits 0
|
|
2. All tests green: `go test -v ./internal/service/finance/` — must show PASS for all tests including existing profit_metrics tests
|
|
3. No write DB leak: `grep -r "GetDbW" ./internal/service/finance/` returns 0 matches
|
|
4. AssetType values correct: `grep -A8 "AssetTypeAll" internal/service/finance/types.go` shows All=0 through Fragment=5
|
|
5. Pointer time fields: `grep "StartTime\|EndTime" internal/service/finance/types.go | grep "\*time.Time"` returns 2 matches
|
|
6. int64 monetary fields only: `grep "Revenue\|Cost\|Profit\b" internal/service/finance/types.go | grep "float64"` returns 0 matches (ProfitRate is the only float64)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- internal/service/finance/types.go: 6 AssetType constants + 2 param structs + 2 result structs, all exported
|
|
- internal/service/finance/service.go: Service interface with 2 methods, read-only constructor, zero GetDbW() references
|
|
- internal/service/finance/service_test.go: SQLite setup helper + 4 seed helpers + 4 tests all passing
|
|
- `go test -v ./internal/service/finance/` exits 0 with PASS for all tests
|
|
- Full build clean: `go build ./...` exits 0
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/01-core-pnl-functions/01-01-SUMMARY.md`
|
|
</output>
|