Issues with the checkout flow¶
Currently the checkout flow has the following issues:
- How do we remove redis locks and use a queue based system to handle the checkout flow?
- First off what is this 'include_pending' parameter within def check_ticket_availability(ticket, quantity, include_pending=False): in tickets/utils/tickets.py?
- The the ticket availability check seems to have a bug where it displays properly on the frontend but when you try to purchase the ticket it says the ticket is not available.
- The ticket checkout flow has be come very complex with the addition of promo codes, donations, and other features. It is difficult to understand and maintain.
- How do we simplify the checkout flow to make it more maintainable and easier to understand?
- How do we improve the checkout flow to make it more robust and reliable?
- How do we break the checkout flow into smaller parts that can be tested and maintained independently?
- Should we modify the ticket model to include different types of tickets to make the logic simpler and more maintainable? (i.e. Free tickets, donation tickets, etc.)
- How do we introduce django history to track changes in ticket prices and quantities?
Draw a mermaid diagram of the checkout flow with the donation ticket feature, the promo code feature, and the ticket availability check feature.
flowchart TD
Start([User on Show Page]) --> SelectTickets[Select Tickets & Quantities]
SelectTickets --> CheckDonationType{Is Donation-Based<br/>Ticket?}
CheckDonationType -->|Yes| CustomDonation[Enter Custom Donation Amount<br/>Minimum = ticket.price]
CheckDonationType -->|No| FixedPrice[Fixed Price Ticket]
CustomDonation --> CheckPromoCode
FixedPrice --> CheckPromoCode
CheckPromoCode{Apply Promo<br/>Code?}
CheckPromoCode -->|Yes| ValidatePromo[Validate Promo Code<br/>Frontend: /api/tickets/validate-promo]
CheckPromoCode -->|No| ContinueCheckout
ValidatePromo --> PromoValid{Promo Valid?}
PromoValid -->|No| ShowPromoError[Show Promo Error]
PromoValid -->|Yes| SetDiscount[Calculate Discount<br/>discount = promo_code.discount<br/>0.0 to 1.0 decimal]
ShowPromoError --> CheckPromoCode
SetDiscount --> ContinueCheckout[Continue to Checkout]
ContinueCheckout --> EnterInfo[Enter Purchaser Info<br/>First Name, Last Name, Email, Phone]
EnterInfo --> SubmitForm[Submit Form]
SubmitForm --> reCAPTCHA[Execute reCAPTCHA v3<br/>action: 'checkout_submit']
reCAPTCHA --> NextJSAPI[POST /api/checkout<br/>Next.js API Route]
NextJSAPI --> VerifyRecaptcha[Verify reCAPTCHA Token<br/>with Google API]
VerifyRecaptcha --> RecaptchaValid{Valid?}
RecaptchaValid -->|No| Error1[Return Error]
RecaptchaValid -->|Yes| GetCSRF[Get CSRF Token]
GetCSRF --> ForwardToDjango[Forward GET Request<br/>to Django /checkout/]
ForwardToDjango --> DjangoValidation[CheckoutSessionView.get<br/>Django Backend]
DjangoValidation --> ValidateParams[Validate Request Params<br/>showId, firstName, lastName, email]
ValidateParams --> ParamsValid{Valid?}
ParamsValid -->|No| Error2[Return Error Response]
ParamsValid -->|Yes| ValidateShow[Validate Show<br/>- Exists<br/>- Published<br/>- Not Ended]
ValidateShow --> ShowValid{Valid?}
ShowValid -->|No| Error2
ShowValid -->|Yes| ValidatePromoBackend{Promo Code<br/>Provided?}
ValidatePromoBackend -->|Yes| CheckPromoCodeBackend[Validate Promo Code<br/>TicketPromoCode.objects.get<br/>code.upper, show_id]
ValidatePromoBackend -->|No| ParseTickets
CheckPromoCodeBackend --> PromoValidBackend{Valid?}
PromoValidBackend -->|No| Error2
PromoValidBackend -->|Yes| ParseTickets[Parse Ticket Orders<br/>- ticketIds<br/>- quantities<br/>- donationAmounts]
ParseTickets --> ValidateDonations[Validate Donation Amounts<br/>- Check if donation-based<br/>- Ensure >= ticket.price for donations<br/>- Max $1000 per ticket]
ValidateDonations --> DonationsValid{Valid?}
DonationsValid -->|No| Error2
DonationsValid -->|Yes| AcquireLocks[Acquire Redis Cache Locks<br/>for each ticket<br/>Lock Timeout: 120s]
AcquireLocks --> LocksAcquired{All Locks<br/>Acquired?}
LocksAcquired -->|No| Error2[Return Error:<br/>Tickets being processed]
LocksAcquired -->|Yes| StartTransaction[Start Database Transaction<br/>atomic]
StartTransaction --> AcquireDBLocks[Acquire Row-Level Locks<br/>Ticket.objects.select_for_update<br/>nowait=False]
AcquireDBLocks --> CheckAvailability[Check Ticket Availability<br/>check_ticket_availability<br/>ticket, quantity, include_pending=True]
CheckAvailability --> AvailabilityLogic{Ticket Availability<br/>Logic}
AvailabilityLogic -->|include_pending=True| CountSuccessfulOrders[Count Only Successful Orders<br/>TicketOrder.objects.filter<br/>ticket=ticket, orders__success=True<br/>.select_for_update.distinct]
AvailabilityLogic -->|include_pending=False| CountSuccessfulOnly[Count Successful Orders Only<br/>No locking, for public checks]
CountSuccessfulOrders --> CalculateRemaining[Calculate Remaining:<br/>ticket.quantity >= total_ordered + quantity]
CountSuccessfulOnly --> CalculateRemaining
CalculateRemaining --> TicketsAvailable{Available?}
TicketsAvailable -->|No| ReleaseLocks1[Release Redis Locks] --> Error2[Return Error:<br/>Ticket not available]
TicketsAvailable -->|Yes| CalculatePrices[Calculate Ticket Prices<br/>base_price = ticket.price + donation_amount]
CalculatePrices --> ApplyPromoDiscount{Promo Code<br/>Exists?}
ApplyPromoDiscount -->|Yes| CalculateDiscount[Apply Promo Discount<br/>discount_amount = base_price * discount<br/>base_price = max base_price - discount_amount, 0]
ApplyPromoDiscount -->|No| CalculateFees
CalculateDiscount --> CalculateFees[Calculate Fees<br/>- Platform Fee: tickets * $1.50<br/>- Processing: discounted_total + platform * 0.029 + $0.30]
CalculateFees --> CheckMaxOrder{Total <=<br/>$5000?}
CheckMaxOrder -->|No| ReleaseLocks2[Release Redis Locks] --> Error2[Return Error:<br/>Order exceeds max]
CheckMaxOrder -->|Yes| CreateOrder[Create Order Record<br/>- Order object<br/>- TicketOrder objects<br/>- Link ticket orders to order]
CreateOrder --> BuildLineItems[Build Stripe Line Items<br/>- Ticket items with prices<br/>- Platform fee item<br/>- Processing fee item]
BuildLineItems --> CommitTransaction[Commit Transaction]
CommitTransaction --> ReleaseLocks3[Release Redis Cache Locks]
ReleaseLocks3 --> CheckTotal{Total Amount<br/>> 0?}
CheckTotal -->|No| FreeOrder[Free Order Path<br/>order.session_id = 'FREE-' + order.id<br/>order.success = False]
CheckTotal -->|Yes| ValidateProducer[Validate Producer Financial<br/>Check Stripe account configured]
FreeOrder --> ReturnFreeURL[Return Free Success URL<br/>/checkout/success?order_id=&session_id=FREE-order.id]
ValidateProducer --> ProducerValid{Valid?}
ProducerValid -->|No| Error2
ProducerValid -->|Yes| CreateStripeSession[Create Stripe Checkout Session<br/>- Line items<br/>- Success URL<br/>- Cancel URL<br/>- Transfer data<br/>- Application fee]
CreateStripeSession --> StripeSuccess{Session<br/>Created?}
StripeSuccess -->|Error| Error2
StripeSuccess -->|Yes| SaveSessionID[Save session_id to Order]
SaveSessionID --> ReturnStripeURL[Return Stripe Checkout URL]
ReturnStripeURL --> UserPays[User Completes Payment<br/>on Stripe]
ReturnFreeURL --> SuccessHandler[SuccessSessionView.get<br/>Success Handler]
UserPays --> RedirectSuccess[Stripe Redirects to<br/>Success URL]
RedirectSuccess --> SuccessHandler
SuccessHandler --> LoadOrder[Load Order by order_id<br/>or session_id]
LoadOrder --> OrderFound{Order<br/>Found?}
OrderFound -->|No| Error3[Return Error]
OrderFound -->|Yes| CheckAlreadySuccess{Already<br/>Successful?}
CheckAlreadySuccess -->|Yes| RedirectShow[Redirect to Show Page]
CheckAlreadySuccess -->|No| UpdateOrderSuccess[Mark Order as Success<br/>order.success = True]
UpdateOrderSuccess --> ExtractPaymentInfo{Is Free<br/>Order?}
ExtractPaymentInfo -->|No| GetStripeSession[Retrieve Stripe Session<br/>Get payment_intent_id<br/>Get charge_id]
ExtractPaymentInfo -->|Yes| CreateAttendees
GetStripeSession --> CreateAttendees[Create Ticket Attendees<br/>For each ticket order:<br/>- Create TicketAttendee records<br/>- Link to ticket order]
CreateAttendees --> DecrementQuantities[Decrement Ticket Quantities<br/>ticket.quantity -= ticket_order.quantity]
DecrementQuantities --> QueueEmail[Queue Confirmation Email<br/>send_ticket_purchased_email.apply_async]
QueueEmail --> QueuePDF[Queue Ticket PDF Generation<br/>generate_send_ticket_pdf.apply_async]
QueuePDF --> QueueReminder[Queue Show Reminder<br/>create_show_reminder.apply_async<br/>24 hours before show]
QueueReminder --> TrackPurchase[Track Purchase Event<br/>Facebook Pixel Conversion API]
TrackPurchase --> UpdateStripeMetadata[Update Stripe Transfer Metadata<br/>Add order/show details to charge]
UpdateStripeMetadata --> SendSlackNotification[Send Slack Notification<br/>if not DEBUG mode]
SendSlackNotification --> RedirectSuccessPage[Redirect to Frontend<br/>/success/show.slug]
style CheckDonationType fill:#e1f5ff
style CheckPromoCode fill:#e1f5ff
style ValidatePromo fill:#fff4e1
style CheckAvailability fill:#ffe1f5
style AvailabilityLogic fill:#ffe1f5
style TicketsAvailable fill:#ffe1f5
style ApplyPromoDiscount fill:#e1f5ff
style CheckTotal fill:#e1f5ff
style FreeOrder fill:#e1ffe1
style CreateStripeSession fill:#e1ffe1
style SuccessHandler fill:#f5e1ff