Skip to content

Commit 6f88cda

Browse files
bchapuisclaude
andcommitted
Fix subscription check for non-manual triggers by resolving userPlan from org billing
All trigger paths (scheduled, queue, email, Discord, Telegram, WhatsApp) were missing userPlan in execution params, causing subscription-gated nodes to always fail. Now derives the plan from organization billing info using the same logic as the billing route. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 97678c8 commit 6f88cda

8 files changed

Lines changed: 67 additions & 52 deletions

File tree

apps/api/src/db/queries.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2137,6 +2137,7 @@ export async function getOrganizationBillingInfo(
21372137
| {
21382138
computeCredits: number;
21392139
subscriptionStatus: string | null;
2140+
currentPeriodEnd: Date | null;
21402141
overageLimit: number | null;
21412142
}
21422143
| undefined
@@ -2145,6 +2146,7 @@ export async function getOrganizationBillingInfo(
21452146
.select({
21462147
computeCredits: organizations.computeCredits,
21472148
subscriptionStatus: organizations.subscriptionStatus,
2149+
currentPeriodEnd: organizations.currentPeriodEnd,
21482150
overageLimit: organizations.overageLimit,
21492151
})
21502152
.from(organizations)
@@ -2153,6 +2155,22 @@ export async function getOrganizationBillingInfo(
21532155
return organization;
21542156
}
21552157

2158+
/**
2159+
* Derive user plan from organization billing info.
2160+
* Pro if has active subscription OR canceled but still in billing period.
2161+
*/
2162+
export function resolveUserPlan(billingInfo: {
2163+
subscriptionStatus: string | null;
2164+
currentPeriodEnd: Date | null;
2165+
}): string {
2166+
const hasProAccess =
2167+
billingInfo.subscriptionStatus === "active" ||
2168+
(billingInfo.subscriptionStatus === "canceled" &&
2169+
billingInfo.currentPeriodEnd !== null &&
2170+
billingInfo.currentPeriodEnd > new Date());
2171+
return hasProAccess ? "pro" : "trial";
2172+
}
2173+
21562174
/**
21572175
* Create a new secret for an organization
21582176
*

apps/api/src/email.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import type { Workflow } from "@dafthunk/types";
22

33
import type { Bindings } from "./context";
4-
import { createDatabase, getOrganizationComputeCredits } from "./db";
4+
import {
5+
createDatabase,
6+
getOrganizationBillingInfo,
7+
resolveUserPlan,
8+
} from "./db";
59
import { getAgentByName } from "./durable-objects/agent-utils";
610
import { createWorkerRuntime } from "./runtime/cloudflare-worker-runtime";
711
import { WorkflowStore } from "./stores/workflow-store";
@@ -164,20 +168,18 @@ async function triggerWorkflowForEmail({
164168
return;
165169
}
166170

167-
// Get organization compute credits
168-
const computeCredits = await getOrganizationComputeCredits(
169-
db,
170-
organizationId
171-
);
172-
if (computeCredits === undefined) {
171+
// Get organization billing info
172+
const billingInfo = await getOrganizationBillingInfo(db, organizationId);
173+
if (billingInfo === undefined) {
173174
console.error("Organization not found");
174175
return;
175176
}
176177

177178
const executionParams = {
178179
userId: "email_trigger",
179180
organizationId,
180-
computeCredits,
181+
computeCredits: billingInfo.computeCredits,
182+
userPlan: resolveUserPlan(billingInfo),
181183
workflow: {
182184
id: workflow.id,
183185
name: workflow.name,

apps/api/src/queue.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { QueueMessage, Workflow } from "@dafthunk/types";
22
import type { Bindings } from "./context";
33
import { createDatabase } from "./db";
44
import {
5-
getOrganizationComputeCredits,
5+
getOrganizationBillingInfo,
66
getQueueTriggersByQueue,
7+
resolveUserPlan,
78
} from "./db/queries";
89
import { getAgentByName } from "./durable-objects/agent-utils";
910
import { createWorkerRuntime } from "./runtime/cloudflare-worker-runtime";
@@ -27,20 +28,21 @@ async function executeWorkflow(
2728
);
2829

2930
try {
30-
// Get organization compute credits
31-
const computeCredits = await getOrganizationComputeCredits(
31+
// Get organization billing info
32+
const billingInfo = await getOrganizationBillingInfo(
3233
db,
3334
workflowInfo.organizationId
3435
);
35-
if (computeCredits === undefined) {
36+
if (billingInfo === undefined) {
3637
console.error("Organization not found");
3738
return;
3839
}
3940

4041
const executionParams = {
4142
userId: "queue_trigger",
4243
organizationId: workflowInfo.organizationId,
43-
computeCredits,
44+
computeCredits: billingInfo.computeCredits,
45+
userPlan: resolveUserPlan(billingInfo),
4446
workflow: {
4547
id: workflowInfo.id,
4648
name: workflowData.name,

apps/api/src/routes/discord-webhook.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
getDiscordBot,
88
getDiscordBotById,
99
getDiscordTriggersByBot,
10-
getOrganizationComputeCredits,
10+
getOrganizationBillingInfo,
11+
resolveUserPlan,
1112
} from "../db";
1213
import { getAgentByName } from "../durable-objects/agent-utils";
1314
import { createWorkerRuntime } from "../runtime/cloudflare-worker-runtime";
@@ -288,19 +289,17 @@ async function executeWorkflow(
288289
return;
289290
}
290291

291-
const computeCredits = await getOrganizationComputeCredits(
292-
db,
293-
organizationId
294-
);
295-
if (computeCredits === undefined) {
292+
const billingInfo = await getOrganizationBillingInfo(db, organizationId);
293+
if (billingInfo === undefined) {
296294
console.error("[DiscordWebhook] Organization not found");
297295
return;
298296
}
299297

300298
const executionParams = {
301299
userId: "discord_trigger",
302300
organizationId,
303-
computeCredits,
301+
computeCredits: billingInfo.computeCredits,
302+
userPlan: resolveUserPlan(billingInfo),
304303
workflow: {
305304
id: workflow.id,
306305
name: workflow.name,

apps/api/src/routes/telegram-webhook.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { Hono } from "hono";
44
import type { ApiContext } from "../context";
55
import {
66
createDatabase,
7-
getOrganizationComputeCredits,
7+
getOrganizationBillingInfo,
88
getTelegramBot,
99
getTelegramSecretTokenByBot,
1010
getTelegramTriggersByBot,
11+
resolveUserPlan,
1112
} from "../db";
1213
import { getAgentByName } from "../durable-objects/agent-utils";
1314
import { createWorkerRuntime } from "../runtime/cloudflare-worker-runtime";
@@ -205,19 +206,17 @@ async function executeWorkflow(
205206
return;
206207
}
207208

208-
const computeCredits = await getOrganizationComputeCredits(
209-
db,
210-
organizationId
211-
);
212-
if (computeCredits === undefined) {
209+
const billingInfo = await getOrganizationBillingInfo(db, organizationId);
210+
if (billingInfo === undefined) {
213211
console.error("[TelegramWebhook] Organization not found");
214212
return;
215213
}
216214

217215
const executionParams = {
218216
userId: "telegram_trigger",
219217
organizationId,
220-
computeCredits,
218+
computeCredits: billingInfo.computeCredits,
219+
userPlan: resolveUserPlan(billingInfo),
221220
workflow: {
222221
id: workflow.id,
223222
name: workflow.name,

apps/api/src/routes/whatsapp-webhook.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { Hono } from "hono";
44
import type { ApiContext } from "../context";
55
import {
66
createDatabase,
7-
getOrganizationComputeCredits,
7+
getOrganizationBillingInfo,
88
getWhatsAppAccount,
99
getWhatsAppTriggersByAccount,
1010
getWhatsAppVerifyTokenByAccount,
11+
resolveUserPlan,
1112
} from "../db";
1213
import { getAgentByName } from "../durable-objects/agent-utils";
1314
import { createWorkerRuntime } from "../runtime/cloudflare-worker-runtime";
@@ -311,19 +312,17 @@ async function executeWorkflow(
311312
return;
312313
}
313314

314-
const computeCredits = await getOrganizationComputeCredits(
315-
db,
316-
organizationId
317-
);
318-
if (computeCredits === undefined) {
315+
const billingInfo = await getOrganizationBillingInfo(db, organizationId);
316+
if (billingInfo === undefined) {
319317
console.error("[WhatsAppWebhook] Organization not found");
320318
return;
321319
}
322320

323321
const executionParams = {
324322
userId: "whatsapp_trigger",
325323
organizationId,
326-
computeCredits,
324+
computeCredits: billingInfo.computeCredits,
325+
userPlan: resolveUserPlan(billingInfo),
327326
workflow: {
328327
id: workflow.id,
329328
name: workflow.name,

apps/api/src/scheduled.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type { Bindings } from "./context";
44
import {
55
createDatabase,
66
getActiveScheduledTriggers,
7-
getOrganizationComputeCredits,
7+
getOrganizationBillingInfo,
8+
resolveUserPlan,
89
} from "./db";
910
import { getAgentByName } from "./durable-objects/agent-utils";
1011
import { createWorkerRuntime } from "./runtime/cloudflare-worker-runtime";
@@ -62,17 +63,18 @@ export async function handleScheduledEvent(
6263
}
6364
const workflowData = workflowWithData.data;
6465

65-
// Get organization compute credits
66-
const computeCredits = await getOrganizationComputeCredits(
66+
// Get organization billing info
67+
const billingInfo = await getOrganizationBillingInfo(
6768
db,
6869
workflow.organizationId
6970
);
70-
if (computeCredits === undefined) continue;
71+
if (billingInfo === undefined) continue;
7172

7273
const executionParams = {
7374
userId: "scheduled_trigger",
7475
organizationId: workflow.organizationId,
75-
computeCredits,
76+
computeCredits: billingInfo.computeCredits,
77+
userPlan: resolveUserPlan(billingInfo),
7678
workflow: {
7779
id: workflow.id,
7880
name: workflow.name,

apps/api/src/services/execution-manager.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88
import type { BlobParameter } from "@dafthunk/runtime";
99
import type { WorkflowExecution, WorkflowState } from "@dafthunk/types";
10-
import { eq } from "drizzle-orm";
1110
import type { Bindings } from "../context";
1211
import { createDatabase } from "../db/index";
13-
import { getOrganization, getOrganizationBillingInfo } from "../db/queries";
14-
import { users } from "../db/schema";
12+
import {
13+
getOrganization,
14+
getOrganizationBillingInfo,
15+
resolveUserPlan,
16+
} from "../db/queries";
1517
import {
1618
WorkflowExecutor,
1719
type WorkflowExecutorParameters,
@@ -55,16 +57,8 @@ export class ExecutionManager {
5557
}
5658
const { computeCredits, subscriptionStatus, overageLimit } = billingInfo;
5759

58-
// Get user's plan if not provided (e.g., for WebSocket-based execution)
59-
let resolvedUserPlan = userPlan;
60-
if (!resolvedUserPlan) {
61-
const [user] = await db
62-
.select({ plan: users.plan })
63-
.from(users)
64-
.where(eq(users.id, userId))
65-
.limit(1);
66-
resolvedUserPlan = user?.plan;
67-
}
60+
// Resolve user plan from org billing info if not provided
61+
const resolvedUserPlan = userPlan || resolveUserPlan(billingInfo);
6862

6963
validateWorkflowForExecution(state);
7064

0 commit comments

Comments
 (0)