160 lines
4.2 KiB
JavaScript
160 lines
4.2 KiB
JavaScript
/**
|
|
* @fileoverview Plan quotas and usage recording.
|
|
* @module services/entitlementService
|
|
*/
|
|
|
|
import { config } from '../config.js';
|
|
import { AppError } from '../errors.js';
|
|
import { getTenant, resolveLimits } from '../store/tenants.js';
|
|
import { getUsage, incrementUsage } from '../store/usage.js';
|
|
import { createDebugger } from '../debug/logger.js';
|
|
|
|
const log = createDebugger('billing');
|
|
|
|
function isUnlimited(value) {
|
|
return value === null || value === undefined;
|
|
}
|
|
|
|
function quotaExceeded({ limit, used, allowOverage }) {
|
|
if (isUnlimited(limit)) return false;
|
|
if (used < limit) return false;
|
|
return !allowOverage;
|
|
}
|
|
|
|
/**
|
|
* Ensure tenant may perform an action under plan limits.
|
|
*
|
|
* @param {string} tenantId
|
|
* @param {'timestamp'|'verify'|'batch_timestamp'} action
|
|
* @param {{ amount?: number }} [options]
|
|
* @returns {{ tenant: object, limits: object, usage: object }}
|
|
*/
|
|
export function checkEntitlement(tenantId, action, { amount = 1 } = {}) {
|
|
const tenant = getTenant(tenantId);
|
|
if (!tenant) {
|
|
throw new AppError('Unknown tenant', { status: 401, code: 'UNAUTHORIZED' });
|
|
}
|
|
|
|
const limits = resolveLimits(tenant);
|
|
const usage = getUsage(tenantId);
|
|
|
|
log.debug('checkEntitlement', {
|
|
tenantId,
|
|
action,
|
|
plan: tenant.plan,
|
|
amount,
|
|
timestampsUsed: usage.timestamps,
|
|
timestampsLimit: limits.timestamps,
|
|
});
|
|
|
|
if (action === 'batch_timestamp') {
|
|
if (!limits.batch) {
|
|
throw new AppError('Batch timestamps require a paid plan', {
|
|
status: 403,
|
|
code: 'PLAN_UPGRADE_REQUIRED',
|
|
details: { upgradeUrl: config.upgradeUrl },
|
|
});
|
|
}
|
|
if (!isUnlimited(limits.batchMaxItems) && amount > limits.batchMaxItems) {
|
|
throw new AppError(`Batch size exceeds plan limit of ${limits.batchMaxItems}`, {
|
|
status: 403,
|
|
code: 'BATCH_LIMIT_EXCEEDED',
|
|
details: { upgradeUrl: config.upgradeUrl, limit: limits.batchMaxItems },
|
|
});
|
|
}
|
|
}
|
|
|
|
if (action === 'timestamp') {
|
|
if (
|
|
quotaExceeded({
|
|
limit: limits.timestamps,
|
|
used: usage.timestamps,
|
|
allowOverage: limits.allowOverage,
|
|
})
|
|
) {
|
|
const err = new AppError('Monthly timestamp quota exceeded', {
|
|
status: 402,
|
|
code: 'QUOTA_EXCEEDED',
|
|
details: {
|
|
limit: limits.timestamps,
|
|
used: usage.timestamps,
|
|
period: 'monthly',
|
|
upgradeUrl: config.upgradeUrl,
|
|
},
|
|
});
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
if (action === 'verify') {
|
|
if (
|
|
quotaExceeded({
|
|
limit: limits.verifications,
|
|
used: usage.verifications,
|
|
allowOverage: limits.allowOverage,
|
|
})
|
|
) {
|
|
throw new AppError('Monthly verification quota exceeded', {
|
|
status: 402,
|
|
code: 'QUOTA_EXCEEDED',
|
|
details: {
|
|
limit: limits.verifications,
|
|
used: usage.verifications,
|
|
period: 'monthly',
|
|
upgradeUrl: config.upgradeUrl,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
return { tenant, limits, usage };
|
|
}
|
|
|
|
/**
|
|
* Record usage after a successful action.
|
|
*
|
|
* @param {string} tenantId
|
|
* @param {string} action
|
|
* @param {{ amount?: number }} [options]
|
|
* @returns {void}
|
|
*/
|
|
export function recordUsage(tenantId, action, { amount = 1 } = {}) {
|
|
const tenant = getTenant(tenantId);
|
|
if (!tenant) return;
|
|
|
|
const limits = resolveLimits(tenant);
|
|
const usage = getUsage(tenantId);
|
|
|
|
if (action === 'timestamp') {
|
|
if (
|
|
!isUnlimited(limits.timestamps) &&
|
|
usage.timestamps >= limits.timestamps &&
|
|
limits.allowOverage
|
|
) {
|
|
incrementUsage(tenantId, 'overage.timestamps', amount);
|
|
}
|
|
incrementUsage(tenantId, 'timestamps', amount);
|
|
}
|
|
|
|
if (action === 'verify') {
|
|
if (
|
|
!isUnlimited(limits.verifications) &&
|
|
usage.verifications >= limits.verifications &&
|
|
limits.allowOverage
|
|
) {
|
|
incrementUsage(tenantId, 'overage.verifications', amount);
|
|
}
|
|
incrementUsage(tenantId, 'verifications', amount);
|
|
}
|
|
|
|
if (action === 'status') {
|
|
incrementUsage(tenantId, 'statusChecks', amount);
|
|
}
|
|
|
|
if (action === 'batch_timestamp') {
|
|
incrementUsage(tenantId, 'batchTimestamps', amount);
|
|
incrementUsage(tenantId, 'timestamps', amount);
|
|
}
|
|
|
|
log.debug('recordUsage', { tenantId, action, amount });
|
|
}
|