diff --git a/.env.example b/.env.example index 2468ef832..8f81f4a5f 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,9 @@ STRIPE_SECRET_KEY=sk_test_dummy_stripe_secret STRIPE_WEBHOOK_SECRET_KEY=whsec_dummy_webhook_secret STRIPE_USAGE_PRICE_ID=price_dummy_usage_id STRIPE_TEAM_FEE_PRICE_ID=price_dummy_team_fee_id +STRIPE_SUBSCRIPTION_100_PRICE_ID=price_dummy_subscription_100_id +STRIPE_SUBSCRIPTION_200_PRICE_ID=price_dummy_subscription_200_id +STRIPE_SUBSCRIPTION_500_PRICE_ID=price_dummy_subscription_500_id # External Services LINKUP_API_KEY=dummy_linkup_key diff --git a/common/src/constants/analytics-events.ts b/common/src/constants/analytics-events.ts index e620fdb72..a3d05e2ae 100644 --- a/common/src/constants/analytics-events.ts +++ b/common/src/constants/analytics-events.ts @@ -30,6 +30,16 @@ export enum AnalyticsEvent { ADVISORY_LOCK_CONTENTION = 'backend.advisory_lock_contention', TRANSACTION_RETRY_THRESHOLD_EXCEEDED = 'backend.transaction_retry_threshold_exceeded', + // Backend - Subscription + SUBSCRIPTION_CREATED = 'backend.subscription_created', + SUBSCRIPTION_CANCELED = 'backend.subscription_canceled', + SUBSCRIPTION_PAYMENT_FAILED = 'backend.subscription_payment_failed', + SUBSCRIPTION_BLOCK_CREATED = 'backend.subscription_block_created', + SUBSCRIPTION_BLOCK_LIMIT_HIT = 'backend.subscription_block_limit_hit', + SUBSCRIPTION_WEEKLY_LIMIT_HIT = 'backend.subscription_weekly_limit_hit', + SUBSCRIPTION_CREDITS_MIGRATED = 'backend.subscription_credits_migrated', + SUBSCRIPTION_TIER_CHANGED = 'backend.subscription_tier_changed', + // Web SIGNUP = 'web.signup', diff --git a/common/src/constants/grant-priorities.ts b/common/src/constants/grant-priorities.ts index a2c1c84c3..49cae0786 100644 --- a/common/src/constants/grant-priorities.ts +++ b/common/src/constants/grant-priorities.ts @@ -1,6 +1,7 @@ import type { GrantType } from '@codebuff/common/types/grant' export const GRANT_PRIORITIES: Record = { + subscription: 10, free: 20, referral: 30, ad: 40, diff --git a/common/src/constants/subscription-plans.ts b/common/src/constants/subscription-plans.ts new file mode 100644 index 000000000..23309e2f4 --- /dev/null +++ b/common/src/constants/subscription-plans.ts @@ -0,0 +1,49 @@ +export const SUBSCRIPTION_DISPLAY_NAME = 'Strong' as const + +export interface TierConfig { + monthlyPrice: number + creditsPerBlock: number + blockDurationHours: number + weeklyCreditsLimit: number +} + +export const SUBSCRIPTION_TIERS = { + 100: { + monthlyPrice: 100, + creditsPerBlock: 400, + blockDurationHours: 5, + weeklyCreditsLimit: 4000, + }, + 200: { + monthlyPrice: 200, + creditsPerBlock: 1200, + blockDurationHours: 5, + weeklyCreditsLimit: 12000, + }, + 500: { + monthlyPrice: 500, + creditsPerBlock: 3200, + blockDurationHours: 5, + weeklyCreditsLimit: 32000, + }, +} as const satisfies Record + +export type SubscriptionTierPrice = keyof typeof SUBSCRIPTION_TIERS + +export const DEFAULT_TIER = SUBSCRIPTION_TIERS[200] + +export function createSubscriptionPriceMappings(priceIds: Record) { + const priceToTier = Object.fromEntries( + Object.entries(priceIds).map(([tier, priceId]) => [priceId, Number(tier) as SubscriptionTierPrice]), + ) as Record + + function getTierFromPriceId(priceId: string): SubscriptionTierPrice | null { + return priceToTier[priceId] ?? null + } + + function getPriceIdFromTier(tier: SubscriptionTierPrice): string | null { + return priceIds[tier] ?? null + } + + return { getTierFromPriceId, getPriceIdFromTier } +} diff --git a/common/src/types/grant.ts b/common/src/types/grant.ts index 93d708cb6..33534a435 100644 --- a/common/src/types/grant.ts +++ b/common/src/types/grant.ts @@ -1,6 +1,7 @@ export type GrantType = | 'free' | 'referral' + | 'subscription' | 'purchase' | 'admin' | 'organization' @@ -9,6 +10,7 @@ export type GrantType = export const GrantTypeValues = [ 'free', 'referral', + 'subscription', 'purchase', 'admin', 'organization', diff --git a/packages/billing/src/__tests__/subscription.test.ts b/packages/billing/src/__tests__/subscription.test.ts new file mode 100644 index 000000000..b563eaf94 --- /dev/null +++ b/packages/billing/src/__tests__/subscription.test.ts @@ -0,0 +1,639 @@ +import { describe, expect, it } from 'bun:test' + +import { + DEFAULT_TIER, + SUBSCRIPTION_TIERS, +} from '@codebuff/common/constants/subscription-plans' +import type { Logger } from '@codebuff/common/types/contracts/logger' + +import { + checkRateLimit, + ensureActiveBlockGrantCallback, + expireActiveBlockGrants, + getWeekEnd, + getWeekStart, + getSubscriptionLimits, + isWeeklyLimitError, + migrateUnusedCredits, +} from '../subscription' + +import type { BlockGrant, SubscriptionRow, WeeklyLimitError } from '../subscription' + +const logger: Logger = { + debug: () => {}, + error: () => {}, + info: () => {}, + warn: () => {}, +} + +// Helper to create a UTC date on a specific day-of-week +// dayOfWeek: 0=Sun, 1=Mon, ..., 6=Sat +function utcDate(year: number, month: number, day: number): Date { + const d = new Date(Date.UTC(year, month - 1, day)) + return d +} + +function createMockSubscription(overrides?: Partial<{ + stripe_subscription_id: string + tier: number + billing_period_start: Date +}>) { + return { + stripe_subscription_id: 'sub-test-123', + tier: 200, + billing_period_start: utcDate(2025, 1, 8), // Wednesday + user_id: 'user-123', + status: 'active', + ...overrides, + } as SubscriptionRow +} + +interface MockCaptures { + insertValues: Record[] + updateSets: Record[] +} + +function createSequentialMock(options: { + selectResults?: unknown[][] + updateResults?: unknown[][] + insertResults?: unknown[][] +}): { conn: any; captures: MockCaptures } { + let selectIdx = 0 + let updateIdx = 0 + let insertIdx = 0 + const captures: MockCaptures = { insertValues: [], updateSets: [] } + + function makeChain(result: unknown, type?: 'insert' | 'update'): Record { + const chain: Record = {} + for (const m of ['from', 'where', 'orderBy', 'limit', 'returning', 'onConflictDoNothing']) { + chain[m] = () => chain + } + chain.values = (data: Record) => { + if (type === 'insert') captures.insertValues.push(data) + return chain + } + chain.set = (data: Record) => { + if (type === 'update') captures.updateSets.push(data) + return chain + } + chain.then = (resolve: (v: unknown) => void, reject?: (e: unknown) => void) => + Promise.resolve(result).then(resolve, reject) + return chain + } + + const conn = { + select: () => { + const result = (options.selectResults ?? [])[selectIdx] ?? [] + selectIdx++ + return makeChain(result) + }, + update: () => { + const result = (options.updateResults ?? [])[updateIdx] ?? [] + updateIdx++ + return makeChain(result, 'update') + }, + insert: () => { + const result = (options.insertResults ?? [])[insertIdx] ?? [] + insertIdx++ + return makeChain(result, 'insert') + }, + } + + return { conn, captures } +} + +describe('subscription', () => { + describe('getWeekStart', () => { + it('should return start of today when now is the same day-of-week as billing start', () => { + // 2025-01-08 is a Wednesday (3) + const billingStart = utcDate(2025, 1, 8) + // 2025-01-15 is also a Wednesday (3) + const now = utcDate(2025, 1, 15) + + const result = getWeekStart(billingStart, now) + + expect(result).toEqual(utcDate(2025, 1, 15)) + }) + + it('should go back to the billing day-of-week when now is later in the week', () => { + // 2025-01-08 is a Wednesday (3) + const billingStart = utcDate(2025, 1, 8) + // 2025-01-17 is a Friday (5) — 2 days after Wednesday + const now = utcDate(2025, 1, 17) + + const result = getWeekStart(billingStart, now) + + // Should go back to Wednesday 2025-01-15 + expect(result).toEqual(utcDate(2025, 1, 15)) + }) + + it('should go back to previous week billing day when now is earlier in the week', () => { + // 2025-01-08 is a Wednesday (3) + const billingStart = utcDate(2025, 1, 8) + // 2025-01-13 is a Monday (1) — before Wednesday + const now = utcDate(2025, 1, 13) + + const result = getWeekStart(billingStart, now) + + // Should go back 5 days to Wednesday 2025-01-08 + expect(result).toEqual(utcDate(2025, 1, 8)) + }) + + it('should handle billing start on Sunday with now on Saturday', () => { + // 2025-01-05 is a Sunday (0) + const billingStart = utcDate(2025, 1, 5) + // 2025-01-18 is a Saturday (6) — 6 days after Sunday + const now = utcDate(2025, 1, 18) + + const result = getWeekStart(billingStart, now) + + // Should go back 6 days to Sunday 2025-01-12 + expect(result).toEqual(utcDate(2025, 1, 12)) + }) + + it('should handle billing start on Saturday with now on Sunday', () => { + // 2025-01-04 is a Saturday (6) + const billingStart = utcDate(2025, 1, 4) + // 2025-01-12 is a Sunday (0) — 1 day after Saturday + const now = utcDate(2025, 1, 12) + + const result = getWeekStart(billingStart, now) + + // Should go back 1 day to Saturday 2025-01-11 + expect(result).toEqual(utcDate(2025, 1, 11)) + }) + + it('should zero out hours/minutes/seconds', () => { + const billingStart = utcDate(2025, 1, 8) // Wednesday + const now = new Date(Date.UTC(2025, 0, 17, 14, 30, 45, 123)) // Friday with time + + const result = getWeekStart(billingStart, now) + + expect(result.getUTCHours()).toBe(0) + expect(result.getUTCMinutes()).toBe(0) + expect(result.getUTCSeconds()).toBe(0) + expect(result.getUTCMilliseconds()).toBe(0) + }) + }) + + describe('getWeekEnd', () => { + it('should return exactly 7 days after week start', () => { + const billingStart = utcDate(2025, 1, 8) // Wednesday + const now = utcDate(2025, 1, 17) // Friday + + const weekStart = getWeekStart(billingStart, now) + const weekEnd = getWeekEnd(billingStart, now) + + const diffMs = weekEnd.getTime() - weekStart.getTime() + const diffDays = diffMs / (24 * 60 * 60 * 1000) + + expect(diffDays).toBe(7) + }) + + it('should return start of next billing-aligned week', () => { + // 2025-01-08 is a Wednesday + const billingStart = utcDate(2025, 1, 8) + // 2025-01-17 is a Friday → week start is Wed 2025-01-15 + const now = utcDate(2025, 1, 17) + + const result = getWeekEnd(billingStart, now) + + // Next Wednesday: 2025-01-22 + expect(result).toEqual(utcDate(2025, 1, 22)) + }) + }) + + describe('isWeeklyLimitError', () => { + it('should return true for WeeklyLimitError', () => { + const error: WeeklyLimitError = { + error: 'weekly_limit_reached', + used: 1000, + limit: 1000, + resetsAt: new Date(), + } + + expect(isWeeklyLimitError(error)).toBe(true) + }) + + it('should return false for BlockGrant', () => { + const grant: BlockGrant = { + grantId: 'grant-1', + credits: 500, + expiresAt: new Date(), + isNew: true, + } + + expect(isWeeklyLimitError(grant)).toBe(false) + }) + }) + + describe('getSubscriptionLimits', () => { + function createConnMock(overrides: Array<{ + credits_per_block: number + block_duration_hours: number + weekly_credit_limit: number + }>) { + return { + select: () => ({ + from: () => ({ + where: () => ({ + limit: () => overrides, + }), + }), + }), + update: () => ({}), + insert: () => ({}), + } as any + } + + it('should use limit override when one exists', async () => { + const conn = createConnMock([{ + credits_per_block: 9999, + block_duration_hours: 10, + weekly_credit_limit: 50000, + }]) + + const result = await getSubscriptionLimits({ + userId: 'user-123', + logger, + conn, + tier: 200, + }) + + expect(result).toEqual({ + creditsPerBlock: 9999, + blockDurationHours: 10, + weeklyCreditsLimit: 50000, + }) + }) + + it('should use tier config when no override exists and tier is valid', async () => { + const conn = createConnMock([]) + + const result = await getSubscriptionLimits({ + userId: 'user-123', + logger, + conn, + tier: 100, + }) + + expect(result).toEqual({ + creditsPerBlock: SUBSCRIPTION_TIERS[100].creditsPerBlock, + blockDurationHours: SUBSCRIPTION_TIERS[100].blockDurationHours, + weeklyCreditsLimit: SUBSCRIPTION_TIERS[100].weeklyCreditsLimit, + }) + }) + + it('should fall back to DEFAULT_TIER when tier is null', async () => { + const conn = createConnMock([]) + + const result = await getSubscriptionLimits({ + userId: 'user-123', + logger, + conn, + tier: null, + }) + + expect(result).toEqual({ + creditsPerBlock: DEFAULT_TIER.creditsPerBlock, + blockDurationHours: DEFAULT_TIER.blockDurationHours, + weeklyCreditsLimit: DEFAULT_TIER.weeklyCreditsLimit, + }) + }) + + it('should fall back to DEFAULT_TIER when tier is invalid', async () => { + const conn = createConnMock([]) + + const result = await getSubscriptionLimits({ + userId: 'user-123', + logger, + conn, + tier: 999, + }) + + expect(result).toEqual({ + creditsPerBlock: DEFAULT_TIER.creditsPerBlock, + blockDurationHours: DEFAULT_TIER.blockDurationHours, + weeklyCreditsLimit: DEFAULT_TIER.weeklyCreditsLimit, + }) + }) + + }) + + describe('migrateUnusedCredits', () => { + const futureDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) + + it('should insert idempotency marker when no unused grants exist', async () => { + const { conn, captures } = createSequentialMock({ + selectResults: [[]], // no unused grants + }) + + await migrateUnusedCredits({ + tx: conn, + userId: 'user-123', + subscriptionId: 'sub-123', + expiresAt: futureDate, + logger, + }) + + expect(captures.insertValues).toHaveLength(1) + expect(captures.insertValues[0].operation_id).toBe('subscribe-migrate-sub-123') + expect(captures.insertValues[0].principal).toBe(0) + expect(captures.insertValues[0].balance).toBe(0) + }) + + it('should zero old grants and create migration grant with correct total', async () => { + const { conn, captures } = createSequentialMock({ + selectResults: [[ + { operation_id: 'g1', balance: 300 }, + { operation_id: 'g2', balance: 200 }, + ]], + }) + + await migrateUnusedCredits({ + tx: conn, + userId: 'user-123', + subscriptionId: 'sub-123', + expiresAt: futureDate, + logger, + }) + + expect(captures.updateSets).toHaveLength(2) + expect(captures.updateSets[0]).toEqual({ + balance: 0, + description: 'Migrated 300 credits to subscribe-migrate-sub-123', + }) + expect(captures.updateSets[1]).toEqual({ + balance: 0, + description: 'Migrated 200 credits to subscribe-migrate-sub-123', + }) + + expect(captures.insertValues).toHaveLength(1) + expect(captures.insertValues[0].principal).toBe(500) + expect(captures.insertValues[0].balance).toBe(500) + expect(captures.insertValues[0].operation_id).toBe('subscribe-migrate-sub-123') + expect(captures.insertValues[0].type).toBe('free') + }) + }) + + describe('expireActiveBlockGrants', () => { + it('should return count of expired grants', async () => { + const { conn } = createSequentialMock({ + updateResults: [[{ operation_id: 'op1' }, { operation_id: 'op2' }]], + }) + + const count = await expireActiveBlockGrants({ + userId: 'user-123', + subscriptionId: 'sub-123', + logger, + conn, + }) + + expect(count).toBe(2) + }) + + it('should return 0 when no active grants exist', async () => { + const { conn } = createSequentialMock({ + updateResults: [[]], + }) + + const count = await expireActiveBlockGrants({ + userId: 'user-123', + subscriptionId: 'sub-123', + logger, + conn, + }) + + expect(count).toBe(0) + }) + }) + + describe('checkRateLimit', () => { + const subscription = createMockSubscription() + + it('should report weekly_limit when usage reaches limit', async () => { + // tier 200 → weeklyCreditsLimit: 12000 + const { conn } = createSequentialMock({ + selectResults: [ + [], // no limit overrides + [{ total: 12000 }], // weekly usage at limit + ], + }) + + const result = await checkRateLimit({ + userId: 'user-123', + subscription, + logger, + conn, + }) + + expect(result.limited).toBe(true) + expect(result.reason).toBe('weekly_limit') + expect(result.canStartNewBlock).toBe(false) + expect(result.weeklyUsed).toBe(12000) + expect(result.weeklyLimit).toBe(SUBSCRIPTION_TIERS[200].weeklyCreditsLimit) + }) + + it('should allow new block when no active block exists', async () => { + const { conn } = createSequentialMock({ + selectResults: [ + [], // no limit overrides + [{ total: 5000 }], // under weekly limit + [], // no active blocks + ], + }) + + const result = await checkRateLimit({ + userId: 'user-123', + subscription, + logger, + conn, + }) + + expect(result.limited).toBe(false) + expect(result.canStartNewBlock).toBe(true) + expect(result.weeklyUsed).toBe(5000) + }) + + it('should report block_exhausted when block has no balance', async () => { + const futureExpiry = new Date(Date.now() + 3 * 60 * 60 * 1000) + const { conn } = createSequentialMock({ + selectResults: [ + [], // no limit overrides + [{ total: 5000 }], // under weekly limit + [{ balance: 0, principal: 1200, expires_at: futureExpiry }], + ], + }) + + const result = await checkRateLimit({ + userId: 'user-123', + subscription, + logger, + conn, + }) + + expect(result.limited).toBe(true) + expect(result.reason).toBe('block_exhausted') + expect(result.blockUsed).toBe(1200) + expect(result.blockLimit).toBe(1200) + }) + + it('should report not limited when block has remaining credits', async () => { + const futureExpiry = new Date(Date.now() + 3 * 60 * 60 * 1000) + const { conn } = createSequentialMock({ + selectResults: [ + [], // no limit overrides + [{ total: 5000 }], // under weekly limit + [{ balance: 800, principal: 1200, expires_at: futureExpiry }], + ], + }) + + const result = await checkRateLimit({ + userId: 'user-123', + subscription, + logger, + conn, + }) + + expect(result.limited).toBe(false) + expect(result.canStartNewBlock).toBe(false) + expect(result.blockUsed).toBe(400) + expect(result.blockLimit).toBe(1200) + }) + }) + + describe('ensureActiveBlockGrantCallback', () => { + const subscription = createMockSubscription() + + it('should return existing active grant', async () => { + const futureExpiry = new Date(Date.now() + 3 * 60 * 60 * 1000) + const { conn } = createSequentialMock({ + selectResults: [ + [{ operation_id: 'existing-grant', balance: 500, expires_at: futureExpiry }], + ], + }) + + const result = await ensureActiveBlockGrantCallback({ + conn, + userId: 'user-123', + subscription, + logger, + }) + + expect(isWeeklyLimitError(result)).toBe(false) + const grant = result as BlockGrant + expect(grant.grantId).toBe('existing-grant') + expect(grant.credits).toBe(500) + expect(grant.isNew).toBe(false) + }) + + it('should return weekly limit error when limit is reached', async () => { + // tier 200 → weeklyCreditsLimit: 12000 + const { conn } = createSequentialMock({ + selectResults: [ + [], // no existing grants + [], // no limit overrides + [{ total: 12000 }], // weekly limit reached + ], + }) + + const result = await ensureActiveBlockGrantCallback({ + conn, + userId: 'user-123', + subscription, + logger, + }) + + expect(isWeeklyLimitError(result)).toBe(true) + const error = result as WeeklyLimitError + expect(error.error).toBe('weekly_limit_reached') + expect(error.used).toBe(12000) + expect(error.limit).toBe(SUBSCRIPTION_TIERS[200].weeklyCreditsLimit) + }) + + it('should create new block grant when none exists', async () => { + const now = new Date('2025-01-15T10:00:00Z') + const { conn } = createSequentialMock({ + selectResults: [ + [], // no existing grants + [], // no limit overrides + [{ total: 0 }], // no weekly usage + ], + insertResults: [ + [{ operation_id: 'new-block-grant' }], + ], + }) + + const result = await ensureActiveBlockGrantCallback({ + conn, + userId: 'user-123', + subscription, + logger, + now, + }) + + expect(isWeeklyLimitError(result)).toBe(false) + const grant = result as BlockGrant + expect(grant.isNew).toBe(true) + expect(grant.grantId).toBe('new-block-grant') + expect(grant.credits).toBe(SUBSCRIPTION_TIERS[200].creditsPerBlock) + expect(grant.expiresAt.getTime()).toBe( + now.getTime() + SUBSCRIPTION_TIERS[200].blockDurationHours * 60 * 60 * 1000, + ) + }) + + it('should cap block credits to weekly remaining', async () => { + // tier 200: creditsPerBlock=1200, weeklyCreditsLimit=12000 + // weekly used=11500 → remaining=500, block capped to 500 + const now = new Date('2025-01-15T10:00:00Z') + const { conn, captures } = createSequentialMock({ + selectResults: [ + [], // no existing grants + [], // no limit overrides + [{ total: 11500 }], // 500 remaining + ], + insertResults: [ + [{ operation_id: 'capped-block' }], + ], + }) + + const result = await ensureActiveBlockGrantCallback({ + conn, + userId: 'user-123', + subscription, + logger, + now, + }) + + expect(isWeeklyLimitError(result)).toBe(false) + const grant = result as BlockGrant + expect(grant.credits).toBe(500) + expect(captures.insertValues[0].principal).toBe(500) + expect(captures.insertValues[0].balance).toBe(500) + }) + + it('should throw when insert returns no grant (duplicate operation)', async () => { + const now = new Date('2025-01-15T10:00:00Z') + const { conn } = createSequentialMock({ + selectResults: [ + [], // no existing grants + [], // no limit overrides + [{ total: 0 }], // no weekly usage + ], + insertResults: [ + [], // empty — simulates onConflictDoNothing + ], + }) + + await expect( + ensureActiveBlockGrantCallback({ + conn, + userId: 'user-123', + subscription, + logger, + now, + }), + ).rejects.toThrow('Failed to create block grant') + }) + }) +}) diff --git a/packages/billing/src/__tests__/usage-service.test.ts b/packages/billing/src/__tests__/usage-service.test.ts index e1f9466c0..c037b6031 100644 --- a/packages/billing/src/__tests__/usage-service.test.ts +++ b/packages/billing/src/__tests__/usage-service.test.ts @@ -19,8 +19,8 @@ const mockBalance = { totalRemaining: 1000, totalDebt: 0, netBalance: 1000, - breakdown: { free: 500, paid: 500, referral: 0, purchase: 0, admin: 0, organization: 0, ad: 0 }, - principals: { free: 500, paid: 500, referral: 0, purchase: 0, admin: 0, organization: 0, ad: 0 }, + breakdown: { free: 500, referral: 0, subscription: 0, purchase: 500, admin: 0, organization: 0, ad: 0 }, + principals: { free: 500, referral: 0, subscription: 0, purchase: 500, admin: 0, organization: 0, ad: 0 }, } describe('usage-service', () => { @@ -49,6 +49,10 @@ describe('usage-service', () => { }), })) + await mockModule('@codebuff/billing/subscription', () => ({ + getActiveSubscription: async () => null, + })) + const { getUserUsageData } = await import('@codebuff/billing/usage-service') const result = await getUserUsageData({ @@ -81,6 +85,10 @@ describe('usage-service', () => { }), })) + await mockModule('@codebuff/billing/subscription', () => ({ + getActiveSubscription: async () => null, + })) + const { getUserUsageData } = await import('@codebuff/billing/usage-service') const result = await getUserUsageData({ @@ -110,6 +118,10 @@ describe('usage-service', () => { }), })) + await mockModule('@codebuff/billing/subscription', () => ({ + getActiveSubscription: async () => null, + })) + const { getUserUsageData } = await import('@codebuff/billing/usage-service') const result = await getUserUsageData({ @@ -140,6 +152,10 @@ describe('usage-service', () => { }), })) + await mockModule('@codebuff/billing/subscription', () => ({ + getActiveSubscription: async () => null, + })) + const { getUserUsageData } = await import('@codebuff/billing/usage-service') const result = await getUserUsageData({ @@ -171,6 +187,10 @@ describe('usage-service', () => { }), })) + await mockModule('@codebuff/billing/subscription', () => ({ + getActiveSubscription: async () => null, + })) + const { getUserUsageData } = await import('@codebuff/billing/usage-service') // Should not throw diff --git a/packages/billing/src/index.ts b/packages/billing/src/index.ts index 9545ea522..ac1cbcdfd 100644 --- a/packages/billing/src/index.ts +++ b/packages/billing/src/index.ts @@ -19,5 +19,11 @@ export * from './usage-service' // Credit delegation export * from './credit-delegation' +// Subscription +export * from './subscription' + +// Subscription webhooks +export * from './subscription-webhooks' + // Utilities export * from './utils' diff --git a/packages/billing/src/subscription-webhooks.ts b/packages/billing/src/subscription-webhooks.ts new file mode 100644 index 000000000..cda205d00 --- /dev/null +++ b/packages/billing/src/subscription-webhooks.ts @@ -0,0 +1,377 @@ +import { trackEvent } from '@codebuff/common/analytics' +import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events' +import { createSubscriptionPriceMappings } from '@codebuff/common/constants/subscription-plans' +import db from '@codebuff/internal/db' +import * as schema from '@codebuff/internal/db/schema' +import { env } from '@codebuff/internal/env' +import { + getStripeId, + getUserByStripeCustomerId, + stripeServer, +} from '@codebuff/internal/util/stripe' +import { eq } from 'drizzle-orm' + +import { expireActiveBlockGrants, handleSubscribe } from './subscription' + +import type { Logger } from '@codebuff/common/types/contracts/logger' +import type Stripe from 'stripe' + +type SubscriptionStatus = (typeof schema.subscriptionStatusEnum.enumValues)[number] + +/** + * Maps a Stripe subscription status to our local enum. + */ +function mapStripeStatus(status: Stripe.Subscription.Status): SubscriptionStatus { + const validStatuses: readonly string[] = schema.subscriptionStatusEnum.enumValues + if (validStatuses.includes(status)) return status as SubscriptionStatus + return 'incomplete' +} + +export const { getTierFromPriceId, getPriceIdFromTier } = createSubscriptionPriceMappings({ + 100: env.STRIPE_SUBSCRIPTION_100_PRICE_ID, + 200: env.STRIPE_SUBSCRIPTION_200_PRICE_ID, + 500: env.STRIPE_SUBSCRIPTION_500_PRICE_ID, +}) + +// --------------------------------------------------------------------------- +// invoice.paid +// --------------------------------------------------------------------------- + +/** + * Handles a paid invoice for a subscription. + * + * - On first payment (`subscription_create`): calls `handleSubscribe` to + * migrate the user's renewal date and unused credits. + * - On every payment: upserts the `subscription` row with fresh billing + * period dates from Stripe. + */ +export async function handleSubscriptionInvoicePaid(params: { + invoice: Stripe.Invoice + logger: Logger +}): Promise { + const { invoice, logger } = params + + if (!invoice.subscription) return + const subscriptionId = getStripeId(invoice.subscription) + + if (!invoice.customer) { + logger.warn( + { invoiceId: invoice.id }, + 'Subscription invoice has no customer ID', + ) + return + } + const customerId = getStripeId(invoice.customer) + + const stripeSub = await stripeServer.subscriptions.retrieve(subscriptionId) + const priceId = stripeSub.items.data[0]?.price.id + if (!priceId) { + logger.error( + { subscriptionId }, + 'Stripe subscription has no price on first item', + ) + return + } + + const tier = getTierFromPriceId(priceId) + if (!tier) { + logger.debug( + { subscriptionId, priceId }, + 'Price ID does not match a Strong tier — skipping', + ) + return + } + + // Look up the user for this customer + const user = await getUserByStripeCustomerId(customerId) + if (!user) { + logger.warn( + { customerId, subscriptionId }, + 'No user found for customer — skipping handleSubscribe', + ) + return + } + const userId = user.id + + // On first invoice, migrate renewal date & credits + if (invoice.billing_reason === 'subscription_create') { + await handleSubscribe({ + userId, + stripeSubscription: stripeSub, + logger, + }) + } + + const status = mapStripeStatus(stripeSub.status) + + // Check for a pending scheduled tier change (downgrade) + const existingSub = await db + .select({ + tier: schema.subscription.tier, + scheduled_tier: schema.subscription.scheduled_tier, + }) + .from(schema.subscription) + .where(eq(schema.subscription.stripe_subscription_id, subscriptionId)) + .limit(1) + + const previousTier = existingSub[0]?.tier + const hadScheduledTier = existingSub[0]?.scheduled_tier != null + + // Upsert subscription row — always apply the Stripe tier and clear + // scheduled_tier so pending downgrades take effect on renewal. + await db + .insert(schema.subscription) + .values({ + stripe_subscription_id: subscriptionId, + stripe_customer_id: customerId, + user_id: userId, + stripe_price_id: priceId, + tier, + scheduled_tier: null, + status, + billing_period_start: new Date(stripeSub.current_period_start * 1000), + billing_period_end: new Date(stripeSub.current_period_end * 1000), + cancel_at_period_end: stripeSub.cancel_at_period_end, + }) + .onConflictDoUpdate({ + target: schema.subscription.stripe_subscription_id, + set: { + status, + user_id: userId, + stripe_price_id: priceId, + tier, + scheduled_tier: null, + billing_period_start: new Date( + stripeSub.current_period_start * 1000, + ), + billing_period_end: new Date(stripeSub.current_period_end * 1000), + cancel_at_period_end: stripeSub.cancel_at_period_end, + updated_at: new Date(), + }, + }) + + // If a scheduled downgrade was applied, expire block grants so the user + // gets new grants at the lower tier's limits. + if (hadScheduledTier) { + await expireActiveBlockGrants({ userId, subscriptionId, logger }) + logger.info( + { userId, subscriptionId, previousTier, tier }, + 'Applied scheduled tier change and expired block grants', + ) + } + + logger.info( + { + subscriptionId, + customerId, + billingReason: invoice.billing_reason, + }, + 'Processed subscription invoice.paid', + ) +} + +// --------------------------------------------------------------------------- +// invoice.payment_failed +// --------------------------------------------------------------------------- + +/** + * Immediately sets the subscription to `past_due` — no grace period. + * User reverts to free-tier behaviour until payment is fixed. + */ +export async function handleSubscriptionInvoicePaymentFailed(params: { + invoice: Stripe.Invoice + logger: Logger +}): Promise { + const { invoice, logger } = params + + if (!invoice.subscription) return + const subscriptionId = getStripeId(invoice.subscription) + let userId = null + if (invoice.customer) { + const customerId = getStripeId(invoice.customer) + const user = await getUserByStripeCustomerId(customerId) + userId = user?.id + } + + await db + .update(schema.subscription) + .set({ + status: 'past_due', + updated_at: new Date(), + }) + .where(eq(schema.subscription.stripe_subscription_id, subscriptionId)) + + trackEvent({ + event: AnalyticsEvent.SUBSCRIPTION_PAYMENT_FAILED, + userId: userId ?? 'system', + properties: { subscriptionId, invoiceId: invoice.id }, + logger, + }) + + logger.warn( + { subscriptionId, invoiceId: invoice.id }, + 'Subscription payment failed — set to past_due', + ) +} + +// --------------------------------------------------------------------------- +// customer.subscription.updated +// --------------------------------------------------------------------------- + +/** + * Syncs plan details and cancellation intent from Stripe. + */ +export async function handleSubscriptionUpdated(params: { + stripeSubscription: Stripe.Subscription + logger: Logger +}): Promise { + const { stripeSubscription, logger } = params + const subscriptionId = stripeSubscription.id + const priceId = stripeSubscription.items.data[0]?.price.id + + if (!priceId) { + logger.error( + { subscriptionId }, + 'Subscription update has no price — skipping', + ) + return + } + + const tier = getTierFromPriceId(priceId) + if (!tier) { + logger.debug( + { subscriptionId, priceId }, + 'Price ID does not match a Strong tier — skipping', + ) + return + } + + const customerId = getStripeId(stripeSubscription.customer) + const user = await getUserByStripeCustomerId(customerId) + if (!user) { + logger.warn( + { customerId, subscriptionId }, + 'No user found for customer — skipping', + ) + return + } + const userId = user.id + + const status = mapStripeStatus(stripeSubscription.status) + + // Check existing tier to detect downgrades. During a downgrade the old + // higher tier is kept in `scheduled_tier` so limits remain until renewal. + const existingSub = await db + .select({ + tier: schema.subscription.tier, + scheduled_tier: schema.subscription.scheduled_tier, + }) + .from(schema.subscription) + .where(eq(schema.subscription.stripe_subscription_id, subscriptionId)) + .limit(1) + + const existingTier = existingSub[0]?.tier + const isDowngrade = existingTier != null && existingTier > tier + + // Upsert — webhook ordering is not guaranteed by Stripe, so this event + // may arrive before invoice.paid creates the row. + await db + .insert(schema.subscription) + .values({ + stripe_subscription_id: subscriptionId, + stripe_customer_id: customerId, + user_id: userId, + stripe_price_id: priceId, + tier, + status, + cancel_at_period_end: stripeSubscription.cancel_at_period_end, + billing_period_start: new Date( + stripeSubscription.current_period_start * 1000, + ), + billing_period_end: new Date( + stripeSubscription.current_period_end * 1000, + ), + }) + .onConflictDoUpdate({ + target: schema.subscription.stripe_subscription_id, + set: { + user_id: userId, + // Downgrade: preserve current tier & stripe_price_id, schedule the + // new tier for the next billing period. + ...(isDowngrade + ? { scheduled_tier: tier } + : { tier, stripe_price_id: priceId, scheduled_tier: null }), + status, + cancel_at_period_end: stripeSubscription.cancel_at_period_end, + billing_period_start: new Date( + stripeSubscription.current_period_start * 1000, + ), + billing_period_end: new Date( + stripeSubscription.current_period_end * 1000, + ), + updated_at: new Date(), + }, + }) + + // If this is an upgrade, expire old block grants so the user gets new + // grants at the higher tier's limits. Also serves as a fallback if the + // route handler's DB update failed. + const isUpgrade = existingTier != null && tier > existingTier + if (isUpgrade) { + await expireActiveBlockGrants({ userId, subscriptionId, logger }) + } + + logger.info( + { + subscriptionId, + cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end, + isDowngrade, + isUpgrade, + }, + isDowngrade + ? 'Processed subscription update — downgrade scheduled for next billing period' + : 'Processed subscription update', + ) +} + +// --------------------------------------------------------------------------- +// customer.subscription.deleted +// --------------------------------------------------------------------------- + +/** + * Marks the subscription as canceled in our database. + */ +export async function handleSubscriptionDeleted(params: { + stripeSubscription: Stripe.Subscription + logger: Logger +}): Promise { + const { stripeSubscription, logger } = params + const subscriptionId = stripeSubscription.id + + const customerId = getStripeId(stripeSubscription.customer) + const user = await getUserByStripeCustomerId(customerId) + const userId = user?.id ?? null + + await db + .update(schema.subscription) + .set({ + status: 'canceled', + scheduled_tier: null, + canceled_at: new Date(), + updated_at: new Date(), + }) + .where(eq(schema.subscription.stripe_subscription_id, subscriptionId)) + + if (userId) { + await expireActiveBlockGrants({ userId, subscriptionId, logger }) + } + + trackEvent({ + event: AnalyticsEvent.SUBSCRIPTION_CANCELED, + userId: userId ?? 'system', + properties: { subscriptionId }, + logger, + }) + + logger.info({ subscriptionId }, 'Subscription canceled') +} diff --git a/packages/billing/src/subscription.ts b/packages/billing/src/subscription.ts new file mode 100644 index 000000000..d83c998b8 --- /dev/null +++ b/packages/billing/src/subscription.ts @@ -0,0 +1,775 @@ +import { trackEvent } from '@codebuff/common/analytics' +import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events' +import { GRANT_PRIORITIES } from '@codebuff/common/constants/grant-priorities' +import { + DEFAULT_TIER, + SUBSCRIPTION_DISPLAY_NAME, + SUBSCRIPTION_TIERS, +} from '@codebuff/common/constants/subscription-plans' + +import type { SubscriptionTierPrice } from '@codebuff/common/constants/subscription-plans' +import db from '@codebuff/internal/db' +import * as schema from '@codebuff/internal/db/schema' +import { withAdvisoryLockTransaction } from '@codebuff/internal/db/transaction' +import { + and, + desc, + eq, + gt, + gte, + isNull, + lt, + lte, + ne, + sql, +} from 'drizzle-orm' + +import type { Logger } from '@codebuff/common/types/contracts/logger' +import type Stripe from 'stripe' + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type SubscriptionRow = typeof schema.subscription.$inferSelect + +export type DbConn = Pick + +export interface SubscriptionLimits { + creditsPerBlock: number + blockDurationHours: number + weeklyCreditsLimit: number +} + +export interface WeeklyUsage { + used: number + limit: number + remaining: number + resetsAt: Date + percentUsed: number +} + +export interface BlockGrant { + grantId: string + credits: number + expiresAt: Date + isNew: boolean +} + +export interface WeeklyLimitError { + error: 'weekly_limit_reached' + used: number + limit: number + resetsAt: Date +} + +export type BlockGrantResult = BlockGrant | WeeklyLimitError + +export function isWeeklyLimitError( + result: BlockGrantResult, +): result is WeeklyLimitError { + return 'error' in result +} + +export interface RateLimitStatus { + limited: boolean + reason?: 'block_exhausted' | 'weekly_limit' + canStartNewBlock: boolean + + blockUsed?: number + blockLimit?: number + blockResetsAt?: Date + + weeklyUsed: number + weeklyLimit: number + weeklyResetsAt: Date + weeklyPercentUsed: number +} + +// --------------------------------------------------------------------------- +// Date helpers +// --------------------------------------------------------------------------- + +function startOfDay(date: Date): Date { + const d = new Date(date) + d.setUTCHours(0, 0, 0, 0) + return d +} + +function addDays(date: Date, days: number): Date { + return new Date(date.getTime() + days * 24 * 60 * 60 * 1000) +} + +function addHours(date: Date, hours: number): Date { + return new Date(date.getTime() + hours * 60 * 60 * 1000) +} + +/** + * Get the start of the current billing-aligned week. + * Weeks start on the same day-of-week as the billing period started. + */ +export function getWeekStart( + billingPeriodStart: Date, + now: Date = new Date(), +): Date { + const billingDayOfWeek = billingPeriodStart.getUTCDay() + const currentDayOfWeek = now.getUTCDay() + const daysBack = (currentDayOfWeek - billingDayOfWeek + 7) % 7 + return startOfDay(addDays(now, -daysBack)) +} + +/** + * Get the end of the current billing-aligned week (start of next week). + */ +export function getWeekEnd( + billingPeriodStart: Date, + now: Date = new Date(), +): Date { + return addDays(getWeekStart(billingPeriodStart, now), 7) +} + +// --------------------------------------------------------------------------- +// Subscription limits +// --------------------------------------------------------------------------- + +/** + * Resolves the effective subscription limits for a user. + * Checks `limit_override` first, then falls back to the default tier constants. + */ +export async function getSubscriptionLimits(params: { + userId: string + logger: Logger + conn?: DbConn + tier?: number | null +}): Promise { + const { userId, logger, conn = db, tier } = params + + const overrides = await conn + .select() + .from(schema.limitOverride) + .where(eq(schema.limitOverride.user_id, userId)) + .limit(1) + + if (overrides.length > 0) { + const o = overrides[0] + logger.debug( + { userId, creditsPerBlock: o.credits_per_block }, + 'Using limit override for user', + ) + return { + creditsPerBlock: o.credits_per_block, + blockDurationHours: o.block_duration_hours, + weeklyCreditsLimit: o.weekly_credit_limit, + } + } + + const tierConfig = + tier != null && tier in SUBSCRIPTION_TIERS + ? SUBSCRIPTION_TIERS[tier as SubscriptionTierPrice] + : DEFAULT_TIER + + return { + creditsPerBlock: tierConfig.creditsPerBlock, + blockDurationHours: tierConfig.blockDurationHours, + weeklyCreditsLimit: tierConfig.weeklyCreditsLimit, + } +} + +// --------------------------------------------------------------------------- +// Weekly usage tracking +// --------------------------------------------------------------------------- + +/** + * Calculates credits consumed from subscription grants during the current + * billing-aligned week. + */ +export async function getWeeklyUsage(params: { + userId: string + billingPeriodStart: Date + weeklyCreditsLimit: number + logger: Logger + conn?: DbConn +}): Promise { + const { + userId, + billingPeriodStart, + weeklyCreditsLimit, + conn = db, + } = params + + const now = new Date() + const weekStart = getWeekStart(billingPeriodStart, now) + const weekEnd = getWeekEnd(billingPeriodStart, now) + + const result = await conn + .select({ + total: sql`COALESCE(SUM(${schema.creditLedger.principal} - ${schema.creditLedger.balance}), 0)`, + }) + .from(schema.creditLedger) + .where( + and( + eq(schema.creditLedger.user_id, userId), + eq(schema.creditLedger.type, 'subscription'), + gte(schema.creditLedger.created_at, weekStart), + lt(schema.creditLedger.created_at, weekEnd), + ), + ) + + const used = Number(result[0]?.total ?? 0) + + return { + used, + limit: weeklyCreditsLimit, + remaining: Math.max(0, weeklyCreditsLimit - used), + resetsAt: weekEnd, + percentUsed: weeklyCreditsLimit > 0 + ? Math.round((used / weeklyCreditsLimit) * 100) + : 0, + } +} + +// --------------------------------------------------------------------------- +// Block grant management +// --------------------------------------------------------------------------- + +/** + * Ensures the user has an active subscription block grant. + * + * 1. Returns the existing active grant if one exists with balance > 0. + * 2. Checks the weekly limit — returns an error if reached. + * 3. Creates a new block grant and returns it. + * + * All operations are serialised under an advisory lock for the user. + */ +export async function ensureActiveBlockGrantCallback(params: { + conn: DbConn + userId: string + subscription: SubscriptionRow + logger: Logger + now?: Date +}): Promise { + const { conn, userId, subscription, logger, now = new Date() } = params + const subscriptionId = subscription.stripe_subscription_id + + // 1. Check for an existing active block grant + const existingGrants = await conn + .select() + .from(schema.creditLedger) + .where( + and( + eq(schema.creditLedger.user_id, userId), + eq(schema.creditLedger.type, 'subscription'), + gt(schema.creditLedger.expires_at, now), + gt(schema.creditLedger.balance, 0), + ), + ) + .orderBy(desc(schema.creditLedger.expires_at)) + .limit(1) + + if (existingGrants.length > 0) { + const g = existingGrants[0] + return { + grantId: g.operation_id, + credits: g.balance, + expiresAt: g.expires_at!, + isNew: false, + } satisfies BlockGrant + } + + // 2. Resolve limits + const limits = await getSubscriptionLimits({ + userId, + logger, + conn, + tier: subscription.tier, + }) + + // 3. Check weekly limit before creating a new block + const weekly = await getWeeklyUsage({ + userId, + billingPeriodStart: subscription.billing_period_start, + weeklyCreditsLimit: limits.weeklyCreditsLimit, + logger, + conn, + }) + + if (weekly.remaining <= 0) { + trackEvent({ + event: AnalyticsEvent.SUBSCRIPTION_WEEKLY_LIMIT_HIT, + userId, + properties: { + subscriptionId, + weeklyUsed: weekly.used, + weeklyLimit: weekly.limit, + }, + logger, + }) + + return { + error: 'weekly_limit_reached', + used: weekly.used, + limit: weekly.limit, + resetsAt: weekly.resetsAt, + } satisfies WeeklyLimitError + } + + // 4. Create new block grant (capped to weekly remaining) + const blockCredits = Math.min(limits.creditsPerBlock, weekly.remaining) + const expiresAt = addHours(now, limits.blockDurationHours) + const operationId = `block-${subscriptionId}-${now.getTime()}` + + const [newGrant] = await conn + .insert(schema.creditLedger) + .values({ + operation_id: operationId, + user_id: userId, + stripe_subscription_id: subscriptionId, + type: 'subscription', + principal: blockCredits, + balance: blockCredits, + priority: GRANT_PRIORITIES.subscription, + expires_at: expiresAt, + description: `${SUBSCRIPTION_DISPLAY_NAME} block (${limits.blockDurationHours}h)`, + }) + .onConflictDoNothing({ target: schema.creditLedger.operation_id }) + .returning() + + if (!newGrant) { + throw new Error( + 'Failed to create block grant — possible duplicate operation', + ) + } + + trackEvent({ + event: AnalyticsEvent.SUBSCRIPTION_BLOCK_CREATED, + userId, + properties: { + subscriptionId, + operationId, + credits: blockCredits, + expiresAt: expiresAt.toISOString(), + weeklyUsed: weekly.used, + weeklyLimit: weekly.limit, + }, + logger, + }) + + logger.info( + { + userId, + subscriptionId, + operationId, + credits: blockCredits, + expiresAt, + }, + 'Created new subscription block grant', + ) + + return { + grantId: newGrant.operation_id, + credits: blockCredits, + expiresAt, + isNew: true, + } satisfies BlockGrant +} + +export async function ensureActiveBlockGrant(params: { + userId: string + subscription: SubscriptionRow + logger: Logger +}): Promise { + const { userId, subscription, logger } = params + const subscriptionId = subscription.stripe_subscription_id + + const { result } = await withAdvisoryLockTransaction({ + callback: async (tx) => { + return ensureActiveBlockGrantCallback({ + conn: tx, + userId, + subscription, + logger, + }) + }, + lockKey: `user:${userId}`, + context: { userId, subscriptionId }, + logger, + }) + + return result +} + +// --------------------------------------------------------------------------- +// Rate limiting +// --------------------------------------------------------------------------- + +/** + * Checks the subscriber's current rate-limit status. + * + * Two layers: + * - **Block**: 5-hour window with a fixed credit allowance + * - **Weekly**: billing-aligned weekly cap + */ +export async function checkRateLimit(params: { + userId: string + subscription: SubscriptionRow + logger: Logger + conn?: DbConn +}): Promise { + const { userId, subscription, logger, conn = db } = params + const now = new Date() + + const limits = await getSubscriptionLimits({ + userId, + logger, + conn, + tier: subscription.tier, + }) + + const weekly = await getWeeklyUsage({ + userId, + billingPeriodStart: subscription.billing_period_start, + weeklyCreditsLimit: limits.weeklyCreditsLimit, + logger, + conn, + }) + + // Weekly limit takes precedence + if (weekly.used >= weekly.limit) { + return { + limited: true, + reason: 'weekly_limit', + canStartNewBlock: false, + weeklyUsed: weekly.used, + weeklyLimit: weekly.limit, + weeklyResetsAt: weekly.resetsAt, + weeklyPercentUsed: weekly.percentUsed, + } + } + + // Find most recent active subscription block grant for this user + const blocks = await conn + .select() + .from(schema.creditLedger) + .where( + and( + eq(schema.creditLedger.user_id, userId), + eq(schema.creditLedger.type, 'subscription'), + gt(schema.creditLedger.expires_at, now), + ), + ) + .orderBy(desc(schema.creditLedger.created_at)) + .limit(1) + + const currentBlock = blocks[0] + + // No active block → can start a new one + if (!currentBlock) { + return { + limited: false, + canStartNewBlock: true, + weeklyUsed: weekly.used, + weeklyLimit: weekly.limit, + weeklyResetsAt: weekly.resetsAt, + weeklyPercentUsed: weekly.percentUsed, + } + } + + // Block active but exhausted + if (currentBlock.balance <= 0) { + return { + limited: true, + reason: 'block_exhausted', + canStartNewBlock: false, + blockUsed: currentBlock.principal, + blockLimit: currentBlock.principal, + blockResetsAt: currentBlock.expires_at!, + weeklyUsed: weekly.used, + weeklyLimit: weekly.limit, + weeklyResetsAt: weekly.resetsAt, + weeklyPercentUsed: weekly.percentUsed, + } + } + + // Block active with credits remaining + return { + limited: false, + canStartNewBlock: false, + blockUsed: currentBlock.principal - currentBlock.balance, + blockLimit: currentBlock.principal, + blockResetsAt: currentBlock.expires_at!, + weeklyUsed: weekly.used, + weeklyLimit: weekly.limit, + weeklyResetsAt: weekly.resetsAt, + weeklyPercentUsed: weekly.percentUsed, + } +} + +// --------------------------------------------------------------------------- +// Block grant expiration +// --------------------------------------------------------------------------- + +export async function expireActiveBlockGrants(params: { + userId: string + subscriptionId: string + logger: Logger + conn?: DbConn +}): Promise { + const { userId, subscriptionId, logger, conn = db } = params + const now = new Date() + + const expired = await conn + .update(schema.creditLedger) + .set({ expires_at: now }) + .where( + and( + eq(schema.creditLedger.user_id, userId), + eq(schema.creditLedger.type, 'subscription'), + gt(schema.creditLedger.expires_at, now), + ), + ) + .returning({ operation_id: schema.creditLedger.operation_id }) + + if (expired.length > 0) { + logger.info( + { userId, subscriptionId, expiredCount: expired.length }, + 'Expired active block grants', + ) + } + + return expired.length +} + +// --------------------------------------------------------------------------- +// Subscription lookup +// --------------------------------------------------------------------------- + +export async function getActiveSubscription(params: { + userId: string + logger: Logger +}): Promise { + const { userId } = params + + const subs = await db + .select() + .from(schema.subscription) + .where( + and( + eq(schema.subscription.user_id, userId), + eq(schema.subscription.status, 'active'), + ), + ) + .orderBy(desc(schema.subscription.updated_at)) + .limit(1) + + return subs[0] ?? null +} + +export async function isSubscriber(params: { + userId: string + logger: Logger +}): Promise { + const sub = await getActiveSubscription(params) + return sub !== null +} + +// --------------------------------------------------------------------------- +// Subscribe flow +// --------------------------------------------------------------------------- + +/** + * Handles the first-time-subscribe side-effects: + * 1. Moves `next_quota_reset` to Stripe's `current_period_end`. + * 2. Migrates unused credits into a single grant aligned to the new reset + * date. + * + * All operations run inside an advisory-locked transaction. + */ +export async function handleSubscribe(params: { + userId: string + stripeSubscription: Stripe.Subscription + logger: Logger +}): Promise { + const { userId, stripeSubscription, logger } = params + const newResetDate = new Date(stripeSubscription.current_period_end * 1000) + + const { result: didMigrate } = await withAdvisoryLockTransaction({ + callback: async (tx) => { + // Idempotency: check if credits were already migrated for this subscription. + // We use the credit_ledger instead of the subscription table because + // handleSubscriptionUpdated may upsert the subscription row before + // invoice.paid fires, which would cause this check to skip migration. + const migrationOpId = `subscribe-migrate-${stripeSubscription.id}` + const existingMigration = await tx + .select({ operation_id: schema.creditLedger.operation_id }) + .from(schema.creditLedger) + .where(eq(schema.creditLedger.operation_id, migrationOpId)) + .limit(1) + + if (existingMigration.length > 0) { + logger.info( + { userId, subscriptionId: stripeSubscription.id }, + 'Credits already migrated — skipping handleSubscribe', + ) + return false + } + + // Move next_quota_reset to align with Stripe billing period + await tx + .update(schema.user) + .set({ next_quota_reset: newResetDate }) + .where(eq(schema.user.id, userId)) + + // Migrate unused credits so nothing is lost + await migrateUnusedCredits({ + tx, + userId, + subscriptionId: stripeSubscription.id, + expiresAt: newResetDate, + logger, + }) + + return true + }, + lockKey: `user:${userId}`, + context: { userId, subscriptionId: stripeSubscription.id }, + logger, + }) + + if (didMigrate) { + trackEvent({ + event: AnalyticsEvent.SUBSCRIPTION_CREATED, + userId, + properties: { + subscriptionId: stripeSubscription.id, + newResetDate: newResetDate.toISOString(), + }, + logger, + }) + + logger.info( + { + userId, + subscriptionId: stripeSubscription.id, + newResetDate, + }, + 'Processed subscribe: reset date moved and credits migrated', + ) + } +} + +// --------------------------------------------------------------------------- +// Internal: credit migration +// --------------------------------------------------------------------------- + +export type DbTransaction = Parameters[0] extends ( + tx: infer T, +) => unknown + ? T + : never + +/** + * Migrates unused credits (any type with a non-null expires_at in the future) + * into a single grant that expires at `expiresAt`. The old grants have their + * balance zeroed. + */ +export async function migrateUnusedCredits(params: { + tx: DbTransaction + userId: string + subscriptionId: string + expiresAt: Date + logger: Logger +}): Promise { + const { tx, userId, subscriptionId, expiresAt, logger } = params + const now = new Date() + + const unusedGrants = await tx + .select() + .from(schema.creditLedger) + .where( + and( + eq(schema.creditLedger.user_id, userId), + gt(schema.creditLedger.balance, 0), + ne(schema.creditLedger.type, 'subscription'), + isNull(schema.creditLedger.org_id), + gt(schema.creditLedger.expires_at, now), + lte(schema.creditLedger.expires_at, expiresAt), + ), + ) + + const totalUnused = unusedGrants.reduce( + (sum, grant) => sum + grant.balance, + 0, + ) + + // Deterministic ID ensures idempotency — duplicate webhook deliveries + // will hit onConflictDoNothing and the handleSubscribe caller checks + // for this operation_id before running. + const operationId = `subscribe-migrate-${subscriptionId}` + + if (totalUnused === 0) { + // Still insert the marker for idempotency so handleSubscribe's check + // short-circuits on duplicate webhook deliveries. + await tx + .insert(schema.creditLedger) + .values({ + operation_id: operationId, + user_id: userId, + type: 'free', + principal: 0, + balance: 0, + priority: GRANT_PRIORITIES.free, + expires_at: expiresAt, + description: 'Migrated credits from subscription transition', + }) + .onConflictDoNothing({ target: schema.creditLedger.operation_id }) + logger.debug({ userId }, 'No unused credits to migrate') + return + } + + // Zero out old grants + for (const grant of unusedGrants) { + await tx + .update(schema.creditLedger) + .set({ + balance: 0, + description: `Migrated ${grant.balance} credits to ${operationId}`, + }) + .where(eq(schema.creditLedger.operation_id, grant.operation_id)) + } + + // Create a single migration grant preserving the total + await tx + .insert(schema.creditLedger) + .values({ + operation_id: operationId, + user_id: userId, + type: 'free', + principal: totalUnused, + balance: totalUnused, + priority: GRANT_PRIORITIES.free, + expires_at: expiresAt, + description: 'Migrated credits from subscription transition', + }) + .onConflictDoNothing({ target: schema.creditLedger.operation_id }) + + trackEvent({ + event: AnalyticsEvent.SUBSCRIPTION_CREDITS_MIGRATED, + userId, + properties: { + totalMigrated: totalUnused, + grantsZeroed: unusedGrants.length, + operationId, + }, + logger, + }) + + logger.info( + { + userId, + totalMigrated: totalUnused, + grantsZeroed: unusedGrants.length, + operationId, + }, + 'Migrated unused credits for subscription transition', + ) +} diff --git a/packages/billing/src/usage-service.ts b/packages/billing/src/usage-service.ts index 04bc659a6..80b6f41fe 100644 --- a/packages/billing/src/usage-service.ts +++ b/packages/billing/src/usage-service.ts @@ -9,16 +9,24 @@ import { calculateOrganizationUsageAndBalance, syncOrganizationBillingCycle, } from './org-billing' +import { getActiveSubscription } from './subscription' import type { CreditBalance } from './balance-calculator' import type { Logger } from '@codebuff/common/types/contracts/logger' +export interface SubscriptionInfo { + status: string + billingPeriodEnd: string + cancelAtPeriodEnd: boolean +} + export interface UserUsageData { usageThisCycle: number balance: CreditBalance nextQuotaReset: string autoTopupTriggered?: boolean autoTopupEnabled?: boolean + subscription?: SubscriptionInfo } export interface OrganizationUsageData { @@ -79,12 +87,24 @@ export async function getUserUsageData(params: { isPersonalContext: true, // isPersonalContext: true to exclude organization credits }) + // Check for active subscription + let subscription: SubscriptionInfo | undefined + const activeSub = await getActiveSubscription({ userId, logger }) + if (activeSub) { + subscription = { + status: activeSub.status, + billingPeriodEnd: activeSub.billing_period_end.toISOString(), + cancelAtPeriodEnd: activeSub.cancel_at_period_end, + } + } + return { usageThisCycle, balance, nextQuotaReset: quotaResetDate.toISOString(), autoTopupTriggered, autoTopupEnabled, + subscription, } } catch (error) { logger.error({ userId, error }, 'Error fetching user usage data') diff --git a/packages/internal/src/db/migrations/0036_handy_silver_sable.sql b/packages/internal/src/db/migrations/0036_handy_silver_sable.sql new file mode 100644 index 000000000..6ede12443 --- /dev/null +++ b/packages/internal/src/db/migrations/0036_handy_silver_sable.sql @@ -0,0 +1,32 @@ +CREATE TYPE "public"."subscription_status" AS ENUM('incomplete', 'incomplete_expired', 'trialing', 'active', 'past_due', 'canceled', 'unpaid', 'paused');--> statement-breakpoint +ALTER TYPE "public"."grant_type" ADD VALUE 'subscription' BEFORE 'purchase';--> statement-breakpoint +CREATE TABLE "limit_override" ( + "user_id" text PRIMARY KEY NOT NULL, + "credits_per_block" integer NOT NULL, + "block_duration_hours" integer NOT NULL, + "weekly_credit_limit" integer NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "subscription" ( + "stripe_subscription_id" text PRIMARY KEY NOT NULL, + "stripe_customer_id" text NOT NULL, + "user_id" text, + "stripe_price_id" text NOT NULL, + "status" "subscription_status" DEFAULT 'active' NOT NULL, + "billing_period_start" timestamp with time zone NOT NULL, + "billing_period_end" timestamp with time zone NOT NULL, + "cancel_at_period_end" boolean DEFAULT false NOT NULL, + "canceled_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "credit_ledger" ADD COLUMN "stripe_subscription_id" text;--> statement-breakpoint +ALTER TABLE "limit_override" ADD CONSTRAINT "limit_override_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "subscription" ADD CONSTRAINT "subscription_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_subscription_customer" ON "subscription" USING btree ("stripe_customer_id");--> statement-breakpoint +CREATE INDEX "idx_subscription_user" ON "subscription" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_subscription_status" ON "subscription" USING btree ("status") WHERE "subscription"."status" = 'active';--> statement-breakpoint +CREATE INDEX "idx_credit_ledger_subscription" ON "credit_ledger" USING btree ("stripe_subscription_id","type","created_at"); \ No newline at end of file diff --git a/packages/internal/src/db/migrations/0037_many_millenium_guard.sql b/packages/internal/src/db/migrations/0037_many_millenium_guard.sql new file mode 100644 index 000000000..ff1bbcd01 --- /dev/null +++ b/packages/internal/src/db/migrations/0037_many_millenium_guard.sql @@ -0,0 +1,3 @@ +DROP INDEX "idx_credit_ledger_subscription";--> statement-breakpoint +ALTER TABLE "subscription" ADD COLUMN "tier" integer;--> statement-breakpoint +CREATE INDEX "idx_credit_ledger_subscription" ON "credit_ledger" USING btree ("user_id","type","created_at"); \ No newline at end of file diff --git a/packages/internal/src/db/migrations/0038_legal_jimmy_woo.sql b/packages/internal/src/db/migrations/0038_legal_jimmy_woo.sql new file mode 100644 index 000000000..e774d0192 --- /dev/null +++ b/packages/internal/src/db/migrations/0038_legal_jimmy_woo.sql @@ -0,0 +1 @@ +ALTER TABLE "subscription" ADD COLUMN "scheduled_tier" integer; \ No newline at end of file diff --git a/packages/internal/src/db/migrations/meta/0036_snapshot.json b/packages/internal/src/db/migrations/meta/0036_snapshot.json new file mode 100644 index 000000000..d2ea08641 --- /dev/null +++ b/packages/internal/src/db/migrations/meta/0036_snapshot.json @@ -0,0 +1,3051 @@ +{ + "id": "14a00b85-f71c-42bf-911c-44fc725de438", + "prevId": "7835ce78-4836-46c4-b91b-5941d93544e9", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ad_impression": { + "name": "ad_impression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ad_text": { + "name": "ad_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta": { + "name": "cta", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "click_url": { + "name": "click_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imp_url": { + "name": "imp_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payout": { + "name": "payout", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true + }, + "credits_granted": { + "name": "credits_granted", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_operation_id": { + "name": "grant_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "served_at": { + "name": "served_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "impression_fired_at": { + "name": "impression_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "clicked_at": { + "name": "clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_ad_impression_user": { + "name": "idx_ad_impression_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ad_impression_imp_url": { + "name": "idx_ad_impression_imp_url", + "columns": [ + { + "expression": "imp_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ad_impression_user_id_user_id_fk": { + "name": "ad_impression_user_id_user_id_fk", + "tableFrom": "ad_impression", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ad_impression_imp_url_unique": { + "name": "ad_impression_imp_url_unique", + "nullsNotDistinct": false, + "columns": [ + "imp_url" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config": { + "name": "agent_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 1) AS INTEGER)", + "type": "stored" + } + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 2) AS INTEGER)", + "type": "stored" + } + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 3) AS INTEGER)", + "type": "stored" + } + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_config_publisher": { + "name": "idx_agent_config_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_publisher_id_publisher_id_fk": { + "name": "agent_config_publisher_id_publisher_id_fk", + "tableFrom": "agent_config", + "tableTo": "publisher", + "columnsFrom": [ + "publisher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_config_publisher_id_id_version_pk": { + "name": "agent_config_publisher_id_id_version_pk", + "columns": [ + "publisher_id", + "id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run": { + "name": "agent_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '/', 1)\n ELSE NULL\n END", + "type": "stored" + } + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(split_part(agent_id, '/', 2), '@', 1)\n ELSE agent_id\n END", + "type": "stored" + } + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '@', 2)\n ELSE NULL\n END", + "type": "stored" + } + }, + "ancestor_run_ids": { + "name": "ancestor_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "root_run_id": { + "name": "root_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[1] ELSE id END", + "type": "stored" + } + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[array_length(ancestor_run_ids, 1)] ELSE NULL END", + "type": "stored" + } + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "COALESCE(array_length(ancestor_run_ids, 1), 1)", + "type": "stored" + } + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "total_steps": { + "name": "total_steps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "direct_credits": { + "name": "direct_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_credits": { + "name": "total_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "status": { + "name": "status", + "type": "agent_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_agent_run_user_id": { + "name": "idx_agent_run_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_parent": { + "name": "idx_agent_run_parent", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_root": { + "name": "idx_agent_run_root", + "columns": [ + { + "expression": "root_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_agent_id": { + "name": "idx_agent_run_agent_id", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_publisher": { + "name": "idx_agent_run_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_status": { + "name": "idx_agent_run_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_ancestors_gin": { + "name": "idx_agent_run_ancestors_gin", + "columns": [ + { + "expression": "ancestor_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_agent_run_completed_publisher_agent": { + "name": "idx_agent_run_completed_publisher_agent", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_recent": { + "name": "idx_agent_run_completed_recent", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_version": { + "name": "idx_agent_run_completed_version", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_user": { + "name": "idx_agent_run_completed_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_user_id_user_id_fk": { + "name": "agent_run_user_id_user_id_fk", + "tableFrom": "agent_run", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_step": { + "name": "agent_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "credits": { + "name": "credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "child_run_ids": { + "name": "child_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "spawned_count": { + "name": "spawned_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "array_length(child_run_ids, 1)", + "type": "stored" + } + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "agent_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_step_number_per_run": { + "name": "unique_step_number_per_run", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_run_id": { + "name": "idx_agent_step_run_id", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_children_gin": { + "name": "idx_agent_step_children_gin", + "columns": [ + { + "expression": "child_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "agent_step_agent_run_id_agent_run_id_fk": { + "name": "agent_step_agent_run_id_agent_run_id_fk", + "tableFrom": "agent_step", + "tableTo": "agent_run", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_ledger": { + "name": "credit_ledger", + "schema": "", + "columns": { + "operation_id": { + "name": "operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal": { + "name": "principal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_credit_ledger_active_balance": { + "name": "idx_credit_ledger_active_balance", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "balance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"credit_ledger\".\"balance\" != 0 AND \"credit_ledger\".\"expires_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_org": { + "name": "idx_credit_ledger_org", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_subscription": { + "name": "idx_credit_ledger_subscription", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_ledger_user_id_user_id_fk": { + "name": "credit_ledger_user_id_user_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credit_ledger_org_id_org_id_fk": { + "name": "credit_ledger_org_id_org_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.encrypted_api_keys": { + "name": "encrypted_api_keys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "api_key_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "encrypted_api_keys_user_id_user_id_fk": { + "name": "encrypted_api_keys_user_id_user_id_fk", + "tableFrom": "encrypted_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "encrypted_api_keys_user_id_type_pk": { + "name": "encrypted_api_keys_user_id_type_pk", + "columns": [ + "user_id", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fingerprint": { + "name": "fingerprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sig_hash": { + "name": "sig_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_eval_results": { + "name": "git_eval_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cost_mode": { + "name": "cost_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoner_model": { + "name": "reasoner_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_model": { + "name": "agent_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.limit_override": { + "name": "limit_override", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credits_per_block": { + "name": "credits_per_block", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "block_duration_hours": { + "name": "block_duration_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "weekly_credit_limit": { + "name": "weekly_credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "limit_override_user_id_user_id_fk": { + "name": "limit_override_user_id_user_id_fk", + "tableFrom": "limit_override", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "\"message\".\"request\" -> -1", + "type": "stored" + } + }, + "reasoning_text": { + "name": "reasoning_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "numeric(100, 20)", + "primaryKey": false, + "notNull": true + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "byok": { + "name": "byok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "message_user_id_idx": { + "name": "message_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_finished_at_user_id_idx": { + "name": "message_finished_at_user_id_idx", + "columns": [ + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_idx": { + "name": "message_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_finished_at_idx": { + "name": "message_org_id_finished_at_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_user_id_user_id_fk": { + "name": "message_user_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_org_id_org_id_fk": { + "name": "message_org_id_org_id_fk", + "tableFrom": "message", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org": { + "name": "org", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_limit": { + "name": "credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_alerts": { + "name": "billing_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weekly_reports": { + "name": "weekly_reports", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_owner_id_user_id_fk": { + "name": "org_owner_id_user_id_fk", + "tableFrom": "org", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_slug_unique": { + "name": "org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + }, + "org_stripe_customer_id_unique": { + "name": "org_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_feature": { + "name": "org_feature", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_org_feature_active": { + "name": "idx_org_feature_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_feature_org_id_org_id_fk": { + "name": "org_feature_org_id_org_id_fk", + "tableFrom": "org_feature", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_feature_org_id_feature_pk": { + "name": "org_feature_org_id_feature_pk", + "columns": [ + "org_id", + "feature" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_invite": { + "name": "org_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_org_invite_token": { + "name": "idx_org_invite_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_email": { + "name": "idx_org_invite_email", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_expires": { + "name": "idx_org_invite_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_invite_org_id_org_id_fk": { + "name": "org_invite_org_id_org_id_fk", + "tableFrom": "org_invite", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_invite_invited_by_user_id_fk": { + "name": "org_invite_invited_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "org_invite_accepted_by_user_id_fk": { + "name": "org_invite_accepted_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": [ + "accepted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_invite_token_unique": { + "name": "org_invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_member": { + "name": "org_member", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_member_org_id_org_id_fk": { + "name": "org_member_org_id_org_id_fk", + "tableFrom": "org_member", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_member_user_id_user_id_fk": { + "name": "org_member_user_id_user_id_fk", + "tableFrom": "org_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_member_org_id_user_id_pk": { + "name": "org_member_org_id_user_id_pk", + "columns": [ + "org_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_repo": { + "name": "org_repo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_org_repo_active": { + "name": "idx_org_repo_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_repo_unique": { + "name": "idx_org_repo_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_repo_org_id_org_id_fk": { + "name": "org_repo_org_id_org_id_fk", + "tableFrom": "org_repo", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_repo_approved_by_user_id_fk": { + "name": "org_repo_approved_by_user_id_fk", + "tableFrom": "org_repo", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publisher": { + "name": "publisher", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "publisher_user_id_user_id_fk": { + "name": "publisher_user_id_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_org_id_org_id_fk": { + "name": "publisher_org_id_org_id_fk", + "tableFrom": "publisher", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_created_by_user_id_fk": { + "name": "publisher_created_by_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "publisher_single_owner": { + "name": "publisher_single_owner", + "value": "(\"publisher\".\"user_id\" IS NOT NULL AND \"publisher\".\"org_id\" IS NULL) OR\n (\"publisher\".\"user_id\" IS NULL AND \"publisher\".\"org_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.referral": { + "name": "referral", + "schema": "", + "columns": { + "referrer_id": { + "name": "referrer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referred_id": { + "name": "referred_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "referral_referrer_id_user_id_fk": { + "name": "referral_referrer_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referrer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "referral_referred_id_user_id_fk": { + "name": "referral_referred_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referred_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_referrer_id_referred_id_pk": { + "name": "referral_referrer_id_referred_id_pk", + "columns": [ + "referrer_id", + "referred_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fingerprint_id": { + "name": "fingerprint_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "session_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'web'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_fingerprint_id_fingerprint_id_fk": { + "name": "session_fingerprint_id_fingerprint_id_fk", + "tableFrom": "session", + "tableTo": "fingerprint", + "columnsFrom": [ + "fingerprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_subscription_customer": { + "name": "idx_subscription_customer", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_user": { + "name": "idx_subscription_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_status": { + "name": "idx_subscription_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_user_id_user_id_fk": { + "name": "subscription_user_id_user_id_fk", + "tableFrom": "subscription", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_failure": { + "name": "sync_failure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_failure_retry": { + "name": "idx_sync_failure_retry", + "columns": [ + { + "expression": "retry_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sync_failure\".\"retry_count\" < 5", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_quota_reset": { + "name": "next_quota_reset", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now() + INTERVAL '1 month'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ref-' || gen_random_uuid()" + }, + "referral_limit": { + "name": "referral_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_stripe_customer_id_unique": { + "name": "user_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + }, + "user_referral_code_unique": { + "name": "user_referral_code_unique", + "nullsNotDistinct": false, + "columns": [ + "referral_code" + ] + }, + "user_discord_id_unique": { + "name": "user_discord_id_unique", + "nullsNotDistinct": false, + "columns": [ + "discord_id" + ] + }, + "user_handle_unique": { + "name": "user_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.agent_run_status": { + "name": "agent_run_status", + "schema": "public", + "values": [ + "running", + "completed", + "failed", + "cancelled" + ] + }, + "public.agent_step_status": { + "name": "agent_step_status", + "schema": "public", + "values": [ + "running", + "completed", + "skipped" + ] + }, + "public.api_key_type": { + "name": "api_key_type", + "schema": "public", + "values": [ + "anthropic", + "gemini", + "openai" + ] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "free", + "referral", + "subscription", + "purchase", + "admin", + "organization", + "ad" + ] + }, + "public.org_role": { + "name": "org_role", + "schema": "public", + "values": [ + "owner", + "admin", + "member" + ] + }, + "public.session_type": { + "name": "session_type", + "schema": "public", + "values": [ + "web", + "pat", + "cli" + ] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "incomplete", + "incomplete_expired", + "trialing", + "active", + "past_due", + "canceled", + "unpaid", + "paused" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/internal/src/db/migrations/meta/0037_snapshot.json b/packages/internal/src/db/migrations/meta/0037_snapshot.json new file mode 100644 index 000000000..c20809668 --- /dev/null +++ b/packages/internal/src/db/migrations/meta/0037_snapshot.json @@ -0,0 +1,3057 @@ +{ + "id": "98d944a6-d8c5-41c6-a491-dc70211eca98", + "prevId": "14a00b85-f71c-42bf-911c-44fc725de438", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ad_impression": { + "name": "ad_impression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ad_text": { + "name": "ad_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta": { + "name": "cta", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "click_url": { + "name": "click_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imp_url": { + "name": "imp_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payout": { + "name": "payout", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true + }, + "credits_granted": { + "name": "credits_granted", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_operation_id": { + "name": "grant_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "served_at": { + "name": "served_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "impression_fired_at": { + "name": "impression_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "clicked_at": { + "name": "clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_ad_impression_user": { + "name": "idx_ad_impression_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ad_impression_imp_url": { + "name": "idx_ad_impression_imp_url", + "columns": [ + { + "expression": "imp_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ad_impression_user_id_user_id_fk": { + "name": "ad_impression_user_id_user_id_fk", + "tableFrom": "ad_impression", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ad_impression_imp_url_unique": { + "name": "ad_impression_imp_url_unique", + "nullsNotDistinct": false, + "columns": [ + "imp_url" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config": { + "name": "agent_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 1) AS INTEGER)", + "type": "stored" + } + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 2) AS INTEGER)", + "type": "stored" + } + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 3) AS INTEGER)", + "type": "stored" + } + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_config_publisher": { + "name": "idx_agent_config_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_publisher_id_publisher_id_fk": { + "name": "agent_config_publisher_id_publisher_id_fk", + "tableFrom": "agent_config", + "tableTo": "publisher", + "columnsFrom": [ + "publisher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_config_publisher_id_id_version_pk": { + "name": "agent_config_publisher_id_id_version_pk", + "columns": [ + "publisher_id", + "id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run": { + "name": "agent_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '/', 1)\n ELSE NULL\n END", + "type": "stored" + } + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(split_part(agent_id, '/', 2), '@', 1)\n ELSE agent_id\n END", + "type": "stored" + } + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '@', 2)\n ELSE NULL\n END", + "type": "stored" + } + }, + "ancestor_run_ids": { + "name": "ancestor_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "root_run_id": { + "name": "root_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[1] ELSE id END", + "type": "stored" + } + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[array_length(ancestor_run_ids, 1)] ELSE NULL END", + "type": "stored" + } + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "COALESCE(array_length(ancestor_run_ids, 1), 1)", + "type": "stored" + } + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "total_steps": { + "name": "total_steps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "direct_credits": { + "name": "direct_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_credits": { + "name": "total_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "status": { + "name": "status", + "type": "agent_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_agent_run_user_id": { + "name": "idx_agent_run_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_parent": { + "name": "idx_agent_run_parent", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_root": { + "name": "idx_agent_run_root", + "columns": [ + { + "expression": "root_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_agent_id": { + "name": "idx_agent_run_agent_id", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_publisher": { + "name": "idx_agent_run_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_status": { + "name": "idx_agent_run_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_ancestors_gin": { + "name": "idx_agent_run_ancestors_gin", + "columns": [ + { + "expression": "ancestor_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_agent_run_completed_publisher_agent": { + "name": "idx_agent_run_completed_publisher_agent", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_recent": { + "name": "idx_agent_run_completed_recent", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_version": { + "name": "idx_agent_run_completed_version", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_user": { + "name": "idx_agent_run_completed_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_user_id_user_id_fk": { + "name": "agent_run_user_id_user_id_fk", + "tableFrom": "agent_run", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_step": { + "name": "agent_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "credits": { + "name": "credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "child_run_ids": { + "name": "child_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "spawned_count": { + "name": "spawned_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "array_length(child_run_ids, 1)", + "type": "stored" + } + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "agent_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_step_number_per_run": { + "name": "unique_step_number_per_run", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_run_id": { + "name": "idx_agent_step_run_id", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_children_gin": { + "name": "idx_agent_step_children_gin", + "columns": [ + { + "expression": "child_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "agent_step_agent_run_id_agent_run_id_fk": { + "name": "agent_step_agent_run_id_agent_run_id_fk", + "tableFrom": "agent_step", + "tableTo": "agent_run", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_ledger": { + "name": "credit_ledger", + "schema": "", + "columns": { + "operation_id": { + "name": "operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal": { + "name": "principal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_credit_ledger_active_balance": { + "name": "idx_credit_ledger_active_balance", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "balance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"credit_ledger\".\"balance\" != 0 AND \"credit_ledger\".\"expires_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_org": { + "name": "idx_credit_ledger_org", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_subscription": { + "name": "idx_credit_ledger_subscription", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_ledger_user_id_user_id_fk": { + "name": "credit_ledger_user_id_user_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credit_ledger_org_id_org_id_fk": { + "name": "credit_ledger_org_id_org_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.encrypted_api_keys": { + "name": "encrypted_api_keys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "api_key_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "encrypted_api_keys_user_id_user_id_fk": { + "name": "encrypted_api_keys_user_id_user_id_fk", + "tableFrom": "encrypted_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "encrypted_api_keys_user_id_type_pk": { + "name": "encrypted_api_keys_user_id_type_pk", + "columns": [ + "user_id", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fingerprint": { + "name": "fingerprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sig_hash": { + "name": "sig_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_eval_results": { + "name": "git_eval_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cost_mode": { + "name": "cost_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoner_model": { + "name": "reasoner_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_model": { + "name": "agent_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.limit_override": { + "name": "limit_override", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credits_per_block": { + "name": "credits_per_block", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "block_duration_hours": { + "name": "block_duration_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "weekly_credit_limit": { + "name": "weekly_credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "limit_override_user_id_user_id_fk": { + "name": "limit_override_user_id_user_id_fk", + "tableFrom": "limit_override", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "\"message\".\"request\" -> -1", + "type": "stored" + } + }, + "reasoning_text": { + "name": "reasoning_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "numeric(100, 20)", + "primaryKey": false, + "notNull": true + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "byok": { + "name": "byok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "message_user_id_idx": { + "name": "message_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_finished_at_user_id_idx": { + "name": "message_finished_at_user_id_idx", + "columns": [ + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_idx": { + "name": "message_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_finished_at_idx": { + "name": "message_org_id_finished_at_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_user_id_user_id_fk": { + "name": "message_user_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_org_id_org_id_fk": { + "name": "message_org_id_org_id_fk", + "tableFrom": "message", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org": { + "name": "org", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_limit": { + "name": "credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_alerts": { + "name": "billing_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weekly_reports": { + "name": "weekly_reports", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_owner_id_user_id_fk": { + "name": "org_owner_id_user_id_fk", + "tableFrom": "org", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_slug_unique": { + "name": "org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + }, + "org_stripe_customer_id_unique": { + "name": "org_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_feature": { + "name": "org_feature", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_org_feature_active": { + "name": "idx_org_feature_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_feature_org_id_org_id_fk": { + "name": "org_feature_org_id_org_id_fk", + "tableFrom": "org_feature", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_feature_org_id_feature_pk": { + "name": "org_feature_org_id_feature_pk", + "columns": [ + "org_id", + "feature" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_invite": { + "name": "org_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_org_invite_token": { + "name": "idx_org_invite_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_email": { + "name": "idx_org_invite_email", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_expires": { + "name": "idx_org_invite_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_invite_org_id_org_id_fk": { + "name": "org_invite_org_id_org_id_fk", + "tableFrom": "org_invite", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_invite_invited_by_user_id_fk": { + "name": "org_invite_invited_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "org_invite_accepted_by_user_id_fk": { + "name": "org_invite_accepted_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": [ + "accepted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_invite_token_unique": { + "name": "org_invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_member": { + "name": "org_member", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_member_org_id_org_id_fk": { + "name": "org_member_org_id_org_id_fk", + "tableFrom": "org_member", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_member_user_id_user_id_fk": { + "name": "org_member_user_id_user_id_fk", + "tableFrom": "org_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_member_org_id_user_id_pk": { + "name": "org_member_org_id_user_id_pk", + "columns": [ + "org_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_repo": { + "name": "org_repo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_org_repo_active": { + "name": "idx_org_repo_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_repo_unique": { + "name": "idx_org_repo_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_repo_org_id_org_id_fk": { + "name": "org_repo_org_id_org_id_fk", + "tableFrom": "org_repo", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_repo_approved_by_user_id_fk": { + "name": "org_repo_approved_by_user_id_fk", + "tableFrom": "org_repo", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publisher": { + "name": "publisher", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "publisher_user_id_user_id_fk": { + "name": "publisher_user_id_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_org_id_org_id_fk": { + "name": "publisher_org_id_org_id_fk", + "tableFrom": "publisher", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_created_by_user_id_fk": { + "name": "publisher_created_by_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "publisher_single_owner": { + "name": "publisher_single_owner", + "value": "(\"publisher\".\"user_id\" IS NOT NULL AND \"publisher\".\"org_id\" IS NULL) OR\n (\"publisher\".\"user_id\" IS NULL AND \"publisher\".\"org_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.referral": { + "name": "referral", + "schema": "", + "columns": { + "referrer_id": { + "name": "referrer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referred_id": { + "name": "referred_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "referral_referrer_id_user_id_fk": { + "name": "referral_referrer_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referrer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "referral_referred_id_user_id_fk": { + "name": "referral_referred_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referred_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_referrer_id_referred_id_pk": { + "name": "referral_referrer_id_referred_id_pk", + "columns": [ + "referrer_id", + "referred_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fingerprint_id": { + "name": "fingerprint_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "session_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'web'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_fingerprint_id_fingerprint_id_fk": { + "name": "session_fingerprint_id_fingerprint_id_fk", + "tableFrom": "session", + "tableTo": "fingerprint", + "columnsFrom": [ + "fingerprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_subscription_customer": { + "name": "idx_subscription_customer", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_user": { + "name": "idx_subscription_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_status": { + "name": "idx_subscription_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_user_id_user_id_fk": { + "name": "subscription_user_id_user_id_fk", + "tableFrom": "subscription", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_failure": { + "name": "sync_failure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_failure_retry": { + "name": "idx_sync_failure_retry", + "columns": [ + { + "expression": "retry_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sync_failure\".\"retry_count\" < 5", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_quota_reset": { + "name": "next_quota_reset", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now() + INTERVAL '1 month'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ref-' || gen_random_uuid()" + }, + "referral_limit": { + "name": "referral_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_stripe_customer_id_unique": { + "name": "user_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + }, + "user_referral_code_unique": { + "name": "user_referral_code_unique", + "nullsNotDistinct": false, + "columns": [ + "referral_code" + ] + }, + "user_discord_id_unique": { + "name": "user_discord_id_unique", + "nullsNotDistinct": false, + "columns": [ + "discord_id" + ] + }, + "user_handle_unique": { + "name": "user_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.agent_run_status": { + "name": "agent_run_status", + "schema": "public", + "values": [ + "running", + "completed", + "failed", + "cancelled" + ] + }, + "public.agent_step_status": { + "name": "agent_step_status", + "schema": "public", + "values": [ + "running", + "completed", + "skipped" + ] + }, + "public.api_key_type": { + "name": "api_key_type", + "schema": "public", + "values": [ + "anthropic", + "gemini", + "openai" + ] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "free", + "referral", + "subscription", + "purchase", + "admin", + "organization", + "ad" + ] + }, + "public.org_role": { + "name": "org_role", + "schema": "public", + "values": [ + "owner", + "admin", + "member" + ] + }, + "public.session_type": { + "name": "session_type", + "schema": "public", + "values": [ + "web", + "pat", + "cli" + ] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "incomplete", + "incomplete_expired", + "trialing", + "active", + "past_due", + "canceled", + "unpaid", + "paused" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/internal/src/db/migrations/meta/0038_snapshot.json b/packages/internal/src/db/migrations/meta/0038_snapshot.json new file mode 100644 index 000000000..60ed1a864 --- /dev/null +++ b/packages/internal/src/db/migrations/meta/0038_snapshot.json @@ -0,0 +1,3063 @@ +{ + "id": "43f3712d-1692-4c3f-a029-54a9c66d293c", + "prevId": "98d944a6-d8c5-41c6-a491-dc70211eca98", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ad_impression": { + "name": "ad_impression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ad_text": { + "name": "ad_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta": { + "name": "cta", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "click_url": { + "name": "click_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imp_url": { + "name": "imp_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payout": { + "name": "payout", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true + }, + "credits_granted": { + "name": "credits_granted", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_operation_id": { + "name": "grant_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "served_at": { + "name": "served_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "impression_fired_at": { + "name": "impression_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "clicked_at": { + "name": "clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_ad_impression_user": { + "name": "idx_ad_impression_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ad_impression_imp_url": { + "name": "idx_ad_impression_imp_url", + "columns": [ + { + "expression": "imp_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ad_impression_user_id_user_id_fk": { + "name": "ad_impression_user_id_user_id_fk", + "tableFrom": "ad_impression", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ad_impression_imp_url_unique": { + "name": "ad_impression_imp_url_unique", + "nullsNotDistinct": false, + "columns": [ + "imp_url" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config": { + "name": "agent_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 1) AS INTEGER)", + "type": "stored" + } + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 2) AS INTEGER)", + "type": "stored" + } + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 3) AS INTEGER)", + "type": "stored" + } + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_agent_config_publisher": { + "name": "idx_agent_config_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_publisher_id_publisher_id_fk": { + "name": "agent_config_publisher_id_publisher_id_fk", + "tableFrom": "agent_config", + "tableTo": "publisher", + "columnsFrom": [ + "publisher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_config_publisher_id_id_version_pk": { + "name": "agent_config_publisher_id_id_version_pk", + "columns": [ + "publisher_id", + "id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run": { + "name": "agent_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '/', 1)\n ELSE NULL\n END", + "type": "stored" + } + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(split_part(agent_id, '/', 2), '@', 1)\n ELSE agent_id\n END", + "type": "stored" + } + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '@', 2)\n ELSE NULL\n END", + "type": "stored" + } + }, + "ancestor_run_ids": { + "name": "ancestor_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "root_run_id": { + "name": "root_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[1] ELSE id END", + "type": "stored" + } + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[array_length(ancestor_run_ids, 1)] ELSE NULL END", + "type": "stored" + } + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "COALESCE(array_length(ancestor_run_ids, 1), 1)", + "type": "stored" + } + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "total_steps": { + "name": "total_steps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "direct_credits": { + "name": "direct_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_credits": { + "name": "total_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "status": { + "name": "status", + "type": "agent_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_agent_run_user_id": { + "name": "idx_agent_run_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_parent": { + "name": "idx_agent_run_parent", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_root": { + "name": "idx_agent_run_root", + "columns": [ + { + "expression": "root_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_agent_id": { + "name": "idx_agent_run_agent_id", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_publisher": { + "name": "idx_agent_run_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_status": { + "name": "idx_agent_run_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_ancestors_gin": { + "name": "idx_agent_run_ancestors_gin", + "columns": [ + { + "expression": "ancestor_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_agent_run_completed_publisher_agent": { + "name": "idx_agent_run_completed_publisher_agent", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_recent": { + "name": "idx_agent_run_completed_recent", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_version": { + "name": "idx_agent_run_completed_version", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_user": { + "name": "idx_agent_run_completed_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_user_id_user_id_fk": { + "name": "agent_run_user_id_user_id_fk", + "tableFrom": "agent_run", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_step": { + "name": "agent_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "credits": { + "name": "credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "child_run_ids": { + "name": "child_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "spawned_count": { + "name": "spawned_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "array_length(child_run_ids, 1)", + "type": "stored" + } + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "agent_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_step_number_per_run": { + "name": "unique_step_number_per_run", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_run_id": { + "name": "idx_agent_step_run_id", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_children_gin": { + "name": "idx_agent_step_children_gin", + "columns": [ + { + "expression": "child_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "agent_step_agent_run_id_agent_run_id_fk": { + "name": "agent_step_agent_run_id_agent_run_id_fk", + "tableFrom": "agent_step", + "tableTo": "agent_run", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_ledger": { + "name": "credit_ledger", + "schema": "", + "columns": { + "operation_id": { + "name": "operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal": { + "name": "principal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_credit_ledger_active_balance": { + "name": "idx_credit_ledger_active_balance", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "balance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"credit_ledger\".\"balance\" != 0 AND \"credit_ledger\".\"expires_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_org": { + "name": "idx_credit_ledger_org", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_subscription": { + "name": "idx_credit_ledger_subscription", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_ledger_user_id_user_id_fk": { + "name": "credit_ledger_user_id_user_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credit_ledger_org_id_org_id_fk": { + "name": "credit_ledger_org_id_org_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.encrypted_api_keys": { + "name": "encrypted_api_keys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "api_key_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "encrypted_api_keys_user_id_user_id_fk": { + "name": "encrypted_api_keys_user_id_user_id_fk", + "tableFrom": "encrypted_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "encrypted_api_keys_user_id_type_pk": { + "name": "encrypted_api_keys_user_id_type_pk", + "columns": [ + "user_id", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fingerprint": { + "name": "fingerprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sig_hash": { + "name": "sig_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_eval_results": { + "name": "git_eval_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cost_mode": { + "name": "cost_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoner_model": { + "name": "reasoner_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_model": { + "name": "agent_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.limit_override": { + "name": "limit_override", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credits_per_block": { + "name": "credits_per_block", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "block_duration_hours": { + "name": "block_duration_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "weekly_credit_limit": { + "name": "weekly_credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "limit_override_user_id_user_id_fk": { + "name": "limit_override_user_id_user_id_fk", + "tableFrom": "limit_override", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "\"message\".\"request\" -> -1", + "type": "stored" + } + }, + "reasoning_text": { + "name": "reasoning_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "numeric(100, 20)", + "primaryKey": false, + "notNull": true + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "byok": { + "name": "byok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "message_user_id_idx": { + "name": "message_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_finished_at_user_id_idx": { + "name": "message_finished_at_user_id_idx", + "columns": [ + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_idx": { + "name": "message_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_finished_at_idx": { + "name": "message_org_id_finished_at_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_user_id_user_id_fk": { + "name": "message_user_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_org_id_org_id_fk": { + "name": "message_org_id_org_id_fk", + "tableFrom": "message", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org": { + "name": "org", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_limit": { + "name": "credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_alerts": { + "name": "billing_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weekly_reports": { + "name": "weekly_reports", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_owner_id_user_id_fk": { + "name": "org_owner_id_user_id_fk", + "tableFrom": "org", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_slug_unique": { + "name": "org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + }, + "org_stripe_customer_id_unique": { + "name": "org_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_feature": { + "name": "org_feature", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_org_feature_active": { + "name": "idx_org_feature_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_feature_org_id_org_id_fk": { + "name": "org_feature_org_id_org_id_fk", + "tableFrom": "org_feature", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_feature_org_id_feature_pk": { + "name": "org_feature_org_id_feature_pk", + "columns": [ + "org_id", + "feature" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_invite": { + "name": "org_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_org_invite_token": { + "name": "idx_org_invite_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_email": { + "name": "idx_org_invite_email", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_expires": { + "name": "idx_org_invite_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_invite_org_id_org_id_fk": { + "name": "org_invite_org_id_org_id_fk", + "tableFrom": "org_invite", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_invite_invited_by_user_id_fk": { + "name": "org_invite_invited_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "org_invite_accepted_by_user_id_fk": { + "name": "org_invite_accepted_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": [ + "accepted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_invite_token_unique": { + "name": "org_invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_member": { + "name": "org_member", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_member_org_id_org_id_fk": { + "name": "org_member_org_id_org_id_fk", + "tableFrom": "org_member", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_member_user_id_user_id_fk": { + "name": "org_member_user_id_user_id_fk", + "tableFrom": "org_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_member_org_id_user_id_pk": { + "name": "org_member_org_id_user_id_pk", + "columns": [ + "org_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_repo": { + "name": "org_repo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_org_repo_active": { + "name": "idx_org_repo_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_repo_unique": { + "name": "idx_org_repo_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_repo_org_id_org_id_fk": { + "name": "org_repo_org_id_org_id_fk", + "tableFrom": "org_repo", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_repo_approved_by_user_id_fk": { + "name": "org_repo_approved_by_user_id_fk", + "tableFrom": "org_repo", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publisher": { + "name": "publisher", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "publisher_user_id_user_id_fk": { + "name": "publisher_user_id_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_org_id_org_id_fk": { + "name": "publisher_org_id_org_id_fk", + "tableFrom": "publisher", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_created_by_user_id_fk": { + "name": "publisher_created_by_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "publisher_single_owner": { + "name": "publisher_single_owner", + "value": "(\"publisher\".\"user_id\" IS NOT NULL AND \"publisher\".\"org_id\" IS NULL) OR\n (\"publisher\".\"user_id\" IS NULL AND \"publisher\".\"org_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.referral": { + "name": "referral", + "schema": "", + "columns": { + "referrer_id": { + "name": "referrer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referred_id": { + "name": "referred_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "referral_referrer_id_user_id_fk": { + "name": "referral_referrer_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referrer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "referral_referred_id_user_id_fk": { + "name": "referral_referred_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referred_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_referrer_id_referred_id_pk": { + "name": "referral_referrer_id_referred_id_pk", + "columns": [ + "referrer_id", + "referred_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fingerprint_id": { + "name": "fingerprint_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "session_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'web'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_fingerprint_id_fingerprint_id_fk": { + "name": "session_fingerprint_id_fingerprint_id_fk", + "tableFrom": "session", + "tableTo": "fingerprint", + "columnsFrom": [ + "fingerprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scheduled_tier": { + "name": "scheduled_tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_subscription_customer": { + "name": "idx_subscription_customer", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_user": { + "name": "idx_subscription_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_status": { + "name": "idx_subscription_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_user_id_user_id_fk": { + "name": "subscription_user_id_user_id_fk", + "tableFrom": "subscription", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_failure": { + "name": "sync_failure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_failure_retry": { + "name": "idx_sync_failure_retry", + "columns": [ + { + "expression": "retry_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sync_failure\".\"retry_count\" < 5", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_quota_reset": { + "name": "next_quota_reset", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now() + INTERVAL '1 month'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ref-' || gen_random_uuid()" + }, + "referral_limit": { + "name": "referral_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_stripe_customer_id_unique": { + "name": "user_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + }, + "user_referral_code_unique": { + "name": "user_referral_code_unique", + "nullsNotDistinct": false, + "columns": [ + "referral_code" + ] + }, + "user_discord_id_unique": { + "name": "user_discord_id_unique", + "nullsNotDistinct": false, + "columns": [ + "discord_id" + ] + }, + "user_handle_unique": { + "name": "user_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.agent_run_status": { + "name": "agent_run_status", + "schema": "public", + "values": [ + "running", + "completed", + "failed", + "cancelled" + ] + }, + "public.agent_step_status": { + "name": "agent_step_status", + "schema": "public", + "values": [ + "running", + "completed", + "skipped" + ] + }, + "public.api_key_type": { + "name": "api_key_type", + "schema": "public", + "values": [ + "anthropic", + "gemini", + "openai" + ] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "free", + "referral", + "subscription", + "purchase", + "admin", + "organization", + "ad" + ] + }, + "public.org_role": { + "name": "org_role", + "schema": "public", + "values": [ + "owner", + "admin", + "member" + ] + }, + "public.session_type": { + "name": "session_type", + "schema": "public", + "values": [ + "web", + "pat", + "cli" + ] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "incomplete", + "incomplete_expired", + "trialing", + "active", + "past_due", + "canceled", + "unpaid", + "paused" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/internal/src/db/migrations/meta/_journal.json b/packages/internal/src/db/migrations/meta/_journal.json index be421313c..067c22194 100644 --- a/packages/internal/src/db/migrations/meta/_journal.json +++ b/packages/internal/src/db/migrations/meta/_journal.json @@ -253,6 +253,27 @@ "when": 1768421756993, "tag": "0035_warm_orphan", "breakpoints": true + }, + { + "idx": 36, + "version": "7", + "when": 1769568664455, + "tag": "0036_handy_silver_sable", + "breakpoints": true + }, + { + "idx": 37, + "version": "7", + "when": 1769637004165, + "tag": "0037_many_millenium_guard", + "breakpoints": true + }, + { + "idx": 38, + "version": "7", + "when": 1769649819008, + "tag": "0038_legal_jimmy_woo", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/internal/src/db/schema.ts b/packages/internal/src/db/schema.ts index 14377741c..24ec326fe 100644 --- a/packages/internal/src/db/schema.ts +++ b/packages/internal/src/db/schema.ts @@ -52,6 +52,17 @@ export const agentStepStatus = pgEnum('agent_step_status', [ 'skipped', ]) +export const subscriptionStatusEnum = pgEnum('subscription_status', [ + 'incomplete', + 'incomplete_expired', + 'trialing', + 'active', + 'past_due', + 'canceled', + 'unpaid', + 'paused', +]) + export const user = pgTable('user', { id: text('id') .primaryKey() @@ -120,6 +131,7 @@ export const creditLedger = pgTable( .notNull() .defaultNow(), org_id: text('org_id').references(() => org.id, { onDelete: 'cascade' }), + stripe_subscription_id: text('stripe_subscription_id'), }, (table) => [ index('idx_credit_ledger_active_balance') @@ -132,6 +144,11 @@ export const creditLedger = pgTable( ) .where(sql`${table.balance} != 0 AND ${table.expires_at} IS NULL`), index('idx_credit_ledger_org').on(table.org_id), + index('idx_credit_ledger_subscription').on( + table.user_id, + table.type, + table.created_at, + ), ], ) @@ -442,6 +459,60 @@ export const adImpression = pgTable( ], ) +// Subscription tables +export const subscription = pgTable( + 'subscription', + { + stripe_subscription_id: text('stripe_subscription_id').primaryKey(), + stripe_customer_id: text('stripe_customer_id').notNull(), + user_id: text('user_id').references(() => user.id, { onDelete: 'cascade' }), + stripe_price_id: text('stripe_price_id').notNull(), + tier: integer('tier'), + scheduled_tier: integer('scheduled_tier'), + status: subscriptionStatusEnum('status').notNull().default('active'), + billing_period_start: timestamp('billing_period_start', { + mode: 'date', + withTimezone: true, + }).notNull(), + billing_period_end: timestamp('billing_period_end', { + mode: 'date', + withTimezone: true, + }).notNull(), + cancel_at_period_end: boolean('cancel_at_period_end') + .notNull() + .default(false), + canceled_at: timestamp('canceled_at', { mode: 'date', withTimezone: true }), + created_at: timestamp('created_at', { mode: 'date', withTimezone: true }) + .notNull() + .defaultNow(), + updated_at: timestamp('updated_at', { mode: 'date', withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => [ + index('idx_subscription_customer').on(table.stripe_customer_id), + index('idx_subscription_user').on(table.user_id), + index('idx_subscription_status') + .on(table.status) + .where(sql`${table.status} = 'active'`), + ], +) + +export const limitOverride = pgTable('limit_override', { + user_id: text('user_id') + .primaryKey() + .references(() => user.id, { onDelete: 'cascade' }), + credits_per_block: integer('credits_per_block').notNull(), + block_duration_hours: integer('block_duration_hours').notNull(), + weekly_credit_limit: integer('weekly_credit_limit').notNull(), + created_at: timestamp('created_at', { mode: 'date', withTimezone: true }) + .notNull() + .defaultNow(), + updated_at: timestamp('updated_at', { mode: 'date', withTimezone: true }) + .notNull() + .defaultNow(), +}) + export type GitEvalMetadata = { numCases?: number // Number of eval cases successfully run (total) avgScore?: number // Average score across all cases diff --git a/packages/internal/src/env-schema.ts b/packages/internal/src/env-schema.ts index 54136b313..042b7e4d2 100644 --- a/packages/internal/src/env-schema.ts +++ b/packages/internal/src/env-schema.ts @@ -21,6 +21,9 @@ export const serverEnvSchema = clientEnvSchema.extend({ STRIPE_WEBHOOK_SECRET_KEY: z.string().min(1), STRIPE_USAGE_PRICE_ID: z.string().min(1), STRIPE_TEAM_FEE_PRICE_ID: z.string().min(1), + STRIPE_SUBSCRIPTION_100_PRICE_ID: z.string().min(1), + STRIPE_SUBSCRIPTION_200_PRICE_ID: z.string().min(1), + STRIPE_SUBSCRIPTION_500_PRICE_ID: z.string().min(1), LOOPS_API_KEY: z.string().min(1), DISCORD_PUBLIC_KEY: z.string().min(1), DISCORD_BOT_TOKEN: z.string().min(1), @@ -61,6 +64,9 @@ export const serverProcessEnv: ServerInput = { STRIPE_WEBHOOK_SECRET_KEY: process.env.STRIPE_WEBHOOK_SECRET_KEY, STRIPE_USAGE_PRICE_ID: process.env.STRIPE_USAGE_PRICE_ID, STRIPE_TEAM_FEE_PRICE_ID: process.env.STRIPE_TEAM_FEE_PRICE_ID, + STRIPE_SUBSCRIPTION_100_PRICE_ID: process.env.STRIPE_SUBSCRIPTION_100_PRICE_ID, + STRIPE_SUBSCRIPTION_200_PRICE_ID: process.env.STRIPE_SUBSCRIPTION_200_PRICE_ID, + STRIPE_SUBSCRIPTION_500_PRICE_ID: process.env.STRIPE_SUBSCRIPTION_500_PRICE_ID, LOOPS_API_KEY: process.env.LOOPS_API_KEY, DISCORD_PUBLIC_KEY: process.env.DISCORD_PUBLIC_KEY, DISCORD_BOT_TOKEN: process.env.DISCORD_BOT_TOKEN, diff --git a/packages/internal/src/util/stripe.ts b/packages/internal/src/util/stripe.ts index f95ebdec2..263df4d13 100644 --- a/packages/internal/src/util/stripe.ts +++ b/packages/internal/src/util/stripe.ts @@ -1,6 +1,15 @@ +import db from '@codebuff/internal/db' +import * as schema from '@codebuff/internal/db/schema' +import { env } from '@codebuff/internal/env' +import { eq } from 'drizzle-orm' import Stripe from 'stripe' -import { env } from '@codebuff/internal/env' +/** + * Extracts the ID string from a Stripe expandable field. + */ +export function getStripeId(expandable: string | { id: string }): string { + return typeof expandable === 'string' ? expandable : expandable.id +} export const stripeServer = new Stripe(env.STRIPE_SECRET_KEY, { apiVersion: '2024-06-20', @@ -15,3 +24,28 @@ export async function getCurrentSubscription(customerId: string) { }) return subscriptions.data[0] } + +/** + * Look up a user by their Stripe customer ID. + */ +export async function getUserByStripeCustomerId( + stripeCustomerId: string, +): Promise<{ + id: string + banned: boolean + email: string + name: string | null +} | null> { + const users = await db + .select({ + id: schema.user.id, + banned: schema.user.banned, + email: schema.user.email, + name: schema.user.name, + }) + .from(schema.user) + .where(eq(schema.user.stripe_customer_id, stripeCustomerId)) + .limit(1) + + return users[0] ?? null +} diff --git a/web/src/app/api/stripe/cancel-subscription/route.ts b/web/src/app/api/stripe/cancel-subscription/route.ts new file mode 100644 index 000000000..d7075802c --- /dev/null +++ b/web/src/app/api/stripe/cancel-subscription/route.ts @@ -0,0 +1,72 @@ +import { getActiveSubscription } from '@codebuff/billing' +import db from '@codebuff/internal/db' +import * as schema from '@codebuff/internal/db/schema' +import { stripeServer } from '@codebuff/internal/util/stripe' +import { eq } from 'drizzle-orm' +import { NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' + +import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options' +import { logger } from '@/util/logger' + +export async function POST() { + const session = await getServerSession(authOptions) + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + const subscription = await getActiveSubscription({ userId, logger }) + if (!subscription) { + return NextResponse.json( + { error: 'No active subscription found.' }, + { status: 404 }, + ) + } + + try { + await stripeServer.subscriptions.update( + subscription.stripe_subscription_id, + { cancel_at_period_end: true }, + ) + } catch (error: unknown) { + const message = + (error as { raw?: { message?: string } })?.raw?.message || + 'Failed to cancel subscription in Stripe.' + logger.error( + { error: message, userId, subscriptionId: subscription.stripe_subscription_id }, + 'Stripe subscription cancellation failed', + ) + return NextResponse.json({ error: message }, { status: 500 }) + } + + try { + await db + .update(schema.subscription) + .set({ cancel_at_period_end: true, scheduled_tier: null, updated_at: new Date() }) + .where( + eq( + schema.subscription.stripe_subscription_id, + subscription.stripe_subscription_id, + ), + ) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + logger.error( + { error: message, userId, subscriptionId: subscription.stripe_subscription_id }, + 'Stripe subscription set to cancel but failed to update local DB — data is inconsistent', + ) + return NextResponse.json( + { error: 'Subscription canceled but failed to update records. Please contact support.' }, + { status: 500 }, + ) + } + + logger.info( + { userId, subscriptionId: subscription.stripe_subscription_id }, + 'Subscription set to cancel at period end', + ) + + return NextResponse.json({ success: true }) +} diff --git a/web/src/app/api/stripe/change-subscription-tier/route.ts b/web/src/app/api/stripe/change-subscription-tier/route.ts new file mode 100644 index 000000000..ac5b9f245 --- /dev/null +++ b/web/src/app/api/stripe/change-subscription-tier/route.ts @@ -0,0 +1,223 @@ +import { + expireActiveBlockGrants, + getActiveSubscription, + getPriceIdFromTier, +} from '@codebuff/billing' +import { trackEvent } from '@codebuff/common/analytics' +import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events' +import { SUBSCRIPTION_TIERS } from '@codebuff/common/constants/subscription-plans' +import db from '@codebuff/internal/db' +import * as schema from '@codebuff/internal/db/schema' +import { stripeServer } from '@codebuff/internal/util/stripe' +import { eq } from 'drizzle-orm' +import { NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' + +import type { SubscriptionTierPrice } from '@codebuff/common/constants/subscription-plans' +import type { NextRequest } from 'next/server' + +import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options' +import { logger } from '@/util/logger' + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions) + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + const user = await db.query.user.findFirst({ + where: eq(schema.user.id, userId), + columns: { banned: true }, + }) + + if (user?.banned) { + logger.warn({ userId }, 'Banned user attempted to change subscription tier') + return NextResponse.json( + { error: 'Your account has been suspended. Please contact support.' }, + { status: 403 }, + ) + } + + const body = await req.json().catch(() => null) + const rawTier = Number(body?.tier) + if (!rawTier || !(rawTier in SUBSCRIPTION_TIERS)) { + return NextResponse.json( + { error: `Invalid tier. Must be one of: ${Object.keys(SUBSCRIPTION_TIERS).join(', ')}.` }, + { status: 400 }, + ) + } + const tier = rawTier as SubscriptionTierPrice + + const subscription = await getActiveSubscription({ userId, logger }) + if (!subscription) { + return NextResponse.json( + { error: 'No active subscription found.' }, + { status: 404 }, + ) + } + + if (subscription.tier == null) { + logger.error( + { userId, subscriptionId: subscription.stripe_subscription_id }, + 'Subscription has no tier configured', + ) + return NextResponse.json( + { error: 'Subscription has no tier configured.' }, + { status: 400 }, + ) + } + + if (tier === subscription.tier && subscription.scheduled_tier == null) { + return NextResponse.json( + { error: 'Already on the requested tier.' }, + { status: 400 }, + ) + } + + if (subscription.scheduled_tier === tier) { + return NextResponse.json( + { error: 'Already scheduled for that tier.' }, + { status: 400 }, + ) + } + + const isCancelDowngrade = tier === subscription.tier && subscription.scheduled_tier != null + const isUpgrade = !isCancelDowngrade && tier > subscription.tier + + const newPriceId = getPriceIdFromTier(tier) + if (!newPriceId) { + return NextResponse.json( + { error: 'Subscription tier not available' }, + { status: 503 }, + ) + } + + try { + const stripeSub = await stripeServer.subscriptions.retrieve( + subscription.stripe_subscription_id, + ) + const itemId = stripeSub.items.data[0]?.id + if (!itemId) { + logger.error( + { userId, subscriptionId: subscription.stripe_subscription_id }, + 'Stripe subscription has no items', + ) + return NextResponse.json( + { error: 'Subscription configuration error.' }, + { status: 500 }, + ) + } + + await stripeServer.subscriptions.update( + subscription.stripe_subscription_id, + { + items: [{ id: itemId, price: newPriceId }], + proration_behavior: isUpgrade ? 'always_invoice' : 'none', + }, + ) + + try { + if (isCancelDowngrade) { + await db + .update(schema.subscription) + .set({ scheduled_tier: null, updated_at: new Date() }) + .where( + eq( + schema.subscription.stripe_subscription_id, + subscription.stripe_subscription_id, + ), + ) + } else if (isUpgrade) { + await Promise.all([ + db + .update(schema.subscription) + .set({ + tier, + stripe_price_id: newPriceId, + scheduled_tier: null, + updated_at: new Date(), + }) + .where( + eq( + schema.subscription.stripe_subscription_id, + subscription.stripe_subscription_id, + ), + ), + expireActiveBlockGrants({ + userId, + subscriptionId: subscription.stripe_subscription_id, + logger, + }), + ]) + } else { + // Downgrade — only schedule the new lower tier for next billing period. + // Keep current tier and stripe_price_id unchanged so limits stay. + await db + .update(schema.subscription) + .set({ + scheduled_tier: tier, + updated_at: new Date(), + }) + .where( + eq( + schema.subscription.stripe_subscription_id, + subscription.stripe_subscription_id, + ), + ) + } + } catch (dbError) { + logger.error( + { error: dbError, userId, subscriptionId: subscription.stripe_subscription_id }, + 'DB update failed after Stripe tier change — webhook will reconcile', + ) + } + + trackEvent({ + event: AnalyticsEvent.SUBSCRIPTION_TIER_CHANGED, + userId, + properties: { + subscriptionId: subscription.stripe_subscription_id, + previousTier: subscription.tier, + newTier: tier, + isUpgrade, + isCancelDowngrade, + }, + logger, + }) + + const logMessage = isCancelDowngrade + ? 'Pending downgrade canceled' + : isUpgrade + ? 'Subscription upgraded — billed immediately' + : 'Subscription downgraded — scheduled for next billing period' + + logger.info( + { + userId, + subscriptionId: subscription.stripe_subscription_id, + previousTier: subscription.tier, + newTier: tier, + isUpgrade, + isCancelDowngrade, + }, + logMessage, + ) + + return NextResponse.json({ success: true, previousTier: subscription.tier, newTier: tier }) + } catch (error: unknown) { + const message = error instanceof Error + ? error.message + : 'Internal server error changing subscription tier.' + logger.error( + { + error, + userId, + subscriptionId: subscription.stripe_subscription_id, + }, + 'Failed to change subscription tier', + ) + return NextResponse.json({ error: message }, { status: 500 }) + } +} diff --git a/web/src/app/api/stripe/create-subscription/route.ts b/web/src/app/api/stripe/create-subscription/route.ts new file mode 100644 index 000000000..202228e70 --- /dev/null +++ b/web/src/app/api/stripe/create-subscription/route.ts @@ -0,0 +1,115 @@ +import { getActiveSubscription, getPriceIdFromTier } from '@codebuff/billing' +import { SUBSCRIPTION_TIERS } from '@codebuff/common/constants/subscription-plans' +import db from '@codebuff/internal/db' +import * as schema from '@codebuff/internal/db/schema' +import { env } from '@codebuff/internal/env' +import { stripeServer } from '@codebuff/internal/util/stripe' +import { eq } from 'drizzle-orm' +import { NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' + +import type { SubscriptionTierPrice } from '@codebuff/common/constants/subscription-plans' +import type { NextRequest } from 'next/server' + +import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options' +import { logger } from '@/util/logger' + +export async function POST(req: NextRequest) { + const session = await getServerSession(authOptions) + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + const body = await req.json().catch(() => null) + const rawTier = Number(body?.tier) + if (!rawTier || !(rawTier in SUBSCRIPTION_TIERS)) { + return NextResponse.json( + { error: `Invalid tier. Must be one of: ${Object.keys(SUBSCRIPTION_TIERS).join(', ')}.` }, + { status: 400 }, + ) + } + const tier = rawTier as SubscriptionTierPrice + + const priceId = getPriceIdFromTier(tier) + if (!priceId) { + return NextResponse.json( + { error: 'Subscription tier not available' }, + { status: 503 }, + ) + } + + const user = await db.query.user.findFirst({ + where: eq(schema.user.id, userId), + columns: { stripe_customer_id: true, banned: true }, + }) + + if (user?.banned) { + logger.warn({ userId }, 'Banned user attempted to create subscription') + return NextResponse.json( + { error: 'Your account has been suspended. Please contact support.' }, + { status: 403 }, + ) + } + + if (!user?.stripe_customer_id) { + return NextResponse.json( + { error: 'Stripe customer not found.' }, + { status: 400 }, + ) + } + + const existing = await getActiveSubscription({ userId, logger }) + if (existing) { + return NextResponse.json( + { error: 'You already have an active subscription.' }, + { status: 409 }, + ) + } + + try { + const checkoutSession = await stripeServer.checkout.sessions.create({ + customer: user.stripe_customer_id, + mode: 'subscription', + line_items: [{ price: priceId, quantity: 1 }], + allow_promotion_codes: true, + success_url: `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/profile?tab=usage&subscription_success=true`, + cancel_url: `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/strong?canceled=true`, + metadata: { + userId, + type: 'strong_subscription', + }, + subscription_data: { + description: `Codebuff Strong — $${tier}/mo`, + metadata: { + userId, + }, + }, + }) + + if (!checkoutSession.url) { + logger.error({ userId }, 'Stripe checkout session created without a URL') + return NextResponse.json( + { error: 'Could not create checkout session.' }, + { status: 500 }, + ) + } + + logger.info( + { userId, sessionId: checkoutSession.id, tier }, + 'Created Strong subscription checkout session', + ) + + return NextResponse.json({ sessionId: checkoutSession.id }) + } catch (error: unknown) { + const message = + (error as { raw?: { message?: string } })?.raw?.message || + 'Internal server error creating subscription.' + logger.error( + { error: message, userId }, + 'Failed to create subscription checkout', + ) + return NextResponse.json({ error: message }, { status: 500 }) + } +} diff --git a/web/src/app/api/stripe/webhook/route.ts b/web/src/app/api/stripe/webhook/route.ts index 65cc0bc5f..5d127b8be 100644 --- a/web/src/app/api/stripe/webhook/route.ts +++ b/web/src/app/api/stripe/webhook/route.ts @@ -2,12 +2,16 @@ import { grantOrganizationCredits, processAndGrantCredit, revokeGrantByOperationId, + handleSubscriptionInvoicePaid, + handleSubscriptionInvoicePaymentFailed, + handleSubscriptionUpdated, + handleSubscriptionDeleted, } from '@codebuff/billing' import db from '@codebuff/internal/db' import * as schema from '@codebuff/internal/db/schema' import { env } from '@codebuff/internal/env' import { sendDisputeNotificationEmail } from '@codebuff/internal/loops' -import { stripeServer } from '@codebuff/internal/util/stripe' +import { getStripeId, stripeServer } from '@codebuff/internal/util/stripe' import { eq } from 'drizzle-orm' import { NextResponse } from 'next/server' @@ -19,9 +23,24 @@ import { evaluateBanConditions, getUserByStripeCustomerId, } from '@/lib/ban-conditions' -import { getStripeCustomerId } from '@/lib/stripe-utils' import { logger } from '@/util/logger' +/** + * Checks whether a Stripe customer ID belongs to an organization. + * + * Uses `org.stripe_customer_id` which is set at org creation time, making it + * reliable regardless of webhook ordering (unlike `stripe_subscription_id` + * which may not be populated yet when early invoice events arrive). + */ +async function isOrgCustomer(stripeCustomerId: string): Promise { + const orgs = await db + .select({ id: schema.org.id }) + .from(schema.org) + .where(eq(schema.org.stripe_customer_id, stripeCustomerId)) + .limit(1) + return orgs.length > 0 +} + async function handleCheckoutSessionCompleted( session: Stripe.Checkout.Session, ) { @@ -220,8 +239,15 @@ async function handleCheckoutSessionCompleted( } } -async function handleSubscriptionEvent(subscription: Stripe.Subscription) { +async function handleOrganizationSubscriptionEvent(subscription: Stripe.Subscription) { const organizationId = subscription.metadata?.organization_id + if (!organizationId) { + logger.warn( + { subscriptionId: subscription.id }, + 'Organization subscription event missing organization_id metadata', + ) + return + } logger.info( { @@ -230,17 +256,9 @@ async function handleSubscriptionEvent(subscription: Stripe.Subscription) { customerId: subscription.customer, organizationId, }, - 'Subscription event received', + 'Organization subscription event received', ) - if (!organizationId) { - logger.warn( - { subscriptionId: subscription.id }, - 'Subscription event received without organization_id in metadata', - ) - return - } - try { // Handle subscription cancellation if (subscription.status === 'canceled') { @@ -301,7 +319,7 @@ async function handleInvoicePaid(invoice: Stripe.Invoice) { let customerId: string | null = null if (invoice.customer) { - customerId = getStripeCustomerId(invoice.customer) + customerId = getStripeId(invoice.customer) } if (creditNotes.data.length > 0) { @@ -354,25 +372,35 @@ const webhookHandler = async (req: NextRequest): Promise => { case 'customer.created': break case 'customer.subscription.created': - case 'customer.subscription.updated': + case 'customer.subscription.updated': { + const sub = event.data.object as Stripe.Subscription + if (sub.metadata?.organization_id) { + await handleOrganizationSubscriptionEvent(sub) + } else { + await handleSubscriptionUpdated({ stripeSubscription: sub, logger }) + } + break + } case 'customer.subscription.deleted': { - await handleSubscriptionEvent(event.data.object as Stripe.Subscription) + const sub = event.data.object as Stripe.Subscription + if (sub.metadata?.organization_id) { + await handleOrganizationSubscriptionEvent(sub) + } else { + await handleSubscriptionDeleted({ stripeSubscription: sub, logger }) + } break } case 'charge.dispute.created': { const dispute = event.data.object as Stripe.Dispute - const chargeId = - typeof dispute.charge === 'string' - ? dispute.charge - : dispute.charge?.id - if (!chargeId) { + if (!dispute.charge) { logger.warn( { disputeId: dispute.id }, 'Dispute received without charge ID', ) break } + const chargeId = getStripeId(dispute.charge) // Get the charge to find the customer const charge = await stripeServer.charges.retrieve(chargeId) @@ -384,9 +412,7 @@ const webhookHandler = async (req: NextRequest): Promise => { break } - const customerId = getStripeCustomerId( - charge.customer as string | Stripe.Customer | Stripe.DeletedCustomer, - ) + const customerId = getStripeId(charge.customer) if (!customerId) { logger.warn( @@ -511,11 +537,39 @@ const webhookHandler = async (req: NextRequest): Promise => { break } case 'invoice.paid': { - await handleInvoicePaid(event.data.object as Stripe.Invoice) + const invoice = event.data.object as Stripe.Invoice + if (invoice.subscription) { + if (!invoice.customer) { + logger.warn( + { invoiceId: invoice.id }, + 'Subscription invoice has no customer — skipping', + ) + } else { + const customerId = getStripeId(invoice.customer) + if (!(await isOrgCustomer(customerId))) { + await handleSubscriptionInvoicePaid({ invoice, logger }) + } + } + } else { + await handleInvoicePaid(invoice) + } break } case 'invoice.payment_failed': { const invoice = event.data.object as Stripe.Invoice + if (invoice.subscription) { + if (!invoice.customer) { + logger.warn( + { invoiceId: invoice.id }, + 'Subscription invoice has no customer — skipping', + ) + } else { + const customerId = getStripeId(invoice.customer) + if (!(await isOrgCustomer(customerId))) { + await handleSubscriptionInvoicePaymentFailed({ invoice, logger }) + } + } + } if ( invoice.metadata?.type === 'auto-topup' && invoice.billing_reason === 'manual' @@ -546,7 +600,7 @@ const webhookHandler = async (req: NextRequest): Promise => { break } default: - console.log(`Unhandled event type ${event.type}`) + logger.debug({ type: event.type }, 'Unhandled Stripe event type') } return NextResponse.json({ received: true }) } catch (err) { diff --git a/web/src/app/api/user/subscription/route.ts b/web/src/app/api/user/subscription/route.ts new file mode 100644 index 000000000..c8d53b8db --- /dev/null +++ b/web/src/app/api/user/subscription/route.ts @@ -0,0 +1,56 @@ +import { + checkRateLimit, + getActiveSubscription, + getSubscriptionLimits, +} from '@codebuff/billing' +import { SUBSCRIPTION_DISPLAY_NAME } from '@codebuff/common/constants/subscription-plans' +import { NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' + +import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options' +import { logger } from '@/util/logger' + +export async function GET() { + const session = await getServerSession(authOptions) + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + const subscription = await getActiveSubscription({ userId, logger }) + + if (!subscription) { + return NextResponse.json({ hasSubscription: false }) + } + + const [rateLimit, limits] = await Promise.all([ + checkRateLimit({ userId, subscription, logger }), + getSubscriptionLimits({ userId, logger, tier: subscription.tier }), + ]) + + return NextResponse.json({ + hasSubscription: true, + displayName: SUBSCRIPTION_DISPLAY_NAME, + subscription: { + status: subscription.status, + billingPeriodEnd: subscription.billing_period_end.toISOString(), + cancelAtPeriodEnd: subscription.cancel_at_period_end, + canceledAt: subscription.canceled_at?.toISOString() ?? null, + tier: subscription.tier, + scheduledTier: subscription.scheduled_tier, + }, + rateLimit: { + limited: rateLimit.limited, + reason: rateLimit.reason, + canStartNewBlock: rateLimit.canStartNewBlock, + blockUsed: rateLimit.blockUsed, + blockLimit: rateLimit.blockLimit, + blockResetsAt: rateLimit.blockResetsAt?.toISOString(), + weeklyUsed: rateLimit.weeklyUsed, + weeklyLimit: rateLimit.weeklyLimit, + weeklyResetsAt: rateLimit.weeklyResetsAt.toISOString(), + weeklyPercentUsed: rateLimit.weeklyPercentUsed, + }, + limits, + }) +} diff --git a/web/src/app/profile/components/usage-display.tsx b/web/src/app/profile/components/usage-display.tsx index dae0f757f..48f90d1a7 100644 --- a/web/src/app/profile/components/usage-display.tsx +++ b/web/src/app/profile/components/usage-display.tsx @@ -10,6 +10,7 @@ import { CreditCard, Star, Megaphone, + Zap, } from 'lucide-react' import React from 'react' @@ -85,6 +86,14 @@ const grantTypeInfo: Record< label: 'Ad Credits', description: 'Earned from viewing ads', }, + subscription: { + bg: 'bg-teal-500', + text: 'text-teal-600 dark:text-teal-400', + gradient: 'from-teal-500/70 to-teal-600/70', + icon: , + label: 'Subscription', + description: 'Credits from your subscription', + }, } interface CreditLeafProps { @@ -234,6 +243,7 @@ export const UsageDisplay = ({ const usedCredits: Record = { free: 0, referral: 0, + subscription: 0, purchase: 0, admin: 0, ad: 0, @@ -252,7 +262,7 @@ export const UsageDisplay = ({ }) // Group credits by expiration type (excluding organization) - const expiringTypes: FilteredGrantType[] = ['free', 'referral'] + const expiringTypes: FilteredGrantType[] = ['free', 'referral', 'subscription'] const nonExpiringTypes: FilteredGrantType[] = ['admin', 'purchase', 'ad'] const expiringTotal = expiringTypes.reduce( diff --git a/web/src/lib/ban-conditions.ts b/web/src/lib/ban-conditions.ts index 2be5352c0..9626b54a3 100644 --- a/web/src/lib/ban-conditions.ts +++ b/web/src/lib/ban-conditions.ts @@ -5,6 +5,8 @@ import { eq } from 'drizzle-orm' import type { Logger } from '@codebuff/common/types/contracts/logger' +export { getUserByStripeCustomerId } from '@codebuff/internal/util/stripe' + // ============================================================================= // CONFIGURATION - Edit these values to adjust ban thresholds // ============================================================================= @@ -102,31 +104,6 @@ const BAN_CONDITIONS: BanCondition[] = [ // PUBLIC API // ============================================================================= -/** - * Look up a user by their Stripe customer ID - */ -export async function getUserByStripeCustomerId( - stripeCustomerId: string, -): Promise<{ - id: string - banned: boolean - email: string - name: string | null -} | null> { - const users = await db - .select({ - id: schema.user.id, - banned: schema.user.banned, - email: schema.user.email, - name: schema.user.name, - }) - .from(schema.user) - .where(eq(schema.user.stripe_customer_id, stripeCustomerId)) - .limit(1) - - return users[0] ?? null -} - /** * Ban a user and log the action */ diff --git a/web/src/lib/stripe-utils.ts b/web/src/lib/stripe-utils.ts index b3cf9ecb7..319e848da 100644 --- a/web/src/lib/stripe-utils.ts +++ b/web/src/lib/stripe-utils.ts @@ -4,12 +4,6 @@ import { eq, or, sql } from 'drizzle-orm' import type Stripe from 'stripe' -export function getStripeCustomerId( - customer: string | Stripe.Customer | Stripe.DeletedCustomer, -): string { - return typeof customer === 'string' ? customer : customer.id -} - export function getSubscriptionItemByType( subscription: Stripe.Subscription, usageType: 'licensed' | 'metered',