Initial import of verae-middleware from zapier monorepo
This commit is contained in:
commit
c6f7d93bbb
66 changed files with 6590 additions and 0 deletions
160
src/services/entitlementService.js
Normal file
160
src/services/entitlementService.js
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* @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 });
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue