Skip to content

Commit c6f50a3

Browse files
committed
feat: Add Segment events for CheckoutIntent state transitions
Track state changes in CheckoutIntent lifecycle by emitting appropriate Segment analytics events on transitions
1 parent e685b8f commit c6f50a3

File tree

3 files changed

+83
-1
lines changed

3 files changed

+83
-1
lines changed

enterprise_access/apps/customer_billing/constants.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ class CheckoutIntentState(StrEnum):
7373
ERRORED_PROVISIONING = 'errored_provisioning'
7474
EXPIRED = 'expired'
7575

76+
class CheckoutIntentSegmentEvents:
77+
"""
78+
Segment events for CheckoutIntent lifecycle tracking.
79+
"""
80+
CREATED = 'edx.server.enterprise-access.checkout-intent-lifecycle.created'
81+
TRANSITION_TO_PAID = 'edx.server.enterprise-access.checkout-intent-lifecycle.transition-to-paid'
82+
TRANSITION_TO_FULFILLED = 'edx.server.enterprise-access.checkout-intent-lifecycle.transition-to-fulfilled'
83+
TRANSITION_TO_ERRORED_STRIPE_CHECKOUT = 'edx.server.enterprise-access.checkout-intent-lifecycle.transition-to-errored-stripe-checkout'
84+
TRANSITION_TO_ERRORED_PROVISIONING = 'edx.server.enterprise-access.checkout-intent-lifecycle.transition-to-errored-provisioning'
85+
7686

7787
ALLOWED_CHECKOUT_INTENT_STATE_TRANSITIONS = {
7888
CheckoutIntentState.CREATED: [

enterprise_access/apps/customer_billing/models.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,23 +185,35 @@ def mark_as_paid(self, stripe_session_id=None):
185185
if self.state == CheckoutIntentState.PAID and stripe_session_id != self.stripe_checkout_session_id:
186186
raise ValueError("Cannot transition from PAID to PAID with a different stripe_checkout_session_id")
187187

188+
previous_state = self.state
188189
self.state = CheckoutIntentState.PAID
189190
if stripe_session_id:
190191
self.stripe_checkout_session_id = stripe_session_id
191192
self.save(update_fields=['state', 'stripe_checkout_session_id', 'modified'])
192193
logger.info(f'CheckoutIntent {self} marked as {CheckoutIntentState.PAID}.')
194+
195+
# Track state transition
196+
from .segment_events import track_checkout_intent_event
197+
track_checkout_intent_event(self, previous_state, self.state)
198+
193199
return self
194200

195201
def mark_as_fulfilled(self, workflow=None):
196202
"""Mark the intent as fulfilled after successful provisioning."""
197203
if not self.is_valid_state_transition(CheckoutIntentState(self.state), CheckoutIntentState.FULFILLED):
198204
raise ValueError(f"Cannot transition from {self.state} to {CheckoutIntentState.FULFILLED}.")
199205

206+
previous_state = self.state
200207
self.state = CheckoutIntentState.FULFILLED
201208
if workflow:
202209
self.workflow = workflow
203210
self.save(update_fields=['state', 'workflow', 'modified'])
204211
logger.info(f'CheckoutIntent {self} marked as {CheckoutIntentState.FULFILLED}.')
212+
213+
# Track state transition
214+
from .segment_events import track_checkout_intent_event
215+
track_checkout_intent_event(self, previous_state, self.state)
216+
205217
return self
206218

207219
def mark_checkout_error(self, error_message):
@@ -212,10 +224,16 @@ def mark_checkout_error(self, error_message):
212224
):
213225
raise ValueError(f"Cannot transition from {self.state} to {CheckoutIntentState.ERRORED_STRIPE_CHECKOUT}.")
214226

227+
previous_state = self.state
215228
self.state = CheckoutIntentState.ERRORED_STRIPE_CHECKOUT
216229
self.last_checkout_error = error_message
217230
self.save(update_fields=['state', 'last_checkout_error', 'modified'])
218231
logger.info(f'CheckoutIntent {self} marked as {CheckoutIntentState.ERRORED_STRIPE_CHECKOUT}.')
232+
233+
# Track state transition
234+
from .segment_events import track_checkout_intent_event
235+
track_checkout_intent_event(self, previous_state, self.state)
236+
219237
return self
220238

221239
def mark_provisioning_error(self, error_message, workflow=None):
@@ -226,12 +244,18 @@ def mark_provisioning_error(self, error_message, workflow=None):
226244
):
227245
raise ValueError(f"Cannot transition from {self.state} to {CheckoutIntentState.ERRORED_PROVISIONING}.")
228246

247+
previous_state = self.state
229248
self.state = CheckoutIntentState.ERRORED_PROVISIONING
230249
self.last_provisioning_error = error_message
231250
if workflow:
232251
self.workflow = workflow
233252
self.save(update_fields=['state', 'last_provisioning_error', 'workflow', 'modified'])
234253
logger.info(f'CheckoutIntent {self} marked as {CheckoutIntentState.ERRORED_PROVISIONING}.')
254+
255+
# Track state transition
256+
from .segment_events import track_checkout_intent_event
257+
track_checkout_intent_event(self, previous_state, self.state)
258+
235259
return self
236260

237261
@property
@@ -429,9 +453,14 @@ def create_intent(
429453
existing_intent.country = country
430454
existing_intent.terms_metadata = terms_metadata
431455
existing_intent.save()
456+
457+
# Track creation event for updated intent
458+
from .segment_events import track_checkout_intent_event
459+
track_checkout_intent_event(existing_intent)
460+
432461
return existing_intent
433462

434-
return cls.objects.create(
463+
intent = cls.objects.create(
435464
user=user,
436465
state=CheckoutIntentState.CREATED,
437466
enterprise_slug=slug,
@@ -442,6 +471,12 @@ def create_intent(
442471
terms_metadata=terms_metadata,
443472
)
444473

474+
# Track creation event
475+
from .segment_events import track_checkout_intent_event
476+
track_checkout_intent_event(intent)
477+
478+
return intent
479+
445480
@classmethod
446481
def for_user(cls, user):
447482
"""
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
Segment event tracking for CheckoutIntent lifecycle events.
3+
"""
4+
5+
from enterprise_access.apps.api.serializers import CheckoutIntentReadOnlySerializer
6+
from enterprise_access.apps.customer_billing.constants import (
7+
CheckoutIntentSegmentEvents,
8+
)
9+
from enterprise_access.apps.track.segment import track_event
10+
11+
12+
def track_checkout_intent_event(checkout_intent, previous_state=None, new_state=None):
13+
"""Track CheckoutIntent lifecycle events (creation and state transitions)."""
14+
properties = CheckoutIntentReadOnlySerializer(checkout_intent).data
15+
properties["previous_state"] = previous_state
16+
properties["new_state"] = new_state or checkout_intent.state
17+
18+
# Get event name based on new state
19+
event_name = _get_event_name_for_state(new_state or checkout_intent.state)
20+
21+
track_event(
22+
lms_user_id=str(checkout_intent.user.id),
23+
event_name=event_name,
24+
properties=properties,
25+
)
26+
27+
28+
def _get_event_name_for_state(new_state):
29+
"""Map state to event name."""
30+
mapping = {
31+
"created": CheckoutIntentSegmentEvents.CREATED,
32+
"paid": CheckoutIntentSegmentEvents.TRANSITION_TO_PAID,
33+
"fulfilled": CheckoutIntentSegmentEvents.TRANSITION_TO_FULFILLED,
34+
"errored_stripe_checkout": CheckoutIntentSegmentEvents.TRANSITION_TO_ERRORED_STRIPE_CHECKOUT,
35+
"errored_provisioning": CheckoutIntentSegmentEvents.TRANSITION_TO_ERRORED_PROVISIONING,
36+
}
37+
return mapping.get(new_state)

0 commit comments

Comments
 (0)