Skip to content

Conversation

@ThyMinimalDev
Copy link
Contributor

@ThyMinimalDev ThyMinimalDev commented Jan 13, 2026

What does this PR do?

Implements a PlatformOrganizationBillingTasker following the pattern established by BookingEmailAndSmsTasker. This tasker will eventually replace the billing processor logic in apps/api/v2/src/modules/billing/billing.processor.ts.

Components created:

  • PlatformOrganizationBillingTasker - Main orchestrator extending the Tasker base class
  • PlatformOrganizationBillingSyncTasker - Synchronous execution version
  • PlatformOrganizationBillingTriggerTasker - Async version using trigger.dev
  • PlatformOrganizationBillingTaskService - Business logic for incrementing subscription usage
  • PlatformBillingRepository - New repository in packages/features/ee/organizations/repositories/ for billing queries
  • trigger.dev task with queue config, schema, and retry settings

Changes to existing code:

  • Added createSubscriptionUsageRecord method to IBillingProviderService interface
  • Implemented the method in StripeBillingService to handle metered subscription usage
  • Added findPlatformOrgByUserId method to OrganizationRepository

Updates since last revision

DI Implementation using @evyweb/ioctopus (features package):

  • Created packages/features/ee/organizations/di/tasker/ directory following the BookingEmailAndSmsTasker DI pattern
  • Added DI tokens, module files, and container files for all billing tasker classes
  • Updated trigger.dev task to use DI container (getPlatformOrganizationBillingTaskService()) instead of manual instantiation

NestJS DI Implementation (API v2):

  • Created PlatformBillingTaskerModule in apps/api/v2/src/lib/modules/
  • Created NestJS service wrappers extending base classes from @calcom/platform-libraries/organizations:
    • PlatformBillingTasker
    • PlatformBillingSyncTaskerService
    • PlatformBillingTriggerTaskerService
    • PlatformBillingTaskService
  • Created PrismaPlatformBillingRepository extending PlatformBillingRepository
  • Created StripeBillingProviderService implementing Pick<IBillingProviderService, "createSubscriptionUsageRecord">
  • Exported IBillingProviderService type from @calcom/platform-libraries/organizations

Error handling fixes (Cubic AI review feedback):

  • Fixed increment-usage.ts to re-throw errors after logging, enabling trigger.dev's retry mechanism
  • Restored maxAttempts: 3 in config.ts to ensure billing task retries are enabled for transient failures

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A - internal implementation
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  1. This PR creates the tasker infrastructure but doesn't wire it up to replace the existing billing processor yet
  2. To test the createSubscriptionUsageRecord method, you would need a Stripe subscription with a metered price item
  3. The trigger.dev task can be tested by triggering it with a valid userId that belongs to a platform organization
  4. The NestJS PlatformBillingTaskerModule is created but not yet imported into any consuming module

Human Review Checklist

  • Verify findPlatformOrgByUserId query correctly finds platform organizations via orgProfiles relation
  • Confirm createSubscriptionUsageRecord correctly identifies metered subscription items
  • Verify trigger.dev queue config and retry settings are appropriate (maxAttempts: 3, concurrencyLimit: 5)
  • Verify DI module dependencies are correctly wired (depsMap keys match constructor parameter names)
  • Verify NestJS module imports don't create circular dependencies (imports OrganizationsModule)
  • Confirm StripeBillingProviderService partial implementation (Pick<IBillingProviderService, ...>) is sufficient
  • Note: PlatformBillingTaskerModule is created but not yet wired up to any consuming module

Checklist

  • My code follows the style guidelines of this project
  • I have checked if my changes generate no new warnings

Link to Devin run: https://app.devin.ai/sessions/220b2aacd19c4dceb080c9a72ea3540f
Link to Devin run (Cubic AI fixes): https://app.devin.ai/sessions/18f96b6f74c64c9294fd29a65dc1fbf3
Requested by: @ThyMinimalDev

… versions

- Create PlatformOrganizationBillingTasker extending Tasker base class
- Add sync tasker (PlatformOrganizationBillingSyncTasker) for synchronous execution
- Add trigger.dev tasker (PlatformOrganizationBillingTriggerTasker) for async execution
- Create trigger.dev task for incrementing subscription usage
- Add PlatformOrganizationBillingTaskService with business logic
- Add PlatformOrganizationBillingRepository for data access
- Add createSubscriptionUsageRecord method to StripeBillingService
- Follow BookingEmailAndSmsTasker pattern for consistency

Co-Authored-By: [email protected] <[email protected]>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration bot and others added 2 commits January 13, 2026 15:11
- Move findPlatformOrgByUserId to OrganizationRepository
- Create new PlatformBillingRepository for billing queries
- Remove PlatformOrganizationBillingRepository (consolidated)
- Update task service and trigger.dev task to use new repositories

Co-Authored-By: [email protected] <[email protected]>
- Create di/tasker/ directory following BookingEmailAndSmsTasker pattern
- Add tokens.ts with DI tokens for all billing tasker classes
- Add module files for PlatformBillingRepository, TaskService, SyncTasker, TriggerTasker, Tasker
- Add container files with getter functions
- Update trigger.dev task to use DI container instead of manual instantiation

Co-Authored-By: [email protected] <[email protected]>
Comment on lines 7 to 11
return this.prismaClient.platformBilling.findUnique({
where: {
id: teamId,
},
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DevinAI add select statement to only get the needed fields

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Added a select statement to only fetch subscriptionId since that's the only field used by the task service.

devin-ai-integration bot and others added 2 commits January 13, 2026 18:48
Only select subscriptionId field since that's the only field used by the task service

Co-Authored-By: [email protected] <[email protected]>
- Add platform billing tasker exports to platform-libraries/organizations.ts
- Export IBillingProviderService type from platform-libraries
- Create StripeBillingProviderService wrapper implementing IBillingProviderService
- Create PrismaPlatformBillingRepository extending PlatformBillingRepository
- Create NestJS service wrappers for all platform billing tasker classes
- Create PlatformBillingTaskerModule with all providers and exports
- Update PlatformOrganizationBillingTaskService to use Pick<IBillingProviderService>

Co-Authored-By: [email protected] <[email protected]>
@keithwillcode keithwillcode added this to the v6.2 milestone Jan 13, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 13, 2026

E2E results are ready!

@ThyMinimalDev ThyMinimalDev marked this pull request as ready for review January 14, 2026 12:01
@ThyMinimalDev ThyMinimalDev requested review from a team as code owners January 14, 2026 12:01
@graphite-app graphite-app bot added core area: core, team members only foundation labels Jan 14, 2026
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 38 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTaskService.ts">

<violation number="1" location="packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTaskService.ts:26">
P1: Dead code: This `if (!team.id)` check will never execute. The `findPlatformOrgByUserId` method uses `findFirstOrThrow`, which throws an exception if no team is found (not returning null). If a team is found, `team.id` is always a valid number.

To properly handle the "user not part of platform org" case, use `findFirst` instead and check for null, or wrap the call in try-catch.</violation>
</file>

<file name="apps/api/v2/src/lib/services/stripe-billing-provider.service.ts">

<violation number="1" location="apps/api/v2/src/lib/services/stripe-billing-provider.service.ts:31">
P2: Silent failure in billing-critical operation. The method logs errors but returns void without propagating failures to the caller. Consider throwing an exception (e.g., `NotFoundException` or `BadRequestException`) so callers can handle failures appropriately and implement retry logic if needed.</violation>
</file>

<file name="apps/api/v2/src/modules/billing/billing.module.ts">

<violation number="1" location="apps/api/v2/src/modules/billing/billing.module.ts:48">
P0: Removing `IsUserInBillingOrg` from providers will break the billing controller. This guard is still used on 4 endpoints (`GET /:teamId/check`, `POST /:teamId/subscribe`, `POST /:teamId/upgrade`, `DELETE /:teamId/unsubscribe`). NestJS needs the guard in providers to resolve its `OrganizationsRepository` dependency.</violation>
</file>

<file name="packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingSyncTasker.ts">

<violation number="1" location="packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingSyncTasker.ts:20">
P3: Grammatical typo: "Delayed task are" should be "Delayed tasks are" (this error appears in all three methods).</violation>
</file>

<file name="packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTasker.ts">

<violation number="1" location="packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTasker.ts:34">
P2: Error is silently discarded in catch block. Consider capturing and logging at least the error message for debugging purposes (avoiding full error object to prevent sensitive data exposure). This applies to all three catch blocks in this file.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

this.logger.info(`PlatformOrganizationBillingTasker incrementUsage success:`, taskResponse, {
userId: payload.userId,
});
} catch {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Error is silently discarded in catch block. Consider capturing and logging at least the error message for debugging purposes (avoiding full error object to prevent sensitive data exposure). This applies to all three catch blocks in this file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/features/ee/organizations/lib/billing/tasker/PlatformOrganizationBillingTasker.ts, line 34:

<comment>Error is silently discarded in catch block. Consider capturing and logging at least the error message for debugging purposes (avoiding full error object to prevent sensitive data exposure). This applies to all three catch blocks in this file.</comment>

<file context>
@@ -0,0 +1,89 @@
+      this.logger.info(`PlatformOrganizationBillingTasker incrementUsage success:`, taskResponse, {
+        userId: payload.userId,
+      });
+    } catch {
+      taskResponse = { runId: "task-failed" };
+      this.logger.error(`PlatformOrganizationBillingTasker incrementUsage failed`, taskResponse, {
</file context>

@github-actions
Copy link
Contributor

Devin AI is addressing Cubic AI's review feedback

New feedback has been sent to the existing Devin session.

View Devin Session

@github-actions
Copy link
Contributor

Devin AI is resolving merge conflicts

This PR has merge conflicts with the main branch. The existing Devin session has been notified to resolve them.

View Devin Session

Devin will:

  1. Merge the latest main into this branch
  2. Resolve any conflicts intelligently
  3. Run lint/type checks to ensure validity
  4. Push the resolved changes

If you prefer to resolve conflicts manually, you can close the Devin session and handle it yourself.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/features/ee/organizations/lib/billing/tasker/trigger/increment-usage.ts">

<violation number="1" location="packages/features/ee/organizations/lib/billing/tasker/trigger/increment-usage.ts:24">
P1: Swallowing the error by returning instead of re-throwing bypasses trigger.dev's retry mechanism. The task config specifies `retry: { maxAttempts: 3 }`, but this retry logic will never execute because the task completes "successfully" after logging the error. For a billing task, this could result in lost usage records without any retry attempts.

If the intent is to prevent retries for certain error types, consider checking the error type and only returning for non-retryable errors (e.g., validation errors), while re-throwing for transient failures (network issues, rate limits).</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

} catch (error) {
if (error instanceof Error) logger.error(error.message);
else logger.error("Unknown error in incrementUsage", { error });
return;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Swallowing the error by returning instead of re-throwing bypasses trigger.dev's retry mechanism. The task config specifies retry: { maxAttempts: 3 }, but this retry logic will never execute because the task completes "successfully" after logging the error. For a billing task, this could result in lost usage records without any retry attempts.

If the intent is to prevent retries for certain error types, consider checking the error type and only returning for non-retryable errors (e.g., validation errors), while re-throwing for transient failures (network issues, rate limits).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/features/ee/organizations/lib/billing/tasker/trigger/increment-usage.ts, line 24:

<comment>Swallowing the error by returning instead of re-throwing bypasses trigger.dev's retry mechanism. The task config specifies `retry: { maxAttempts: 3 }`, but this retry logic will never execute because the task completes "successfully" after logging the error. For a billing task, this could result in lost usage records without any retry attempts.

If the intent is to prevent retries for certain error types, consider checking the error type and only returning for non-retryable errors (e.g., validation errors), while re-throwing for transient failures (network issues, rate limits).</comment>

<file context>
@@ -21,7 +21,7 @@ export const incrementUsage: TaskWithSchema<typeof INCREMENT_USAGE_JOB_ID, typeo
         if (error instanceof Error) logger.error(error.message);
         else logger.error("Unknown error in incrementUsage", { error });
-        throw error;
+        return;
       }
     },
</file context>

Fix confidence (alpha): 9/10

Suggested change
return;
throw error;

✅ Addressed in 465e137

@github-actions
Copy link
Contributor

Devin AI is addressing Cubic AI's review feedback

A Devin session has been created to address the issues identified by Cubic AI.

View Devin Session

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/features/ee/organizations/lib/billing/tasker/trigger/config.ts">

<violation number="1" location="packages/features/ee/organizations/lib/billing/tasker/trigger/config.ts:15">
P1: Setting `maxAttempts: 1` disables all retries, contradicting the PR description which states the error handling fix enables 'trigger.dev's retry mechanism (`maxAttempts: 3`)'. For billing operations interacting with Stripe, having no retries is risky - transient failures could cause lost billing records. Consider keeping `maxAttempts: 3` as originally intended.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

queue: platformBillingQueue,
machine: "small-2x",
retry: {
maxAttempts: 1,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Setting maxAttempts: 1 disables all retries, contradicting the PR description which states the error handling fix enables 'trigger.dev's retry mechanism (maxAttempts: 3)'. For billing operations interacting with Stripe, having no retries is risky - transient failures could cause lost billing records. Consider keeping maxAttempts: 3 as originally intended.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/features/ee/organizations/lib/billing/tasker/trigger/config.ts, line 15:

<comment>Setting `maxAttempts: 1` disables all retries, contradicting the PR description which states the error handling fix enables 'trigger.dev's retry mechanism (`maxAttempts: 3`)'. For billing operations interacting with Stripe, having no retries is risky - transient failures could cause lost billing records. Consider keeping `maxAttempts: 3` as originally intended.</comment>

<file context>
@@ -5,14 +5,14 @@ type PlatformBillingTask = Pick<Parameters<typeof schemaTask>[0], "machine" | "r
   machine: "small-2x",
   retry: {
-    maxAttempts: 3,
+    maxAttempts: 1,
     factor: 1.8,
     minTimeoutInMs: 500,
</file context>

Fix confidence (alpha): 10/10

Suggested change
maxAttempts: 1,
maxAttempts: 3,

✅ Addressed in e07fab9

@github-actions
Copy link
Contributor

Devin AI is addressing Cubic AI's review feedback

New feedback has been sent to the existing Devin session.

View Devin Session


async getBillingData(teamId: number) {
async getBillingData(teamId: number): Promise<{
team: Awaited<ReturnType<(typeof OrganizationsRepository.prototype)["findByIdIncludeBilling"]>>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of this

@devin can you replace this a inteface type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DevinAI implement this feedback

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/api/v2/src/modules/billing/services/billing.service.ts">

<violation number="1" location="apps/api/v2/src/modules/billing/services/billing.service.ts:43">
P2: The `BillingData` type uses `plan: string` instead of the original `plan: PlatformPlan | "none"`. This type widening loses type safety - consumers can no longer rely on TypeScript to validate that only valid plan values are used. Consider updating the `BillingData` type definition in `billing-service.interface.ts` to use `plan: PlatformPlan | "none"` to preserve type safety.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

}

async getBillingData(teamId: number) {
async getBillingData(teamId: number): Promise<BillingData> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The BillingData type uses plan: string instead of the original plan: PlatformPlan | "none". This type widening loses type safety - consumers can no longer rely on TypeScript to validate that only valid plan values are used. Consider updating the BillingData type definition in billing-service.interface.ts to use plan: PlatformPlan | "none" to preserve type safety.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/v2/src/modules/billing/services/billing.service.ts, line 43:

<comment>The `BillingData` type uses `plan: string` instead of the original `plan: PlatformPlan | "none"`. This type widening loses type safety - consumers can no longer rely on TypeScript to validate that only valid plan values are used. Consider updating the `BillingData` type definition in `billing-service.interface.ts` to use `plan: PlatformPlan | "none"` to preserve type safety.</comment>

<file context>
@@ -40,11 +40,7 @@ export class BillingService implements IBillingService, OnModuleDestroy {
-    status: "valid" | "no_subscription" | "no_billing";
-    plan: PlatformPlan | "none";
-  }> {
+  async getBillingData(teamId: number): Promise<BillingData> {
     const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
 
</file context>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DevinAI check if this feedback is valid and implement if needed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Cubic AI feedback is valid. The BillingData type was using plan: string which loses type safety compared to the original plan: PlatformPlan | "none".

I've updated the BillingData type in billing-service.interface.ts to use plan: PlatformPlan | "none" to preserve type safety.

@github-actions
Copy link
Contributor

Devin AI is resolving merge conflicts

This PR has merge conflicts with the main branch. A Devin session has been created to automatically resolve them.

View Devin Session

Devin will:

  1. Merge the latest main into this branch
  2. Resolve any conflicts intelligently
  3. Run lint/type checks to ensure validity
  4. Push the resolved changes

If you prefer to resolve conflicts manually, you can close the Devin session and handle it yourself.

@github-actions
Copy link
Contributor

Devin AI is resolving merge conflicts

This PR has merge conflicts with the main branch. A Devin session has been created to automatically resolve them.

View Devin Session

Devin will:

  1. Merge the latest main into this branch
  2. Resolve any conflicts intelligently
  3. Run lint/type checks to ensure validity
  4. Push the resolved changes

If you prefer to resolve conflicts manually, you can close the Devin session and handle it yourself.

Copy link
Member

@sean-brydon sean-brydon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested trigger versions and sync versions seemed to work very well! Nice job!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants