d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
159 lines
4.3 KiB
TypeScript
159 lines
4.3 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { dbChainMock, dbChainMockFns, drizzleOrmMock } from '@sim/testing'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
vi.mock('@sim/db', () => dbChainMock)
|
|
|
|
vi.mock('drizzle-orm', () => {
|
|
const sqlTag = () => {
|
|
const obj: { as: () => typeof obj } = { as: () => obj }
|
|
return obj
|
|
}
|
|
return {
|
|
...drizzleOrmMock,
|
|
sql: Object.assign(sqlTag, { raw: sqlTag }),
|
|
sum: () => ({ as: () => 'sum' }),
|
|
}
|
|
})
|
|
|
|
vi.mock('@/lib/billing/constants', () => ({
|
|
DAILY_REFRESH_RATE: 0.01,
|
|
}))
|
|
|
|
import {
|
|
computeDailyRefreshConsumed,
|
|
getDailyRefreshDollars,
|
|
} from '@/lib/billing/credits/daily-refresh'
|
|
|
|
describe('computeDailyRefreshConsumed', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('returns 0 when planDollars is 0', async () => {
|
|
const result = await computeDailyRefreshConsumed({
|
|
userIds: ['user-1'],
|
|
periodStart: new Date('2026-03-01'),
|
|
planDollars: 0,
|
|
})
|
|
expect(result).toBe(0)
|
|
expect(dbChainMockFns.groupBy).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns 0 when userIds is empty', async () => {
|
|
const result = await computeDailyRefreshConsumed({
|
|
userIds: [],
|
|
periodStart: new Date('2026-03-01'),
|
|
planDollars: 25,
|
|
})
|
|
expect(result).toBe(0)
|
|
expect(dbChainMockFns.groupBy).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns 0 when periodEnd is before periodStart', async () => {
|
|
const result = await computeDailyRefreshConsumed({
|
|
userIds: ['user-1'],
|
|
periodStart: new Date('2026-03-10'),
|
|
periodEnd: new Date('2026-03-01'),
|
|
planDollars: 25,
|
|
})
|
|
expect(result).toBe(0)
|
|
})
|
|
|
|
it('caps each day at the daily refresh allowance', async () => {
|
|
dbChainMockFns.groupBy.mockResolvedValueOnce([
|
|
{ dayIndex: 0, dayTotal: '0.50' },
|
|
{ dayIndex: 1, dayTotal: '0.10' },
|
|
{ dayIndex: 2, dayTotal: '1.00' },
|
|
])
|
|
|
|
const result = await computeDailyRefreshConsumed({
|
|
userIds: ['user-1'],
|
|
periodStart: new Date('2026-03-01'),
|
|
periodEnd: new Date('2026-03-04'),
|
|
planDollars: 25,
|
|
})
|
|
|
|
// Daily refresh = $25 * 0.01 = $0.25/day
|
|
// Day 0: MIN(0.50, 0.25) = 0.25
|
|
// Day 1: MIN(0.10, 0.25) = 0.10
|
|
// Day 2: MIN(1.00, 0.25) = 0.25
|
|
// Total = 0.60
|
|
expect(result).toBe(0.6)
|
|
})
|
|
|
|
it('returns 0 when no usage rows exist', async () => {
|
|
dbChainMockFns.groupBy.mockResolvedValueOnce([])
|
|
|
|
const result = await computeDailyRefreshConsumed({
|
|
userIds: ['user-1'],
|
|
periodStart: new Date('2026-03-01'),
|
|
periodEnd: new Date('2026-03-04'),
|
|
planDollars: 25,
|
|
})
|
|
|
|
expect(result).toBe(0)
|
|
})
|
|
|
|
it('multiplies daily refresh by seats', async () => {
|
|
dbChainMockFns.groupBy.mockResolvedValueOnce([{ dayIndex: 0, dayTotal: '2.00' }])
|
|
|
|
const result = await computeDailyRefreshConsumed({
|
|
userIds: ['user-1', 'user-2', 'user-3'],
|
|
periodStart: new Date('2026-03-01'),
|
|
periodEnd: new Date('2026-03-02'),
|
|
planDollars: 100,
|
|
seats: 3,
|
|
})
|
|
|
|
// Daily refresh = $100 * 0.01 * 3 seats = $3.00/day
|
|
// Day 0: MIN(2.00, 3.00) = 2.00
|
|
expect(result).toBe(2.0)
|
|
})
|
|
|
|
it('caps at refresh even with high usage and multiple seats', async () => {
|
|
dbChainMockFns.groupBy.mockResolvedValueOnce([{ dayIndex: 0, dayTotal: '50.00' }])
|
|
|
|
const result = await computeDailyRefreshConsumed({
|
|
userIds: ['user-1', 'user-2'],
|
|
periodStart: new Date('2026-03-01'),
|
|
periodEnd: new Date('2026-03-02'),
|
|
planDollars: 100,
|
|
seats: 2,
|
|
})
|
|
|
|
// Daily refresh = $100 * 0.01 * 2 seats = $2.00/day
|
|
// Day 0: MIN(50.00, 2.00) = 2.00
|
|
expect(result).toBe(2.0)
|
|
})
|
|
|
|
it('handles null dayTotal gracefully', async () => {
|
|
dbChainMockFns.groupBy.mockResolvedValueOnce([{ dayIndex: 0, dayTotal: null }])
|
|
|
|
const result = await computeDailyRefreshConsumed({
|
|
userIds: ['user-1'],
|
|
periodStart: new Date('2026-03-01'),
|
|
periodEnd: new Date('2026-03-02'),
|
|
planDollars: 25,
|
|
})
|
|
|
|
expect(result).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('getDailyRefreshDollars', () => {
|
|
it('computes correct daily refresh for Pro ($25)', () => {
|
|
expect(getDailyRefreshDollars(25)).toBe(0.25)
|
|
})
|
|
|
|
it('computes correct daily refresh for Max ($100)', () => {
|
|
expect(getDailyRefreshDollars(100)).toBe(1.0)
|
|
})
|
|
|
|
it('returns 0 for $0 plan', () => {
|
|
expect(getDailyRefreshDollars(0)).toBe(0)
|
|
})
|
|
})
|