Skip to content

feat: Add command for adding schedules to subscriptions#707

Open
calvin-codecov wants to merge 6 commits intomainfrom
cy/stripe_yearly_sub_sched_action
Open

feat: Add command for adding schedules to subscriptions#707
calvin-codecov wants to merge 6 commits intomainfrom
cy/stripe_yearly_sub_sched_action

Conversation

@calvin-codecov
Copy link
Contributor

@calvin-codecov calvin-codecov commented Feb 10, 2026

Adds general command for the team to run to bulk add subscription schedules for Owners. It will procure the matching owners based on the arguments and process each.

Takes these as user provided arguments:

  • end date
  • target plan (if it is switching plans)
  • dry-run
  • renew-date-gte
  • renew-date-lte
  • owner ids array allowlist
  • current plan that we want to filter owners by
  • owner ids array exclude list
  • limit for # owners to run for
  • ownerid cursor for batching

Handles multiple scenarios for each owner

  • if this subscription already has had this applied and the dates match up, skip
  • if the dates don't match up, update the date
  • if this subscription has a unrelated schedule, add to the schelude
  • create a subscription schedule if it doesn't have a schedule already

The command will return a summary of what happened and the owner id for the last acted upon owner.

PR also adds to the modify_subscription webhook handler logic to reapply the schedule if the subscription encounters an upgrade after the command has been run as upgrades will release schedules.


Note

Medium Risk
Touches Stripe billing/scheduling flows and adds a bulk command that can modify many subscriptions; correctness around schedule metadata/end dates and error recovery is important but changes are localized and covered by tests.

Overview
Adds a new apply_subscription_schedules Django management command to bulk create/update Stripe subscription schedules that transition (optionally) to a target plan and then cancel at a specified end date, with filtering, batching, idempotent skip behavior, and --dry-run support.

Introduces shared schedule task_signature constants and a reusable _create_end_date_schedule helper, and updates StripeService.modify_subscription so upgrades that release schedules will detect prior cancellation schedules and recreate/restore the end-date schedule (including recovery when the upgrade Stripe call fails). Includes comprehensive unit tests for the command and the new upgrade/schedule-recreation behaviors.

Written by Cursor Bugbot for commit a526ac1. This will update automatically on new commits. Configure here.

@calvin-codecov calvin-codecov force-pushed the cy/stripe_yearly_sub_sched_action branch from a1aafd4 to 7e2f900 Compare February 10, 2026 23:20
@calvin-codecov calvin-codecov changed the title feat: Add command for adding end dates to annual plans feat: Add command for adding schedules to subscriptions Feb 10, 2026
Comment on lines -349 to -350
# If the user is not in a schedule, update immediately
# If the user is in a schedule, update the existing schedule
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This comment was actually already no longer correct and had not been updated. This handler has been releasing the existing schedule for a long time.

@calvin-codecov calvin-codecov force-pushed the cy/stripe_yearly_sub_sched_action branch from 938002c to f36f356 Compare February 12, 2026 17:52
@sentry
Copy link

sentry bot commented Feb 12, 2026

Codecov Report

❌ Patch coverage is 78.78788% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.20%. Comparing base (cf5ee52) to head (a526ac1).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...anagement/commands/apply_subscription_schedules.py 77.56% 35 Missing ⚠️
apps/codecov-api/services/billing.py 82.50% 7 Missing ⚠️

❌ Your patch check has failed because the patch coverage (78.78%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #707      +/-   ##
==========================================
- Coverage   92.25%   92.20%   -0.06%     
==========================================
  Files        1302     1303       +1     
  Lines       47854    48050     +196     
  Branches     1628     1628              
==========================================
+ Hits        44149    44303     +154     
- Misses       3396     3438      +42     
  Partials      309      309              
Flag Coverage Δ
apiunit 96.16% <78.78%> (-0.20%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@codecov-notifications
Copy link

codecov-notifications bot commented Feb 12, 2026

Codecov Report

❌ Patch coverage is 78.78788% with 42 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...anagement/commands/apply_subscription_schedules.py 77.56% 35 Missing ⚠️
apps/codecov-api/services/billing.py 82.50% 7 Missing ⚠️

❌ Your patch check has failed because the patch coverage (78.78%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

📢 Thoughts on this report? Let us know!

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

log.info(
f"Restored end-date schedule for owner {owner.ownerid} after upgrade failure"
)
raise
Copy link

Choose a reason for hiding this comment

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

Error recovery can mask original StripeError exception

Medium Severity

The schedule recovery code inside the except stripe.StripeError block (lines 486–502) calls stripe.Subscription.retrieve and _create_end_date_schedule (which itself makes two Stripe API calls) without its own try/except. If any of those calls fail, the new exception replaces the original upgrade failure, and the raise on line 503 is never reached. This masks the root cause of the upgrade failure. The post-success path at lines 512–533 correctly wraps _create_end_date_schedule in try/except to handle exactly this scenario — the error recovery path needs the same treatment.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor

@thomasrockhu-codecov thomasrockhu-codecov left a comment

Choose a reason for hiding this comment

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

Probably need Ajay to really review the logic

If subscription is not provided, it is retrieved from Stripe.
"""
if subscription is None:
subscription = stripe.Subscription.retrieve(owner.stripe_subscription_id)
Copy link
Contributor

Choose a reason for hiding this comment

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

what happens if this fails?

Comment on lines +65 to +68
if phase2_plan_id is None:
phase2_plan_id = phase1_plan_id
if phase2_quantity is None:
phase2_quantity = phase1_quantity
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can this be done before subscription call? or change the argument ordering. helps to read the code more smoothly

Comment on lines +72 to +74
"task_signature": task_signature,
"end_date": end_date.strftime("%Y-%m-%d"),
"script_version": "1.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: alphabetize

# An increase in seats and/or plan implies the user is upgrading, hence 'is_upgrading' is a consequence
# of proration_behavior providing an invoice, in this case, != "none"
# TODO: change this to "self._is_upgrading_seats(owner, desired_plan) or self._is_extending_term(owner, desired_plan)"
is_upgrading = (
Copy link
Contributor

Choose a reason for hiding this comment

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

does this also handle downgrading and the comment is off?

# payment_behavior="pending_if_incomplete",
)
except stripe.StripeError:
# Upgrade payment failed but we already released the schedule so add back an end-date schedule so the user doesn't lose it
Copy link
Contributor

Choose a reason for hiding this comment

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

does the schedule release have to happen after the above step then? would that prevent the need to rollback?

self.style.WARNING("DRY RUN MODE - No changes will be made to Stripe")
)

# Build queryset of owners to process
Copy link
Contributor

Choose a reason for hiding this comment

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

dat comment

owners = list(owners[:limit])

# Output summary
self.stdout.write(f"Total matching owners: {total_matching}")
Copy link
Contributor

Choose a reason for hiding this comment

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

we probably want to write to a file here as well

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.

2 participants