sub2api/backend/internal/service/user_subscription_daily_quota_test.go
benjamin a4884b4e75 fix(subscription): 将日卡改为一次性每日配额
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-18 21:09:11 +08:00

179 lines
6.1 KiB
Go

package service
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
)
type dailyResetTrackingUserSubRepo struct {
userSubRepoNoop
resetDailyCalled bool
}
func (r *dailyResetTrackingUserSubRepo) ResetDailyUsage(context.Context, int64, 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, startOfDay(renewed.StartsAt), *renewed.DailyWindowStart)
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 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.Now()
startsAt := now.Add(-48 * 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.AddDate(0, 0, 2),
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, "热路径不应清零日卡已用额度")
}