| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- package service
- import (
- "context"
- "errors"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
- "github.com/stretchr/testify/require"
- )
- type dailyResetTrackingUserSubRepo struct {
- userSubRepoNoop
- resetDailyCalled bool
- }
- func (r *dailyResetTrackingUserSubRepo) ResetDailyUsage(context.Context, int64, *time.Time, time.Time) error {
- r.resetDailyCalled = true
- return nil
- }
- func TestAssignOrExtendSubscription_ExpiredDailyCardStartsNewOneTimeQuota(t *testing.T) {
- groupRepo := &subscriptionGroupRepoStub{
- group: &Group{ID: 1, SubscriptionType: SubscriptionTypeSubscription},
- }
- subRepo := newSubscriptionUserSubRepoStub()
- oldStart := time.Now().AddDate(0, 0, -3)
- oldWindowStart := startOfDay(oldStart)
- subRepo.seed(&UserSubscription{
- ID: 100,
- UserID: 200,
- GroupID: 1,
- StartsAt: oldStart,
- ExpiresAt: oldStart.AddDate(0, 0, 1),
- Status: SubscriptionStatusExpired,
- DailyWindowStart: &oldWindowStart,
- WeeklyWindowStart: &oldWindowStart,
- MonthlyWindowStart: &oldWindowStart,
- DailyUsageUSD: 10,
- WeeklyUsageUSD: 20,
- MonthlyUsageUSD: 30,
- Notes: "old",
- })
- svc := NewSubscriptionService(groupRepo, subRepo, nil, nil, nil)
- renewed, reused, err := svc.AssignOrExtendSubscription(context.Background(), &AssignSubscriptionInput{
- UserID: 200,
- GroupID: 1,
- ValidityDays: 1,
- Notes: "new",
- })
- require.NoError(t, err)
- require.True(t, reused)
- require.True(t, renewed.HasOneTimeDailyQuota(), "过期后重新购买 1 日卡仍应被识别为一次性日额度")
- require.Equal(t, SubscriptionStatusActive, renewed.Status)
- require.True(t, renewed.StartsAt.After(oldStart), "重新购买过期订阅时应重置当前周期 StartsAt")
- require.False(t, renewed.ExpiresAt.After(renewed.StartsAt.AddDate(0, 0, 1)))
- require.NotNil(t, renewed.DailyWindowStart)
- require.Equal(t, timezone.StartOfDay(renewed.StartsAt), *renewed.DailyWindowStart, "续期后日窗口应锚定当天 0 点")
- require.Equal(t, 0.0, renewed.DailyUsageUSD)
- require.Equal(t, 0.0, renewed.WeeklyUsageUSD)
- require.Equal(t, 0.0, renewed.MonthlyUsageUSD)
- require.Equal(t, "old\nnew", renewed.Notes)
- }
- func TestAssignOrExtendSubscription_ExpiredSubscriptionAppendsMatchingNotes(t *testing.T) {
- groupRepo := &subscriptionGroupRepoStub{
- group: &Group{ID: 1, SubscriptionType: SubscriptionTypeSubscription},
- }
- subRepo := newSubscriptionUserSubRepoStub()
- oldStart := time.Now().AddDate(0, 0, -3)
- subRepo.seed(&UserSubscription{
- ID: 101,
- UserID: 201,
- GroupID: 1,
- StartsAt: oldStart,
- ExpiresAt: oldStart.AddDate(0, 0, 1),
- Status: SubscriptionStatusExpired,
- Notes: "same",
- })
- svc := NewSubscriptionService(groupRepo, subRepo, nil, nil, nil)
- renewed, reused, err := svc.AssignOrExtendSubscription(context.Background(), &AssignSubscriptionInput{
- UserID: 201,
- GroupID: 1,
- ValidityDays: 1,
- Notes: "same",
- })
- require.NoError(t, err)
- require.True(t, reused)
- require.Equal(t, "same\nsame", renewed.Notes)
- }
- func TestUserSubscriptionNeedsDailyReset_DailyCardKeepsOneTimeQuota(t *testing.T) {
- start := time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC)
- dailyWindowStart := time.Date(2026, 5, 18, 0, 0, 0, 0, time.UTC)
- sub := &UserSubscription{
- StartsAt: start,
- ExpiresAt: start.Add(24 * time.Hour),
- DailyWindowStart: &dailyWindowStart,
- DailyUsageUSD: 10,
- }
- require.True(t, sub.HasOneTimeDailyQuota())
- require.False(t, sub.NeedsDailyResetAt(dailyWindowStart.Add(25*time.Hour)), "日卡应作为一次性配额,跨 0 点后不再刷新日额度")
- }
- func TestUserSubscriptionNeedsDailyReset_MultiDaySubscriptionStillRefreshes(t *testing.T) {
- start := time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC)
- dailyWindowStart := time.Date(2026, 5, 18, 0, 0, 0, 0, time.UTC)
- sub := &UserSubscription{
- StartsAt: start,
- ExpiresAt: start.AddDate(0, 0, 2),
- DailyWindowStart: &dailyWindowStart,
- }
- require.False(t, sub.HasOneTimeDailyQuota())
- require.True(t, sub.NeedsDailyResetAt(dailyWindowStart.Add(24*time.Hour)), "多日订阅仍应按 24 小时日窗口刷新")
- }
- func TestUserSubscriptionDailyResetTime_DailyCardReturnsExpiry(t *testing.T) {
- start := time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC)
- dailyWindowStart := time.Date(2026, 5, 18, 0, 0, 0, 0, time.UTC)
- expiresAt := start.Add(24 * time.Hour)
- sub := &UserSubscription{
- StartsAt: start,
- ExpiresAt: expiresAt,
- DailyWindowStart: &dailyWindowStart,
- }
- resetAt := sub.DailyResetTime()
- require.NotNil(t, resetAt)
- require.Equal(t, expiresAt, *resetAt, "日卡展示的日额度结束时间应为订阅过期时间")
- }
- func TestCheckAndResetWindows_DailyCardDoesNotResetDailyUsage(t *testing.T) {
- now := time.Now()
- startsAt := now.Add(-23 * time.Hour)
- dailyWindowStart := now.Add(-25 * time.Hour)
- repo := &dailyResetTrackingUserSubRepo{}
- svc := NewSubscriptionService(groupRepoNoop{}, repo, nil, nil, nil)
- sub := &UserSubscription{
- ID: 1,
- UserID: 10,
- GroupID: 20,
- StartsAt: startsAt,
- ExpiresAt: startsAt.Add(24 * time.Hour),
- DailyUsageUSD: 10,
- DailyWindowStart: &dailyWindowStart,
- }
- err := svc.CheckAndResetWindows(context.Background(), sub)
- require.NoError(t, err)
- require.False(t, repo.resetDailyCalled, "日卡作为一次性配额,过了 24 小时日窗口也不应重置 daily usage")
- require.Equal(t, 10.0, sub.DailyUsageUSD)
- }
- func TestCheckAndResetWindows_MultiDaySubscriptionStillResetsDailyUsage(t *testing.T) {
- now := time.Date(2026, 7, 29, 12, 0, 0, 0, time.UTC)
- startsAt := now.Add(-48 * time.Hour)
- dailyWindowStart := now.Add(-25 * time.Hour)
- repo := &dailyResetTrackingUserSubRepo{}
- svc := NewSubscriptionService(groupRepoNoop{}, repo, nil, nil, nil)
- svc.now = func() time.Time { return now }
- sub := &UserSubscription{
- ID: 1,
- UserID: 10,
- GroupID: 20,
- StartsAt: startsAt,
- ExpiresAt: startsAt.AddDate(0, 0, 4),
- DailyUsageUSD: 10,
- DailyWindowStart: &dailyWindowStart,
- }
- err := svc.CheckAndResetWindows(context.Background(), sub)
- require.NoError(t, err)
- require.True(t, repo.resetDailyCalled, "多日订阅仍应重置过期 daily window")
- require.Equal(t, 0.0, sub.DailyUsageUSD)
- }
- func TestValidateAndCheckLimits_DailyCardDoesNotAllowSecondQuotaAfterMidnight(t *testing.T) {
- start := time.Now().Add(-23 * time.Hour)
- dailyWindowStart := time.Now().Add(-25 * time.Hour)
- dailyLimit := 10.0
- sub := &UserSubscription{
- Status: SubscriptionStatusActive,
- StartsAt: start,
- ExpiresAt: start.Add(24 * time.Hour),
- DailyWindowStart: &dailyWindowStart,
- DailyUsageUSD: dailyLimit + 0.01,
- }
- group := &Group{
- SubscriptionType: SubscriptionTypeSubscription,
- DailyLimitUSD: &dailyLimit,
- }
- svc := NewSubscriptionService(groupRepoNoop{}, userSubRepoNoop{}, nil, nil, nil)
- needsMaintenance, err := svc.ValidateAndCheckLimits(sub, group)
- require.False(t, needsMaintenance, "日卡跨过日窗口后不应触发 daily reset 维护")
- require.True(t, errors.Is(err, ErrDailyLimitExceeded))
- require.Equal(t, dailyLimit+0.01, sub.DailyUsageUSD, "热路径不应清零日卡已用额度")
- }
|