Domain: Checkout

Scenario: Cart → Checkout → Order Creation

1. Business Goal

Checkout transforms shopping cart items into a paid order. The flow includes:

  1. Cart management: Add/remove items, update quantities
  2. Checkout creation: Convert cart to checkout with pricing
  3. Promotions: Apply coupons, automatic discounts
  4. Points: Redeem loyalty points
  5. Payment: Process Stripe/PayPal payment
  6. Order creation: Convert checkout to order on success

2. Trigger & Entry

API Method Description Request Body
/cart/promoter POST Add to cart { promoterVariantId, quantity, postId, customFields }
/cart PUT Update cart { cartItemId, quantity }
/cart GET Get cart items { selected }
/order/checkout POST Create checkout { cartItemIds[] }
/order/promotions POST Apply promotion { promotionIds[] }
/order/points-and-promotions POST Apply points & promos { promotionIds[], usePoints }
/order/buy-now POST Express checkout (skip cart) { promoterVariantId, quantity, postId }

3. Data Flow (Cart → Checkout → Order)

sequenceDiagram
    participant Consumer as Consumer
    participant Cart as CartService
    participant Checkout as CheckoutService
    participant Payment as PaymentService
    participant Order as OrdersService
    participant DB as PostgreSQL
    participant Stripe as Stripe API

    Note over Consumer: STEP 1: Add to Cart
    Consumer->>Cart: POST /cart/promoter
    Cart->>DB: SELECT PromoterProductVariant
    Cart->>DB: UPSERT Cart (consumerId, variantId, quantity)
    Cart-->>Consumer: { cart }

    Note over Consumer: STEP 2: Create Checkout
    Consumer->>Checkout: POST /order/checkout
    Checkout->>Cart: getCartMapBySelf(consumerId)
    Cart-->>Checkout: { carts, variants, posts }

    Note over Checkout: Delete existing draft checkout
    Checkout->>DB: DELETE Checkout WHERE consumerId AND status=DRAFT

    Note over Checkout: Create checkout session
    Checkout->>DB: INSERT Checkout (id=orderId, orderNumber, status=DRAFT)
    Checkout->>DB: INSERT CheckoutLineItem[]

    Note over Checkout: Run 8 checkout status processors
    Checkout->>Checkout: checkoutOnInit()
    Checkout->>Checkout: checkoutOnPostLimitQuantity()
    Checkout->>Checkout: checkoutOnVariantQuantityConstraints()
    Checkout->>Checkout: checkoutOnSampleOrSubscriptionDiscount()
    Checkout->>Checkout: checkoutOnPostFreeShipping()
    Checkout->>Checkout: checkoutOnAutomaticCoupon()
    Checkout->>Checkout: checkoutOnSubscriptionFreeShipping()
    Checkout->>Checkout: checkoutOnTax()

    Note over Checkout: Calculate totals
    Checkout->>Checkout: calcCheckoutResponse()
    Checkout->>DB: UPDATE Checkout (totals)

    Checkout-->>Consumer: { checkout, lineItems, totals }

    Note over Consumer: STEP 3: Apply Promotions (optional)
    Consumer->>Checkout: POST /order/promotions
    Checkout->>Checkout: applyPromotions()
    Checkout->>DB: UPDATE CheckoutLineItem (discounts)
    Checkout->>DB: UPDATE Checkout (totals)
    Checkout-->>Consumer: Updated checkout

    Note over Consumer: STEP 4: Process Payment
    Consumer->>Payment: POST /payment/stripe/intent
    Payment->>Stripe: POST /v1/payment_intents
    Stripe-->>Payment: { clientSecret, amount }
    Payment-->>Consumer: { clientSecret }

    Note over Consumer: STEP 5: Confirm & Create Order
    Consumer->>Order: POST /order (with payment confirmation)
    Order->>DB: SELECT Checkout WHERE id
    Order->>Order: validatePayment()

    Note over Order: Create Order record
    Order->>DB: INSERT Order (id=checkout.id, status=IN_PROGRESS)
    Order->>DB: INSERT OrderLineItem[] (from CheckoutLineItem)
    Order->>DB: UPDATE Checkout (status=COMPLETED)

    Note over Order: Create MerchantOrders
    Order->>DB: INSERT MerchantOrder[] (per merchant)
    Order->>DB: INSERT MerchantLineItem[]

    Note over Order: Create PromoterOrder (if applicable)
    Order->>DB: INSERT PromoterOrder
    Order->>DB: INSERT PromoterLineItem[]

    Note over Order: Clear cart
    Order->>DB: DELETE Cart WHERE consumerId AND lineItemIds

    Order-->>Consumer: { order, orderNumber }

4. DB Operations (Chronological)

Step Table Action PK/FK Touched Notes
1 Cart UPSERT consumerId (FK), promoterProductVariantId (FK) Add/update cart item
2 Checkout INSERT id (PK), consumerId (FK) status=DRAFT, orderNumber generated
3 CheckoutLineItem INSERT id (PK), checkoutId (FK) Cart items converted
4 Checkout UPDATE id (PK) After promotions/tax calculation
5 Order INSERT id (PK), consumerId (FK) status=IN_PROGRESS
6 OrderLineItem INSERT id (PK), orderId (FK) From CheckoutLineItem
7 MerchantOrder INSERT id (PK), orderId (FK), merchantId (FK) Per merchant
8 MerchantLineItem INSERT id (PK), merchantOrderId (FK) Commission calc
9 PromoterOrder INSERT (if promo) id (PK), orderId (FK), promoterId (FK) If promoter involved
10 Cart DELETE consumerId (FK) Clear purchased items

5. Pricing Calculation

Checkout Line Item Price:

// Base price
lineItemPrice = unitPrice * quantity

// Discounts (stackable)
sampleDiscount = (product.isSample) ? sampleDiscountAmount : 0
subscriptionDiscount = (user.hasSubscription) ? subscriptionDiscountAmount : 0
couponDiscount = appliedCouponDiscount
unitDiscount = sampleDiscount + subscriptionDiscount + couponDiscount

// Subtotal
lineItemSubtotal = lineItemPrice - unitDiscount

// Commission (if applicable)
commissionAmount = lineItemSubtotal * commissionRate

// Final
lineItemTotal = lineItemSubtotal + lineItemTax - lineItemPointsDollars

Checkout Totals:

totalLineItemPrice = SUM(lineItemPrice)
totalLineItemDiscount = SUM(unitDiscount * quantity)
totalLineItemSubtotal = SUM(lineItemSubtotal)

totalShippingSubtotal = calculatedShipping
totalShippingDiscount = (freeShipping) ? totalShippingSubtotal : 0

totalProductTax = SUM(lineItemTax)
totalShippingTax = calculatedShippingTax
totalTax = totalProductTax + totalShippingTax

totalToPay = totalLineItemSubtotal + totalShippingSubtotal + totalTax
           - totalLineItemPointsDollars

6. Checkout Status Processors

// 8 status processors run in sequence
checkoutOnInit()
  → Initialize default values

checkoutOnPostLimitQuantity()
  → Enforce post-level quantity limits
  → Error if exceeded

checkoutOnVariantQuantityConstraints()
  → Validate min/max purchase quantities
  → Apply pack size requirements

checkoutOnSampleOrSubscriptionDiscount()
  → Apply sample product discounts
  → Apply subscription discounts

checkoutOnPostFreeShipping()
  → Apply post-level free shipping
  → Check post.freeShippingEnabled

checkoutOnAutomaticCoupon()
  → Apply auto-applied coupons
  → Check promotion.autoApply

checkoutOnSubscriptionFreeShipping()
  → Apply subscription free shipping

checkoutOnTax()
  → Calculate product tax (if enabled)
  → Calculate shipping tax
  → Use TaxJar or merchant tax settings

7. Event/Queue Messages

Message Producer → Consumer Payload Snippet
checkout.created OrderModule → Queue { checkoutId, consumerId }
payment.completed PaymentModule → OrderModule { checkoutId, paymentIntentId }
order.created OrderModule → Queue { orderId, orderNumber }
cart.updated CartService → Queue { consumerId, cartItems[] }

8. File Map (Top 5 Must-Read)

File Purpose
src/cart/cart.service.ts Cart CRUD operations
src/order/checkout.service.ts Checkout creation & processors
src/payment/payment.service.ts Stripe/PayPal integration
src/order/orders.service.ts Order creation logic
src/order/checkout.entity.ts Checkout domain entity

9. DB Schema Snippet (Prisma)

model Cart {
  id                       String  @id @default(uuid()) @db.Uuid
  consumerId               String  @map("consumer_id") @db.Uuid
  promoterProductVariantId String  @map("promoter_product_variant_id") @db.Uuid
  variantInfo              Json    @map("variant_info")
  price                    Float
  quantity                 Int
  subtotal                 Float
  commissionRate           Float   @map("commission_rate")
  isGuest                  Boolean @default(false) @map("is_guest")
  selected                 Boolean @default(true)
  customFields             Json?   @map("custom_fields")
  postId                   String? @map("post_id") @db.Uuid
  subscriptionPlanOptionId String? @map("subscription_plan_option_id") @db.Uuid
  subdomainUserId          String? @map("subdomain_user_id") @db.Uuid
  affiliateCode            String? @map("affiliate_code")

  @@unique([consumerId, promoterProductVariantId, postId, customFields])
}

model Checkout {
  id                          String         @id @default(uuid()) @db.Uuid
  consumerId                  String         @map("consumer_id") @db.Uuid
  status                      CheckoutStatus @default(DRAFT)
  orderNumber                 String         @unique @map("order_number")
  shippingAddressInfo         Json?          @map("shipping_address_info")

  // Pricing
  totalToPay                  Float          @default(0.0) @map("total_to_pay")
  totalPaid                   Float          @default(0.0) @map("total_paid")
  totalLineItemPrice          Float          @default(0.0) @map("total_line_item_price")
  totalLineItemDiscount       Float          @default(0.0) @map("total_line_item_discount")
  totalLineItemSubtotal       Float          @default(0.0) @map("total_line_item_subtotal")

  // Shipping
  totalShippingPrice          Float          @default(0.0) @map("total_shipping_price")
  totalShippingDiscount       Float          @default(0.0) @map("total_shipping_discount")
  totalShippingSubtotal       Float          @default(0.0) @map("total_shipping_subtotal")

  // Tax
  totalTax                    Float          @default(0.0)
  totalProductTax             Float          @default(0.0) @map("total_product_tax")
  totalShippingTax            Float          @default(0.0) @map("total_shipping_tax")

  // Points
  totalLineItemPointsUsed     Int            @default(0) @map("total_line_item_points_used")
  totalLineItemPointsDollars  Float          @default(0.0) @map("total_line_item_points_dollars")

  // Promotions
  promotionIds                String[]       @map("promotion_ids")
  promotions                  Json?

  lineItems                   CheckoutLineItem[]

  createdAt                   DateTime       @default(now()) @map("created_at")
  updatedAt                   DateTime       @updatedAt @map("updated_at")
}

model CheckoutLineItem {
  id                     String   @id @default(uuid()) @db.Uuid
  checkoutId             String   @map("checkout_id") @db.Uuid
  lineItemNumber         String   @map("line_item_number")
  Checkout               Checkout @relation(fields: [checkoutId], references: [id], onDelete: Cascade)

  // Product references
  promoterVariantId      String   @map("promoter_variant_id") @db.Uuid
  merchantVariantId      String   @map("merchant_variant_id") @db.Uuid
  variantInfo            Json     @map("variant_info")

  // Participants
  consumerId              String   @map("consumer_id") @db.Uuid
  merchantId              String   @map("merchant_id") @db.Uuid
  promoterId              String?  @map("promoter_id") @db.Uuid

  title                   String?

  // Quantity & Pricing
  quantity                Int      @default(1)
  unitPrice               Float    @default(0.0) @map("unit_price")
  lineItemPrice           Float    @default(0.0) @map("line_item_price")

  // Discounts
  sampleDiscount          Float    @default(0.0) @map("sample_discount")
  subscriptionDiscount    Float    @default(0.0) @map("subscription_discount")
  couponDiscount          Float    @default(0.0) @map("coupon_discount")
  unitDiscount            Float    @default(0.0) @map("unit_discount")
  lineItemDiscount        Float    @default(0.0) @map("line_item_discount")

  // Totals
  lineItemSubtotal        Float    @default(0.0) @map("line_item_subtotal")

  // Points
  lineItemPointsUsed      Int      @default(0) @map("line_item_points_used")
  lineItemPointsDollars   Float    @default(0.0) @map("line_item_points_dollars")

  // Commission
  commissionAmount        Float    @default(0.0) @map("commission_amount")

  createdAt               DateTime @default(now()) @map("created_at")
  updatedAt               DateTime @updatedAt @map("updated_at")

  @@index([checkoutId])
}

enum CheckoutStatus {
  DRAFT
  COMPLETED
  FAILED
}

10. Express Checkout (Buy Now)

Skip cart, go directly to checkout:

// POST /order/buy-now
interface BuyNowRequest {
  promoterVariantId: string;
  quantity: number;
  postId?: string;
  customFields?: Record<string, any>;
}

// Creates checkout directly without cart
// Same flow as regular checkout after checkout creation

results matching ""

    No results matching ""