package handler import ( "context" "strings" "testing" logmodel "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/lottery" "gorm.io/gorm" ) func TestCommission_RequiresIdempotencyKey(t *testing.T) { db, _, cleanup := newHandlerTestDB(t) defer cleanup() h := NewCommissionHandler(CommissionDeps{ Ledger: &fakeLedger{}, UpdateCommission: func(context.Context, int64, int64, ...*gorm.DB) error { t.Fatal("must NOT call UpdateCommission without idempotency key") return nil }, WriteCommissionLog: func(*gorm.DB, int64, uint16, int64, string) error { return nil }, }) _, err := h.Dispatch(context.Background(), db, lottery.DispatchRequest{}) if err == nil || !strings.Contains(err.Error(), "IdempotencyKey") { t.Fatalf("expected IdempotencyKey error, got %v", err) } } func TestCommission_RequiresTx(t *testing.T) { h := NewCommissionHandler(CommissionDeps{Ledger: &fakeLedger{}}) _, err := h.Dispatch(context.Background(), nil, lottery.DispatchRequest{IdempotencyKey: "k"}) if err == nil || !strings.Contains(err.Error(), "transaction") { t.Fatalf("expected tx error, got %v", err) } } func TestCommission_IdempotentHitDoesNotWrite(t *testing.T) { db, _, cleanup := newHandlerTestDB(t) defer cleanup() ledger := &fakeLedger{ reserveFn: func(_ context.Context, _ *gorm.DB, _ lottery.GrantLedger) (*lottery.GrantLedger, bool, error) { return &lottery.GrantLedger{ Payload: `{"message":"佣金已到账 300"}`, }, true, nil }, } h := NewCommissionHandler(CommissionDeps{ Ledger: ledger, UpdateCommission: func(context.Context, int64, int64, ...*gorm.DB) error { t.Fatal("must NOT UpdateCommission on idempotent hit") return nil }, WriteCommissionLog: func(*gorm.DB, int64, uint16, int64, string) error { t.Fatal("must NOT WriteCommissionLog on idempotent hit") return nil }, }) res, err := h.Dispatch(context.Background(), db, lottery.DispatchRequest{ UserId: 42, Prize: lottery.Prize{Config: `{"amount_cents":300}`}, IdempotencyKey: "lottery:100:200", }) if err != nil { t.Fatalf("Dispatch: %v", err) } if res.Message != "佣金已到账 300" { t.Fatalf("expected replay message, got %q", res.Message) } } func TestCommission_FirstTimeWritesCommissionAndLog(t *testing.T) { db, mock, cleanup := newHandlerTestDB(t) defer cleanup() ledger := &fakeLedger{ reserveFn: func(_ context.Context, _ *gorm.DB, _ lottery.GrantLedger) (*lottery.GrantLedger, bool, error) { return &lottery.GrantLedger{Id: 11}, false, nil }, } var ( updateCommissionCalled = false writeLogCalled = false writeLogType uint16 writeLogAmount int64 writeLogOrderNo string ) h := NewCommissionHandler(CommissionDeps{ Ledger: ledger, UpdateCommission: func(_ context.Context, uid, amount int64, _ ...*gorm.DB) error { updateCommissionCalled = true if uid != 42 || amount != 300 { t.Fatalf("UpdateCommission got (uid=%d, amount=%d)", uid, amount) } return nil }, WriteCommissionLog: func(_ *gorm.DB, objectID int64, logType uint16, amount int64, orderNo string) error { writeLogCalled = true writeLogType = logType writeLogAmount = amount writeLogOrderNo = orderNo if objectID != 42 { t.Fatalf("WriteCommissionLog objectID=%d, want 42", objectID) } return nil }, }) mock.ExpectExec("UPDATE `lottery_grant_ledger`"). WillReturnResult(sqlmockAnyResult()) _, err := h.Dispatch(context.Background(), db, lottery.DispatchRequest{ UserId: 42, ActivityId: 100, DrawId: 200, Prize: lottery.Prize{Config: `{"amount_cents":300}`}, IdempotencyKey: "lottery:100:200", }) if err != nil { t.Fatalf("Dispatch: %v", err) } if !updateCommissionCalled || !writeLogCalled { t.Fatalf("expected both UpdateCommission and WriteCommissionLog to be called (uc=%v wl=%v)", updateCommissionCalled, writeLogCalled) } if writeLogType != logmodel.CommissionTypeLottery { t.Fatalf("expected CommissionTypeLottery(%d), got %d", logmodel.CommissionTypeLottery, writeLogType) } if writeLogAmount != 300 { t.Fatalf("expected amount 300, got %d", writeLogAmount) } if writeLogOrderNo != "lottery:100:200" { t.Fatalf("expected orderNo to reuse ExternalRef, got %q", writeLogOrderNo) } } func TestCommission_BadConfigRejected(t *testing.T) { db, _, cleanup := newHandlerTestDB(t) defer cleanup() h := NewCommissionHandler(CommissionDeps{ Ledger: &fakeLedger{}, UpdateCommission: func(context.Context, int64, int64, ...*gorm.DB) error { return nil }, WriteCommissionLog: func(*gorm.DB, int64, uint16, int64, string) error { return nil }, }) tests := []struct { name string config string }{ {name: "invalid json", config: `{bad`}, {name: "zero amount", config: `{"amount_cents":0}`}, {name: "negative amount", config: `{"amount_cents":-1}`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := h.Dispatch(context.Background(), db, lottery.DispatchRequest{ Prize: lottery.Prize{Config: tt.config}, IdempotencyKey: "k", }) if err == nil { t.Fatalf("expected error on %s", tt.name) } }) } } func TestCommission_MissingDepsFailsFast(t *testing.T) { db, _, cleanup := newHandlerTestDB(t) defer cleanup() h := NewCommissionHandler(CommissionDeps{ Ledger: &fakeLedger{}, WriteCommissionLog: func(*gorm.DB, int64, uint16, int64, string) error { return nil }, }) _, err := h.Dispatch(context.Background(), db, lottery.DispatchRequest{ IdempotencyKey: "k", Prize: lottery.Prize{Config: `{"amount_cents":1}`}, }) if err == nil { t.Fatalf("expected error when deps missing") } }