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_tomust equal user’suser_group) - Private Productions: If
is_private = true, user must havelimit_to_extra_groupin theirextra_groups - On-Sale Status: Only
on_sale = trueproductions 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:
- Pre-order verification:
VerifyCouponRPC for UI preview - 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:
- Checks unpaid order limit (default: 5 per user)
- Validates production exists and is available
- Applies coupon discount if provided
- Creates order with final calculated price
- 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_balancecan be used (notfrozen_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:
- Look up order and production details
- Find master package of the package series (current version at purchase time)
- Create N package queue items (N = production’s
package_amount) - Link items to order via
by_orderfield (for refund tracking) - Publish
PackageQueuePushEvent - 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
Activeat 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 activationActive: Currently providing serviceConsumed: 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
-
Transaction Boundaries
- Balance payment: Single transaction includes balance deduction + order update
- Use
FOR UPDATEto lock balance row during payment - Order delivery: May need transaction spanning shop and telecom databases
-
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
-
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
- All service APIs exposed via
-
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)
-
Missing Implementation
- Order delivery hook (consumes
OrderPaidEvent) is not yet implemented - This is why orders remain stuck in
Paidstatus instead of moving toDelivered - Implementation location: Either
modules/shop/src/hooks/delivery.rsormodules/telecom/src/hooks/order.rs
- Order delivery hook (consumes
For Frontend Developers
-
Order Status Polling
- After payment, poll
GetOrderDetailuntil status becomesDelivered - Recommended interval: 2-3 seconds
- Timeout: ~60 seconds (suggest manual refresh after)
- After payment, poll
-
ePay Redirect Handling
- Save order ID before redirecting to gateway
- User redirects back via
epay_return_urlconfigured in shop config - On return: Check order status (payment may take a few seconds to process)
-
Error Handling
- All operations return result enums (not exceptions)
- Check result type before accessing response data
- Common errors:
TooManyUnpaid,CouponInvalid,NotEnoughBalance,OrderNotFound
-
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
-
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:
| Field | Default | Purpose |
|---|---|---|
max_unpaid_orders | 5 | Maximum unpaid orders per user |
auto_cancel_after | 30m | Timeout 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_timeandend_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_urlnot 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
- User with valid permissions can see productions
- Coupon applies correct discount
- Order creation succeeds with valid inputs
- Payment updates order status to
Paid - Packages are delivered automatically
- First package activates immediately
- 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
- Production - Product catalog, version control, and package series
- Order System - Order entity, status lifecycle, and admin operations
- Shop Module Introduction - Module overview and architecture
- Package Queue - Package delivery and activation mechanics
- Coupon System - Coupon types, validation rules, and management (if documented)
- Account Balance - Balance operations and gift cards (if documented)