Skip to content

[codex] Fix Stripe subscription change tracking#1933

Merged
riderx merged 4 commits into
mainfrom
codex/fix-stripe-subscription-change-tracking
Apr 22, 2026
Merged

[codex] Fix Stripe subscription change tracking#1933
riderx merged 4 commits into
mainfrom
codex/fix-stripe-subscription-change-tracking

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Apr 22, 2026

Summary (AI generated)

  • Fix Stripe subscription webhook classification so user:subscribe_upgraded:* only fires on real monthly-to-yearly billing changes
  • Add from/to subscription metadata (plan_name, plan_type, previous_plan_name, previous_plan_type) to subscription tracking events
  • Add focused unit coverage for cadence changes, same-cadence plan switches, and downgrade cases

Motivation (AI generated)

The Stripe webhook parser was treating any product switch as an upgrade and then labeling the subscription event with the current interval. That produced false user:subscribe_upgraded:monthly events for same-cadence plan changes and did not expose clear before/after metadata for billing cadence changes.

Business Impact (AI generated)

This keeps billing lifecycle analytics and notifications accurate, reduces noisy upgrade events, and gives downstream automation enough metadata to understand exactly what changed.

Test Plan (AI generated)

  • bunx vitest run tests/stripe-subscription-events.unit.test.ts tests/stripe-event-paid-at.unit.test.ts tests/stripe-country.unit.test.ts
  • bunx eslint supabase/functions/_backend/utils/stripe.ts supabase/functions/_backend/utils/stripe_event.ts supabase/functions/_backend/triggers/stripe_event.ts tests/stripe-subscription-events.unit.test.ts
  • Pre-commit vue-tsc --noEmit hook passed during commit

Generated with AI

Summary by CodeRabbit

  • Improvements
    • More accurate detection of upgrades vs. plan changes (monthly↔yearly transitions).
    • Richer subscription metadata in tracking events (current + previous plan names and plan types).
    • Dynamic tracking event names that reflect subscription status and transition type.
  • Tests
    • New unit tests validating subscription event classification, metadata construction, and tracking behavior across scenarios.
  • Chores
    • Adjusted seed/reset sequence behavior during data seeding.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 22705e4b-efab-46f3-9808-2b321ae84e42

📥 Commits

Reviewing files that changed from the base of the PR and between 9326216 and 9c00c85.

📒 Files selected for processing (1)
  • supabase/seed.sql

📝 Walkthrough

Walkthrough

Adds helpers to build richer subscription/plan-change metadata, tracks previous price IDs, changes upgrade detection to cadence-based (month→year), refactors tracking to a centralized subscription tracking state and dynamic event names, and adds unit tests for subscription event classification.

Changes

Cohort / File(s) Summary
Trigger: subscription tracking & metadata
supabase/functions/_backend/triggers/stripe_event.ts
Added PlanRow and helpers (getSubscriptionTrackingState, buildSubscriptionEventMetadata, getPlanChangeTrackingEventName); set statusName from tracking state; conditionally fetch previousPlan; emit plan-change and subscription events with richer metadata and dynamic event names; exported these helpers via stripeEventTestUtils.
Stripe utils: data shape & extraction
supabase/functions/_backend/utils/stripe.ts, supabase/functions/_backend/utils/stripe_event.ts
Added previousPriceId?: string to StripeData; select a “licensed” subscription item (fallback to first); populate previousPriceId/previousProductId; derive interval restricted to month
Tests: subscription event scenarios
tests/stripe-subscription-events.unit.test.ts
New Vitest suite asserting classification and metadata for customer.subscription.updated (checks previousPriceId, previousProductId, isUpgrade, shouldSendPlanChange, statusName, tracking event name, and built metadata across upgrade/plan-change/downgrade cases).
DB seed tweak
supabase/seed.sql
Adjusted setval('public.apikeys_id_seq', 111, true) third argument from falsetrue during reset_and_seed_data sequence setup.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through webhook code at dawn,

I matched old prices to the new with bouncy yawn,
Month or year, names and types in tow,
Events now tell where the subscriptions go. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix Stripe subscription change tracking' clearly and concisely describes the main change: correcting how Stripe subscription webhooks are classified and tracked.
Description check ✅ Passed The PR description includes a comprehensive summary, clear motivation, business impact explanation, and detailed test plan with specific commands executed—covering all key template sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-stripe-subscription-change-tracking

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented Apr 22, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing codex/fix-stripe-subscription-change-tracking (9c00c85) with main (abbd1c1)

Open in CodSpeed

@riderx riderx marked this pull request as ready for review April 22, 2026 11:40
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
supabase/functions/_backend/triggers/stripe_event.ts (1)

425-449: ⚠️ Potential issue | 🟠 Major

Use a neutral tracking label for non-upgrade plan changes.

This branch now runs for same-cadence product switches too, but the emitted event is still 'User Upgraded'. That will keep notifications/analytics mislabeled even though user:subscribe_upgraded:* was fixed.

💡 Suggested fix
     if (trackingState.shouldSendPlanChange && stripeData.previousProductId) {
       const previousProduct = await supabaseAdmin(c)
         .from('plans')
         .select()
         .eq('stripe_id', stripeData.previousProductId)
         .single()
       previousPlan = previousProduct.data
       const planChangeMetadata = buildSubscriptionEventMetadata(stripeData, plan, previousPlan)
+      const planChangeEventName = trackingState.statusName === 'upgraded'
+        ? 'User Upgraded'
+        : 'User Plan Changed'
       await sendEventToTracking(c, {
         bento: {
           cron: '* * * * *',
           data: planChangeMetadata,
           event: 'user:plan_change',
           preferenceKey: 'credit_usage',
           uniqId: 'user:plan_change',
         },
         channel: 'usage',
-        event: 'User Upgraded',
+        event: planChangeEventName,
         icon: '💰',
         sentToBento: true,
         user_id: org.id,
         groups: { organization: org.id },
         notify: true,
         tags: planChangeMetadata,
       })
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/triggers/stripe_event.ts` around lines 425 - 449,
The top-level event name passed to sendEventToTracking is hardcoded to 'User
Upgraded' even for same-cadence or non-upgrade plan switches; change this by
computing a neutral label (e.g., 'User Plan Changed' or 'Plan Changed') and only
use 'User Upgraded' when the change is actually an upgrade (compare current plan
vs previousPlan in the branch where trackingState.shouldSendPlanChange &&
stripeData.previousProductId). Replace the hardcoded 'User Upgraded' in the
sendEventToTracking call (and keep bento.event as 'user:plan_change') with a
variable like eventLabel determined from plan and previousPlan so non-upgrade
switches are emitted with the neutral label.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@supabase/functions/_backend/triggers/stripe_event.ts`:
- Around line 425-449: The top-level event name passed to sendEventToTracking is
hardcoded to 'User Upgraded' even for same-cadence or non-upgrade plan switches;
change this by computing a neutral label (e.g., 'User Plan Changed' or 'Plan
Changed') and only use 'User Upgraded' when the change is actually an upgrade
(compare current plan vs previousPlan in the branch where
trackingState.shouldSendPlanChange && stripeData.previousProductId). Replace the
hardcoded 'User Upgraded' in the sendEventToTracking call (and keep bento.event
as 'user:plan_change') with a variable like eventLabel determined from plan and
previousPlan so non-upgrade switches are emitted with the neutral label.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ad24304-3f40-4502-aaff-54e5fbc9a577

📥 Commits

Reviewing files that changed from the base of the PR and between 99dcd2e and 16d8fb8.

📒 Files selected for processing (4)
  • supabase/functions/_backend/triggers/stripe_event.ts
  • supabase/functions/_backend/utils/stripe.ts
  • supabase/functions/_backend/utils/stripe_event.ts
  • tests/stripe-subscription-events.unit.test.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16d8fb82fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/functions/_backend/triggers/stripe_event.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
supabase/functions/_backend/triggers/stripe_event.ts (1)

424-458: ⚠️ Potential issue | 🟠 Major

Check dbError2 before emitting user:plan_change.

Lines 438-454 can send tracking even when the preceding stripe_info update fails, because the error guard runs afterward. That makes analytics inconsistent with persisted state.

Suggested fix
     const { error: dbError2 } = await supabaseAdmin(c)
       .from('stripe_info')
       .update(updateData)
       .eq('customer_id', stripeData.data.customer_id)
+
+    if (dbError2) {
+      return quickError(404, 'succeeded_customer_id_not_found', `succeeded: customer_id not found`, { dbError2, stripeData })
+    }
+
     let previousPlan: PlanRow | null = null
     if (trackingState.shouldSendPlanChange && stripeData.previousProductId) {
       const previousProduct = await supabaseAdmin(c)
         .from('plans')
         .select()
@@
         tags: planChangeMetadata,
       })
     }
-
-    if (dbError2) {
-      return quickError(404, 'succeeded_customer_id_not_found', `succeeded: customer_id not found`, { dbError2, stripeData })
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/triggers/stripe_event.ts` around lines 424 - 458,
The stripe_info update error (dbError2) is checked after emitting the
plan-change tracking event, so tracking can be sent even if the DB update
failed; move the guard to check dbError2 immediately after the
supabaseAdmin(...).update(...) call and return the quickError (using
quickError(404, 'succeeded_customer_id_not_found', ... , { dbError2, stripeData
})) before any logic that builds previousPlan or calls sendEventToTracking
(i.e., before referencing trackingState.shouldSendPlanChange,
stripeData.previousProductId, previousPlan, and sendEventToTracking) to ensure
tracking is only emitted when the update succeeded.
🧹 Nitpick comments (1)
supabase/functions/_backend/triggers/stripe_event.ts (1)

665-669: Consider moving these test helpers into a small pure helper module.

stripeEventTestUtils works, but it keeps expanding the runtime export surface of the webhook handler just so tests can reach pure functions. Pulling buildSubscriptionEventMetadata, getPlanChangeTrackingEventName, and getSubscriptionTrackingState into a dedicated helper file would keep the trigger module narrower.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/triggers/stripe_event.ts` around lines 665 - 669,
Extract the pure helpers buildSubscriptionEventMetadata,
getPlanChangeTrackingEventName, and getSubscriptionTrackingState into a new
small helper module and export them there; in the webhook trigger module (where
stripeEventTestUtils is defined) import those functions instead of keeping their
implementations and remove them from the runtime export surface (leave
getPaidAtUpdate and other runtime-only exports as-is); update tests to import
the moved functions from the new helper module; ensure function signatures and
any shared types remain unchanged and update any references in the trigger file
to use the imported helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@supabase/functions/_backend/triggers/stripe_event.ts`:
- Around line 424-458: The stripe_info update error (dbError2) is checked after
emitting the plan-change tracking event, so tracking can be sent even if the DB
update failed; move the guard to check dbError2 immediately after the
supabaseAdmin(...).update(...) call and return the quickError (using
quickError(404, 'succeeded_customer_id_not_found', ... , { dbError2, stripeData
})) before any logic that builds previousPlan or calls sendEventToTracking
(i.e., before referencing trackingState.shouldSendPlanChange,
stripeData.previousProductId, previousPlan, and sendEventToTracking) to ensure
tracking is only emitted when the update succeeded.

---

Nitpick comments:
In `@supabase/functions/_backend/triggers/stripe_event.ts`:
- Around line 665-669: Extract the pure helpers buildSubscriptionEventMetadata,
getPlanChangeTrackingEventName, and getSubscriptionTrackingState into a new
small helper module and export them there; in the webhook trigger module (where
stripeEventTestUtils is defined) import those functions instead of keeping their
implementations and remove them from the runtime export surface (leave
getPaidAtUpdate and other runtime-only exports as-is); update tests to import
the moved functions from the new helper module; ensure function signatures and
any shared types remain unchanged and update any references in the trigger file
to use the imported helpers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7da29989-dd61-432c-8df2-88bc377641d8

📥 Commits

Reviewing files that changed from the base of the PR and between 16d8fb8 and 6388572.

📒 Files selected for processing (2)
  • supabase/functions/_backend/triggers/stripe_event.ts
  • tests/stripe-subscription-events.unit.test.ts
✅ Files skipped from review due to trivial changes (1)
  • tests/stripe-subscription-events.unit.test.ts

@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit 57ef8c8 into main Apr 22, 2026
15 checks passed
@riderx riderx deleted the codex/fix-stripe-subscription-change-tracking branch April 22, 2026 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant