@@ -10,6 +10,100 @@ import { RateLimiter } from '@/services/queue/RateLimiter'
1010
1111const logger = createLogger ( 'ExecutionPreprocessing' )
1212
13+ const BILLING_ERROR_MESSAGES = {
14+ BILLING_REQUIRED :
15+ 'Unable to resolve billing account. This workflow cannot execute without a valid billing account.' ,
16+ BILLING_ERROR_GENERIC : 'Error resolving billing account' ,
17+ } as const
18+
19+ /**
20+ * Attempts to resolve billing actor with fallback for resume contexts.
21+ * Returns the resolved actor user ID or null if resolution fails and should block execution.
22+ *
23+ * For resume contexts, this function allows fallback to the workflow owner if workspace
24+ * billing cannot be resolved, ensuring users can complete their paused workflows even
25+ * if billing configuration changes mid-execution.
26+ *
27+ * @returns Object containing actorUserId (null if should block) and shouldBlock flag
28+ */
29+ async function resolveBillingActorWithFallback ( params : {
30+ requestId : string
31+ workflowId : string
32+ workspaceId : string
33+ executionId : string
34+ triggerType : string
35+ workflowRecord : WorkflowRecord
36+ userId : string
37+ isResumeContext : boolean
38+ baseActorUserId : string | null
39+ failureReason : 'null' | 'error'
40+ error ?: unknown
41+ loggingSession ?: LoggingSession
42+ } ) : Promise <
43+ { actorUserId : string ; shouldBlock : false } | { actorUserId : null ; shouldBlock : true }
44+ > {
45+ const {
46+ requestId,
47+ workflowId,
48+ workspaceId,
49+ executionId,
50+ triggerType,
51+ workflowRecord,
52+ userId,
53+ isResumeContext,
54+ baseActorUserId,
55+ failureReason,
56+ error,
57+ loggingSession,
58+ } = params
59+
60+ if ( baseActorUserId ) {
61+ return { actorUserId : baseActorUserId , shouldBlock : false }
62+ }
63+
64+ const workflowOwner = workflowRecord . userId ?. trim ( )
65+ if ( isResumeContext && workflowOwner ) {
66+ const logMessage =
67+ failureReason === 'null'
68+ ? '[BILLING_FALLBACK] Workspace billing account is null. Using workflow owner for billing.'
69+ : '[BILLING_FALLBACK] Exception during workspace billing resolution. Using workflow owner for billing.'
70+
71+ logger . warn ( `[${ requestId } ] ${ logMessage } ` , {
72+ workflowId,
73+ workspaceId,
74+ fallbackUserId : workflowOwner ,
75+ ...( error ? { error } : { } ) ,
76+ } )
77+
78+ return { actorUserId : workflowOwner , shouldBlock : false }
79+ }
80+
81+ const fallbackUserId = workflowRecord . userId || userId || 'unknown'
82+ const errorMessage =
83+ failureReason === 'null'
84+ ? BILLING_ERROR_MESSAGES . BILLING_REQUIRED
85+ : BILLING_ERROR_MESSAGES . BILLING_ERROR_GENERIC
86+
87+ logger . warn ( `[${ requestId } ] ${ errorMessage } ` , {
88+ workflowId,
89+ workspaceId,
90+ ...( error ? { error } : { } ) ,
91+ } )
92+
93+ await logPreprocessingError ( {
94+ workflowId,
95+ executionId,
96+ triggerType,
97+ requestId,
98+ userId : fallbackUserId ,
99+ workspaceId,
100+ errorMessage,
101+ loggingSession,
102+ } )
103+
104+ return { actorUserId : null , shouldBlock : true }
105+ }
106+
13107export interface PreprocessExecutionOptions {
14108 // Required fields
15109 workflowId : string
@@ -84,7 +178,6 @@ export async function preprocessExecution(
84178 if ( records . length === 0 ) {
85179 logger . warn ( `[${ requestId } ] Workflow not found: ${ workflowId } ` )
86180
87- // Log error to database
88181 await logPreprocessingError ( {
89182 workflowId,
90183 executionId,
@@ -176,47 +269,21 @@ export async function preprocessExecution(
176269 }
177270
178271 if ( ! actorUserId ) {
179- // Special handling for resume context: allow fallback to workflow owner
180- if ( isResumeContext && workflowRecord . userId ) {
181- actorUserId = workflowRecord . userId
182- logger . warn (
183- `[${ requestId } ] Billing account resolution failed in resume context. Falling back to workflow owner.` ,
184- {
185- workflowId,
186- workspaceId,
187- fallbackUserId : actorUserId ,
188- }
189- )
190- // Log warning but don't block execution
191- await logPreprocessingError ( {
192- workflowId,
193- executionId,
194- triggerType,
195- requestId,
196- userId : actorUserId ,
197- workspaceId,
198- errorMessage :
199- 'Warning: Workspace billing account could not be resolved. Using workflow owner for billing. Please verify workspace billing settings.' ,
200- loggingSession : providedLoggingSession ,
201- } )
202- } else {
203- logger . warn ( `[${ requestId } ] Unable to resolve billing account` , {
204- workflowId,
205- workspaceId,
206- } )
207-
208- await logPreprocessingError ( {
209- workflowId,
210- executionId,
211- triggerType,
212- requestId,
213- userId : workflowRecord . userId || userId || 'unknown' ,
214- workspaceId,
215- errorMessage :
216- 'Unable to resolve billing account. This workflow cannot execute without a valid billing account.' ,
217- loggingSession : providedLoggingSession ,
218- } )
272+ const result = await resolveBillingActorWithFallback ( {
273+ requestId,
274+ workflowId,
275+ workspaceId,
276+ executionId,
277+ triggerType,
278+ workflowRecord,
279+ userId,
280+ isResumeContext,
281+ baseActorUserId : actorUserId ,
282+ failureReason : 'null' ,
283+ loggingSession : providedLoggingSession ,
284+ } )
219285
286+ if ( result . shouldBlock ) {
220287 return {
221288 success : false ,
222289 error : {
@@ -226,47 +293,28 @@ export async function preprocessExecution(
226293 } ,
227294 }
228295 }
296+
297+ actorUserId = result . actorUserId
229298 }
230299 } catch ( error ) {
231300 logger . error ( `[${ requestId } ] Error resolving billing actor` , { error, workflowId } )
232301
233- // Special handling for resume context: allow fallback on error
234- if ( isResumeContext && workflowRecord . userId ) {
235- actorUserId = workflowRecord . userId
236- logger . warn (
237- `[${ requestId } ] Billing account resolution error in resume context. Falling back to workflow owner.` ,
238- {
239- workflowId,
240- workspaceId,
241- fallbackUserId : actorUserId ,
242- error,
243- }
244- )
245- // Log warning but continue execution
246- await logPreprocessingError ( {
247- workflowId,
248- executionId,
249- triggerType,
250- requestId,
251- userId : actorUserId ,
252- workspaceId,
253- errorMessage :
254- 'Warning: Error resolving workspace billing account. Using workflow owner for billing. Please verify workspace billing settings.' ,
255- loggingSession : providedLoggingSession ,
256- } )
257- // Continue to next step
258- } else {
259- await logPreprocessingError ( {
260- workflowId,
261- executionId,
262- triggerType,
263- requestId,
264- userId : workflowRecord . userId || userId || 'unknown' ,
265- workspaceId,
266- errorMessage : 'Error resolving billing account' ,
267- loggingSession : providedLoggingSession ,
268- } )
302+ const result = await resolveBillingActorWithFallback ( {
303+ requestId,
304+ workflowId,
305+ workspaceId,
306+ executionId,
307+ triggerType,
308+ workflowRecord,
309+ userId,
310+ isResumeContext,
311+ baseActorUserId : null ,
312+ failureReason : 'error' ,
313+ error,
314+ loggingSession : providedLoggingSession ,
315+ } )
269316
317+ if ( result . shouldBlock ) {
270318 return {
271319 success : false ,
272320 error : {
@@ -276,6 +324,8 @@ export async function preprocessExecution(
276324 } ,
277325 }
278326 }
327+
328+ actorUserId = result . actorUserId
279329 }
280330
281331 // ========== STEP 4: Get User Subscription ==========
0 commit comments