diff --git a/apps/webapp/app/lib/ingest.server.ts b/apps/webapp/app/lib/ingest.server.ts index efc72e0..2bc7557 100644 --- a/apps/webapp/app/lib/ingest.server.ts +++ b/apps/webapp/app/lib/ingest.server.ts @@ -3,6 +3,7 @@ import { IngestionStatus } from "@core/database"; import { EpisodeType } from "@core/types"; import { type z } from "zod"; import { prisma } from "~/db.server"; +import { hasCredits } from "~/services/billing.server"; import { type IngestBodyRequest, ingestTask } from "~/trigger/ingest/ingest"; import { ingestDocumentTask } from "~/trigger/ingest/ingest-document"; @@ -27,6 +28,16 @@ export const addToQueue = async ( ); } + // Check if workspace has sufficient credits before processing + const hasSufficientCredits = await hasCredits( + user.Workspace?.id as string, + "addEpisode", + ); + + if (!hasSufficientCredits) { + throw new Error("no credits"); + } + const queuePersist = await prisma.ingestionQueue.create({ data: { data: body, diff --git a/apps/webapp/app/routes/api.v1.activity.tsx b/apps/webapp/app/routes/api.v1.activity.tsx index a8a6e86..701cfa7 100644 --- a/apps/webapp/app/routes/api.v1.activity.tsx +++ b/apps/webapp/app/routes/api.v1.activity.tsx @@ -75,11 +75,13 @@ const { action, loader } = createActionApiRoute( if (user.Workspace?.id) { try { await triggerWebhookDelivery(activity.id, user.Workspace.id); - logger.log("Webhook delivery triggered for activity", { activityId: activity.id }); + logger.log("Webhook delivery triggered for activity", { + activityId: activity.id, + }); } catch (webhookError) { - logger.error("Failed to trigger webhook delivery", { - activityId: activity.id, - error: webhookError + logger.error("Failed to trigger webhook delivery", { + activityId: activity.id, + error: webhookError, }); // Don't fail the entire request if webhook delivery fails } diff --git a/apps/webapp/app/services/billing.server.ts b/apps/webapp/app/services/billing.server.ts index 89fef99..fa4584c 100644 --- a/apps/webapp/app/services/billing.server.ts +++ b/apps/webapp/app/services/billing.server.ts @@ -6,7 +6,11 @@ */ import { prisma } from "~/db.server"; -import { getPlanConfig } from "~/config/billing.server"; +import { + BILLING_CONFIG, + getPlanConfig, + isBillingEnabled, +} from "~/config/billing.server"; import type { PlanType, Subscription } from "@prisma/client"; export type CreditOperation = "addEpisode" | "search" | "chatMessage"; @@ -221,3 +225,51 @@ export async function getUsageSummary(workspaceId: string) { }, }; } + +/** + * Check if workspace has sufficient credits + */ +export async function hasCredits( + workspaceId: string, + operation: CreditOperation, + amount?: number, +): Promise { + // If billing is disabled, always return true + if (!isBillingEnabled()) { + return true; + } + + const creditCost = amount || BILLING_CONFIG.creditCosts[operation]; + + const workspace = await prisma.workspace.findUnique({ + where: { id: workspaceId }, + include: { + Subscription: true, + user: { + include: { + UserUsage: true, + }, + }, + }, + }); + + if (!workspace?.user?.UserUsage || !workspace.Subscription) { + return false; + } + + const userUsage = workspace.user.UserUsage; + // const subscription = workspace.Subscription; + + // If has available credits, return true + if (userUsage.availableCredits >= creditCost) { + return true; + } + + // If overage is enabled (Pro/Max), return true + // if (subscription.enableUsageBilling) { + // return true; + // } + + // Free plan with no credits left + return false; +} diff --git a/apps/webapp/app/trigger/utils/utils.ts b/apps/webapp/app/trigger/utils/utils.ts index 282a63d..2cb26ce 100644 --- a/apps/webapp/app/trigger/utils/utils.ts +++ b/apps/webapp/app/trigger/utils/utils.ts @@ -650,6 +650,23 @@ export async function deductCredits( }), ]); } else { + await prisma.userUsage.update({ + where: { id: userUsage.id }, + data: { + availableCredits: 0, + usedCredits: userUsage.usedCredits + creditCost, + // Update usage breakdown + ...(operation === "addEpisode" && { + episodeCreditsUsed: userUsage.episodeCreditsUsed + creditCost, + }), + ...(operation === "search" && { + searchCreditsUsed: userUsage.searchCreditsUsed + creditCost, + }), + ...(operation === "chatMessage" && { + chatCreditsUsed: userUsage.chatCreditsUsed + creditCost, + }), + }, + }); } } } diff --git a/apps/webapp/app/utils/mcp/memory.ts b/apps/webapp/app/utils/mcp/memory.ts index c4cdd90..eab3569 100644 --- a/apps/webapp/app/utils/mcp/memory.ts +++ b/apps/webapp/app/utils/mcp/memory.ts @@ -5,6 +5,8 @@ import { SearchService } from "~/services/search.server"; import { SpaceService } from "~/services/space.server"; import { deepSearch } from "~/trigger/deep-search"; import { IntegrationLoader } from "./integration-loader"; +import { hasCredits } from "~/services/billing.server"; +import { prisma } from "~/db.server"; const searchService = new SearchService(); const spaceService = new SpaceService(); @@ -277,6 +279,30 @@ async function handleUserProfile(userId: string) { // Handler for memory_ingest async function handleMemoryIngest(args: any) { try { + const workspace = await prisma.workspace.findFirst({ + where: { + userId: args.userId, + }, + }); + + // Check if workspace has sufficient credits before processing + const hasSufficientCredits = await hasCredits( + workspace?.id as string, + "addEpisode", + ); + + if (!hasSufficientCredits) { + return { + content: [ + { + type: "text", + text: `Error ingesting data: your credits have expired`, + }, + ], + isError: true, + }; + } + // Use spaceIds from args if provided, otherwise use spaceId from query params const spaceIds = args.spaceIds || (args.spaceId ? [args.spaceId] : undefined); diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 292cd3b..223c337 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -79,8 +79,8 @@ "@tiptap/pm": "^2.11.9", "@tiptap/react": "^2.11.9", "@tiptap/starter-kit": "2.11.9", - "@trigger.dev/react-hooks": "4.0.0-v4-beta.22", - "@trigger.dev/sdk": "4.0.0-v4-beta.22", + "@trigger.dev/react-hooks": "4.0.4", + "@trigger.dev/sdk": "4.0.4", "ai": "4.3.19", "axios": "^1.10.0", "bullmq": "^5.53.2", @@ -152,7 +152,7 @@ "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.7", - "@trigger.dev/build": "4.0.0-v4-beta.22", + "@trigger.dev/build": "4.0.4", "@types/compression": "^1.7.2", "@types/d3": "^7.4.3", "@types/express": "^4.17.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5da3d4d..051005e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -476,11 +476,11 @@ importers: specifier: 2.11.9 version: 2.11.9 '@trigger.dev/react-hooks': - specifier: 4.0.0-v4-beta.22 - version: 4.0.0-v4-beta.22(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 4.0.4 + version: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@trigger.dev/sdk': - specifier: 4.0.0-v4-beta.22 - version: 4.0.0-v4-beta.22(ai@4.3.19(react@18.3.1)(zod@3.25.76))(zod@3.25.76) + specifier: 4.0.4 + version: 4.0.4(ai@4.3.19(react@18.3.1)(zod@3.25.76))(zod@3.25.76) ai: specifier: 4.3.19 version: 4.3.19(react@18.3.1)(zod@3.25.76) @@ -690,8 +690,8 @@ importers: specifier: ^4.1.7 version: 4.1.9(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0)) '@trigger.dev/build': - specifier: 4.0.0-v4-beta.22 - version: 4.0.0-v4-beta.22(typescript@5.8.3) + specifier: 4.0.4 + version: 4.0.4(typescript@5.8.3) '@types/compression': specifier: ^1.7.2 version: 1.8.1 @@ -2863,12 +2863,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@1.30.1': - resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.0.1': resolution: {integrity: sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -3053,10 +3047,6 @@ packages: resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==} engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.28.0': - resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} - engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.36.0': resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==} engines: {node: '>=14'} @@ -5538,31 +5528,27 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 - '@trigger.dev/build@4.0.0-v4-beta.22': - resolution: {integrity: sha512-W+SfA7soXzD7f2GQQL2Q4x3+JQkJijcVjgKpNuwPdZcgI++YpJkPzJ5RlT98flErU8ZvuiL26SAur2tvObrZgA==} + '@trigger.dev/build@4.0.4': + resolution: {integrity: sha512-W3mP+RBkcYOrNYTTmQ/WdU6LB+2Tk1S6r3OjEWqXEPsXLEEw6BzHTHZBirHYX4lWRBL9jVkL+/H74ycyNfzRjg==} engines: {node: '>=18.20.0'} - '@trigger.dev/core@4.0.0-v4-beta.22': - resolution: {integrity: sha512-FVaVNsW3KQgYEWStr80Iu+1l4KMyHPVU4QbV55pLQp7d126jOuP+hXYp7LhnYVZtgcQLIZSC0VjJc/UYwr4D6g==} + '@trigger.dev/core@4.0.4': + resolution: {integrity: sha512-c5myttkNhqaqvLlEz3ttE1qEsULlD6ILBge5FAfEtMv9HVS/pNlgvMKrdFMefaGO/bE4HoxrNGdJsY683Kq32w==} engines: {node: '>=18.20.0'} - '@trigger.dev/core@4.0.0-v4-beta.27': - resolution: {integrity: sha512-PJzW07GbxeHKigZ0AiO4aAtDdb2r5iioI7P6TLTqp3XfsxLb1ezPNv3zt6dy1uvZsefGR/EO4y7X0VN1pJyLTA==} - engines: {node: '>=18.20.0'} - - '@trigger.dev/react-hooks@4.0.0-v4-beta.22': - resolution: {integrity: sha512-hWBoxEkNSM+IcFsUlFEJBcMZGmpaYGPy5k/o+iK9QNLURiQsKEYGYoBzKlA7iP0cVPwhIV1eNlsPediNRQyTsA==} + '@trigger.dev/react-hooks@4.0.4': + resolution: {integrity: sha512-tgyaGKwFTbVaD4QZdR5GBc2R7T/yq+vHpWw506ys75Mo9uEZN0rGmw7g5q1Pe4XJvsdDiVjcxcJ4tK8zwUM5Zg==} engines: {node: '>=18.20.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^18.0 || ^19.0 || ^19.0.0-rc - '@trigger.dev/sdk@4.0.0-v4-beta.22': - resolution: {integrity: sha512-yRv9G/KODpItU16Iv6gCfLQ2SjhGq443zTlYKY3XZf4HHIuByOhkOKYPRpl82FmJprL8DxnT7V0pma4kfHBzPQ==} + '@trigger.dev/sdk@4.0.4': + resolution: {integrity: sha512-54krRw9SN1CGm5u17JBzu0hNzRf1u37jKbSFFngPJjUOltOgi/owey5+KNu1rGthabhOBK2VKzvKEd4sn08RCA==} engines: {node: '>=18.20.0'} peerDependencies: - ai: ^4.2.0 - zod: ^3.0.0 + ai: ^4.2.0 || ^5.0.0 + zod: ^3.0.0 || ^4.0.0 peerDependenciesMeta: ai: optional: true @@ -8990,10 +8976,6 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. - lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} @@ -15270,11 +15252,6 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.25.1 - '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.28.0 - '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15524,8 +15501,6 @@ snapshots: '@opentelemetry/semantic-conventions@1.25.1': {} - '@opentelemetry/semantic-conventions@1.28.0': {} - '@opentelemetry/semantic-conventions@1.36.0': {} '@oslojs/asn1@1.0.0': @@ -18414,9 +18389,9 @@ snapshots: '@tiptap/core': 2.25.0(@tiptap/pm@2.25.0) '@tiptap/pm': 2.25.0 - '@trigger.dev/build@4.0.0-v4-beta.22(typescript@5.8.3)': + '@trigger.dev/build@4.0.4(typescript@5.8.3)': dependencies: - '@trigger.dev/core': 4.0.0-v4-beta.22 + '@trigger.dev/core': 4.0.4 pkg-types: 1.3.1 tinyglobby: 0.2.14 tsconfck: 3.1.3(typescript@5.8.3) @@ -18426,46 +18401,7 @@ snapshots: - typescript - utf-8-validate - '@trigger.dev/core@4.0.0-v4-beta.22': - dependencies: - '@bugsnag/cuid': 3.2.1 - '@electric-sql/client': 1.0.0-beta.1 - '@google-cloud/precise-date': 4.0.0 - '@jsonhero/path': 1.0.21 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.52.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) - '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node': 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0) - '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.25.1 - dequal: 2.0.3 - eventsource: 3.0.7 - eventsource-parser: 3.0.3 - execa: 8.0.1 - humanize-duration: 3.33.0 - jose: 5.10.0 - nanoid: 3.3.8 - prom-client: 15.1.3 - socket.io: 4.7.4 - socket.io-client: 4.7.5 - std-env: 3.9.0 - superjson: 2.2.2 - tinyexec: 0.3.2 - zod: 3.23.8 - zod-error: 1.5.0 - zod-validation-error: 1.5.0(zod@3.23.8) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@trigger.dev/core@4.0.0-v4-beta.27': + '@trigger.dev/core@4.0.4': dependencies: '@bugsnag/cuid': 3.2.1 '@electric-sql/client': 1.0.0-beta.1 @@ -18488,7 +18424,6 @@ snapshots: execa: 8.0.1 humanize-duration: 3.33.0 jose: 5.10.0 - lodash.get: 4.4.2 nanoid: 3.3.8 prom-client: 15.1.3 socket.io: 4.7.4 @@ -18505,9 +18440,9 @@ snapshots: - supports-color - utf-8-validate - '@trigger.dev/react-hooks@4.0.0-v4-beta.22(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@trigger.dev/react-hooks@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@trigger.dev/core': 4.0.0-v4-beta.27 + '@trigger.dev/core': 4.0.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) swr: 2.3.3(react@18.3.1) @@ -18516,15 +18451,14 @@ snapshots: - supports-color - utf-8-validate - '@trigger.dev/sdk@4.0.0-v4-beta.22(ai@4.3.19(react@18.3.1)(zod@3.25.76))(zod@3.25.76)': + '@trigger.dev/sdk@4.0.4(ai@4.3.19(react@18.3.1)(zod@3.25.76))(zod@3.25.76)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.52.1 - '@opentelemetry/semantic-conventions': 1.25.1 - '@trigger.dev/core': 4.0.0-v4-beta.22 + '@opentelemetry/semantic-conventions': 1.36.0 + '@trigger.dev/core': 4.0.4 chalk: 5.4.1 cronstrue: 2.59.0 - debug: 4.4.1(supports-color@10.0.0) + debug: 4.4.3 evt: 2.5.9 slug: 6.1.0 ulid: 2.4.0 @@ -20393,7 +20327,6 @@ snapshots: debug@4.4.3: dependencies: ms: 2.1.3 - optional: true decamelize-keys@1.1.1: dependencies: @@ -22568,8 +22501,6 @@ snapshots: lodash.defaults@4.2.0: {} - lodash.get@4.4.2: {} - lodash.isarguments@3.1.0: {} lodash.isplainobject@4.0.6: {}