From c8e46b8dc3f64799d21f55178ad5d7606b21c06f Mon Sep 17 00:00:00 2001 From: waleedlatif1 Date: Thu, 14 Aug 2025 11:19:13 -0700 Subject: [PATCH 1/5] fix(billing): clinet-side envvar for billing --- .../undeployed-parent-deployed-child.test.ts | 251 +++++++++++++++++ .../workflow-handler-integration.test.ts | 259 ++++++++++++++++++ apps/sim/lib/env.ts | 1 - apps/sim/lib/environment.ts | 4 +- 4 files changed, 512 insertions(+), 3 deletions(-) create mode 100644 apps/sim/executor/handlers/workflow/undeployed-parent-deployed-child.test.ts create mode 100644 apps/sim/executor/handlers/workflow/workflow-handler-integration.test.ts diff --git a/apps/sim/executor/handlers/workflow/undeployed-parent-deployed-child.test.ts b/apps/sim/executor/handlers/workflow/undeployed-parent-deployed-child.test.ts new file mode 100644 index 00000000000..05c4e7f728e --- /dev/null +++ b/apps/sim/executor/handlers/workflow/undeployed-parent-deployed-child.test.ts @@ -0,0 +1,251 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { BlockType } from '@/executor/consts' +import type { ExecutionContext } from '@/executor/types' +import type { SerializedBlock } from '@/serializer/types' +import { WorkflowBlockHandler } from './workflow-handler' + +// Mock dependencies +vi.mock('@/lib/auth/internal', () => ({ + generateInternalToken: vi.fn(() => Promise.resolve('mock-internal-token')), +})) + +vi.mock('@/lib/urls/utils', () => ({ + getBaseUrl: vi.fn(() => 'http://localhost:3000'), +})) + +vi.mock('@/lib/logs/console/logger', () => ({ + createLogger: vi.fn(() => ({ + info: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + warn: vi.fn(), + })), +})) + +vi.mock('@/stores/workflows/registry/store', () => ({ + useWorkflowRegistry: { + getState: vi.fn(() => ({ + workflows: { + '594431a6-18bb-481c-8b27-84502bf480e5': { + name: 'runWorkflow', + id: '594431a6-18bb-481c-8b27-84502bf480e5', + }, + }, + })), + }, +})) + +describe('WorkflowBlockHandler - Undeployed Parent → Deployed Child Issue', () => { + let handler: WorkflowBlockHandler + let mockFetch: any + + beforeEach(() => { + handler = new WorkflowBlockHandler() + mockFetch = vi.fn() + global.fetch = mockFetch + global.performance = { now: vi.fn(() => Date.now()) } as any + }) + + it('should reproduce the exact issue: undeployed parent workflow trying to execute deployed child', async () => { + // Simulate the exact scenario from the bug report + const workflowBlock: SerializedBlock = { + id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', // Actual block ID from debug data + metadata: { + id: BlockType.WORKFLOW, + name: 'Workflow 1', + type: 'workflow', + }, + inputs: {}, + outputs: {}, + } + + const inputs = { + workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', // Child workflow ID + input: undefined, + } + + const context: ExecutionContext = { + workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', // Undeployed parent workflow + environmentVariables: {}, + workflowVariables: {}, + } + + // Scenario 1: Child workflow returns 404 (the actual bug) + console.log('Testing scenario: Child workflow returns 404 despite existing in database') + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + json: vi.fn().mockResolvedValue({ error: 'Workflow not found' }), + }) + + await expect(handler.execute(workflowBlock, inputs, context)).rejects.toThrow( + 'Child workflow 594431a6-18bb-481c-8b27-84502bf480e5 not found' + ) + + // Verify the API call was made with internal token + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost:3000/api/workflows/594431a6-18bb-481c-8b27-84502bf480e5', + { + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer mock-internal-token', + }, + } + ) + + console.log('✅ Successfully reproduced the bug - 404 error for existing child workflow') + }) + + it('should test potential fix: different authentication or retry logic', async () => { + const workflowBlock: SerializedBlock = { + id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', + metadata: { + id: BlockType.WORKFLOW, + name: 'Workflow 1', + type: 'workflow', + }, + inputs: {}, + outputs: {}, + } + + const inputs = { + workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', + input: undefined, + } + + const context: ExecutionContext = { + workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', + environmentVariables: {}, + workflowVariables: {}, + } + + // Test what happens if we first get 404, then retry and succeed + // This could help identify if it's a timing/race condition issue + console.log('Testing scenario: Retry mechanism for failed child workflow loads') + + let callCount = 0 + mockFetch.mockImplementation(() => { + callCount++ + if (callCount === 1) { + // First call fails with 404 + return Promise.resolve({ + ok: false, + status: 404, + statusText: 'Not Found', + json: vi.fn().mockResolvedValue({ error: 'Workflow not found' }), + }) + } + // Second call succeeds (simulating eventual consistency) + return Promise.resolve({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ + data: { + name: 'runWorkflow', + state: { + blocks: { + 'starter-block': { + id: 'starter-block', + type: 'starter', + name: 'Start', + subBlocks: {}, + }, + }, + edges: [], + loops: {}, + parallels: {}, + }, + variables: {}, + }, + }), + }) + }) + + // Current implementation should fail on first 404 + await expect(handler.execute(workflowBlock, inputs, context)).rejects.toThrow( + 'Child workflow 594431a6-18bb-481c-8b27-84502bf480e5 not found' + ) + + expect(callCount).toBe(1) // Should only try once with current implementation + console.log('✅ Current implementation fails immediately on 404 (no retry)') + }) + + it('should test potential root cause: execution stack interference', async () => { + console.log('Testing if execution stack from PR #927 affects child workflow loading') + + // The PR #927 changed execution stack management + // This test checks if that interferes with child workflow execution + + const workflowBlock: SerializedBlock = { + id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', + metadata: { + id: BlockType.WORKFLOW, + name: 'Workflow 1', + type: 'workflow', + }, + inputs: {}, + outputs: {}, + } + + const inputs = { + workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', + } + + // Test with the parent workflow already in execution stack + const context: ExecutionContext = { + workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', + environmentVariables: {}, + workflowVariables: {}, + } + + // Mock successful child workflow load + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ + data: { + name: 'runWorkflow', + state: { + blocks: { start: { id: 'start', type: 'starter', name: 'Start', subBlocks: {} } }, + edges: [], + loops: {}, + parallels: {}, + }, + variables: {}, + }, + }), + }) + + // Mock successful execution + vi.doMock('@/executor', () => ({ + Executor: vi.fn().mockImplementation(() => ({ + execute: vi.fn().mockResolvedValue({ + success: true, + output: { result: 'success' }, + }), + })), + })) + + vi.doMock('@/serializer', () => ({ + Serializer: vi.fn().mockImplementation(() => ({ + serializeWorkflow: vi.fn().mockReturnValue({ + blocks: {}, + edges: [], + blockInputMap: {}, + blockOutputMap: {}, + }), + })), + })) + + try { + const result = await handler.execute(workflowBlock, inputs, context) + console.log('✅ Execution succeeded when child workflow is accessible') + expect(result).toBeDefined() + } catch (error) { + console.log(`❌ Execution failed: ${error.message}`) + // This would indicate if execution stack or other issues interfere + } + }) +}) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler-integration.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler-integration.test.ts new file mode 100644 index 00000000000..1e276c8072a --- /dev/null +++ b/apps/sim/executor/handlers/workflow/workflow-handler-integration.test.ts @@ -0,0 +1,259 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { BlockType } from '@/executor/consts' +import type { ExecutionContext } from '@/executor/types' +import type { SerializedBlock } from '@/serializer/types' +import { WorkflowBlockHandler } from './workflow-handler' + +// Mock the external dependencies +vi.mock('@/lib/auth/internal', () => ({ + generateInternalToken: vi.fn(() => Promise.resolve('mock-token')), +})) + +vi.mock('@/lib/urls/utils', () => ({ + getBaseUrl: vi.fn(() => 'http://localhost:3000'), +})) + +vi.mock('@/lib/logs/console/logger', () => ({ + createLogger: vi.fn(() => ({ + info: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + warn: vi.fn(), + })), +})) + +vi.mock('@/stores/workflows/registry/store', () => ({ + useWorkflowRegistry: { + getState: vi.fn(() => ({ + workflows: { + '594431a6-18bb-481c-8b27-84502bf480e5': { + name: 'runWorkflow', + id: '594431a6-18bb-481c-8b27-84502bf480e5', + }, + }, + })), + }, +})) + +// Mock the serializer +vi.mock('@/serializer', () => ({ + Serializer: vi.fn().mockImplementation(() => ({ + serializeWorkflow: vi.fn().mockReturnValue({ + blocks: {}, + edges: [], + blockInputMap: {}, + blockOutputMap: {}, + }), + })), +})) + +// Mock the executor +const mockExecutorResult = { + success: true, + output: { test: 'result' }, + metadata: { duration: 1000 }, +} + +vi.mock('@/executor', () => ({ + Executor: vi.fn().mockImplementation(() => ({ + execute: vi.fn().mockResolvedValue(mockExecutorResult), + })), +})) + +describe('WorkflowBlockHandler Integration Test - Child Workflow Not Found', () => { + let handler: WorkflowBlockHandler + let mockFetch: any + + beforeEach(() => { + handler = new WorkflowBlockHandler() + + // Mock global fetch + mockFetch = vi.fn() + global.fetch = mockFetch + + // Mock performance for timing + global.performance = { + now: vi.fn(() => Date.now()), + } as any + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should reproduce the "Child workflow not found" error from real data', async () => { + // This is the actual workflow block from the failing parent workflow + const workflowBlock: SerializedBlock = { + id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', + metadata: { + id: BlockType.WORKFLOW, + name: 'Workflow 1', + type: 'workflow', + }, + inputs: {}, + outputs: {}, + } + + // This represents the subblock values that would reference the child workflow + const inputs = { + workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', // The child workflow that "doesn't exist" + input: undefined, + } + + const context: ExecutionContext = { + workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', // Parent workflow ID + environmentVariables: {}, + workflowVariables: {}, + } + + // Test case 1: Simulate 404 response (child workflow not found) + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + json: vi.fn().mockResolvedValue({ error: 'Workflow not found' }), + }) + + // Should throw the exact error we're seeing + await expect(handler.execute(workflowBlock, inputs, context)).rejects.toThrow( + 'Child workflow 594431a6-18bb-481c-8b27-84502bf480e5 not found' + ) + + // Verify the fetch was called with correct parameters + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost:3000/api/workflows/594431a6-18bb-481c-8b27-84502bf480e5', + { + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer mock-token', + }, + } + ) + }) + + it('should reproduce the error with 403 response (access denied)', async () => { + const workflowBlock: SerializedBlock = { + id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', + metadata: { + id: BlockType.WORKFLOW, + name: 'Workflow 1', + type: 'workflow', + }, + inputs: {}, + outputs: {}, + } + + const inputs = { + workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', + input: undefined, + } + + const context: ExecutionContext = { + workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', + environmentVariables: {}, + workflowVariables: {}, + } + + // Test case 2: Simulate 403 response (access denied - possible cross-workspace issue) + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 403, + statusText: 'Forbidden', + json: vi.fn().mockResolvedValue({ error: 'Access denied' }), + }) + + // Should still throw "not found" error but log access issue + await expect(handler.execute(workflowBlock, inputs, context)).rejects.toThrow( + 'Error in child workflow "runWorkflow": Child workflow 594431a6-18bb-481c-8b27-84502bf480e5 exists but could not be loaded. This may be due to workspace access restrictions or the workflow may be in a different workspace.' + ) + }) + + it('should succeed when child workflow exists and is accessible', async () => { + const workflowBlock: SerializedBlock = { + id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', + metadata: { + id: BlockType.WORKFLOW, + name: 'Workflow 1', + type: 'workflow', + }, + inputs: {}, + outputs: {}, + } + + const inputs = { + workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', + input: { test: 'input' }, + } + + const context: ExecutionContext = { + workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', + environmentVariables: {}, + workflowVariables: {}, + } + + // Mock successful workflow fetch + const mockWorkflowResponse = { + data: { + name: 'runWorkflow', + state: { + blocks: { + 'starter-block': { + id: 'starter-block', + type: 'starter', + name: 'Start', + subBlocks: {}, + }, + }, + edges: [], + loops: {}, + parallels: {}, + }, + variables: {}, + }, + } + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValue(mockWorkflowResponse), + }) + + const result = await handler.execute(workflowBlock, inputs, context) + + expect(result).toEqual({ + success: true, + childWorkflowName: 'runWorkflow', + result: mockExecutorResult, + }) + }) + + it('should handle network errors gracefully', async () => { + const workflowBlock: SerializedBlock = { + id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', + metadata: { + id: BlockType.WORKFLOW, + name: 'Workflow 1', + type: 'workflow', + }, + inputs: {}, + outputs: {}, + } + + const inputs = { + workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', + } + + const context: ExecutionContext = { + workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', + environmentVariables: {}, + workflowVariables: {}, + } + + // Simulate network error + mockFetch.mockRejectedValueOnce(new Error('Network error')) + + await expect(handler.execute(workflowBlock, inputs, context)).rejects.toThrow( + 'Error in child workflow "runWorkflow": Child workflow 594431a6-18bb-481c-8b27-84502bf480e5 not found' + ) + }) +}) diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index 6e1b175159e..ccf5f907437 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -32,7 +32,6 @@ export const env = createEnv({ REDIS_URL: z.string().url().optional(), // Redis connection string for caching/sessions // Payment & Billing - BILLING_ENABLED: z.boolean().optional(), // Enable billing enforcement and usage tracking STRIPE_SECRET_KEY: z.string().min(1).optional(), // Stripe secret key for payment processing STRIPE_BILLING_WEBHOOK_SECRET: z.string().min(1).optional(), // Webhook secret for billing events STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(), // General Stripe webhook secret diff --git a/apps/sim/lib/environment.ts b/apps/sim/lib/environment.ts index a0de1876678..b26db5cfbc5 100644 --- a/apps/sim/lib/environment.ts +++ b/apps/sim/lib/environment.ts @@ -1,7 +1,7 @@ /** * Environment utility functions for consistent environment detection across the application */ -import { env, isTruthy } from './env' +import { env } from './env' /** * Is the application running in production mode @@ -26,7 +26,7 @@ export const isHosted = env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai' /** * Is billing enforcement enabled */ -export const isBillingEnabled = isTruthy(env.BILLING_ENABLED) +export const isBillingEnabled = env.NEXT_PUBLIC_BILLING_ENABLED /** * Get cost multiplier based on environment From 30cc749c8e8d7a498c20d9d0039c5b89b53d8628 Mon Sep 17 00:00:00 2001 From: waleedlatif1 Date: Thu, 14 Aug 2025 11:21:10 -0700 Subject: [PATCH 2/5] remove unrelated files --- .../undeployed-parent-deployed-child.test.ts | 251 ----------------- .../workflow-handler-integration.test.ts | 259 ------------------ 2 files changed, 510 deletions(-) delete mode 100644 apps/sim/executor/handlers/workflow/undeployed-parent-deployed-child.test.ts delete mode 100644 apps/sim/executor/handlers/workflow/workflow-handler-integration.test.ts diff --git a/apps/sim/executor/handlers/workflow/undeployed-parent-deployed-child.test.ts b/apps/sim/executor/handlers/workflow/undeployed-parent-deployed-child.test.ts deleted file mode 100644 index 05c4e7f728e..00000000000 --- a/apps/sim/executor/handlers/workflow/undeployed-parent-deployed-child.test.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { BlockType } from '@/executor/consts' -import type { ExecutionContext } from '@/executor/types' -import type { SerializedBlock } from '@/serializer/types' -import { WorkflowBlockHandler } from './workflow-handler' - -// Mock dependencies -vi.mock('@/lib/auth/internal', () => ({ - generateInternalToken: vi.fn(() => Promise.resolve('mock-internal-token')), -})) - -vi.mock('@/lib/urls/utils', () => ({ - getBaseUrl: vi.fn(() => 'http://localhost:3000'), -})) - -vi.mock('@/lib/logs/console/logger', () => ({ - createLogger: vi.fn(() => ({ - info: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - warn: vi.fn(), - })), -})) - -vi.mock('@/stores/workflows/registry/store', () => ({ - useWorkflowRegistry: { - getState: vi.fn(() => ({ - workflows: { - '594431a6-18bb-481c-8b27-84502bf480e5': { - name: 'runWorkflow', - id: '594431a6-18bb-481c-8b27-84502bf480e5', - }, - }, - })), - }, -})) - -describe('WorkflowBlockHandler - Undeployed Parent → Deployed Child Issue', () => { - let handler: WorkflowBlockHandler - let mockFetch: any - - beforeEach(() => { - handler = new WorkflowBlockHandler() - mockFetch = vi.fn() - global.fetch = mockFetch - global.performance = { now: vi.fn(() => Date.now()) } as any - }) - - it('should reproduce the exact issue: undeployed parent workflow trying to execute deployed child', async () => { - // Simulate the exact scenario from the bug report - const workflowBlock: SerializedBlock = { - id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', // Actual block ID from debug data - metadata: { - id: BlockType.WORKFLOW, - name: 'Workflow 1', - type: 'workflow', - }, - inputs: {}, - outputs: {}, - } - - const inputs = { - workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', // Child workflow ID - input: undefined, - } - - const context: ExecutionContext = { - workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', // Undeployed parent workflow - environmentVariables: {}, - workflowVariables: {}, - } - - // Scenario 1: Child workflow returns 404 (the actual bug) - console.log('Testing scenario: Child workflow returns 404 despite existing in database') - - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 404, - statusText: 'Not Found', - json: vi.fn().mockResolvedValue({ error: 'Workflow not found' }), - }) - - await expect(handler.execute(workflowBlock, inputs, context)).rejects.toThrow( - 'Child workflow 594431a6-18bb-481c-8b27-84502bf480e5 not found' - ) - - // Verify the API call was made with internal token - expect(mockFetch).toHaveBeenCalledWith( - 'http://localhost:3000/api/workflows/594431a6-18bb-481c-8b27-84502bf480e5', - { - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer mock-internal-token', - }, - } - ) - - console.log('✅ Successfully reproduced the bug - 404 error for existing child workflow') - }) - - it('should test potential fix: different authentication or retry logic', async () => { - const workflowBlock: SerializedBlock = { - id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', - metadata: { - id: BlockType.WORKFLOW, - name: 'Workflow 1', - type: 'workflow', - }, - inputs: {}, - outputs: {}, - } - - const inputs = { - workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', - input: undefined, - } - - const context: ExecutionContext = { - workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', - environmentVariables: {}, - workflowVariables: {}, - } - - // Test what happens if we first get 404, then retry and succeed - // This could help identify if it's a timing/race condition issue - console.log('Testing scenario: Retry mechanism for failed child workflow loads') - - let callCount = 0 - mockFetch.mockImplementation(() => { - callCount++ - if (callCount === 1) { - // First call fails with 404 - return Promise.resolve({ - ok: false, - status: 404, - statusText: 'Not Found', - json: vi.fn().mockResolvedValue({ error: 'Workflow not found' }), - }) - } - // Second call succeeds (simulating eventual consistency) - return Promise.resolve({ - ok: true, - status: 200, - json: vi.fn().mockResolvedValue({ - data: { - name: 'runWorkflow', - state: { - blocks: { - 'starter-block': { - id: 'starter-block', - type: 'starter', - name: 'Start', - subBlocks: {}, - }, - }, - edges: [], - loops: {}, - parallels: {}, - }, - variables: {}, - }, - }), - }) - }) - - // Current implementation should fail on first 404 - await expect(handler.execute(workflowBlock, inputs, context)).rejects.toThrow( - 'Child workflow 594431a6-18bb-481c-8b27-84502bf480e5 not found' - ) - - expect(callCount).toBe(1) // Should only try once with current implementation - console.log('✅ Current implementation fails immediately on 404 (no retry)') - }) - - it('should test potential root cause: execution stack interference', async () => { - console.log('Testing if execution stack from PR #927 affects child workflow loading') - - // The PR #927 changed execution stack management - // This test checks if that interferes with child workflow execution - - const workflowBlock: SerializedBlock = { - id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', - metadata: { - id: BlockType.WORKFLOW, - name: 'Workflow 1', - type: 'workflow', - }, - inputs: {}, - outputs: {}, - } - - const inputs = { - workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', - } - - // Test with the parent workflow already in execution stack - const context: ExecutionContext = { - workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', - environmentVariables: {}, - workflowVariables: {}, - } - - // Mock successful child workflow load - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - json: vi.fn().mockResolvedValue({ - data: { - name: 'runWorkflow', - state: { - blocks: { start: { id: 'start', type: 'starter', name: 'Start', subBlocks: {} } }, - edges: [], - loops: {}, - parallels: {}, - }, - variables: {}, - }, - }), - }) - - // Mock successful execution - vi.doMock('@/executor', () => ({ - Executor: vi.fn().mockImplementation(() => ({ - execute: vi.fn().mockResolvedValue({ - success: true, - output: { result: 'success' }, - }), - })), - })) - - vi.doMock('@/serializer', () => ({ - Serializer: vi.fn().mockImplementation(() => ({ - serializeWorkflow: vi.fn().mockReturnValue({ - blocks: {}, - edges: [], - blockInputMap: {}, - blockOutputMap: {}, - }), - })), - })) - - try { - const result = await handler.execute(workflowBlock, inputs, context) - console.log('✅ Execution succeeded when child workflow is accessible') - expect(result).toBeDefined() - } catch (error) { - console.log(`❌ Execution failed: ${error.message}`) - // This would indicate if execution stack or other issues interfere - } - }) -}) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler-integration.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler-integration.test.ts deleted file mode 100644 index 1e276c8072a..00000000000 --- a/apps/sim/executor/handlers/workflow/workflow-handler-integration.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { BlockType } from '@/executor/consts' -import type { ExecutionContext } from '@/executor/types' -import type { SerializedBlock } from '@/serializer/types' -import { WorkflowBlockHandler } from './workflow-handler' - -// Mock the external dependencies -vi.mock('@/lib/auth/internal', () => ({ - generateInternalToken: vi.fn(() => Promise.resolve('mock-token')), -})) - -vi.mock('@/lib/urls/utils', () => ({ - getBaseUrl: vi.fn(() => 'http://localhost:3000'), -})) - -vi.mock('@/lib/logs/console/logger', () => ({ - createLogger: vi.fn(() => ({ - info: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - warn: vi.fn(), - })), -})) - -vi.mock('@/stores/workflows/registry/store', () => ({ - useWorkflowRegistry: { - getState: vi.fn(() => ({ - workflows: { - '594431a6-18bb-481c-8b27-84502bf480e5': { - name: 'runWorkflow', - id: '594431a6-18bb-481c-8b27-84502bf480e5', - }, - }, - })), - }, -})) - -// Mock the serializer -vi.mock('@/serializer', () => ({ - Serializer: vi.fn().mockImplementation(() => ({ - serializeWorkflow: vi.fn().mockReturnValue({ - blocks: {}, - edges: [], - blockInputMap: {}, - blockOutputMap: {}, - }), - })), -})) - -// Mock the executor -const mockExecutorResult = { - success: true, - output: { test: 'result' }, - metadata: { duration: 1000 }, -} - -vi.mock('@/executor', () => ({ - Executor: vi.fn().mockImplementation(() => ({ - execute: vi.fn().mockResolvedValue(mockExecutorResult), - })), -})) - -describe('WorkflowBlockHandler Integration Test - Child Workflow Not Found', () => { - let handler: WorkflowBlockHandler - let mockFetch: any - - beforeEach(() => { - handler = new WorkflowBlockHandler() - - // Mock global fetch - mockFetch = vi.fn() - global.fetch = mockFetch - - // Mock performance for timing - global.performance = { - now: vi.fn(() => Date.now()), - } as any - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - it('should reproduce the "Child workflow not found" error from real data', async () => { - // This is the actual workflow block from the failing parent workflow - const workflowBlock: SerializedBlock = { - id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', - metadata: { - id: BlockType.WORKFLOW, - name: 'Workflow 1', - type: 'workflow', - }, - inputs: {}, - outputs: {}, - } - - // This represents the subblock values that would reference the child workflow - const inputs = { - workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', // The child workflow that "doesn't exist" - input: undefined, - } - - const context: ExecutionContext = { - workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', // Parent workflow ID - environmentVariables: {}, - workflowVariables: {}, - } - - // Test case 1: Simulate 404 response (child workflow not found) - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 404, - statusText: 'Not Found', - json: vi.fn().mockResolvedValue({ error: 'Workflow not found' }), - }) - - // Should throw the exact error we're seeing - await expect(handler.execute(workflowBlock, inputs, context)).rejects.toThrow( - 'Child workflow 594431a6-18bb-481c-8b27-84502bf480e5 not found' - ) - - // Verify the fetch was called with correct parameters - expect(mockFetch).toHaveBeenCalledWith( - 'http://localhost:3000/api/workflows/594431a6-18bb-481c-8b27-84502bf480e5', - { - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer mock-token', - }, - } - ) - }) - - it('should reproduce the error with 403 response (access denied)', async () => { - const workflowBlock: SerializedBlock = { - id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', - metadata: { - id: BlockType.WORKFLOW, - name: 'Workflow 1', - type: 'workflow', - }, - inputs: {}, - outputs: {}, - } - - const inputs = { - workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', - input: undefined, - } - - const context: ExecutionContext = { - workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', - environmentVariables: {}, - workflowVariables: {}, - } - - // Test case 2: Simulate 403 response (access denied - possible cross-workspace issue) - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 403, - statusText: 'Forbidden', - json: vi.fn().mockResolvedValue({ error: 'Access denied' }), - }) - - // Should still throw "not found" error but log access issue - await expect(handler.execute(workflowBlock, inputs, context)).rejects.toThrow( - 'Error in child workflow "runWorkflow": Child workflow 594431a6-18bb-481c-8b27-84502bf480e5 exists but could not be loaded. This may be due to workspace access restrictions or the workflow may be in a different workspace.' - ) - }) - - it('should succeed when child workflow exists and is accessible', async () => { - const workflowBlock: SerializedBlock = { - id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', - metadata: { - id: BlockType.WORKFLOW, - name: 'Workflow 1', - type: 'workflow', - }, - inputs: {}, - outputs: {}, - } - - const inputs = { - workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', - input: { test: 'input' }, - } - - const context: ExecutionContext = { - workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', - environmentVariables: {}, - workflowVariables: {}, - } - - // Mock successful workflow fetch - const mockWorkflowResponse = { - data: { - name: 'runWorkflow', - state: { - blocks: { - 'starter-block': { - id: 'starter-block', - type: 'starter', - name: 'Start', - subBlocks: {}, - }, - }, - edges: [], - loops: {}, - parallels: {}, - }, - variables: {}, - }, - } - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - json: vi.fn().mockResolvedValue(mockWorkflowResponse), - }) - - const result = await handler.execute(workflowBlock, inputs, context) - - expect(result).toEqual({ - success: true, - childWorkflowName: 'runWorkflow', - result: mockExecutorResult, - }) - }) - - it('should handle network errors gracefully', async () => { - const workflowBlock: SerializedBlock = { - id: 'd33f82fa-3d12-41d3-aeb3-c0e2d8fb1d02', - metadata: { - id: BlockType.WORKFLOW, - name: 'Workflow 1', - type: 'workflow', - }, - inputs: {}, - outputs: {}, - } - - const inputs = { - workflowId: '594431a6-18bb-481c-8b27-84502bf480e5', - } - - const context: ExecutionContext = { - workflowId: '0bebc8f9-8563-4d4a-93d2-2a0b4d949897', - environmentVariables: {}, - workflowVariables: {}, - } - - // Simulate network error - mockFetch.mockRejectedValueOnce(new Error('Network error')) - - await expect(handler.execute(workflowBlock, inputs, context)).rejects.toThrow( - 'Error in child workflow "runWorkflow": Child workflow 594431a6-18bb-481c-8b27-84502bf480e5 not found' - ) - }) -}) From bba8b57bff6a576471eb112eb22ffe06dd75dca3 Mon Sep 17 00:00:00 2001 From: waleedlatif1 Date: Fri, 15 Aug 2025 10:03:00 -0700 Subject: [PATCH 3/5] fix(billing): add billing enforcement for webhook executions, consolidate implementation --- apps/sim/app/api/billing/update-cost/route.ts | 38 ++++++++---------- apps/sim/app/api/copilot/methods/route.ts | 23 +---------- .../app/api/webhooks/trigger/[path]/route.ts | 40 ++++++++++++++++++- .../settings-navigation.tsx | 5 +-- .../settings-modal/settings-modal.tsx | 5 +-- .../w/components/sidebar/sidebar.tsx | 5 +-- .../lib/billing/calculations/usage-monitor.ts | 2 +- apps/sim/lib/copilot/utils.ts | 21 ++++++++++ apps/sim/lib/env.ts | 2 +- apps/sim/lib/environment.ts | 5 ++- apps/sim/lib/logs/execution/logger.ts | 8 +++- 11 files changed, 92 insertions(+), 62 deletions(-) create mode 100644 apps/sim/lib/copilot/utils.ts diff --git a/apps/sim/app/api/billing/update-cost/route.ts b/apps/sim/app/api/billing/update-cost/route.ts index e31f614cda4..18c06ddd6ad 100644 --- a/apps/sim/app/api/billing/update-cost/route.ts +++ b/apps/sim/app/api/billing/update-cost/route.ts @@ -2,8 +2,9 @@ import crypto from 'crypto' import { eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' +import { checkInternalApiKey } from '@/lib/copilot/utils' import { env } from '@/lib/env' -import { isProd } from '@/lib/environment' +import { isBillingEnabled, isProd } from '@/lib/environment' import { createLogger } from '@/lib/logs/console/logger' import { db } from '@/db' import { userStats } from '@/db/schema' @@ -11,7 +12,6 @@ import { calculateCost } from '@/providers/utils' const logger = createLogger('billing-update-cost') -// Schema for the request body const UpdateCostSchema = z.object({ userId: z.string().min(1, 'User ID is required'), input: z.number().min(0, 'Input tokens must be a non-negative number'), @@ -19,26 +19,6 @@ const UpdateCostSchema = z.object({ model: z.string().min(1, 'Model is required'), }) -// Authentication function (reused from copilot/methods route) -function checkInternalApiKey(req: NextRequest) { - const apiKey = req.headers.get('x-api-key') - const expectedApiKey = env.INTERNAL_API_SECRET - - if (!expectedApiKey) { - return { success: false, error: 'Internal API key not configured' } - } - - if (!apiKey) { - return { success: false, error: 'API key required' } - } - - if (apiKey !== expectedApiKey) { - return { success: false, error: 'Invalid API key' } - } - - return { success: true } -} - /** * POST /api/billing/update-cost * Update user cost based on token usage with internal API key auth @@ -50,6 +30,20 @@ export async function POST(req: NextRequest) { try { logger.info(`[${requestId}] Update cost request started`) + // Skip cost tracking if billing is disabled + if (!isBillingEnabled) { + logger.debug(`[${requestId}] Billing is disabled, skipping cost update`) + return NextResponse.json({ + success: true, + message: 'Billing disabled, cost update skipped', + data: { + billingEnabled: false, + processedAt: new Date().toISOString(), + requestId, + }, + }) + } + // Check authentication (internal API key) const authResult = checkInternalApiKey(req) if (!authResult.success) { diff --git a/apps/sim/app/api/copilot/methods/route.ts b/apps/sim/app/api/copilot/methods/route.ts index 2c9df5ca7a0..e5ef776c246 100644 --- a/apps/sim/app/api/copilot/methods/route.ts +++ b/apps/sim/app/api/copilot/methods/route.ts @@ -2,7 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { copilotToolRegistry } from '@/lib/copilot/tools/server-tools/registry' import type { NotificationStatus } from '@/lib/copilot/types' -import { env } from '@/lib/env' +import { checkInternalApiKey } from '@/lib/copilot/utils' import { createLogger } from '@/lib/logs/console/logger' import { getRedisClient } from '@/lib/redis' import { createErrorResponse } from '@/app/api/copilot/methods/utils' @@ -240,33 +240,12 @@ async function interruptHandler(toolCallId: string): Promise<{ } } -// Schema for method execution const MethodExecutionSchema = z.object({ methodId: z.string().min(1, 'Method ID is required'), params: z.record(z.any()).optional().default({}), toolCallId: z.string().nullable().optional().default(null), }) -// Simple internal API key authentication -function checkInternalApiKey(req: NextRequest) { - const apiKey = req.headers.get('x-api-key') - const expectedApiKey = env.INTERNAL_API_SECRET - - if (!expectedApiKey) { - return { success: false, error: 'Internal API key not configured' } - } - - if (!apiKey) { - return { success: false, error: 'API key required' } - } - - if (apiKey !== expectedApiKey) { - return { success: false, error: 'Invalid API key' } - } - - return { success: true } -} - /** * POST /api/copilot/methods * Execute a method based on methodId with internal API key auth diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.ts index 77c2a5c65b8..437a384faaf 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.ts @@ -1,6 +1,7 @@ import { tasks } from '@trigger.dev/sdk/v3' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' +import { checkServerSideUsageLimits } from '@/lib/billing' import { createLogger } from '@/lib/logs/console/logger' import { handleSlackChallenge, @@ -245,7 +246,44 @@ export async function POST( // Continue processing - better to risk rate limit bypass than fail webhook } - // --- PHASE 4: Queue webhook execution via trigger.dev --- + // --- PHASE 4: Usage limit check --- + try { + const usageCheck = await checkServerSideUsageLimits(foundWorkflow.userId) + if (usageCheck.isExceeded) { + logger.warn( + `[${requestId}] User ${foundWorkflow.userId} has exceeded usage limits. Skipping webhook execution.`, + { + currentUsage: usageCheck.currentUsage, + limit: usageCheck.limit, + workflowId: foundWorkflow.id, + provider: foundWebhook.provider, + } + ) + + // Return 200 to prevent webhook provider retries, but indicate usage limit exceeded + if (foundWebhook.provider === 'microsoftteams') { + // Microsoft Teams requires specific response format + return NextResponse.json({ + type: 'message', + text: 'Usage limit exceeded. Please upgrade your plan to continue.', + }) + } + + // Simple error response for other providers (return 200 to prevent retries) + return NextResponse.json({ message: 'Usage limit exceeded' }, { status: 200 }) + } + + logger.debug(`[${requestId}] Usage limit check passed for webhook`, { + provider: foundWebhook.provider, + currentUsage: usageCheck.currentUsage, + limit: usageCheck.limit, + }) + } catch (usageError) { + logger.error(`[${requestId}] Error checking webhook usage limits:`, usageError) + // Continue processing - better to risk usage limit bypass than fail webhook + } + + // --- PHASE 5: Queue webhook execution via trigger.dev --- try { // Queue the webhook execution task const handle = await tasks.trigger('webhook-execution', { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx index 36d0bef210e..6f0bc1d22b3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx @@ -8,7 +8,7 @@ import { UserCircle, Users, } from 'lucide-react' -import { getEnv } from '@/lib/env' +import { isBillingEnabled } from '@/lib/environment' import { cn } from '@/lib/utils' import { useSubscriptionStore } from '@/stores/subscription/store' @@ -98,9 +98,6 @@ export function SettingsNavigation({ const { getSubscriptionStatus } = useSubscriptionStore() const subscription = getSubscriptionStatus() - // Get billing status - const isBillingEnabled = getEnv('NEXT_PUBLIC_BILLING_ENABLED') || false - const navigationItems = allNavigationItems.filter((item) => { if (item.hideWhenBillingDisabled && !isBillingEnabled) { return false diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx index bbf156b8428..d897cb928f5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react' import { X } from 'lucide-react' import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui' -import { getEnv } from '@/lib/env' +import { isBillingEnabled } from '@/lib/environment' import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import { @@ -44,9 +44,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { const { activeOrganization } = useOrganizationStore() const hasLoadedInitialData = useRef(false) - // Get billing status - const isBillingEnabled = getEnv('NEXT_PUBLIC_BILLING_ENABLED') || false - useEffect(() => { async function loadAllSettings() { if (!open) return diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index be151412573..7fe9c36e223 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -5,7 +5,7 @@ import { HelpCircle, LibraryBig, ScrollText, Search, Settings, Shapes } from 'lu import { useParams, usePathname, useRouter } from 'next/navigation' import { Button, ScrollArea, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui' import { useSession } from '@/lib/auth-client' -import { getEnv } from '@/lib/env' +import { isBillingEnabled } from '@/lib/environment' import { createLogger } from '@/lib/logs/console/logger' import { generateWorkspaceName } from '@/lib/naming' import { cn } from '@/lib/utils' @@ -196,9 +196,6 @@ export function Sidebar() { const userPermissions = useUserPermissionsContext() const isLoading = workflowsLoading || sessionLoading - // Get billing status - const isBillingEnabled = getEnv('NEXT_PUBLIC_BILLING_ENABLED') || false - // Add state to prevent multiple simultaneous workflow creations const [isCreatingWorkflow, setIsCreatingWorkflow] = useState(false) // Add state to prevent multiple simultaneous workspace creations diff --git a/apps/sim/lib/billing/calculations/usage-monitor.ts b/apps/sim/lib/billing/calculations/usage-monitor.ts index a99fb6a2c1e..d451ee768d4 100644 --- a/apps/sim/lib/billing/calculations/usage-monitor.ts +++ b/apps/sim/lib/billing/calculations/usage-monitor.ts @@ -187,7 +187,7 @@ export async function checkServerSideUsageLimits(userId: string): Promise<{ return { isExceeded: false, currentUsage: 0, - limit: 1000, + limit: 99999, } } diff --git a/apps/sim/lib/copilot/utils.ts b/apps/sim/lib/copilot/utils.ts new file mode 100644 index 00000000000..29835821963 --- /dev/null +++ b/apps/sim/lib/copilot/utils.ts @@ -0,0 +1,21 @@ +import type { NextRequest } from 'next/server' +import { env } from '@/lib/env' + +export function checkInternalApiKey(req: NextRequest) { + const apiKey = req.headers.get('x-api-key') + const expectedApiKey = env.INTERNAL_API_SECRET + + if (!expectedApiKey) { + return { success: false, error: 'Internal API key not configured' } + } + + if (!apiKey) { + return { success: false, error: 'API key required' } + } + + if (apiKey !== expectedApiKey) { + return { success: false, error: 'Invalid API key' } + } + + return { success: true } +} diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index ccf5f907437..4314fa04009 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -233,6 +233,6 @@ export const env = createEnv({ // Need this utility because t3-env is returning string for boolean values. export const isTruthy = (value: string | boolean | number | undefined) => - typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value) + typeof value === 'string' ? value.toLowerCase() === 'true' || value === '1' : Boolean(value) export { getEnv } diff --git a/apps/sim/lib/environment.ts b/apps/sim/lib/environment.ts index b26db5cfbc5..a6a491d9f33 100644 --- a/apps/sim/lib/environment.ts +++ b/apps/sim/lib/environment.ts @@ -1,7 +1,7 @@ /** * Environment utility functions for consistent environment detection across the application */ -import { env } from './env' +import { env, getEnv, isTruthy } from './env' /** * Is the application running in production mode @@ -26,7 +26,8 @@ export const isHosted = env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai' /** * Is billing enforcement enabled */ -export const isBillingEnabled = env.NEXT_PUBLIC_BILLING_ENABLED +export const isBillingEnabled = + isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) || !!env.NEXT_PUBLIC_BILLING_ENABLED /** * Get cost multiplier based on environment diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index 8acf498297f..862a1f7095c 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -1,6 +1,6 @@ import { eq, sql } from 'drizzle-orm' import { v4 as uuidv4 } from 'uuid' -import { getCostMultiplier } from '@/lib/environment' +import { getCostMultiplier, isBillingEnabled } from '@/lib/environment' import { createLogger } from '@/lib/logs/console/logger' import { snapshotService } from '@/lib/logs/execution/snapshot/service' import type { @@ -274,6 +274,12 @@ export class ExecutionLogger implements IExecutionLoggerService { }, trigger: ExecutionTrigger['type'] ): Promise { + // Skip cost tracking if billing is disabled + if (!isBillingEnabled) { + logger.debug('Billing is disabled, skipping user stats cost update') + return + } + if (costSummary.totalCost <= 0) { logger.debug('No cost to update in user stats') return From c74e531d89d42eaee3cc3cff5c97993d6e036274 Mon Sep 17 00:00:00 2001 From: waleedlatif1 Date: Fri, 15 Aug 2025 11:06:06 -0700 Subject: [PATCH 4/5] cleanup --- apps/sim/app/api/billing/update-cost/route.ts | 1 - apps/sim/app/api/chat/route.test.ts | 6 +++++- apps/sim/lib/billing/subscriptions/utils.test.ts | 3 +++ apps/sim/lib/email/unsubscribe.test.ts | 1 + apps/sim/lib/logs/execution/logger.ts | 1 - 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/api/billing/update-cost/route.ts b/apps/sim/app/api/billing/update-cost/route.ts index 18c06ddd6ad..2fdf3d502db 100644 --- a/apps/sim/app/api/billing/update-cost/route.ts +++ b/apps/sim/app/api/billing/update-cost/route.ts @@ -30,7 +30,6 @@ export async function POST(req: NextRequest) { try { logger.info(`[${requestId}] Update cost request started`) - // Skip cost tracking if billing is disabled if (!isBillingEnabled) { logger.debug(`[${requestId}] Billing is disabled, skipping cost update`) return NextResponse.json({ diff --git a/apps/sim/app/api/chat/route.test.ts b/apps/sim/app/api/chat/route.test.ts index 90ae6c4ff16..f1172695a39 100644 --- a/apps/sim/app/api/chat/route.test.ts +++ b/apps/sim/app/api/chat/route.test.ts @@ -246,7 +246,10 @@ describe('Chat API Route', () => { NEXT_PUBLIC_APP_URL: 'http://localhost:3000', }, isTruthy: (value: string | boolean | number | undefined) => - typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value), + typeof value === 'string' + ? value.toLowerCase() === 'true' || value === '1' + : Boolean(value), + getEnv: (variable: string) => process.env[variable], })) const validData = { @@ -291,6 +294,7 @@ describe('Chat API Route', () => { }, isTruthy: (value: string | boolean | number | undefined) => typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value), + getEnv: (variable: string) => process.env[variable], })) const validData = { diff --git a/apps/sim/lib/billing/subscriptions/utils.test.ts b/apps/sim/lib/billing/subscriptions/utils.test.ts index 72cf404c3f9..1a13647e9f3 100644 --- a/apps/sim/lib/billing/subscriptions/utils.test.ts +++ b/apps/sim/lib/billing/subscriptions/utils.test.ts @@ -8,6 +8,9 @@ vi.mock('@/lib/env', () => ({ TEAM_TIER_COST_LIMIT: 40, ENTERPRISE_TIER_COST_LIMIT: 200, }, + isTruthy: (value: string | boolean | number | undefined) => + typeof value === 'string' ? value.toLowerCase() === 'true' || value === '1' : Boolean(value), + getEnv: (variable: string) => process.env[variable], })) describe('Subscription Utilities', () => { diff --git a/apps/sim/lib/email/unsubscribe.test.ts b/apps/sim/lib/email/unsubscribe.test.ts index ba000ef428f..390c0801b30 100644 --- a/apps/sim/lib/email/unsubscribe.test.ts +++ b/apps/sim/lib/email/unsubscribe.test.ts @@ -12,6 +12,7 @@ vi.mock('@/lib/env', () => ({ }, isTruthy: (value: string | boolean | number | undefined) => typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value), + getEnv: (variable: string) => process.env[variable], })) describe('unsubscribe utilities', () => { diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index 862a1f7095c..018704ccf0b 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -274,7 +274,6 @@ export class ExecutionLogger implements IExecutionLoggerService { }, trigger: ExecutionTrigger['type'] ): Promise { - // Skip cost tracking if billing is disabled if (!isBillingEnabled) { logger.debug('Billing is disabled, skipping user stats cost update') return From f09564d0662c36ed42f5315d508fc627529fc364 Mon Sep 17 00:00:00 2001 From: waleedlatif1 Date: Fri, 15 Aug 2025 12:24:03 -0700 Subject: [PATCH 5/5] add back server envvar --- apps/sim/lib/env.ts | 1 + apps/sim/lib/environment.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index 4314fa04009..d2c72fbb5fe 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -43,6 +43,7 @@ export const env = createEnv({ TEAM_TIER_COST_LIMIT: z.number().optional(), // Cost limit for team tier users STRIPE_ENTERPRISE_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for enterprise tier ENTERPRISE_TIER_COST_LIMIT: z.number().optional(), // Cost limit for enterprise tier users + BILLING_ENABLED: z.boolean().optional(), // Enable billing enforcement and usage tracking // Email & Communication RESEND_API_KEY: z.string().min(1).optional(), // Resend API key for transactional emails diff --git a/apps/sim/lib/environment.ts b/apps/sim/lib/environment.ts index a6a491d9f33..46ff3358f5e 100644 --- a/apps/sim/lib/environment.ts +++ b/apps/sim/lib/environment.ts @@ -27,7 +27,7 @@ export const isHosted = env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai' * Is billing enforcement enabled */ export const isBillingEnabled = - isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) || !!env.NEXT_PUBLIC_BILLING_ENABLED + isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) || isTruthy(env.BILLING_ENABLED) /** * Get cost multiplier based on environment