222 lines
11 KiB
Go

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)
}
}