Skip to content

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

Queuing System