Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ordering Flow

This document explains the complete purchase flow in the Helium shop system - how a user goes from browsing products to receiving service access. It covers the conceptual flow, cross-module interactions, and important implementation notes that developers should remember.

Flow Overview

User Journey:
Browse Products → Apply Coupon → Create Order → Pay → Receive Packages → Access Nodes

System Flow:
┌─────────────┐      ┌──────────────┐      ┌─────────────┐      ┌──────────────┐
│  Production │ ───> │    Order     │ ───> │   Payment   │ ───> │   Package    │
│   Service   │      │   Service    │      │   Process   │      │    Queue     │
└─────────────┘      └──────────────┘      └─────────────┘      └──────────────┘
       │                     │                      │                    │
       ▼                     ▼                      ▼                    ▼
  Catalog with         Order Creation        Payment Methods        Service
  Access Control       + Pricing Logic      (ePay / Balance)       Activation

Key Concepts

1. Production Visibility

Productions (products) are filtered by access control before users can see them:

  • User Group: Basic tier matching (visible_to must equal user’s user_group)
  • Private Productions: If is_private = true, user must have limit_to_extra_group in their extra_groups
  • On-Sale Status: Only on_sale = true productions are visible

This means the same codebase can show different catalogs to different user tiers.

2. Coupon Validation

Coupons are validated TWICE in the flow:

  1. Pre-order verification: VerifyCoupon RPC for UI preview
  2. Order creation validation: Re-validated when creating order (security)

Why twice? The coupon state can change between preview and order creation (e.g., usage limit reached). Always validate at order creation to prevent abuse.

Discount Types:

  • Rate Discount: Percentage off (e.g., 10% → 0.1 rate)
  • Amount Discount: Fixed amount off with minimum threshold

Validation Rules:

  • Time window (start_time ≤ now ≤ end_time)
  • Global usage limit (total uses across all users)
  • Per-account usage limit (uses per individual user)
  • Minimum amount requirement (for amount-based discounts)

3. Order Creation

When creating an order, the system:

  1. Checks unpaid order limit (default: 5 per user)
  2. Validates production exists and is available
  3. Applies coupon discount if provided
  4. Creates order with final calculated price
  5. Publishes OrderCreatedEvent (for tracking)

Important: The final price is locked at order creation time. Even if production price changes later, the order amount remains unchanged.

4. Payment Methods

Two payment paths with different characteristics:

ePay Gateway (AliPay, WeChat, USDT)

  • User Flow: Redirect to external gateway → Complete payment → Redirect back
  • Callback Flow: Gateway sends async callback to server → Signature verification → Update order
  • Security: All callbacks MUST verify signature using provider’s key
  • Idempotency: Callbacks can be replayed; check order status before processing

Architecture:

User Payment on Gateway
    ↓
Gateway POST /api/shop/epay/callback
    ↓
Publish EpayCallback event to RabbitMQ (immediate return)
    ↓
EpayHook consumer processes callback
    ↓
Verify signature → Update order → Publish OrderPaidEvent

Why async? The HTTP callback must return immediately to the gateway (within 2-3 seconds). Processing happens async via message queue.

Account Balance

  • Transaction Safety: Atomic operation with pessimistic locking (FOR UPDATE)
  • Balance Types: Only available_balance can be used (not frozen_balance)
  • Audit Trail: Every balance change logged to user_balance_change_log

Why pessimistic lock? Prevents race conditions if user attempts multiple simultaneous payments.

5. Package Delivery

Trigger: OrderPaidEvent published after payment confirmation

Expected Flow (delivery hook needs implementation):

OrderPaidEvent → DeliveryHook → Create Package Queue Items → Update Order Status

What happens:

  1. Look up order and production details
  2. Find master package of the package series (current version at purchase time)
  3. Create N package queue items (N = production’s package_amount)
  4. Link items to order via by_order field (for refund tracking)
  5. Publish PackageQueuePushEvent
  6. Update order status to Delivered

Critical: The package version is snapshot at purchase time. If the master package changes later, existing orders deliver the old version (version isolation).

Implementation Status: ⚠️ The delivery hook that consumes OrderPaidEvent is not yet visible in the codebase. This is the missing link between payment and package delivery.

6. Service Activation

Trigger: PackageQueuePushEvent published after package queue creation

Activation Logic:

  • Only ONE package per user can be Active at a time
  • If no active package: Activate oldest queued package (FIFO)
  • If active package exists: New packages remain in queue
  • When active package expires: Next queued package auto-activates

Package States:

  • InQueue: Waiting for activation
  • Active: Currently providing service
  • Consumed: Expired (time or traffic limit)
  • Cancelled: Refunded or cancelled by admin

7. Node Access

With an active package, users gain access to nodes filtered by available_group:

Active Package (available_group = 1)
    ↓
Query: WHERE node.available_groups && ARRAY[1]
    ↓
Returns: All nodes that include group 1 in their available_groups array

Access Control Chain:

Purchase Production → Receive Package → Package Activates → Defines available_group → Filters Nodes

No active package = no node access (empty list).

Cross-Module Interactions

Shop → Telecom (Package Delivery)

Event: OrderPaidEvent

  • Exchange: shop
  • Routing Key: order_paid
  • Consumer: Delivery hook (needs implementation in shop or telecom module)
  • Purpose: Trigger package queue creation after payment

Telecom → Telecom (Package Activation)

Event: PackageQueuePushEvent

  • Exchange: telecom
  • Routing Key: package_queue_push
  • Consumer: TelecomPackageQueueHook
  • Purpose: Auto-activate first package if user has none active

Shop → Market (Affiliate Rewards)

Event: OrderPaidEvent

  • Consumer: Market module
  • Purpose: Calculate and distribute affiliate commissions

Important Implementation Notes

For Backend Developers

  1. Transaction Boundaries

    • Balance payment: Single transaction includes balance deduction + order update
    • Use FOR UPDATE to lock balance row during payment
    • Order delivery: May need transaction spanning shop and telecom databases
  2. Event-Driven Architecture

    • Always publish events AFTER database commit (not before)
    • Events must be idempotent (can be replayed safely)
    • Use separate consumers for cross-module communication
  3. Processor Pattern [[memory:6079830]]

    • All service APIs exposed via Processor<Input, Result<Output, Error>>
    • No object-oriented methods for business logic
    • See existing services for reference
  4. Security Considerations

    • ePay callbacks: MUST verify signature before processing
    • Balance operations: Use pessimistic locks to prevent race conditions
    • Coupon validation: Re-validate at order creation (not just preview)
  5. Missing Implementation

    • Order delivery hook (consumes OrderPaidEvent) is not yet implemented
    • This is why orders remain stuck in Paid status instead of moving to Delivered
    • Implementation location: Either modules/shop/src/hooks/delivery.rs or modules/telecom/src/hooks/order.rs

For Frontend Developers

  1. Order Status Polling

    • After payment, poll GetOrderDetail until status becomes Delivered
    • Recommended interval: 2-3 seconds
    • Timeout: ~60 seconds (suggest manual refresh after)
  2. ePay Redirect Handling

    • Save order ID before redirecting to gateway
    • User redirects back via epay_return_url configured in shop config
    • On return: Check order status (payment may take a few seconds to process)
  3. Error Handling

    • All operations return result enums (not exceptions)
    • Check result type before accessing response data
    • Common errors: TooManyUnpaid, CouponInvalid, NotEnoughBalance, OrderNotFound
  4. Balance vs ePay Decision

    • Check user balance before showing payment options
    • ePay: Redirect flow, user leaves app temporarily
    • Balance: Instant payment, better UX if sufficient balance
  5. Coupon UI/UX

    • Verify coupon before order creation (show discount preview)
    • Display applicable conditions (time window, usage limit, min amount)
    • Show final price after discount in order summary

Configuration

Shop module config stored in Redis under key shop:

FieldDefaultPurpose
max_unpaid_orders5Maximum unpaid orders per user
auto_cancel_after30mTimeout for automatic order cancellation
epay_notify_url-Server callback URL for payment notifications
epay_return_url-User redirect URL after payment

Common Issues and Solutions

Order Creation Fails with TooManyUnpaid

Cause: User has >= max_unpaid_orders unpaid orders
Solution: Cancel old unpaid orders or complete payment

Coupon Shows as Invalid

Causes:

  • Outside time window (check start_time and end_time)
  • Usage limit exceeded (global or per-account)
  • Production price below minimum amount (for amount-based discounts)

Solution: Check coupon conditions and inform user why it’s invalid

Balance Payment Fails

Causes:

  • Insufficient available_balance (frozen balance cannot be used)
  • Order already paid or cancelled
  • Concurrent payment attempt (transaction conflict)

Solution: Refresh balance, check order status, retry if conflict

ePay Callback Not Received

Causes:

  • epay_notify_url not publicly accessible
  • Firewall blocking gateway IPs
  • Signature verification failed

Solution: Check server logs, verify network config, confirm provider credentials

Order Stuck in Paid Status

Cause: Delivery hook not running or not implemented
Solution: Check RabbitMQ consumer status, verify OrderPaidEvent is being consumed

Packages Not Activating

Cause: PackageQueuePushEvent not triggering or hook not running
Solution: Check telecom module hooks, verify event publishing

Workflow Diagram

┌────────────────────────────────────────────────────────────────┐
│                        User Purchase Flow                       │
└────────────────────────────────────────────────────────────────┘

1. Browse Productions (filtered by user group + extra groups)
   └─> ProductionService.ListUserProduction

2. [Optional] Verify Coupon
   └─> CouponService.VerifyCoupon

3. Create Order (with optional coupon)
   └─> OrderService.CreateOrder
   └─> Validates: unpaid limit, production exists, coupon valid
   └─> Calculates final price with discount
   └─> Publishes: OrderCreatedEvent

4a. Pay with ePay
    └─> OrderService.GetEpayUrl (generate payment URL)
    └─> User redirects to gateway
    └─> Gateway calls back: POST /api/shop/epay/callback
    └─> EpayHook consumes EpayCallback event
    └─> OrderService.PayOrderWithEpay
    └─> Verifies signature, updates order
    └─> Publishes: OrderPaidEvent

4b. Pay with Balance
    └─> OrderService.PayOrderWithBalance
    └─> Atomic transaction: lock balance + deduct + update order
    └─> Publishes: OrderPaidEvent

5. Deliver Packages [⚠️ Needs Implementation]
   └─> DeliveryHook consumes OrderPaidEvent
   └─> Finds master package of production's package series
   └─> Creates N package queue items (N = package_amount)
   └─> Updates order status to Delivered
   └─> Publishes: PackageQueuePushEvent

6. Activate Service
   └─> TelecomPackageQueueHook consumes PackageQueuePushEvent
   └─> If no active package: activates oldest queued package
   └─> Publishes: PackageActivateEvent

7. User Accesses Nodes
   └─> Active package defines available_group
   └─> Nodes filtered by: available_groups && ARRAY[user_group]
   └─> User can generate subscription links and connect

Testing Considerations

Happy Path Testing

  1. User with valid permissions can see productions
  2. Coupon applies correct discount
  3. Order creation succeeds with valid inputs
  4. Payment updates order status to Paid
  5. Packages are delivered automatically
  6. First package activates immediately
  7. User can access nodes matching package group

Edge Cases to Test

  • User at unpaid order limit (should reject new orders)
  • Coupon usage limit reached between verification and order creation
  • Production deleted after order created but before payment
  • Duplicate payment callbacks (idempotency check)
  • Concurrent balance payments (transaction locking)
  • User already has active package (new packages should queue)
  • Package series has no master package (should fail gracefully)

Error Recovery

  • Payment succeeds but delivery fails (manual intervention needed)
  • Partial delivery (transaction rollback)
  • Event replay (idempotent processing)
  • Network timeout during ePay redirect (order remains unpaid, can retry)

See Also