package usecase import ( "context" "errors" "testing" "apps/backend/internal/module/growth/domain" "apps/backend/internal/module/growth/repository" "github.com/stretchr/testify/require" ) func TestOC_01_CreateOnPublished(t *testing.T) { svc := New(repository.NewMemory()) const originalSentAt int64 = 1_700_000_000_000_000_000 e, err := svc.RecordPublished(context.Background(), 1001, domain.SourceScoutOutreach, "post1", "acc1", originalSentAt) require.NoError(t, err) require.Equal(t, domain.StatusObserving, e.Status) require.Equal(t, domain.KindReach, e.Kind) // idempotent e2, err := svc.RecordPublished(context.Background(), 1001, domain.SourceScoutOutreach, "post1", "acc1", originalSentAt+1_000_000) require.NoError(t, err) require.Equal(t, e.ID, e2.ID) require.Equal(t, originalSentAt, e2.SentAt) } func TestOC_02_03_ApplySignals(t *testing.T) { svc := New(repository.NewMemory()) e, _ := svc.RecordPublished(context.Background(), 1, domain.SourceScoutOutreach, "p", "a", 0) e, err := svc.ApplyObserveSignals(context.Background(), e.ID, 1, 0, 0) require.NoError(t, err) require.Equal(t, domain.ConfidenceConfirmed, e.Confidence) require.Equal(t, domain.KindReply, e.Kind) e3, _ := svc.RecordPublished(context.Background(), 1, domain.SourceScoutOutreach, "p2", "a", 0) e3, err = svc.ApplyObserveSignals(context.Background(), e3.ID, 0, 0, 2) require.NoError(t, err) require.Equal(t, domain.ConfidencePossible, e3.Confidence) sum, err := svc.Summary(context.Background(), 1, 0, 0) require.NoError(t, err) require.Equal(t, 2, sum.FollowsPossible) } func TestOC_04_05_Conversion(t *testing.T) { svc := New(repository.NewMemory()) e, _ := svc.RecordPublished(context.Background(), 1, domain.SourceScoutOutreach, "p", "a", 0) e, err := svc.ReportConversion(context.Background(), 1, e.ID, 1000, "成交", "TWD") require.NoError(t, err) require.True(t, e.HasConversion) require.Equal(t, domain.KindConversion, e.Kind) sum, err := svc.Summary(context.Background(), 1, 0, 0) require.NoError(t, err) require.Equal(t, 1, sum.Conversions) require.Equal(t, 1000.0, sum.ConversionAmount) require.NoError(t, svc.DeleteConversion(context.Background(), 1, e.ID)) sum, _ = svc.Summary(context.Background(), 1, 0, 0) require.Equal(t, 0, sum.Conversions) } func TestOC_08_DegradedStillWorks(t *testing.T) { svc := New(repository.NewMemory()) require.Equal(t, "degraded", svc.SignalProbe) e, err := svc.RecordPublished(context.Background(), 1, domain.SourceScoutOutreach, "p", "", 0) require.NoError(t, err) _, err = svc.ReportConversion(context.Background(), 1, e.ID, 100, "", "") require.NoError(t, err) sum, err := svc.Summary(context.Background(), 1, 0, 0) require.NoError(t, err) require.Equal(t, 1, sum.Reach) } func TestCU_01_02_03_GenerateCheckup(t *testing.T) { svc := New(repository.NewMemory()) _, _ = svc.RecordPublished(context.Background(), 9, domain.SourceScoutOutreach, "x", "a", 0) c, err := svc.GenerateCheckup(context.Background(), 9, "Asia/Taipei", false) require.NoError(t, err) require.Equal(t, domain.CheckupReady, c.Status) require.Len(t, c.Actions, 3) for _, f := range c.Findings { require.NotEmpty(t, f.Evidence) } // first force ok (total gens=2); third gen in 7d window blocked _, err = svc.GenerateCheckup(context.Background(), 9, "Asia/Taipei", true) require.NoError(t, err) _, err = svc.GenerateCheckup(context.Background(), 9, "Asia/Taipei", true) require.ErrorIs(t, err, domain.ErrRateLimited) } func TestAH_01_03_HealthGate(t *testing.T) { svc := New(repository.NewMemory()) h, err := svc.RecomputeHealth(context.Background(), 1, "acc", HealthSignals{ Sends24h: 30, IntervalViolations: 5, FailRate7d: 0.6, TokenBad: true, }) require.NoError(t, err) require.Equal(t, domain.HealthThrottle, h.Level) _, err = svc.EnsureAutoSendAllowed(context.Background(), 1, "acc") require.ErrorIs(t, err, domain.ErrThrottle) h2, err := svc.RecomputeHealth(context.Background(), 1, "acc2", HealthSignals{ Sends24h: 15, IntervalViolations: 2, FailRate7d: 0.3, }) require.NoError(t, err) require.Equal(t, domain.HealthWarn, h2.Level) warn, err := svc.EnsureAutoSendAllowed(context.Background(), 1, "acc2") require.NoError(t, err) require.NotEmpty(t, warn) } func TestIR_01_03_InviteReward(t *testing.T) { svc := New(repository.NewMemory()) svc.PlanQuota = func(id string) int { if id == "@inviter" { return 2000 // pro inviter → month cap 1000 } return 120 } r, err := svc.OnFirstPaidPlan(context.Background(), 2, 1, "starter") require.NoError(t, err) require.Equal(t, domain.RewardCredited, r.Status) require.Equal(t, 100, r.Points) // second time skipped r2, err := svc.OnFirstPaidPlan(context.Background(), 2, 1, "pro") require.NoError(t, err) require.Equal(t, domain.RewardSkipped, r2.Status) // pro first r3, err := svc.OnFirstPaidPlan(context.Background(), 3, 1, "pro") require.NoError(t, err) require.Equal(t, 300, r3.Points) } func TestWS_01_02_Workspace(t *testing.T) { svc := New(repository.NewMemory()) list, cur, err := svc.ListWorkspaces(context.Background(), 5) require.NoError(t, err) require.Len(t, list, 1) require.NotEmpty(t, cur) w2, err := svc.CreateWorkspace(context.Background(), 5, "客戶 A") require.NoError(t, err) require.NoError(t, svc.SwitchWorkspace(context.Background(), 5, w2.ID)) list, cur, err = svc.ListWorkspaces(context.Background(), 5) require.NoError(t, err) require.Equal(t, w2.ID, cur) require.Len(t, list, 2) } func TestWS_03_04_ReviewGate(t *testing.T) { svc := New(repository.NewMemory()) list, _, _ := svc.ListWorkspaces(context.Background(), 7) ws := list[0] on := true _, err := svc.UpdateWorkspace(context.Background(), 7, ws.ID, "", &on) require.NoError(t, err) err = svc.EnsureReviewAllowsAutoSend(context.Background(), 7, ws.ID, "play", "p1") require.True(t, errors.Is(err, domain.ErrReviewPending)) r, err := svc.SubmitReview(context.Background(), 7, "play", "p1", ws.ID) require.NoError(t, err) _, err = svc.DecideReview(context.Background(), 7, r.ID, domain.ReviewApproved, "") require.NoError(t, err) require.NoError(t, svc.EnsureReviewAllowsAutoSend(context.Background(), 7, ws.ID, "play", "p1")) } func TestWS_05_ExportReport(t *testing.T) { svc := New(repository.NewMemory()) list, _, _ := svc.ListWorkspaces(context.Background(), 8) fn, body, err := svc.ExportReportMarkdown(context.Background(), 8, list[0].ID, "2026-07") require.NoError(t, err) require.Contains(t, fn, "2026-07") require.Contains(t, body, "月報") }