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, periodEnd, _ := periodRange(TypeDaily, now) 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 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) } }