thread-master/apps/backend/internal/module/crm/usecase/followup_scan_test.go

115 lines
3.4 KiB
Go
Raw Normal View History

2026-08-19 02:01:15 +00:00
package usecase
import (
"context"
"testing"
"time"
"apps/backend/internal/module/crm/domain"
"apps/backend/internal/module/crm/repository"
)
const day = int64(24 * time.Hour)
func seedFollowUp(t *testing.T, repo domain.Repository, days int) (*domain.Contact, *domain.FollowUp) {
t.Helper()
ctx := context.Background()
c, err := repo.UpsertContactByIdentity(ctx, &domain.Contact{
OwnerUID: 7, AuthorHandle: "buyer", FollowUpDays: days,
})
if err != nil {
t.Fatal(err)
}
f := &domain.FollowUp{
ID: domain.NewID(), OwnerUID: 7, ContactID: c.ID,
DueAt: 1, Status: domain.FollowUpScheduled,
CreatedAt: 1, UpdatedAt: 1,
}
if err := repo.SaveFollowUp(ctx, f); err != nil {
t.Fatal(err)
}
return c, f
}
// FU-01FU-03第一次通知後要再等一個間隔才第二次通知第二次之後才 escalated。
func TestScanFollowUpsNotifiesTwiceThenEscalates(t *testing.T) {
ctx := context.Background()
repo := repository.NewMemory()
svc := New(repo)
_, f := seedFollowUp(t, repo, 3)
now := int64(10 * day)
if n, err := svc.ScanFollowUps(ctx, now); err != nil || n != 1 {
t.Fatalf("first scan n=%d err=%v", n, err)
}
first, err := repo.GetFollowUp(ctx, f.ID)
if err != nil || first.Status != domain.FollowUpNotified || first.NotifiedCount != 1 {
t.Fatalf("after first scan=%+v err=%v", first, err)
}
if first.DueAt != now+3*day {
t.Fatalf("first notify must push due_at by the contact interval, got %d", first.DueAt)
}
// 還沒到下一次到期:不可重複通知
if n, err := svc.ScanFollowUps(ctx, now+day); err != nil || n != 0 {
t.Fatalf("premature rescan n=%d err=%v", n, err)
}
if n, err := svc.ScanFollowUps(ctx, now+3*day); err != nil || n != 1 {
t.Fatalf("second scan n=%d err=%v", n, err)
}
second, err := repo.GetFollowUp(ctx, f.ID)
if err != nil || second.Status != domain.FollowUpEscalated || second.NotifiedCount != 2 {
t.Fatalf("after second scan=%+v err=%v", second, err)
}
// escalated 是終點:不再被掃到
if n, err := svc.ScanFollowUps(ctx, now+30*day); err != nil || n != 0 {
t.Fatalf("escalated must stop scanning n=%d err=%v", n, err)
}
}
// FU-04延後把到期日後移並回到 scheduled。
func TestSnoozeReturnsToScheduled(t *testing.T) {
ctx := context.Background()
repo := repository.NewMemory()
svc := New(repo)
_, f := seedFollowUp(t, repo, 3)
if _, err := svc.ScanFollowUps(ctx, 10*day); err != nil {
t.Fatal(err)
}
before := domain.NowNano()
got, err := svc.SnoozeFollowUp(ctx, 7, f.ID, 5)
if err != nil {
t.Fatal(err)
}
if got.Status != domain.FollowUpScheduled {
t.Fatalf("snooze status = %s, want scheduled", got.Status)
}
if got.DueAt < before+5*day {
t.Fatalf("snooze must push due_at at least 5 days out, got %d", got.DueAt)
}
}
func TestScanFollowUpsFallsBackToDefaultInterval(t *testing.T) {
ctx := context.Background()
repo := repository.NewMemory()
svc := New(repo)
f := &domain.FollowUp{
ID: domain.NewID(), OwnerUID: 7, ContactID: "missing-contact",
DueAt: 1, Status: domain.FollowUpScheduled, CreatedAt: 1, UpdatedAt: 1,
}
if err := repo.SaveFollowUp(ctx, f); err != nil {
t.Fatal(err)
}
now := int64(10 * day)
if _, err := svc.ScanFollowUps(ctx, now); err != nil {
t.Fatal(err)
}
got, err := repo.GetFollowUp(ctx, f.ID)
if err != nil || got.DueAt != now+int64(domain.DefaultFollowUpDays)*day {
t.Fatalf("missing contact should use default interval, got %+v err=%v", got, err)
}
}