package threshold_activity import ( "context" "testing" "time" ) func TestCountEffectiveInvites_OnlyCountsInvitesCreatedAfterActivityStart(t *testing.T) { svc, _, db := newThresholdTestService(t) ctx := context.Background() now := time.Now() activityStart := now.Add(-2 * time.Hour) periodStart := activityStart periodEnd := now.Add(time.Hour) mustExec(t, db, `INSERT INTO user_invites (inviter_id, invitee_id, invite_code, created_at, updated_at) VALUES (1, 2, 'INV1', ?, ?)`, activityStart.Add(-time.Hour), activityStart.Add(-time.Hour)) mustExec(t, db, `INSERT INTO user_invites (inviter_id, invitee_id, invite_code, created_at, updated_at) VALUES (1, 3, 'INV1', ?, ?)`, activityStart.Add(10*time.Minute), activityStart.Add(10*time.Minute)) mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (2, 'OLD_INV', 1, 1200, 1200, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0)) mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (3, 'NEW_INV', 1, 1500, 1500, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0)) count, err := svc.countEffectiveInvites(ctx, 1, activityStart, periodStart, periodEnd, 1000) if err != nil { t.Fatalf("countEffectiveInvites failed: %v", err) } if count != 1 { t.Fatalf("expected 1 effective invite after activity start, got %d", count) } } func TestEvaluateQualification_UsesConfiguredActivityTimeRangeForSpend(t *testing.T) { svc, _, db := newThresholdTestService(t) ctx := context.Background() now := time.Date(2026, 6, 10, 12, 0, 0, 0, time.Local) activity := Activity{ ID: 4, Title: "自定义消费时间", Type: TypeMonthly, QualificationMode: QualificationModeSpendOnly, SpendThresholdAmount: 1000, MinParticipants: 1, StartTime: now.Add(-time.Hour), EndTime: now.Add(time.Hour), DrawTime: now.Add(2 * time.Hour), Status: StatusActive, } mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (40, 'CUSTOM_BEFORE', 1, 5000, 5000, 2, ?, ?, ?, ?)`, activity.StartTime.Add(-time.Second), activity.StartTime.Add(-time.Second), activity.StartTime.Add(-time.Second), time.Unix(0, 0)) mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (40, 'CUSTOM_INSIDE', 1, 1000, 1000, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0)) mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (40, 'CUSTOM_AFTER', 1, 7000, 7000, 2, ?, ?, ?, ?)`, activity.EndTime.Add(2*time.Second), activity.EndTime.Add(2*time.Second), activity.EndTime.Add(2*time.Second), time.Unix(0, 0)) progress, _, err := svc.evaluateQualification(ctx, activity, 40) if err != nil { t.Fatalf("evaluateQualification failed: %v", err) } if progress.CurrentPaid != 1000 { t.Fatalf("expected only configured-range spend 1000, got %d", progress.CurrentPaid) } if !progress.SpendQualified { t.Fatalf("expected spend to be qualified") } } func TestEvaluateQualification_UsesConfiguredActivityTimeRangeForInvites(t *testing.T) { svc, _, db := newThresholdTestService(t) ctx := context.Background() now := time.Date(2026, 6, 10, 12, 0, 0, 0, time.Local) activity := Activity{ ID: 5, Title: "自定义邀请时间", Type: TypeMonthly, QualificationMode: QualificationModeInviteOnly, InviteThresholdCount: 1, InviteEffectiveAmount: 1000, MinParticipants: 1, StartTime: now.Add(-time.Hour), EndTime: now.Add(time.Hour), DrawTime: now.Add(2 * time.Hour), Status: StatusActive, } mustExec(t, db, `INSERT INTO user_invites (inviter_id, invitee_id, invite_code, created_at, updated_at) VALUES (50, 51, 'INV50', ?, ?)`, activity.StartTime.Add(time.Minute), activity.StartTime.Add(time.Minute)) mustExec(t, db, `INSERT INTO user_invites (inviter_id, invitee_id, invite_code, created_at, updated_at) VALUES (50, 52, 'INV50', ?, ?)`, activity.StartTime.Add(time.Minute), activity.StartTime.Add(time.Minute)) mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (51, 'INV_BEFORE', 1, 1500, 1500, 2, ?, ?, ?, ?)`, activity.StartTime.Add(-time.Second), activity.StartTime.Add(-time.Second), activity.StartTime.Add(-time.Second), time.Unix(0, 0)) mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (51, 'INV_AFTER', 1, 1500, 1500, 2, ?, ?, ?, ?)`, activity.EndTime.Add(2*time.Second), activity.EndTime.Add(2*time.Second), activity.EndTime.Add(2*time.Second), time.Unix(0, 0)) mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (52, 'INV_INSIDE', 1, 1200, 1200, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0)) progress, _, err := svc.evaluateQualification(ctx, activity, 50) if err != nil { t.Fatalf("evaluateQualification failed: %v", err) } if progress.EffectiveInviteCount != 1 { t.Fatalf("expected only one effective invite in configured range, got %d", progress.EffectiveInviteCount) } if !progress.InviteQualified { t.Fatalf("expected invite to be qualified") } } func TestJoin_SpendOnlyQualifiedWritesParticipant(t *testing.T) { svc, _, db := newThresholdTestService(t) ctx := context.Background() now := time.Now() activity := Activity{ ID: 1, Title: "消费门槛", Type: TypeDaily, QualificationMode: QualificationModeSpendOnly, SpendThresholdAmount: 1000, MinParticipants: 1, StartTime: now.Add(-time.Hour), EndTime: now.Add(24 * time.Hour), DrawTime: now.Add(25 * time.Hour), Status: StatusActive, } mustInsertActivity(t, db, activity) mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (10, 'SPEND_OK', 1, 1500, 1500, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0)) if err := svc.Join(ctx, activity.ID, 10); err != nil { t.Fatalf("Join failed: %v", err) } var participant Participant if err := db.Where("activity_id = ? AND user_id = ?", activity.ID, 10).First(&participant).Error; err != nil { t.Fatalf("query participant failed: %v", err) } if participant.QualificationSource != QualificationSourceSpend { t.Fatalf("expected qualification source spend, got %s", participant.QualificationSource) } if participant.PaidAmountSnapshot != 1500 { t.Fatalf("expected paid snapshot 1500, got %d", participant.PaidAmountSnapshot) } } func TestJoin_InviteOnlyQualifiedAndDuplicateRejected(t *testing.T) { svc, _, db := newThresholdTestService(t) ctx := context.Background() now := time.Now() activity := Activity{ ID: 2, Title: "邀请门槛", Type: TypeDaily, QualificationMode: QualificationModeInviteOnly, InviteThresholdCount: 1, InviteEffectiveAmount: 1000, MinParticipants: 1, StartTime: now.Add(-time.Hour), EndTime: now.Add(24 * time.Hour), DrawTime: now.Add(25 * time.Hour), Status: StatusActive, } mustInsertActivity(t, db, activity) mustExec(t, db, `INSERT INTO user_invites (inviter_id, invitee_id, invite_code, created_at, updated_at) VALUES (20, 21, 'INV20', ?, ?)`, now, now) mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (21, 'INVITE_OK', 1, 1200, 1200, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0)) if err := svc.Join(ctx, activity.ID, 20); err != nil { t.Fatalf("Join failed: %v", err) } var participant Participant if err := db.Where("activity_id = ? AND user_id = ?", activity.ID, 20).First(&participant).Error; err != nil { t.Fatalf("query participant failed: %v", err) } if participant.QualificationSource != QualificationSourceInvite { t.Fatalf("expected qualification source invite, got %s", participant.QualificationSource) } if participant.EffectiveInviteCountSnapshot != 1 { t.Fatalf("expected invite snapshot 1, got %d", participant.EffectiveInviteCountSnapshot) } if err := svc.Join(ctx, activity.ID, 20); err == nil { t.Fatalf("expected duplicate join to fail") } var total int64 if err := db.Model(&Participant{}).Where("activity_id = ? AND user_id = ?", activity.ID, 20).Count(&total).Error; err != nil { t.Fatalf("count participant failed: %v", err) } if total != 1 { t.Fatalf("expected only 1 participant row after duplicate join, got %d", total) } } func TestListJoinableActivitiesForUser_ReflectsJoinedState(t *testing.T) { svc, _, db := newThresholdTestService(t) ctx := context.Background() now := time.Now() activity := Activity{ ID: 3, Title: "任一达标", Type: TypeDaily, QualificationMode: QualificationModeEither, SpendThresholdAmount: 1000, MinParticipants: 1, StartTime: now.Add(-time.Hour), EndTime: now.Add(24 * time.Hour), DrawTime: now.Add(25 * time.Hour), Status: StatusActive, } mustInsertActivity(t, db, activity) mustExec(t, db, `INSERT INTO orders (user_id, order_no, source_type, actual_amount, total_amount, status, paid_at, created_at, updated_at, cancelled_at) VALUES (30, 'EITHER_OK', 1, 1000, 1000, 2, ?, ?, ?, ?)`, now, now, now, time.Unix(0, 0)) items, err := svc.ListJoinableActivitiesForUser(ctx, 30) if err != nil { t.Fatalf("ListJoinableActivitiesForUser failed: %v", err) } if len(items) != 1 { t.Fatalf("expected 1 activity, got %d", len(items)) } if !items[0].CanJoin || items[0].Joined { t.Fatalf("expected activity to be joinable before join, got canJoin=%v joined=%v", items[0].CanJoin, items[0].Joined) } if err := svc.Join(ctx, activity.ID, 30); err != nil { t.Fatalf("Join failed: %v", err) } items, err = svc.ListJoinableActivitiesForUser(ctx, 30) if err != nil { t.Fatalf("ListJoinableActivitiesForUser after join failed: %v", err) } if len(items) != 1 { t.Fatalf("expected 1 activity after join, got %d", len(items)) } if items[0].CanJoin || !items[0].Joined { t.Fatalf("expected activity to be joined and not joinable, got canJoin=%v joined=%v", items[0].CanJoin, items[0].Joined) } }