Skip to content

Commit 2e09234

Browse files
committed
feat: upcoming invoice amount due
1 parent 499cea1 commit 2e09234

File tree

5 files changed

+79
-1
lines changed

5 files changed

+79
-1
lines changed

enterprise_access/apps/api/v1/views/customer_billing.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,3 +517,14 @@ def list(self, request, *args, **kwargs):
517517
Lists ``StripeEventSummary`` records, filtered by given subscription plan uuid.
518518
"""
519519
return super().list(request, *args, **kwargs)
520+
521+
@action(
522+
detail=False,
523+
methods=['get'],
524+
url_path='first-invoice-upcoming-amount-due',
525+
)
526+
def first_invoice_upcoming_amount_due(self, request, *args, **kwargs):
527+
summary = self.get_queryset().filter(event_type='customer.subscription.created').first()
528+
return Response({
529+
'upcoming_invoice_amount_due': summary.upcoming_invoice_amount_due,
530+
}, content_type='application/json')
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.25 on 2025-10-31 18:26
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('customer_billing', '0016_alter_checkoutintent_uuid_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='stripeeventsummary',
15+
name='upcoming_invoice_amount_due',
16+
field=models.IntegerField(blank=True, help_text='Upcoming invoice amount due related to this event/subscription', null=True),
17+
),
18+
]

enterprise_access/apps/customer_billing/models.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from simple_history.models import HistoricalRecords
2424
from simple_history.utils import bulk_update_with_history
2525

26+
from enterprise_access.apps.customer_billing import stripe_api
2627
from enterprise_access.apps.customer_billing.constants import ALLOWED_CHECKOUT_INTENT_STATE_TRANSITIONS
2728

2829
from .constants import INTENT_RESERVATION_DURATION_MINUTES, CheckoutIntentState
@@ -736,6 +737,11 @@ class StripeEventSummary(TimeStampedModel):
736737
blank=True,
737738
help_text='Currency of the invoice'
738739
)
740+
upcoming_invoice_amount_due = models.IntegerField(
741+
null=True,
742+
blank=True,
743+
help_text='Upcoming invoice amount due related to this event/subscription'
744+
)
739745

740746
class Meta:
741747
db_table = 'customer_billing_stripe_event_summary'
@@ -824,6 +830,34 @@ def populate_with_summary_data(self):
824830
if self.invoice_unit_amount is None:
825831
self.invoice_unit_amount = int(self.invoice_unit_amount_decimal)
826832

833+
def update_upcoming_invoice_amount_due(self):
834+
"""
835+
Updates the `amount_due` of this record from the first upcoming invoice
836+
associated with this customer's subscription.
837+
"""
838+
self.checkout_intent = CheckoutIntent.objects.first()
839+
if not self.checkout_intent:
840+
logger.warning(
841+
'Cannot update with upcoming invoice for event %s, no checkout intent exists',
842+
self.event_id,
843+
)
844+
return
845+
stripe_subscription_id = self.stripe_subscription_id
846+
stripe_customer_id = self.checkout_intent.stripe_customer_id
847+
848+
if not (stripe_subscription_id and stripe_customer_id):
849+
logger.warning('Cannot update with upcoming invoice for event %s', self.event_id)
850+
return
851+
852+
# Now call the stripe invoice upcoming API
853+
upcoming_invoice = stripe_api.upcoming_invoice(stripe_customer_id, stripe_subscription_id)
854+
if not upcoming_invoice:
855+
logger.warning('No upcoming invoice exists for event %s', self.event_id)
856+
return
857+
858+
self.upcoming_invoice_amount_due = upcoming_invoice.amount_due
859+
self.save(update_fields=['upcoming_invoice_amount_due'])
860+
827861
@staticmethod
828862
def _timestamp_to_datetime(timestamp):
829863
"""Convert Unix timestamp to Django datetime."""

enterprise_access/apps/customer_billing/stripe_api.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,15 @@ def get_stripe_trialing_subscription(
240240
limit=1,
241241
)
242242
return subscription_list.data[0] if subscription_list.data else None
243+
244+
245+
@stripe_cache()
246+
def upcoming_invoice(stripe_customer_id, stripe_subscription_id):
247+
"""
248+
https://docs.stripe.com/changelog/basil/2025-03-31/invoice-preview-api-deprecations
249+
https://docs.stripe.com/api/invoices/create_preview?architecture-style=resources
250+
"""
251+
return stripe.Invoice.create_preview(
252+
customer=stripe_customer_id,
253+
subscription=stripe_subscription_id,
254+
)

enterprise_access/apps/customer_billing/stripe_event_handlers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import stripe
99

10-
from enterprise_access.apps.customer_billing.models import CheckoutIntent, StripeEventData
10+
from enterprise_access.apps.customer_billing.models import CheckoutIntent, StripeEventData, StripeEventSummary
1111
from enterprise_access.apps.customer_billing.stripe_event_types import StripeEventType
1212
from enterprise_access.apps.customer_billing.tasks import send_trial_cancellation_email_task
1313

@@ -210,6 +210,9 @@ def subscription_created(event: stripe.Event) -> None:
210210
except stripe.StripeError as e:
211211
logger.error(f'Failed to enable pending updates for subscription {subscription.id}: {e}')
212212

213+
summary = StripeEventSummary.objects.get(event_id=event.id)
214+
summary.update_upcoming_invoice_amount_due()
215+
213216
@on_stripe_event('customer.subscription.updated')
214217
@staticmethod
215218
def subscription_updated(event: stripe.Event) -> None:

0 commit comments

Comments
 (0)