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
| File | Responsibility |
|---|
QuotaService.ts | Seat/feature quota enforcement, usage tracking, threshold warnings |
storageQuotaHelper.ts | File-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
| Event | Trigger |
|---|
SystemEventType.QUOTA_EXCEEDED | checkQuota() returns allowed: false |
SystemEventType.QUOTA_WARNING | Usage 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(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():
| Resource | Potential | Professional | Ultimate |
|---|
| Users | plan max | plan max | unlimited |
| Projects | plan max | unlimited | unlimited |
| Storage (GB) | 10 | varies | varies |
| AI Generations/mo | plan max | plan max | plan max |
| Agent Credits/mo | 0 | plan max | plan max |
| Agent Runs/mo | 0 | plan max | unlimited |
Exact values live in TIER_LIMITS and Organization.subscription.features; the table above is illustrative.
Callers
| Caller | Uses |
|---|
src/core/BaseService.ts | quotaService.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 |