// monorepo/cloud/maplefile-backend/internal/usecase/user/quota_helper.go package user import ( "context" "fmt" "time" "github.com/gocql/gocql" "go.uber.org/zap" dom_storagedailyusage "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/storagedailyusage" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror" ) // Default storage quota limit: 10GB const DefaultStorageQuotaBytes int64 = 10 * 1024 * 1024 * 1024 // UserStorageQuotaHelperUseCase provides storage quota validation type UserStorageQuotaHelperUseCase interface { HasEnoughQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) (bool, error) CheckAndReserveQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) error ReleaseQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) error OnFileDeleted(ctx context.Context, userID gocql.UUID, sizeBytes int64) error } type userStorageQuotaHelperUseCaseImpl struct { logger *zap.Logger storageDailyUsageRepository dom_storagedailyusage.StorageDailyUsageRepository } // NewUserStorageQuotaHelperUseCase creates a new storage quota helper use case func NewUserStorageQuotaHelperUseCase( logger *zap.Logger, storageDailyUsageRepository dom_storagedailyusage.StorageDailyUsageRepository, ) UserStorageQuotaHelperUseCase { return &userStorageQuotaHelperUseCaseImpl{ logger: logger.Named("UserStorageQuotaHelper"), storageDailyUsageRepository: storageDailyUsageRepository, } } // HasEnoughQuota checks if user has enough storage quota func (uc *userStorageQuotaHelperUseCaseImpl) HasEnoughQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) (bool, error) { // Get current storage usage from most recent day today := time.Now().UTC().Truncate(24 * time.Hour) usage, err := uc.storageDailyUsageRepository.GetByUserAndDay(ctx, userID, today) var currentUsage int64 = 0 if err == nil && usage != nil { currentUsage = usage.TotalBytes } // Check if adding the new size would exceed the quota newTotal := currentUsage + sizeBytes hasQuota := newTotal <= DefaultStorageQuotaBytes uc.logger.Debug("Quota check", zap.String("user_id", userID.String()), zap.Int64("current_usage", currentUsage), zap.Int64("requested_size", sizeBytes), zap.Int64("new_total", newTotal), zap.Int64("quota_limit", DefaultStorageQuotaBytes), zap.Bool("has_quota", hasQuota)) return hasQuota, nil } // CheckAndReserveQuota reserves storage quota for a user func (uc *userStorageQuotaHelperUseCaseImpl) CheckAndReserveQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) error { hasQuota, err := uc.HasEnoughQuota(ctx, userID, sizeBytes) if err != nil { uc.logger.Error("Failed to check quota", zap.String("user_id", userID.String()), zap.Int64("size_bytes", sizeBytes), zap.Error(err)) return httperror.NewForInternalServerErrorWithSingleField("message", "Failed to check storage quota") } if !hasQuota { uc.logger.Warn("User exceeded storage quota", zap.String("user_id", userID.String()), zap.Int64("requested_size", sizeBytes)) return httperror.NewForBadRequestWithSingleField( "storage_quota", fmt.Sprintf("Storage quota exceeded. You are trying to upload %d bytes, but your quota limit is %d GB. Please delete some files or upgrade your plan.", sizeBytes, DefaultStorageQuotaBytes/(1024*1024*1024))) } // Note: Actual quota reservation would be tracked in a separate table // For now, we just validate and rely on the storage events to track actual usage uc.logger.Info("Quota check passed", zap.String("user_id", userID.String()), zap.Int64("size_bytes", sizeBytes)) return nil } // ReleaseQuota releases previously reserved storage quota func (uc *userStorageQuotaHelperUseCaseImpl) ReleaseQuota(ctx context.Context, userID gocql.UUID, sizeBytes int64) error { // Note: In a full implementation, this would release a reservation // For now, we just log the release since we're not tracking reservations separately uc.logger.Debug("Quota release requested", zap.String("user_id", userID.String()), zap.Int64("size_bytes", sizeBytes)) return nil } // OnFileDeleted handles quota updates when a file is deleted func (uc *userStorageQuotaHelperUseCaseImpl) OnFileDeleted(ctx context.Context, userID gocql.UUID, sizeBytes int64) error { // Note: This is a no-op because storage usage tracking is handled by storage events // The actual storage decrease is recorded via IncrementUsage with negative values uc.logger.Debug("File deleted notification", zap.String("user_id", userID.String()), zap.Int64("size_bytes", sizeBytes)) return nil }