feat: add quota entitlements (#383)

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
This commit is contained in:
Jasper Van
2026-05-08 19:21:34 -04:00
committed by GitHub
parent 0b65e2dc15
commit bf8a4f5877
19 changed files with 3691 additions and 85 deletions
+17
View File
@@ -0,0 +1,17 @@
CREATE TABLE `org_quota_entitlements` (
`id` text PRIMARY KEY NOT NULL,
`org_id` text NOT NULL,
`resource_type` text NOT NULL,
`source` text NOT NULL,
`source_id` text NOT NULL,
`bytes` integer NOT NULL,
`starts_at` integer NOT NULL,
`expires_at` integer,
`status` text NOT NULL,
`metadata` text,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE INDEX `org_quota_entitlements_org_resource_idx` ON `org_quota_entitlements` (`org_id`,`resource_type`,`status`);--> statement-breakpoint
CREATE UNIQUE INDEX `org_quota_entitlements_source_resource_uniq` ON `org_quota_entitlements` (`source`,`source_id`,`resource_type`);
File diff suppressed because it is too large Load Diff
+7
View File
@@ -197,6 +197,13 @@
"when": 1778246297143,
"tag": "0028_move-cloud-store-settings-to-system-options",
"breakpoints": true
},
{
"idx": 29,
"version": "6",
"when": 1778277914288,
"tag": "0029_wet_betty_brant",
"breakpoints": true
}
]
}
+22
View File
@@ -47,6 +47,28 @@ export const orgQuotas = sqliteTable('org_quotas', {
trafficPeriod: text('traffic_period').notNull().default('1970-01'),
})
export const orgQuotaEntitlements = sqliteTable(
'org_quota_entitlements',
{
id: text('id').primaryKey(),
orgId: text('org_id').notNull(),
resourceType: text('resource_type').notNull(),
source: text('source').notNull(),
sourceId: text('source_id').notNull(),
bytes: integer('bytes').notNull(),
startsAt: integer('starts_at', { mode: 'timestamp_ms' }).notNull(),
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }),
status: text('status').notNull(),
metadata: text('metadata'),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
},
(t) => [
index('org_quota_entitlements_org_resource_idx').on(t.orgId, t.resourceType, t.status),
uniqueIndex('org_quota_entitlements_source_resource_uniq').on(t.source, t.sourceId, t.resourceType),
],
)
export const webhookEvents = sqliteTable(
'webhook_events',
{
+286 -12
View File
@@ -1646,7 +1646,7 @@ describe('Quota Store API', () => {
await expect(res.json()).resolves.toMatchObject({ success: true, duplicate: false })
})
it('valid Cloud quota-change webhook updates org quota once and records audit', async () => {
it('valid Cloud quota-change webhook records active entitlement once and records audit', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await adminHeaders(app)
@@ -1678,8 +1678,16 @@ describe('Quota Store API', () => {
expect(events).toEqual([{ status: 'processed', error: null, processedAt: expect.any(Number) }])
const quotaRes = await app.request('/api/quotas/me', { headers })
const quota = (await quotaRes.json()) as { baseQuota: number; quota: number }
const quota = (await quotaRes.json()) as { baseQuota: number; entitlementQuota: number; quota: number }
expect(quota.baseQuota).toBe(before[0].quota)
expect(quota.entitlementQuota).toBe(4096)
expect(quota.quota).toBe(before[0].quota + 4096)
const entitlement = await db.all<{ bytes: number; status: string; sourceId: string }>(sql`
SELECT bytes, status, source_id AS sourceId
FROM org_quota_entitlements
WHERE org_id = ${orgId} AND resource_type = 'storage'
`)
expect(entitlement).toEqual([{ bytes: 4096, status: 'active', sourceId: 'order-1' }])
const audit = await db.all<{ action: string; metadata: string }>(
sql`SELECT action, metadata FROM activity_events WHERE org_id = ${orgId} ORDER BY created_at DESC LIMIT 1`,
)
@@ -1687,6 +1695,168 @@ describe('Quota Store API', () => {
expect(JSON.parse(audit[0].metadata)).toMatchObject({ eventId: 'evt-1', storageBytes: 4096, trafficBytes: 0 })
})
it('accumulates repeated Cloud increases for the same order and resource', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await adminHeaders(app)
await seedSettings(app, headers)
const orgId = await getFirstOrgId(db)
const first = await postWebhook(
app,
JSON.stringify({
eventId: 'evt-repeat-increase-1',
cloudOrderId: 'order-repeat-increase',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'increase',
storageBytes: 4096,
trafficBytes: 0,
source: 'stripe',
}),
)
const second = await postWebhook(
app,
JSON.stringify({
eventId: 'evt-repeat-increase-2',
cloudOrderId: 'order-repeat-increase',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'increase',
storageBytes: 2048,
trafficBytes: 0,
source: 'stripe',
}),
)
expect(first.status).toBe(200)
expect(second.status).toBe(200)
const entitlements = await db.all<{ bytes: number; status: string }>(
sql`SELECT bytes, status FROM org_quota_entitlements WHERE source_id = 'order-repeat-increase' AND resource_type = 'storage'`,
)
expect(entitlements).toEqual([{ bytes: 6144, status: 'active' }])
const quotaRes = await app.request('/api/quotas/me', { headers })
const quota = (await quotaRes.json()) as { entitlementQuota: number }
expect(quota.entitlementQuota).toBe(6144)
})
it('decreases accumulated Cloud order entitlement bytes without revoking the remainder', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await adminHeaders(app)
await seedSettings(app, headers)
const orgId = await getFirstOrgId(db)
await postWebhook(
app,
JSON.stringify({
eventId: 'evt-partial-decrease-inc-1',
cloudOrderId: 'order-partial-decrease',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'increase',
storageBytes: 4096,
trafficBytes: 0,
source: 'stripe',
}),
)
await postWebhook(
app,
JSON.stringify({
eventId: 'evt-partial-decrease-inc-2',
cloudOrderId: 'order-partial-decrease',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'increase',
storageBytes: 2048,
trafficBytes: 0,
source: 'stripe',
}),
)
const decrease = await postWebhook(
app,
JSON.stringify({
eventId: 'evt-partial-decrease-dec',
cloudOrderId: 'order-partial-decrease',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'decrease',
storageBytes: 2048,
trafficBytes: 0,
source: 'stripe',
}),
)
expect(decrease.status).toBe(200)
const entitlements = await db.all<{ bytes: number; status: string }>(
sql`SELECT bytes, status FROM org_quota_entitlements WHERE source_id = 'order-partial-decrease' AND resource_type = 'storage'`,
)
expect(entitlements).toEqual([{ bytes: 4096, status: 'active' }])
const quotaRes = await app.request('/api/quotas/me', { headers })
const quota = (await quotaRes.json()) as { entitlementQuota: number }
expect(quota.entitlementQuota).toBe(4096)
})
it('restarts entitlement bytes when a new increase follows full revocation', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await adminHeaders(app)
await seedSettings(app, headers)
const orgId = await getFirstOrgId(db)
await postWebhook(
app,
JSON.stringify({
eventId: 'evt-reactivate-inc-1',
cloudOrderId: 'order-reactivate',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'increase',
storageBytes: 4096,
trafficBytes: 0,
source: 'stripe',
}),
)
await postWebhook(
app,
JSON.stringify({
eventId: 'evt-reactivate-dec',
cloudOrderId: 'order-reactivate',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'decrease',
storageBytes: 4096,
trafficBytes: 0,
source: 'stripe',
}),
)
const increase = await postWebhook(
app,
JSON.stringify({
eventId: 'evt-reactivate-inc-2',
cloudOrderId: 'order-reactivate',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'increase',
storageBytes: 2048,
trafficBytes: 0,
source: 'stripe',
}),
)
expect(increase.status).toBe(200)
const entitlements = await db.all<{ bytes: number; status: string }>(
sql`SELECT bytes, status FROM org_quota_entitlements WHERE source_id = 'order-reactivate' AND resource_type = 'storage'`,
)
expect(entitlements).toEqual([{ bytes: 2048, status: 'active' }])
const quotaRes = await app.request('/api/quotas/me', { headers })
const quota = (await quotaRes.json()) as { entitlementQuota: number }
expect(quota.entitlementQuota).toBe(2048)
})
it('rejects legacy order delivery event types', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
@@ -1708,13 +1878,20 @@ describe('Quota Store API', () => {
await expect(res.json()).resolves.toEqual({ error: 'invalid_payload' })
})
it('applies storage decreases without going below zero', async () => {
it('storage decreases revoke matching Cloud order entitlements without changing base quota', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await adminHeaders(app)
await seedSettings(app, headers)
const orgId = await getFirstOrgId(db)
await db.run(sql`UPDATE org_quotas SET quota = 2048 WHERE org_id = ${orgId}`)
const now = Date.now()
await db.run(sql`
INSERT INTO org_quota_entitlements
(id, org_id, resource_type, source, source_id, bytes, starts_at, expires_at, status, metadata, created_at, updated_at)
VALUES
('ent-storage-decrease', ${orgId}, 'storage', 'cloud_order', 'order-storage-decrease', 4096, ${now}, NULL, 'active', NULL, ${now}, ${now})
`)
const res = await postWebhook(
app,
@@ -1731,10 +1908,14 @@ describe('Quota Store API', () => {
expect(res.status).toBe(200)
const rows = await db.all<{ quota: number }>(sql`SELECT quota FROM org_quotas WHERE org_id = ${orgId}`)
expect(rows[0].quota).toBe(0)
expect(rows[0].quota).toBe(2048)
const entitlements = await db.all<{ status: string }>(
sql`SELECT status FROM org_quota_entitlements WHERE source_id = 'order-storage-decrease'`,
)
expect(entitlements).toEqual([{ status: 'revoked' }])
})
it('applies traffic increases and decreases', async () => {
it('records traffic increases and revokes them on matching decreases', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await adminHeaders(app)
@@ -1757,7 +1938,7 @@ describe('Quota Store API', () => {
app,
JSON.stringify({
eventId: 'evt-traffic-decrease',
cloudOrderId: 'order-traffic-decrease',
cloudOrderId: 'order-traffic-increase',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'decrease',
@@ -1766,10 +1947,15 @@ describe('Quota Store API', () => {
}),
)
const rows = await db.all<{ trafficQuota: number }>(
sql`SELECT traffic_quota AS trafficQuota FROM org_quotas WHERE org_id = ${orgId}`,
const rows = await db.all<{ trafficQuota: number; status: string }>(
sql`SELECT bytes AS trafficQuota, status FROM org_quota_entitlements WHERE source_id = 'order-traffic-increase'`,
)
expect(rows[0].trafficQuota).toBe(3072)
expect(rows[0]).toEqual({ trafficQuota: 3072, status: 'active' })
const quotaRes = await app.request('/api/quotas/me', { headers })
const quota = (await quotaRes.json()) as { entitlementTrafficQuota: number; trafficQuota: number }
expect(quota.entitlementTrafficQuota).toBe(3072)
expect(quota.trafficQuota).toBe(3072)
})
it('processes same-order increase then decrease as two independent events', async () => {
@@ -1814,6 +2000,10 @@ describe('Quota Store API', () => {
const rows = await db.all<{ quota: number }>(sql`SELECT quota FROM org_quotas WHERE org_id = ${orgId}`)
expect(rows[0].quota).toBe(8192)
const entitlements = await db.all<{ status: string }>(
sql`SELECT status FROM org_quota_entitlements WHERE source_id = 'order-reversal-test'`,
)
expect(entitlements).toEqual([{ status: 'revoked' }])
const deliveries = await db.all<{ eventId: string; status: string }>(
sql`SELECT event_id AS eventId, status FROM webhook_events WHERE event_id IN ('evt-order-increase', 'evt-order-decrease') ORDER BY created_at`,
@@ -1830,6 +2020,60 @@ describe('Quota Store API', () => {
expect(auditRows.map((r) => r.action)).toContain('quota_order_increase')
})
it('does not fall back to base quota when a second decrease sees an already revoked entitlement', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await adminHeaders(app)
await seedSettings(app, headers)
const orgId = await getFirstOrgId(db)
await db.run(sql`UPDATE org_quotas SET quota = 8192 WHERE org_id = ${orgId}`)
await postWebhook(
app,
JSON.stringify({
eventId: 'evt-double-decrease-increase',
cloudOrderId: 'order-double-decrease',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'increase',
storageBytes: 4096,
trafficBytes: 0,
source: 'stripe',
}),
)
const firstDecrease = await postWebhook(
app,
JSON.stringify({
eventId: 'evt-double-decrease-first',
cloudOrderId: 'order-double-decrease',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'decrease',
storageBytes: 4096,
trafficBytes: 0,
source: 'stripe',
}),
)
const secondDecrease = await postWebhook(
app,
JSON.stringify({
eventId: 'evt-double-decrease-second',
cloudOrderId: 'order-double-decrease',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'decrease',
storageBytes: 4096,
trafficBytes: 0,
source: 'stripe',
}),
)
expect(firstDecrease.status).toBe(200)
expect(secondDecrease.status).toBe(200)
const rows = await db.all<{ quota: number }>(sql`SELECT quota FROM org_quotas WHERE org_id = ${orgId}`)
expect(rows[0].quota).toBe(8192)
})
it('replaying the same decrease event is idempotent and does not double-deduct', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
@@ -1865,6 +2109,36 @@ describe('Quota Store API', () => {
expect(rows[0].quota).toBe(6144)
})
it('reverses pre-migration Cloud order quota stored in base quota', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await adminHeaders(app)
await seedSettings(app, headers)
const orgId = await getFirstOrgId(db)
await db.run(sql`UPDATE org_quotas SET quota = 8192, traffic_quota = 4096 WHERE org_id = ${orgId}`)
const decrease = await postWebhook(
app,
JSON.stringify({
eventId: 'evt-legacy-base-decrease',
cloudOrderId: 'order-legacy-base',
targetOrgId: orgId,
eventType: 'order.quota_changed',
direction: 'decrease',
storageBytes: 2048,
trafficBytes: 1024,
source: 'stripe',
}),
)
expect(decrease.status).toBe(200)
await expect(decrease.json()).resolves.toMatchObject({ success: true, duplicate: false })
const rows = await db.all<{ quota: number; trafficQuota: number }>(
sql`SELECT quota, traffic_quota AS trafficQuota FROM org_quotas WHERE org_id = ${orgId}`,
)
expect(rows[0]).toEqual({ quota: 6144, trafficQuota: 3072 })
})
it('records decrease audit event with correct action and metadata', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
@@ -1937,10 +2211,10 @@ describe('Quota Store API', () => {
expect(decrease.status).toBe(200)
await expect(decrease.json()).resolves.toMatchObject({ success: true, duplicate: false })
const rows = await db.all<{ trafficQuota: number }>(
sql`SELECT traffic_quota AS trafficQuota FROM org_quotas WHERE org_id = ${orgId}`,
const rows = await db.all<{ status: string }>(
sql`SELECT status FROM org_quota_entitlements WHERE source_id = 'order-traffic-reversal'`,
)
expect(rows[0].trafficQuota).toBe(0)
expect(rows[0].status).toBe('revoked')
})
it('rejects failed delivery retries when the payload hash changes', async () => {
@@ -1,7 +1,7 @@
import { eq, sql } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { orgQuotas } from '../db/schema.js'
import { orgQuotaEntitlements, orgQuotas } from '../db/schema.js'
import { currentTrafficPeriod } from '../services/effective-quota.js'
import { S3Service } from '../services/s3.js'
import { authedHeaders, createTestApp, seedProLicense } from '../test/setup.js'
@@ -80,6 +80,28 @@ async function setOrgQuota(
}
}
async function addStorageEntitlement(
db: Awaited<ReturnType<typeof createTestApp>>['db'],
orgId: string,
bytes: number,
) {
const now = new Date()
await db.insert(orgQuotaEntitlements).values({
id: nanoid(),
orgId,
resourceType: 'storage',
source: 'test',
sourceId: nanoid(),
bytes,
startsAt: now,
expiresAt: null,
status: 'active',
metadata: null,
createdAt: now,
updatedAt: now,
})
}
// ─── POST /api/objects/copy — quota enforcement ──────────────────────────
describe('POST /api/objects/copy — quota enforcement', () => {
@@ -332,6 +354,49 @@ describe('PATCH /api/objects/:id (action: confirm) — quota enforcement via con
expect(quotaRows[0].used).toBe(350)
})
it('uses active storage entitlements when confirming upload', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 100, 90)
await addStorageEntitlement(db, orgId, 100)
await insertFile(db, orgId, { id: 'm-done-entitlement', name: 'entitled.txt', size: 50, status: 'draft' })
const res = await app.request('/api/objects/m-done-entitlement', {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'confirm' }),
})
expect(res.status).toBe(200)
const quotaRows = await db.all<{ used: number; quota: number }>(
sql`SELECT used, quota FROM org_quotas WHERE org_id = ${orgId}`,
)
expect(quotaRows[0]).toEqual({ used: 140, quota: 100 })
})
it('enforces storage entitlements when base quota is unlimited', async () => {
const { app, db } = await createTestApp()
await seedProLicense(db)
const headers = await authedHeaders(app)
await insertStorage(db)
const orgId = await getOrgId(db)
await setOrgQuota(db, orgId, 0, 90)
await addStorageEntitlement(db, orgId, 100)
await insertFile(db, orgId, { id: 'm-done-zero-base-entitlement', name: 'limited.txt', size: 11, status: 'draft' })
const res = await app.request('/api/objects/m-done-zero-base-entitlement', {
method: 'PATCH',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'confirm' }),
})
expect(res.status).toBe(422)
await expect(res.json()).resolves.toMatchObject({ error: 'Quota exceeded' })
})
it('returns 200 and increments storages.used when quota allows', async () => {
const { app, db } = await createTestApp()
const headers = await authedHeaders(app)
+63
View File
@@ -241,6 +241,38 @@ describe('Admin Quotas API', () => {
expect(body.items[0].orgName).toBeTruthy()
expect(body.items[0].orgType).toBe('personal')
})
it('GET /api/admin/quotas lists effective quota with active entitlements', async () => {
const { app, db } = await createTestApp()
const headers = await adminHeaders(app)
const orgs = await db.all<{ id: string }>(
sql`SELECT o.id FROM organization o WHERE o.metadata LIKE '%"type":"personal"%' LIMIT 1`,
)
const orgId = orgs[0].id
const now = Date.now()
await db.run(sql`UPDATE org_quotas SET quota = 5000, traffic_quota = 1000 WHERE org_id = ${orgId}`)
await db.run(sql`
INSERT INTO org_quota_entitlements
(id, org_id, resource_type, source, source_id, bytes, starts_at, expires_at, status, metadata, created_at, updated_at)
VALUES
('ent-admin-storage', ${orgId}, 'storage', 'test', 'admin-storage', 3000, ${now}, NULL, 'active', NULL, ${now}, ${now}),
('ent-admin-traffic', ${orgId}, 'traffic', 'test', 'admin-traffic', 2000, ${now}, NULL, 'active', NULL, ${now}, ${now}),
('ent-admin-revoked', ${orgId}, 'storage', 'test', 'admin-revoked', 9000, ${now}, NULL, 'revoked', NULL, ${now}, ${now})
`)
const res = await app.request('/api/admin/quotas', { headers })
expect(res.status).toBe(200)
const body = (await res.json()) as { items: Array<Record<string, unknown>> }
expect(body.items[0]).toMatchObject({
baseQuota: 5000,
entitlementQuota: 3000,
quota: 8000,
baseTrafficQuota: 1000,
entitlementTrafficQuota: 2000,
trafficQuota: 3000,
})
})
})
describe('User Quotas API — /api/quotas', () => {
@@ -308,6 +340,37 @@ describe('User Quotas API — /api/quotas', () => {
expect(body.trafficUsed).toBe(0)
})
it('GET /api/quotas/me returns base quota plus active entitlements', async () => {
const { app, db } = await createTestApp()
const adminH = await adminHeaders(app)
const orgs = await db.all<{ id: string }>(
sql`SELECT o.id FROM organization o WHERE o.metadata LIKE '%"type":"personal"%' LIMIT 1`,
)
const orgId = orgs[0].id
const now = Date.now()
await db.run(sql`UPDATE org_quotas SET quota = 1000, traffic_quota = 2000 WHERE org_id = ${orgId}`)
await db.run(sql`
INSERT INTO org_quota_entitlements
(id, org_id, resource_type, source, source_id, bytes, starts_at, expires_at, status, metadata, created_at, updated_at)
VALUES
('ent-user-storage', ${orgId}, 'storage', 'test', 'user-storage', 4000, ${now}, NULL, 'active', NULL, ${now}, ${now}),
('ent-user-traffic', ${orgId}, 'traffic', 'test', 'user-traffic', 6000, ${now}, NULL, 'active', NULL, ${now}, ${now})
`)
const res = await app.request('/api/quotas/me', { headers: adminH })
expect(res.status).toBe(200)
const body = (await res.json()) as Record<string, unknown>
expect(body).toMatchObject({
baseQuota: 1000,
entitlementQuota: 4000,
quota: 5000,
baseTrafficQuota: 2000,
entitlementTrafficQuota: 6000,
trafficQuota: 8000,
})
})
it('admin quota updates current org quota without historical grant aggregation', async () => {
const { app, db } = await createTestApp()
const adminH = await adminHeaders(app)
+23 -3
View File
@@ -4,7 +4,7 @@ import { Hono } from 'hono'
import { nanoid } from 'nanoid'
import { z } from 'zod'
import { organization } from '../db/auth-schema'
import { orgQuotas } from '../db/schema'
import { orgQuotaEntitlements, orgQuotas } from '../db/schema'
import { requireAdmin, requireAuth } from '../middleware/auth'
import type { Env } from '../middleware/platform'
import { recordActivity } from '../services/activity'
@@ -21,6 +21,7 @@ const adminQuotas = new Hono<Env>()
.get('/', async (c) => {
const db = c.get('platform').db
const period = currentTrafficPeriod()
const now = new Date()
await db
.update(orgQuotas)
@@ -32,7 +33,10 @@ const adminQuotas = new Hono<Env>()
id: orgQuotas.id,
orgId: orgQuotas.orgId,
baseQuota: orgQuotas.quota,
entitlementQuota: activeEntitlementBytesSql('storage', now),
used: orgQuotas.used,
baseTrafficQuota: orgQuotas.trafficQuota,
entitlementTrafficQuota: activeEntitlementBytesSql('traffic', now),
trafficQuota: orgQuotas.trafficQuota,
trafficUsed: orgQuotas.trafficUsed,
trafficPeriod: orgQuotas.trafficPeriod,
@@ -47,9 +51,12 @@ const adminQuotas = new Hono<Env>()
id: r.id,
orgId: r.orgId,
baseQuota: r.baseQuota,
quota: r.baseQuota,
entitlementQuota: r.entitlementQuota,
quota: r.baseQuota + r.entitlementQuota,
used: r.used,
trafficQuota: r.trafficQuota,
baseTrafficQuota: r.baseTrafficQuota,
entitlementTrafficQuota: r.entitlementTrafficQuota,
trafficQuota: r.trafficQuota + r.entitlementTrafficQuota,
trafficUsed: r.trafficUsed,
trafficPeriod: r.trafficPeriod,
orgName: r.orgName,
@@ -120,3 +127,16 @@ function parseOrgType(metadata: string | null): string {
return 'unknown'
}
}
function activeEntitlementBytesSql(resourceType: 'storage' | 'traffic', now: Date) {
const timestamp = now.getTime()
return sql<number>`(
SELECT COALESCE(SUM(${orgQuotaEntitlements.bytes}), 0)
FROM ${orgQuotaEntitlements}
WHERE ${orgQuotaEntitlements.orgId} = ${orgQuotas.orgId}
AND ${orgQuotaEntitlements.resourceType} = ${resourceType}
AND ${orgQuotaEntitlements.status} = 'active'
AND ${orgQuotaEntitlements.startsAt} <= ${timestamp}
AND (${orgQuotaEntitlements.expiresAt} IS NULL OR ${orgQuotaEntitlements.expiresAt} > ${timestamp})
)`
}
+161 -29
View File
@@ -1,34 +1,57 @@
import { describe, expect, it } from 'vitest'
import { activityEvents, orgQuotas, webhookEvents } from '../db/schema'
import { activityEvents, orgQuotaEntitlements, orgQuotas, webhookEvents } from '../db/schema'
import type { Database } from '../platform/interface'
import { processCloudOrderQuotaChange } from './cloud-store'
function createAsyncDb(quotaRows: Array<{ id: string }> = [{ id: 'quota-1' }]) {
function createAsyncDb(
quotaRows: Array<{ id: string }> = [{ id: 'quota-1' }],
entitlementRevokeRows: Array<{ id: string }> = [{ id: 'entitlement-revoked' }],
existingEntitlementRows: Array<{ id: string }> = [],
) {
const state = {
audits: 0,
webhookStatus: '',
quotaUpdates: 0,
entitlementInserts: 0,
entitlementRevokes: 0,
legacyQuotaUpdates: 0,
}
const db = {
constructor: { name: 'AsyncTestDatabase' },
transaction: async (fn: (tx: unknown) => Promise<void>) => fn(db),
insert: (table: unknown) => ({
values: async (values: Record<string, unknown>) => {
if (table === webhookEvents) state.webhookStatus = String(values.status)
if (table === activityEvents) state.audits += 1
values: (values: Record<string, unknown>) => {
const apply = () => {
if (table === webhookEvents) state.webhookStatus = String(values.status)
if (table === activityEvents) state.audits += 1
if (table === orgQuotaEntitlements) state.entitlementInserts += Array.isArray(values) ? values.length : 1
}
if (table === orgQuotaEntitlements) return { onConflictDoUpdate: async () => apply() }
return Promise.resolve(apply())
},
}),
select: () => ({
from: (table: unknown) => ({
where: () => ({
limit: async () => {
if (table === orgQuotas) return quotaRows
if (table === orgQuotaEntitlements) return existingEntitlementRows
return []
},
}),
}),
}),
update: (table: unknown) => ({
set: (values: Record<string, unknown>) => ({
where: () => {
if (table === orgQuotas) {
if (table === orgQuotaEntitlements) {
return {
returning: async () => {
state.quotaUpdates += 1
return quotaRows
state.entitlementRevokes += 1
return entitlementRevokeRows
},
}
}
if (table === orgQuotas) state.legacyQuotaUpdates += 1
if (table === webhookEvents) state.webhookStatus = String(values.status)
return Promise.resolve()
},
@@ -53,7 +76,8 @@ function createUniqueConflictDb(existing: { id: string; payloadHash: string; sta
const state = {
audits: 0,
webhookStatus: existing?.status ?? '',
quotaUpdates: 0,
entitlementInserts: 0,
entitlementRevokes: 0,
}
const db = {
constructor: { name: 'AsyncTestDatabase' },
@@ -62,24 +86,29 @@ function createUniqueConflictDb(existing: { id: string; payloadHash: string; sta
values: async (values: Record<string, unknown>) => {
if (table === webhookEvents) throw new Error('UNIQUE constraint failed: webhook_events.source, event_id')
if (table === activityEvents) state.audits += 1
if (table === orgQuotaEntitlements) state.entitlementInserts += Array.isArray(values) ? values.length : 1
return values
},
}),
select: () => ({
from: () => ({
from: (table: unknown) => ({
where: () => ({
limit: async () => (existing ? [existing] : []),
limit: async () => {
if (table === webhookEvents) return existing ? [existing] : []
if (table === orgQuotas) return [{ id: 'quota-retry' }]
return []
},
}),
}),
}),
update: (table: unknown) => ({
set: (values: Record<string, unknown>) => ({
where: () => {
if (table === orgQuotas) {
if (table === orgQuotaEntitlements) {
return {
returning: async () => {
state.quotaUpdates += 1
return [{ id: 'quota-retry' }]
state.entitlementRevokes += 1
return [{ id: 'entitlement-revoked' }]
},
}
}
@@ -93,11 +122,16 @@ function createUniqueConflictDb(existing: { id: string; payloadHash: string; sta
return { db: db as unknown as Database, state }
}
function createSyncDb() {
function createSyncDb(
entitlementRevokeRows: Array<{ id: string }> = [{ id: 'entitlement-revoked' }],
existingEntitlementRows: Array<{ id: string }> = [],
) {
const state = {
audits: 0,
webhookStatus: '',
quotaUpdates: 0,
entitlementInserts: 0,
entitlementRevokes: 0,
legacyQuotaUpdates: 0,
}
class BetterSQLite3Database {
@@ -108,30 +142,50 @@ function createSyncDb() {
insert(table: unknown) {
return {
values: (values: Record<string, unknown>) => {
const apply = () => {
if (table === webhookEvents) state.webhookStatus = String(values.status)
if (table === activityEvents) state.audits += 1
if (table === orgQuotaEntitlements) state.entitlementInserts += Array.isArray(values) ? values.length : 1
}
return {
run: () => {
if (table === webhookEvents) state.webhookStatus = String(values.status)
if (table === activityEvents) state.audits += 1
},
run: apply,
onConflictDoUpdate: () => ({ run: apply }),
}
},
}
}
select() {
return {
from: (table: unknown) => ({
where: () => ({
limit: () => ({
all: () => {
if (table === orgQuotas) return [{ id: 'quota-sync' }]
if (table === orgQuotaEntitlements) return existingEntitlementRows
return []
},
}),
}),
}),
}
}
update(table: unknown) {
return {
set: (values: Record<string, unknown>) => ({
where: () => {
if (table === orgQuotas) {
if (table === orgQuotaEntitlements) {
return {
returning: () => ({
all: () => {
state.quotaUpdates += 1
return [{ id: 'quota-sync' }]
state.entitlementRevokes += 1
return entitlementRevokeRows
},
}),
}
}
if (table === orgQuotas) state.legacyQuotaUpdates += 1
state.webhookStatus = String(values.status)
return { run: () => undefined }
},
@@ -160,7 +214,7 @@ describe('processCloudOrderQuotaChange', () => {
duplicate: false,
eventId: 'evt-async',
})
expect(state).toMatchObject({ audits: 1, webhookStatus: 'processed', quotaUpdates: 1 })
expect(state).toMatchObject({ audits: 1, webhookStatus: 'processed', entitlementInserts: 1 })
})
it('processes quota change with a sync transaction database', async () => {
@@ -179,7 +233,85 @@ describe('processCloudOrderQuotaChange', () => {
duplicate: false,
eventId: 'evt-sync',
})
expect(state).toMatchObject({ audits: 1, webhookStatus: 'processed', quotaUpdates: 1 })
expect(state).toMatchObject({ audits: 1, webhookStatus: 'processed', entitlementRevokes: 1 })
})
it('applies legacy base quota decrease with a sync transaction database', async () => {
const { db, state } = createSyncDb([], [])
const event = {
eventId: 'evt-sync-legacy',
eventType: 'order.quota_changed' as const,
cloudOrderId: 'order-sync-legacy',
targetOrgId: 'org-sync-legacy',
direction: 'decrease' as const,
storageBytes: 4096,
trafficBytes: 0,
}
await expect(processCloudOrderQuotaChange(db, event, JSON.stringify(event), 'hash-sync-legacy')).resolves.toEqual({
duplicate: false,
eventId: 'evt-sync-legacy',
})
expect(state).toMatchObject({ audits: 1, webhookStatus: 'processed', legacyQuotaUpdates: 1 })
})
it('does not apply sync legacy base decrease when a matching entitlement already exists', async () => {
const { db, state } = createSyncDb([], [{ id: 'entitlement-revoked' }])
const event = {
eventId: 'evt-sync-existing',
eventType: 'order.quota_changed' as const,
cloudOrderId: 'order-sync-existing',
targetOrgId: 'org-sync-existing',
direction: 'decrease' as const,
storageBytes: 4096,
trafficBytes: 0,
}
await expect(processCloudOrderQuotaChange(db, event, JSON.stringify(event), 'hash-sync-existing')).resolves.toEqual(
{
duplicate: false,
eventId: 'evt-sync-existing',
},
)
expect(state).toMatchObject({ audits: 1, webhookStatus: 'processed', legacyQuotaUpdates: 0 })
})
it('applies legacy base quota decrease when no entitlement exists for the Cloud order', async () => {
const { db, state } = createAsyncDb([{ id: 'quota-legacy' }], [], [])
const event = {
eventId: 'evt-legacy-decrease',
eventType: 'order.quota_changed' as const,
cloudOrderId: 'order-legacy-decrease',
targetOrgId: 'org-legacy',
direction: 'decrease' as const,
storageBytes: 4096,
trafficBytes: 2048,
}
await expect(processCloudOrderQuotaChange(db, event, JSON.stringify(event), 'hash-legacy')).resolves.toEqual({
duplicate: false,
eventId: 'evt-legacy-decrease',
})
expect(state).toMatchObject({ audits: 1, webhookStatus: 'processed', legacyQuotaUpdates: 1 })
})
it('does not apply legacy base decrease when a matching entitlement already exists', async () => {
const { db, state } = createAsyncDb([{ id: 'quota-existing-entitlement' }], [], [{ id: 'entitlement-revoked' }])
const event = {
eventId: 'evt-existing-entitlement-decrease',
eventType: 'order.quota_changed' as const,
cloudOrderId: 'order-existing-entitlement',
targetOrgId: 'org-existing-entitlement',
direction: 'decrease' as const,
storageBytes: 4096,
trafficBytes: 0,
}
await expect(processCloudOrderQuotaChange(db, event, JSON.stringify(event), 'hash-existing')).resolves.toEqual({
duplicate: false,
eventId: 'evt-existing-entitlement-decrease',
})
expect(state).toMatchObject({ audits: 1, webhookStatus: 'processed', legacyQuotaUpdates: 0 })
})
it('marks async quota change failed when the target quota is missing', async () => {
@@ -197,7 +329,7 @@ describe('processCloudOrderQuotaChange', () => {
await expect(processCloudOrderQuotaChange(db, event, JSON.stringify(event), 'hash-missing')).rejects.toThrow(
'target_quota_missing',
)
expect(state).toMatchObject({ audits: 0, webhookStatus: 'failed', quotaUpdates: 1 })
expect(state).toMatchObject({ audits: 0, webhookStatus: 'failed', entitlementRevokes: 0 })
})
it('surfaces quota change webhook insert failures', async () => {
@@ -236,7 +368,7 @@ describe('processCloudOrderQuotaChange', () => {
duplicate: true,
eventId: 'evt-duplicate',
})
expect(state).toMatchObject({ audits: 0, webhookStatus: 'processed', quotaUpdates: 0 })
expect(state).toMatchObject({ audits: 0, webhookStatus: 'processed', entitlementInserts: 0 })
})
it('rejects duplicate webhook events when the payload hash changes', async () => {
@@ -280,6 +412,6 @@ describe('processCloudOrderQuotaChange', () => {
duplicate: false,
eventId: 'evt-retry',
})
expect(state).toMatchObject({ audits: 1, webhookStatus: 'processed', quotaUpdates: 1 })
expect(state).toMatchObject({ audits: 1, webhookStatus: 'processed', entitlementRevokes: 2 })
})
})
+227 -25
View File
@@ -3,7 +3,7 @@ import type { CloudStoreSettings, CloudStoreTarget } from '@shared/types'
import { and, eq, inArray, sql } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { member, organization, user } from '../db/auth-schema'
import { activityEvents, orgQuotas, systemOptions, webhookEvents } from '../db/schema'
import { activityEvents, orgQuotaEntitlements, orgQuotas, systemOptions, webhookEvents } from '../db/schema'
import { loadActiveLicenseBinding } from '../licensing/license-state'
import type { Database } from '../platform/interface'
@@ -154,43 +154,33 @@ async function processQuotaChangeTransaction(
}
async function applyQuotaChange(db: Database, event: CloudOrderQuotaChange): Promise<void> {
const rows = await db
.update(orgQuotas)
.set(quotaUpdateValues(event))
.where(eq(orgQuotas.orgId, event.targetOrgId))
.returning({ id: orgQuotas.id })
await requireTargetQuota(db, event.targetOrgId)
if (rows.length === 0) throw new Error('target_quota_missing')
const now = new Date(event.occurredAt ?? Date.now())
if (event.direction === 'increase') {
await insertQuotaEntitlements(db, event, now)
return
}
await revokeQuotaEntitlements(db, event, now)
}
function applyQuotaChangeSync(db: Database, event: CloudOrderQuotaChange): void {
const rows = (
db
.update(orgQuotas)
.set(quotaUpdateValues(event))
.where(eq(orgQuotas.orgId, event.targetOrgId))
.returning({ id: orgQuotas.id }) as {
db.select({ id: orgQuotas.id }).from(orgQuotas).where(eq(orgQuotas.orgId, event.targetOrgId)).limit(1) as {
all(): Array<{ id: string }>
}
).all()
if (rows.length === 0) throw new Error('target_quota_missing')
}
function quotaUpdateValues(event: CloudOrderQuotaChange) {
return {
quota: quotaUpdateExpression(orgQuotas.quota, event.storageBytes, event.direction),
trafficQuota: quotaUpdateExpression(orgQuotas.trafficQuota, event.trafficBytes, event.direction),
const now = new Date(event.occurredAt ?? Date.now())
if (event.direction === 'increase') {
insertQuotaEntitlementsSync(db, event, now)
return
}
}
function quotaUpdateExpression(
column: typeof orgQuotas.quota | typeof orgQuotas.trafficQuota,
bytes: number,
direction: CloudOrderQuotaChange['direction'],
) {
if (direction === 'increase') return sql`${column} + ${bytes}`
return sql`MAX(0, ${column} - ${bytes})`
revokeQuotaEntitlementsSync(db, event, now)
}
async function recordQuotaChangeAudit(db: Database, event: CloudOrderQuotaChange): Promise<void> {
@@ -222,6 +212,218 @@ function quotaChangeAuditValues(event: CloudOrderQuotaChange): typeof activityEv
}
}
async function requireTargetQuota(db: Database, orgId: string): Promise<void> {
const rows = await db.select({ id: orgQuotas.id }).from(orgQuotas).where(eq(orgQuotas.orgId, orgId)).limit(1)
if (rows.length === 0) throw new Error('target_quota_missing')
}
async function insertQuotaEntitlements(db: Database, event: CloudOrderQuotaChange, now: Date): Promise<void> {
const values = quotaEntitlementValues(event, now)
if (values.length === 0) return
for (const value of values) {
await db
.insert(orgQuotaEntitlements)
.values(value)
.onConflictDoUpdate({
target: [orgQuotaEntitlements.source, orgQuotaEntitlements.sourceId, orgQuotaEntitlements.resourceType],
set: quotaEntitlementIncreaseValues(value, now),
})
}
}
function insertQuotaEntitlementsSync(db: Database, event: CloudOrderQuotaChange, now: Date): void {
const values = quotaEntitlementValues(event, now)
if (values.length === 0) return
for (const value of values) {
;(
db
.insert(orgQuotaEntitlements)
.values(value)
.onConflictDoUpdate({
target: [orgQuotaEntitlements.source, orgQuotaEntitlements.sourceId, orgQuotaEntitlements.resourceType],
set: quotaEntitlementIncreaseValues(value, now),
}) as { run(): void }
).run()
}
}
async function revokeQuotaEntitlements(db: Database, event: CloudOrderQuotaChange, now: Date): Promise<void> {
const storageRevoked = await revokeQuotaEntitlement(db, event, 'storage', event.storageBytes, now)
const trafficRevoked = await revokeQuotaEntitlement(db, event, 'traffic', event.trafficBytes, now)
await applyLegacyQuotaDecrease(db, event, storageRevoked, trafficRevoked)
}
function revokeQuotaEntitlementsSync(db: Database, event: CloudOrderQuotaChange, now: Date): void {
const storageRevoked = revokeQuotaEntitlementSync(db, event, 'storage', event.storageBytes, now)
const trafficRevoked = revokeQuotaEntitlementSync(db, event, 'traffic', event.trafficBytes, now)
applyLegacyQuotaDecreaseSync(db, event, storageRevoked, trafficRevoked)
}
async function revokeQuotaEntitlement(
db: Database,
event: CloudOrderQuotaChange,
resourceType: 'storage' | 'traffic',
bytes: number,
now: Date,
): Promise<boolean> {
if (bytes === 0) return true
const rows = await db
.update(orgQuotaEntitlements)
.set(quotaEntitlementDecreaseValues(bytes, now))
.where(quotaEntitlementMatch(event, resourceType))
.returning({ id: orgQuotaEntitlements.id })
if (rows.length > 0) return true
const existing = await db
.select({ id: orgQuotaEntitlements.id })
.from(orgQuotaEntitlements)
.where(quotaEntitlementSourceMatch(event, resourceType))
.limit(1)
return existing.length > 0
}
function revokeQuotaEntitlementSync(
db: Database,
event: CloudOrderQuotaChange,
resourceType: 'storage' | 'traffic',
bytes: number,
now: Date,
): boolean {
if (bytes === 0) return true
const rows = (
db
.update(orgQuotaEntitlements)
.set(quotaEntitlementDecreaseValues(bytes, now))
.where(quotaEntitlementMatch(event, resourceType)) as {
returning(fields: { id: typeof orgQuotaEntitlements.id }): { all(): Array<{ id: string }> }
}
)
.returning({ id: orgQuotaEntitlements.id })
.all()
if (rows.length > 0) return true
const existing = (
db
.select({ id: orgQuotaEntitlements.id })
.from(orgQuotaEntitlements)
.where(quotaEntitlementSourceMatch(event, resourceType))
.limit(1) as { all(): Array<{ id: string }> }
).all()
return existing.length > 0
}
async function applyLegacyQuotaDecrease(
db: Database,
event: CloudOrderQuotaChange,
storageRevoked: boolean,
trafficRevoked: boolean,
): Promise<void> {
const values = legacyQuotaDecreaseValues(event, storageRevoked, trafficRevoked)
if (!values) return
await db.update(orgQuotas).set(values).where(eq(orgQuotas.orgId, event.targetOrgId))
}
function applyLegacyQuotaDecreaseSync(
db: Database,
event: CloudOrderQuotaChange,
storageRevoked: boolean,
trafficRevoked: boolean,
): void {
const values = legacyQuotaDecreaseValues(event, storageRevoked, trafficRevoked)
if (!values) return
;(db.update(orgQuotas).set(values).where(eq(orgQuotas.orgId, event.targetOrgId)) as { run(): void }).run()
}
function legacyQuotaDecreaseValues(
event: CloudOrderQuotaChange,
storageRevoked: boolean,
trafficRevoked: boolean,
): Partial<typeof orgQuotas.$inferInsert> | null {
const values: Partial<typeof orgQuotas.$inferInsert> = {}
if (!storageRevoked && event.storageBytes > 0)
values.quota = sql`MAX(0, ${orgQuotas.quota} - ${event.storageBytes})` as unknown as number
if (!trafficRevoked && event.trafficBytes > 0) {
values.trafficQuota = sql`MAX(0, ${orgQuotas.trafficQuota} - ${event.trafficBytes})` as unknown as number
}
return Object.keys(values).length === 0 ? null : values
}
function quotaEntitlementValues(event: CloudOrderQuotaChange, now: Date): (typeof orgQuotaEntitlements.$inferInsert)[] {
return [
quotaEntitlementValue(event, 'storage', event.storageBytes, now),
quotaEntitlementValue(event, 'traffic', event.trafficBytes, now),
].filter((value): value is typeof orgQuotaEntitlements.$inferInsert => value !== null)
}
function quotaEntitlementIncreaseValues(value: typeof orgQuotaEntitlements.$inferInsert, now: Date) {
return {
bytes: sql`CASE
WHEN ${orgQuotaEntitlements.status} = 'active' THEN ${orgQuotaEntitlements.bytes} + ${value.bytes}
ELSE ${value.bytes}
END`,
status: 'active',
expiresAt: value.expiresAt,
metadata: value.metadata,
updatedAt: now,
}
}
function quotaEntitlementDecreaseValues(bytes: number, now: Date) {
return {
bytes: sql`MAX(0, ${orgQuotaEntitlements.bytes} - ${bytes})`,
status:
sql`CASE WHEN ${orgQuotaEntitlements.bytes} <= ${bytes} THEN 'revoked' ELSE 'active' END` as unknown as string,
updatedAt: now,
}
}
function quotaEntitlementValue(
event: CloudOrderQuotaChange,
resourceType: 'storage' | 'traffic',
bytes: number,
now: Date,
): typeof orgQuotaEntitlements.$inferInsert | null {
if (bytes === 0) return null
return {
id: nanoid(),
orgId: event.targetOrgId,
resourceType,
source: 'cloud_order',
sourceId: event.cloudOrderId,
bytes,
startsAt: now,
expiresAt: null,
status: 'active',
metadata: JSON.stringify(quotaEntitlementMetadata(event)),
createdAt: now,
updatedAt: now,
}
}
function quotaEntitlementMatch(event: CloudOrderQuotaChange, resourceType: 'storage' | 'traffic') {
return and(quotaEntitlementSourceMatch(event, resourceType), eq(orgQuotaEntitlements.status, 'active'))
}
function quotaEntitlementSourceMatch(event: CloudOrderQuotaChange, resourceType: 'storage' | 'traffic') {
return and(
eq(orgQuotaEntitlements.orgId, event.targetOrgId),
eq(orgQuotaEntitlements.resourceType, resourceType),
eq(orgQuotaEntitlements.source, 'cloud_order'),
eq(orgQuotaEntitlements.sourceId, event.cloudOrderId),
)
}
function quotaEntitlementMetadata(event: CloudOrderQuotaChange) {
return {
eventId: event.eventId,
eventType: event.eventType,
source: event.source ?? null,
packageId: event.packageId ?? null,
terminalUserId: event.terminalUserId ?? null,
terminalUserEmail: event.terminalUserEmail ?? null,
}
}
async function beginWebhookEvent(
db: Database,
event: CloudOrderQuotaChange,
+120 -1
View File
@@ -1,11 +1,12 @@
import { eq, sql } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { describe, expect, it } from 'vitest'
import { orgQuotas } from '../db/schema.js'
import { orgQuotaEntitlements, orgQuotas } from '../db/schema.js'
import { createTestApp } from '../test/setup.js'
import {
consumeTrafficIfQuotaAllows,
getEffectiveQuota,
hasQuotaForBytes,
hasTrafficQuotaForBytes,
refundTraffic,
} from './effective-quota.js'
@@ -35,6 +36,39 @@ describe('effective quota', () => {
})
})
it('adds active entitlement bytes to effective storage and traffic quota', async () => {
const { db } = await createTestApp()
const orgId = nanoid()
const now = new Date('2026-05-06T00:00:00Z')
await db.insert(orgQuotas).values({
id: nanoid(),
orgId,
quota: 1000,
used: 250,
trafficQuota: 2000,
trafficUsed: 500,
trafficPeriod: '2026-05',
})
await db.insert(orgQuotaEntitlements).values([
entitlement(orgId, 'storage', 'active-storage', 300, 'active', now),
entitlement(orgId, 'traffic', 'active-traffic', 700, 'active', now),
entitlement(orgId, 'storage', 'revoked-storage', 900, 'revoked', now),
{
...entitlement(orgId, 'storage', 'expired-storage', 900, 'active', now),
expiresAt: new Date('2026-05-05T00:00:00Z'),
},
])
await expect(getEffectiveQuota(db, orgId, now)).resolves.toMatchObject({
baseQuota: 1000,
entitlementQuota: 300,
quota: 1300,
baseTrafficQuota: 2000,
entitlementTrafficQuota: 700,
trafficQuota: 2700,
})
})
it('resets monthly traffic usage when the period changes', async () => {
const { db } = await createTestApp()
const orgId = nanoid()
@@ -79,6 +113,48 @@ describe('effective quota', () => {
expect(rows[0].trafficUsed).toBe(1000)
})
it('consumes traffic against active traffic entitlements', async () => {
const { db } = await createTestApp()
const orgId = nanoid()
const now = new Date('2026-05-06T00:00:00Z')
await db.insert(orgQuotas).values({
id: nanoid(),
orgId,
quota: 1000,
used: 0,
trafficQuota: 1000,
trafficUsed: 900,
trafficPeriod: '2026-05',
})
await db.insert(orgQuotaEntitlements).values(entitlement(orgId, 'traffic', 'traffic-overage', 500, 'active', now))
await expect(consumeTrafficIfQuotaAllows(db, orgId, 400, now)).resolves.toBe(true)
await expect(consumeTrafficIfQuotaAllows(db, orgId, 201, now)).resolves.toBe(false)
const rows = await db.select().from(orgQuotas).where(eq(orgQuotas.orgId, orgId))
expect(rows[0].trafficUsed).toBe(1300)
})
it('treats zero base traffic quota as limited when traffic entitlements exist', async () => {
const { db } = await createTestApp()
const orgId = nanoid()
const now = new Date('2026-05-06T00:00:00Z')
await db.insert(orgQuotas).values({
id: nanoid(),
orgId,
quota: 0,
used: 0,
trafficQuota: 0,
trafficUsed: 400,
trafficPeriod: '2026-05',
})
await db.insert(orgQuotaEntitlements).values(entitlement(orgId, 'traffic', 'traffic-zero-base', 500, 'active', now))
await expect(hasTrafficQuotaForBytes(db, orgId, 100, now)).resolves.toBe(true)
await expect(consumeTrafficIfQuotaAllows(db, orgId, 100, now)).resolves.toBe(true)
await expect(consumeTrafficIfQuotaAllows(db, orgId, 1, now)).resolves.toBe(false)
})
it('refunds current monthly traffic usage', async () => {
const { db } = await createTestApp()
const orgId = nanoid()
@@ -179,4 +255,47 @@ describe('effective quota', () => {
await expect(hasTrafficQuotaForBytes(db, orgId, 1024, now)).resolves.toBe(true)
await expect(consumeTrafficIfQuotaAllows(db, orgId, 1024, now)).resolves.toBe(true)
})
it('treats zero base storage quota as limited when storage entitlements exist', async () => {
const { db } = await createTestApp()
const orgId = nanoid()
const now = new Date('2026-05-06T00:00:00Z')
await db.insert(orgQuotas).values({
id: nanoid(),
orgId,
quota: 0,
used: 400,
trafficQuota: 0,
trafficUsed: 0,
trafficPeriod: '2026-05',
})
await db.insert(orgQuotaEntitlements).values(entitlement(orgId, 'storage', 'storage-zero-base', 500, 'active', now))
await expect(hasQuotaForBytes(db, orgId, 100)).resolves.toBe(true)
await expect(hasQuotaForBytes(db, orgId, 101)).resolves.toBe(false)
})
})
function entitlement(
orgId: string,
resourceType: 'storage' | 'traffic',
sourceId: string,
bytes: number,
status: string,
now: Date,
): typeof orgQuotaEntitlements.$inferInsert {
return {
id: nanoid(),
orgId,
resourceType,
source: 'test',
sourceId,
bytes,
startsAt: now,
expiresAt: null,
status,
metadata: null,
createdAt: now,
updatedAt: now,
}
}
+69 -9
View File
@@ -1,12 +1,15 @@
import { eq, sql } from 'drizzle-orm'
import { orgQuotas, storages } from '../db/schema'
import { and, eq, or, sql } from 'drizzle-orm'
import { orgQuotaEntitlements, orgQuotas, storages } from '../db/schema'
import type { Database } from '../platform/interface'
export interface EffectiveQuota {
orgId: string
baseQuota: number
entitlementQuota: number
quota: number
used: number
baseTrafficQuota: number
entitlementTrafficQuota: number
trafficQuota: number
trafficUsed: number
trafficPeriod: string
@@ -39,14 +42,20 @@ export async function getEffectiveQuota(db: Database, orgId: string, now = new D
const quotaRow = quotaRows[0]
const baseQuota = quotaRow?.baseQuota ?? 0
const entitlementQuota = await activeEntitlementBytes(db, orgId, 'storage', now)
const entitlementTrafficQuota = await activeEntitlementBytes(db, orgId, 'traffic', now)
const trafficUsed = quotaRow && quotaRow.trafficPeriod === period ? quotaRow.trafficUsed : 0
const trafficPeriod = quotaRow?.trafficPeriod === period ? quotaRow.trafficPeriod : period
const baseTrafficQuota = quotaRow?.trafficQuota ?? 0
return {
orgId,
baseQuota,
quota: baseQuota,
entitlementQuota,
quota: baseQuota + entitlementQuota,
used: quotaRow?.used ?? 0,
trafficQuota: quotaRow?.trafficQuota ?? 0,
baseTrafficQuota,
entitlementTrafficQuota,
trafficQuota: baseTrafficQuota + entitlementTrafficQuota,
trafficUsed,
trafficPeriod,
}
@@ -55,7 +64,7 @@ export async function getEffectiveQuota(db: Database, orgId: string, now = new D
export async function hasQuotaForBytes(db: Database, orgId: string, bytes: number): Promise<boolean> {
if (bytes <= 0) return true
const quota = await getEffectiveQuota(db, orgId)
if (quota.baseQuota === 0) return true
if (quota.baseQuota === 0 && quota.entitlementQuota === 0) return true
return quota.used + bytes <= quota.quota
}
@@ -67,7 +76,7 @@ export async function hasTrafficQuotaForBytes(
): Promise<boolean> {
if (bytes <= 0) return true
const quota = await getEffectiveQuota(db, orgId, now)
if (quota.trafficQuota === 0) return true
if (quota.baseTrafficQuota === 0 && quota.entitlementTrafficQuota === 0) return true
return quota.trafficUsed + bytes <= quota.trafficQuota
}
@@ -87,25 +96,33 @@ export async function consumeTrafficIfQuotaAllows(
if (quotaRows.length === 0) return true
if (quotaRows[0].trafficPeriod !== period) {
const entitlementBytes = activeEntitlementBytesSql(orgId, 'traffic', now)
const updated = await db
.update(orgQuotas)
.set({ trafficUsed: bytes, trafficPeriod: period })
.where(
sql`${orgQuotas.orgId} = ${orgId}
AND ${orgQuotas.trafficPeriod} != ${period}
AND (${orgQuotas.trafficQuota} = 0 OR ${bytes} <= ${orgQuotas.trafficQuota})`,
AND (
(${orgQuotas.trafficQuota} = 0 AND ${entitlementBytes} = 0)
OR ${bytes} <= ${orgQuotas.trafficQuota} + ${entitlementBytes}
)`,
)
.returning({ id: orgQuotas.id })
if (updated.length > 0) return true
}
const entitlementBytes = activeEntitlementBytesSql(orgId, 'traffic', now)
const updated = await db
.update(orgQuotas)
.set({ trafficUsed: sql`${orgQuotas.trafficUsed} + ${bytes}` })
.where(
sql`${orgQuotas.orgId} = ${orgId}
AND ${orgQuotas.trafficPeriod} = ${period}
AND (${orgQuotas.trafficQuota} = 0 OR ${orgQuotas.trafficUsed} + ${bytes} <= ${orgQuotas.trafficQuota})`,
AND (
(${orgQuotas.trafficQuota} = 0 AND ${entitlementBytes} = 0)
OR ${orgQuotas.trafficUsed} + ${bytes} <= ${orgQuotas.trafficQuota} + ${entitlementBytes}
)`,
)
.returning({ id: orgQuotas.id })
@@ -129,16 +146,21 @@ export async function incrementUsageIfEffectiveQuotaAllows(
storageId: string,
bytes: number,
teamQuotaEnabled = true,
now = new Date(),
): Promise<boolean> {
if (teamQuotaEnabled) {
const rows = await db.select({ id: orgQuotas.id }).from(orgQuotas).where(eq(orgQuotas.orgId, orgId)).limit(1)
if (rows.length > 0) {
const entitlementBytes = activeEntitlementBytesSql(orgId, 'storage', now)
const updated = await db
.update(orgQuotas)
.set({ used: sql`${orgQuotas.used} + ${bytes}` })
.where(
sql`${orgQuotas.orgId} = ${orgId}
AND (${orgQuotas.quota} = 0 OR ${orgQuotas.used} + ${bytes} <= ${orgQuotas.quota})`,
AND (
(${orgQuotas.quota} = 0 AND ${entitlementBytes} = 0)
OR ${orgQuotas.used} + ${bytes} <= ${orgQuotas.quota} + ${entitlementBytes}
)`,
)
.returning({ id: orgQuotas.id })
@@ -153,3 +175,41 @@ export async function incrementUsageIfEffectiveQuotaAllows(
return true
}
async function activeEntitlementBytes(
db: Database,
orgId: string,
resourceType: 'storage' | 'traffic',
now: Date,
): Promise<number> {
const rows = await db
.select({ bytes: sql<number>`COALESCE(SUM(${orgQuotaEntitlements.bytes}), 0)` })
.from(orgQuotaEntitlements)
.where(activeEntitlementWhere(orgId, resourceType, now))
return rows[0]?.bytes ?? 0
}
function activeEntitlementWhere(orgId: string, resourceType: 'storage' | 'traffic', now: Date) {
const timestamp = now.getTime()
return and(
eq(orgQuotaEntitlements.orgId, orgId),
eq(orgQuotaEntitlements.resourceType, resourceType),
eq(orgQuotaEntitlements.status, 'active'),
sql`${orgQuotaEntitlements.startsAt} <= ${timestamp}`,
or(sql`${orgQuotaEntitlements.expiresAt} IS NULL`, sql`${orgQuotaEntitlements.expiresAt} > ${timestamp}`),
)
}
function activeEntitlementBytesSql(orgId: string, resourceType: 'storage' | 'traffic', now: Date) {
const timestamp = now.getTime()
return sql`(
SELECT COALESCE(SUM(${orgQuotaEntitlements.bytes}), 0)
FROM ${orgQuotaEntitlements}
WHERE ${orgQuotaEntitlements.orgId} = ${orgId}
AND ${orgQuotaEntitlements.resourceType} = ${resourceType}
AND ${orgQuotaEntitlements.status} = 'active'
AND ${orgQuotaEntitlements.startsAt} <= ${timestamp}
AND (${orgQuotaEntitlements.expiresAt} IS NULL OR ${orgQuotaEntitlements.expiresAt} > ${timestamp})
)`
}
+16 -2
View File
@@ -1,7 +1,7 @@
import { and, count, desc, eq, inArray, or, sql } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { member, organization, user } from '../db/auth-schema'
import { orgQuotas } from '../db/schema'
import { orgQuotaEntitlements, orgQuotas } from '../db/schema'
import type { Database } from '../platform/interface'
import { currentTrafficPeriod } from './effective-quota'
@@ -33,6 +33,7 @@ export async function listUsers(
): Promise<{ items: UserWithOrg[]; total: number }> {
const offset = (page - 1) * pageSize
const term = search?.trim().toLowerCase()
const now = new Date()
const filter = term
? or(
sql`lower(${user.name}) like ${`%${term}%`}`,
@@ -59,7 +60,7 @@ export async function listUsers(
orgId: organization.id,
orgName: organization.name,
quotaUsed: orgQuotas.used,
quotaTotal: orgQuotas.quota,
quotaTotal: sql<number>`COALESCE(${orgQuotas.quota}, 0) + ${activeStorageEntitlementBytesSql(now)}`,
})
.from(user)
.leftJoin(organization, eq(organization.slug, sql`'personal-' || ${user.id}`))
@@ -77,6 +78,19 @@ export async function listUsers(
return { items, total }
}
function activeStorageEntitlementBytesSql(now: Date) {
const timestamp = now.getTime()
return sql`(
SELECT COALESCE(SUM(${orgQuotaEntitlements.bytes}), 0)
FROM ${orgQuotaEntitlements}
WHERE ${orgQuotaEntitlements.orgId} = ${organization.id}
AND ${orgQuotaEntitlements.resourceType} = 'storage'
AND ${orgQuotaEntitlements.status} = 'active'
AND ${orgQuotaEntitlements.startsAt} <= ${timestamp}
AND (${orgQuotaEntitlements.expiresAt} IS NULL OR ${orgQuotaEntitlements.expiresAt} > ${timestamp})
)`
}
export async function setUserStatus(db: Database, userId: string, status: 'active' | 'disabled'): Promise<boolean> {
const existing = await db.select({ id: user.id }).from(user).where(eq(user.id, userId))
if (existing.length === 0) return false
+18
View File
@@ -137,6 +137,24 @@ const APP_SCHEMA_SQL = `
traffic_used INTEGER NOT NULL DEFAULT 0,
traffic_period TEXT NOT NULL DEFAULT '1970-01'
);
CREATE TABLE IF NOT EXISTS org_quota_entitlements (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
resource_type TEXT NOT NULL,
source TEXT NOT NULL,
source_id TEXT NOT NULL,
bytes INTEGER NOT NULL,
starts_at INTEGER NOT NULL,
expires_at INTEGER,
status TEXT NOT NULL,
metadata TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS org_quota_entitlements_org_resource_idx
ON org_quota_entitlements(org_id, resource_type, status);
CREATE UNIQUE INDEX IF NOT EXISTS org_quota_entitlements_source_resource_uniq
ON org_quota_entitlements(source, source_id, resource_type);
CREATE TABLE IF NOT EXISTS webhook_events (
id TEXT PRIMARY KEY,
source TEXT NOT NULL,
+4
View File
@@ -37,8 +37,12 @@ export interface Storage {
export interface OrgQuota {
id: string
orgId: string
baseQuota: number
entitlementQuota: number
quota: number
used: number
baseTrafficQuota: number
entitlementTrafficQuota: number
trafficQuota: number
trafficUsed: number
trafficPeriod: string
@@ -106,8 +106,11 @@ describe('QuotaPanel', () => {
vi.mocked(getUserQuota).mockResolvedValue({
orgId: 'org-1',
baseQuota: 100,
entitlementQuota: 0,
quota: 100,
used: 25,
baseTrafficQuota: 0,
entitlementTrafficQuota: 0,
trafficQuota: 0,
trafficUsed: 0,
trafficPeriod: '2026-05',
@@ -126,8 +129,11 @@ describe('QuotaPanel', () => {
vi.mocked(getUserQuota).mockResolvedValue({
orgId: 'org-1',
baseQuota: 100,
entitlementQuota: 0,
quota: 100,
used: 25,
baseTrafficQuota: 0,
entitlementTrafficQuota: 0,
trafficQuota: 0,
trafficUsed: 0,
trafficPeriod: '2026-05',
@@ -145,8 +151,11 @@ describe('QuotaPanel', () => {
vi.mocked(getUserQuota).mockResolvedValue({
orgId: 'org-1',
baseQuota: 100,
entitlementQuota: 100,
quota: 200,
used: 25,
baseTrafficQuota: 0,
entitlementTrafficQuota: 0,
trafficQuota: 0,
trafficUsed: 0,
trafficPeriod: '2026-05',
+19 -1
View File
@@ -1180,7 +1180,18 @@ describe('api', () => {
it('fetches quotas list', async () => {
const payload = {
items: [
{ orgId: 'org1', quota: 1024, used: 512, trafficQuota: 2048, trafficUsed: 256, trafficPeriod: '2026-05' },
{
orgId: 'org1',
baseQuota: 1024,
entitlementQuota: 0,
quota: 1024,
used: 512,
baseTrafficQuota: 2048,
entitlementTrafficQuota: 0,
trafficQuota: 2048,
trafficUsed: 256,
trafficPeriod: '2026-05',
},
],
total: 1,
}
@@ -1204,8 +1215,12 @@ describe('api', () => {
it('puts quota for an org and returns updated quota', async () => {
const updated = {
orgId: 'org1',
baseQuota: 2048,
entitlementQuota: 0,
quota: 2048,
used: 512,
baseTrafficQuota: 4096,
entitlementTrafficQuota: 0,
trafficQuota: 4096,
trafficUsed: 256,
trafficPeriod: '2026-05',
@@ -1234,8 +1249,11 @@ describe('api', () => {
const payload = {
orgId: 'org1',
baseQuota: 1024,
entitlementQuota: 0,
quota: 1024,
used: 256,
baseTrafficQuota: 2048,
entitlementTrafficQuota: 0,
trafficQuota: 2048,
trafficUsed: 512,
trafficPeriod: '2026-05',
+15 -2
View File
@@ -275,7 +275,16 @@ export function batchUpdateUserQuota(ids: string[], quota: number) {
export type QuotaItem = Pick<
OrgQuota,
'orgId' | 'quota' | 'used' | 'trafficQuota' | 'trafficUsed' | 'trafficPeriod'
| 'orgId'
| 'baseQuota'
| 'entitlementQuota'
| 'quota'
| 'used'
| 'baseTrafficQuota'
| 'entitlementTrafficQuota'
| 'trafficQuota'
| 'trafficUsed'
| 'trafficPeriod'
> & {
orgName?: string
orgType?: string
@@ -301,9 +310,13 @@ export function getUserQuota() {
{
orgId: string
baseQuota: number
entitlementQuota: number
quota: number
used: number
} & Pick<OrgQuota, 'trafficQuota' | 'trafficUsed' | 'trafficPeriod'>
} & Pick<
OrgQuota,
'baseTrafficQuota' | 'entitlementTrafficQuota' | 'trafficQuota' | 'trafficUsed' | 'trafficPeriod'
>
>(userQuotas.me.$get())
}
@@ -138,8 +138,11 @@ describe('StoragePage', () => {
vi.mocked(getUserQuota).mockResolvedValue({
orgId: 'org-1',
baseQuota: 1024,
entitlementQuota: 512,
quota: 1536,
used: 0,
baseTrafficQuota: 0,
entitlementTrafficQuota: 0,
trafficQuota: 0,
trafficUsed: 0,
trafficPeriod: '2026-05',