Skip to main content

Documentation Index

Fetch the complete documentation index at: https://grantmaster.dev/llms.txt

Use this file to discover all available pages before exploring further.

shared/billing — API Reference

The shared/billing module enforces subscription quotas at the data layer. It prevents any domain service from creating resources beyond what the tenant’s subscription tier permits, and provides storage-specific helpers for file upload validation.

Module Map

FileResponsibility
QuotaService.tsSeat/feature quota enforcement, usage tracking, threshold warnings
storageQuotaHelper.tsFile-upload-specific quota wrappers

QuotaService

QuotaService is the single source of truth for whether an organisation may create a new resource. It is called automatically by BaseService.validateQuota() before any create operation in services that extend BaseService.

Singleton Export

import { quotaService } from '@/shared/billing/QuotaService';
A pre-constructed singleton is exported. All domain services use this instance; only tests inject custom instances via the constructor.

Constructor (DI)

new QuotaService(
  logger?: ILogger,        // default: shared logger
  eventBus?: IEventBus,   // default: core eventBus
  timeProvider?: ITimeProvider
)

checkQuota()

The primary enforcement entry point.
const result = await quotaService.checkQuota({
  organizationId: 'org-123',
  resourceType: 'users',       // see ResourceType below
  action: 'create',            // 'create' enforces limit; 'update' always passes
  additionalCount: 1,          // number of new resources (default: 1)
});

if (!result.allowed) {
  throw new BusinessLogicError(result.reason!);
}
QuotaCheckResponse
interface QuotaCheckResponse {
  allowed: boolean;
  reason?: string;             // human-readable when allowed = false
  limit: number;               // max allowed, or -1 for unlimited
  current: number;             // usage before this request
  available: number;           // remaining capacity (or -1 for unlimited)
  requiresUpgrade?: boolean;   // true if quota exceeded
  suggestedTier?: SubscriptionTier;
  percentageUsed: number;      // 0–100
}

ResourceType

type ResourceType =
  | 'users'          // maps to usageMetrics.activeUsers
  | 'projects'       // maps to usageMetrics.totalProjects
  | 'storage'        // maps to usageMetrics.storageUsedGB (in GB)
  | 'ai_generations' // maps to usageMetrics.aiGenerationsThisMonth
  | 'agent_credits'  // maps to usageMetrics.agentCreditsUsedThisMonth
  | 'agent_runs';    // maps to usageMetrics.agentRunsThisMonth
Limits per resource type are pulled from Organization.subscription.features (and TIER_LIMITS for agent run caps). A limit of -1 means unlimited.

incrementUsage() / decrementUsage()

Called non-critically (failures are logged to Sentry but do not break the operation) after successful create/delete:
await quotaService.incrementUsage(organizationId, 'users', 1);
await quotaService.decrementUsage(organizationId, 'projects', 1);
These update the relevant usageMetrics.* field in the organizations/{id} document via Firestore increment().

Warning Thresholds

QuotaService automatically emits QUOTA_WARNING events when usage crosses 80% and 90% of the limit. These are emitted only once per threshold crossing (checked by comparing current percentage before and after the increment).

EventBus Events

EventTrigger
SystemEventType.QUOTA_EXCEEDEDcheckQuota() returns allowed: false
SystemEventType.QUOTA_WARNINGUsage crosses 80% or 90% of limit
Both events are emitted with userId: 'system' (no user context at enforcement time). Consumers typically send upgrade-prompt notifications to org owners. QuotaExceededPayload
{
  resourceType: 'employees' | 'projects' | 'storage' | 'ai_generations' | 'agent_credits' | 'agent_runs';
  limit: number;
  current: number;
  available: number;
  attempted: number;
  subscriptionTier: SubscriptionTier;
  suggestedTier?: SubscriptionTier;
  requiresUpgrade: true;
}
QuotaWarningPayload — same fields plus threshold: 80 | 90.

storageQuotaHelper

Convenience wrappers for file upload flows that convert bytes to GB before calling QuotaService.
import {
  checkStorageQuota,
  validateFileUpload,
  incrementStorageUsage,
  decrementStorageUsage,
  formatFileSize,
} from '@/shared/billing/storageQuotaHelper';

checkStorageQuota()

const { allowed, reason } = await checkStorageQuota(orgId, fileSizeBytes);
Converts fileSizeBytes to GB and delegates to quotaService.checkQuota({ resourceType: 'storage', action: 'create' }).

validateFileUpload()

// Throws BusinessLogicError if quota exceeded
await validateFileUpload(orgId, fileSizeBytes, fileName);
Preferred call site before uploading to Firebase Storage.

incrementStorageUsage() / decrementStorageUsage()

Non-blocking; called after upload confirmation and after file deletion respectively.

formatFileSize()

formatFileSize(1536000)  // → "1.46 MB"
Utility for display strings in upload error messages.

Tier Limits Reference

Subscription tier limits are defined in src/config/entitlements.ts (TIER_LIMITS) and referenced by QuotaService.getQuotaLimits():
ResourcePotentialProfessionalUltimate
Usersplan maxplan maxunlimited
Projectsplan maxunlimitedunlimited
Storage (GB)10variesvaries
AI Generations/moplan maxplan maxplan max
Agent Credits/mo0plan maxplan max
Agent Runs/mo0plan maxunlimited
Exact values live in TIER_LIMITS and Organization.subscription.features; the table above is illustrative.

Callers

CallerUses
src/core/BaseService.tsquotaService.checkQuota() before every create; incrementUsage() after
src/features/documents/validateFileUpload(), incrementStorageUsage(), decrementStorageUsage()
src/features/billing/Reads QuotaCheckResponse to surface upgrade prompts in UI
src/features/agents/Checks agent_credits and agent_runs quotas before task execution