package usage import ( "context" "github.com/wangjia/pangolin/server/internal/apierr" ) // defaultFreeDailyMinutes is the口径 fallback when the free plan row has a NULL // daily_minutes (should not happen given the seed, but we stay safe). const defaultFreeDailyMinutes = 10 // CheckFreeConnect enforces the free-plan connect gate and returns the user's // remaining minutes for today (UTC). It is called by #5's connect endpoint to // derive the free credential's TTL. // // Rules: // - plan.ad_gate == false (pro/team): not minute-gated. Returns Unlimited // when daily_minutes is NULL, otherwise the remaining minutes for the day. // - plan.ad_gate == true (free): today's ad_unlocked_at must be set and // minutes_used must be below daily_minutes (free = 10). Returns the // remaining minutes; otherwise a bilingual semantic error // (AD_NOT_UNLOCKED / QUOTA_EXHAUSTED). func (svc *Service) CheckFreeConnect(ctx context.Context, userID int64) (remainingMinutes int, apiErr *apierr.Error) { plan, err := svc.store.EffectivePlan(ctx, userID) if err != nil { return 0, apierr.ErrInternal } limit := defaultFreeDailyMinutes if plan.DailyMinutes.Valid { limit = int(plan.DailyMinutes.Int64) } // Paid plans (no ad gate). if !plan.AdGate { if !plan.DailyMinutes.Valid { return Unlimited, nil } day, err := svc.store.GetDay(ctx, userID, utcToday()) if err != nil { return 0, apierr.ErrInternal } remaining := limit if day != nil { remaining = limit - day.MinutesUsed } if remaining < 0 { remaining = 0 } return remaining, nil } // Free plan: require ad unlock + remaining minutes. day, err := svc.store.GetDay(ctx, userID, utcToday()) if err != nil { return 0, apierr.ErrInternal } if day == nil || !day.AdUnlockedAt.Valid { return 0, apierr.ErrAdNotUnlocked } if day.MinutesUsed >= limit { return 0, apierr.ErrQuotaExhausted } return limit - day.MinutesUsed, nil }