Skip to content

Conversation

@kiram15
Copy link
Contributor

@kiram15 kiram15 commented Jan 8, 2026

Description:
Changing the endpoint (previously named first_upcoming_invoice_amount_due) to get_stripe_subscription_plan_info and adding an additional field canceled_date.

Jira:
ENT-11295

Merge checklist:

  • ./manage.py makemigrations has been run
    • Note: This must be run if you modified any models.
      • It may or may not make a migration depending on exactly what you modified, but it should still be run.

Post merge:

  • Ensure that your changes went out to the stage instance
  • Deploy to prod instance

Copilot AI review requested due to automatic review settings January 8, 2026 23:46
@kiram15 kiram15 requested review from a team as code owners January 8, 2026 23:46
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances the Stripe event summary endpoint to include subscription cancellation information. The endpoint now returns a canceled_date field in addition to the existing upcoming_invoice_amount_due and currency fields, enabling the admin portal to display subscription cancellation status.

Key Changes:

  • Added retrieval of customer.subscription.updated events to extract cancellation dates
  • Updated endpoint response to include canceled_date field
  • Enhanced tests to verify cancellation date behavior in both scenarios (with and without cancellation)

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
enterprise_access/apps/api/v1/views/customer_billing.py Modified first_upcoming_invoice_amount_due method to query for subscription updated events and include cancellation date in response
enterprise_access/apps/api/v1/tests/test_stripe_event_summary_views.py Added test data for subscription updated events and new test case to verify canceled_date functionality

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Member

@pwnage101 pwnage101 left a comment

Choose a reason for hiding this comment

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

Approved, with recommendation for a possible helper passthrough.

detail=False,
methods=['get'],
url_path='first-invoice-upcoming-amount-due',
url_path='get-stripe-subscription-plan-info',
Copy link
Member

Choose a reason for hiding this comment

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

FWIW, to mitigate deployment risk I think we could introduce a simple passthrough:

@action(
    detail=False,
    methods=['get'],
    url_path='first-invoice-upcoming-amount-due',
)
def first_upcoming_invoice_amount_due(self, request, *args, **kwargs):
    """
    Deprecated first-invoice-upcoming-amount-due endpoint.

    Temporary passthrough to aid with transitioning to get-stripe-subscription-plan-info.
    """
    return self.get_stripe_subscription_plan_info(request, *args, **kwargs)

'currency': summary.currency,
'upcoming_invoice_amount_due': created_event_summary.upcoming_invoice_amount_due,
'currency': created_event_summary.currency,
'canceled_date': canceled_date,
Copy link
Member

Choose a reason for hiding this comment

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

Does this actually work without casting it to a string first? Or does that happen automatically somewhere in the Response base code?

Copy link
Member

Choose a reason for hiding this comment

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

Aside: I didn't mention anything because you were just editing a pre-existing pattern, but at some point a couple years ago Titans adopted a new REST pattern of using input/output serializers which would avoid this ambiguity and also automatically result in an automatically correct Redoc. In this case just to move forward I'd be open to just manually serializing a string as Alex suggests might be needed, but in the future consider instead using an output serializer.

Copilot AI review requested due to automatic review settings January 9, 2026 22:05
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +286 to +300
def test_get_canceled_date(self):
# updated Stripe event triggered after cancellation
self.stripe_event_data = StripeEventData.objects.create(
event_id='evt_test_subscription_update',
event_type='customer.subscription.updated',
checkout_intent=self.checkout_intent,
data=self.subscription_updated_event_data,
)

canceled_datetime = datetime(2021, 9, 15, 0, 0, 0, tzinfo=tz.utc)

test_summary = StripeEventSummary.objects.filter(event_id='evt_test_subscription_update').first()
test_summary.subscription_cancel_at = canceled_datetime
test_summary.subscription_plan_uuid = self.subscription_plan_uuid
test_summary.save(update_fields=['subscription_cancel_at', 'subscription_plan_uuid'])
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The test creates a subscription.updated event but doesn't verify the order_by behavior when multiple updated events exist. Consider adding a test case that creates multiple customer.subscription.updated events with different timestamps to ensure the most recent cancellation date is returned correctly.

Copilot uses AI. Check for mistakes.
Comment on lines 626 to +629
return Response({
'upcoming_invoice_amount_due': summary.upcoming_invoice_amount_due,
'currency': summary.currency,
'upcoming_invoice_amount_due': created_event_summary.upcoming_invoice_amount_due,
'currency': created_event_summary.currency,
'canceled_date': canceled_date,
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The endpoint always returns upcoming_invoice_amount_due and currency from the created_event_summary, but if the subscription has been updated (e.g., plan changed, price changed), these values might be outdated. Consider whether the updated_event_summary should be checked first for these values if it exists and has more recent data.

Copilot uses AI. Check for mistakes.
import uuid
from datetime import timedelta
from datetime import datetime, timedelta
from datetime import timezone as tz
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Importing timezone from two different modules (datetime.timezone as tz on line 6 and django.utils.timezone on line 10) can be confusing. The code uses django.utils.timezone in setUp (line 204) and datetime.timezone as tz in test_get_canceled_date (line 295). Consider using a consistent approach throughout the file, preferably django.utils.timezone since it's Django-aware and already used elsewhere in the codebase.

Copilot uses AI. Check for mistakes.
Comment on lines +624 to 625
if not (subscription_plan_uuid and created_event_summary):
return Response({})
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The condition on line 624 checks both subscription_plan_uuid and created_event_summary, returning an empty response if either is missing. However, if only the created event is missing but the updated event exists with cancellation information, the endpoint won't return the cancellation date even though that information is available. This makes the API less useful in edge cases. Consider separating these concerns and returning available information even if some fields are missing.

Copilot uses AI. Check for mistakes.
Comment on lines 600 to 610
@action(
detail=False,
methods=['get'],
url_path='first-invoice-upcoming-amount-due',
url_path='get-stripe-subscription-plan-info',
)
def first_upcoming_invoice_amount_due(self, request, *args, **kwargs):
def get_stripe_subscription_plan_info(self, request, *args, **kwargs):
"""
Given a license-manager SubscriptionPlan uuid, returns an upcoming
invoice amount due, dervied from Stripe's preview invoice API.
Given a license-manager SubscriptionPlan uuid, returns information needed for the
Subscription management page on admin portal, like the upcoming subscription price
and if the subscription has been cancelled
"""
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The renamed endpoint is missing API documentation and permission checks. The list method on line 594 has both @extend_schema decorator (for API documentation) and @permission_required decorator (for authorization), but this action endpoint doesn't have either. This creates a security concern as the endpoint may be accessible without proper authorization checks, and it lacks proper API documentation for consumers.

Copilot uses AI. Check for mistakes.
Subscription management page on admin portal, like the upcoming subscription price
and if the subscription has been cancelled
"""
subscription_plan_uuid = self.request.query_params.get('subscription_plan_uuid')
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Missing input validation for the subscription_plan_uuid parameter. If subscription_plan_uuid is None or empty, the queries will still execute but will return incorrect results. Consider adding explicit validation similar to the validation in the get_queryset method on line 574-575 to return a proper error response.

Suggested change
subscription_plan_uuid = self.request.query_params.get('subscription_plan_uuid')
subscription_plan_uuid = self.request.query_params.get('subscription_plan_uuid')
if not subscription_plan_uuid:
raise exceptions.ValidationError(detail='subscription_plan_uuid query param is required')

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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.

4 participants