Account Balance
Account Balance is the user’s internal wallet system in Helium. It holds monetary value that users can use to pay for orders without external payment gateways. The system tracks two types of balance (available and frozen) and maintains a complete audit trail of all balance changes.
Core Concept
Each user has a single balance record with two components:
pub struct UserBalance {
pub id: i64,
pub user_id: Uuid,
pub available_balance: Decimal, // Spendable balance
pub frozen_balance: Decimal, // Temporarily locked
}
Available Balance: The amount user can spend on orders or withdraw. This is what users see as their “wallet balance”.
Frozen Balance: Temporarily locked funds that cannot be spent. Used for scenarios where balance needs to be reserved but not immediately consumed (e.g., pending transactions, dispute holds).
Balance Change Types
All balance modifications are categorized into four types:
- Deposit: Adds to available balance (gift card redemption, admin top-up, refunds)
- Consume: Deducts from available balance (order payment, admin deduction)
- Freeze: Moves available balance to frozen balance (hold funds)
- Unfreeze: Moves frozen balance back to available balance (release hold)
pub enum UserBalanceChangeType {
Deposit, // available_balance + amount
Consume, // available_balance - amount
Freeze, // available_balance - amount, frozen_balance + amount
Unfreeze, // frozen_balance - amount, available_balance + amount
}
Every balance change is automatically logged in user_balance_change_log with timestamp, amount, reason, and change type.
User Operations
Get Balance
Users query their current balance status:
Service: UserBalanceService::GetMyBalance
Returns the user’s UserBalance with both available and frozen amounts, or None if balance has not been initialized (should never happen post-registration).
List Balance Changes
Users can view their transaction history with pagination:
Service: UserBalanceService::ListMyBalanceChanges
Parameters:
limit,offset: Pagination controlsasc: Sort order (ascending/descending by created_at)
Returns a list of UserBalanceChangeLog entries showing:
- Change amount (positive for deposits/unfreezes, negative for consumes/freezes)
- Reason string (human-readable explanation)
- Change type
- Timestamp
Redeem Gift Card
Users can redeem gift cards to add balance:
Service: GiftCardService::RedeemGiftCardRequest
Flow:
- Validate gift card exists and is not used/expired
- Verify user exists
- Add card amount to user’s available balance (transaction)
- Log balance change with reason “Redeem Gift Card”
- Mark gift card as redeemed with user ID and timestamp
Result Types:
Success: Balance credited, card redeemedCardNotFound: Invalid secret codeAlreadyUsed: Card already redeemed by someoneExpired: Card past valid_until dateUserNotFound: User account doesn’t exist
Important: Gift card redemption is transactional. If any step fails, the entire operation rolls back.
Payment with Balance
Users can pay for orders using their available balance:
Service: OrderService::PayOrderWithBalance
Flow:
- Verify order exists, belongs to user, and is unpaid
- Check user has sufficient available balance
- Transaction begins:
- Deduct order amount from available balance
- Log balance change with order reference
- Update order status to Paid
- Record paid_at timestamp
- Emit
OrderPaidEventfor downstream processing
Result Types:
Success: Order paid, balance deductedOrderNotFound: Invalid order or already paidNotEnoughBalance: Insufficient funds
Transaction Safety: The entire payment operation (balance deduction, log creation, order update) happens in a single database transaction. If any step fails, no changes are persisted.
Balance Initialization
User balances are automatically initialized when a new user registers:
Hook: RegisterHook consumes UserRegisterEvent from the auth module
Process:
- Creates balance record with
available_balance = 0andfrozen_balance = 0 - Uses
UpdateUserBalancewith zero diffs (upsert behavior) - No change log entry created (zero change, reason is empty string)
Note: The UpdateUserBalance entity operation has built-in upsert logic:
INSERT INTO user_balance (user_id) VALUES ($1)
ON CONFLICT (user_id) DO NOTHING
RETURNING *
This means calling UpdateUserBalance on a non-existent user will initialize their balance first, then apply the change.
Admin Operations
Administrators can manage user balances through ManageService. All operations require appropriate AdminRole permissions and are logged in the audit system.
Change User Balance
Operation: AdminChangeUserBalance
Permissions: Moderator, SuperAdmin, CustomerSupport
Parameters:
user_id: Target useramount: Change amount (always positive, type determines operation)reason: Human-readable explanation (required for audit)change_type: One of Deposit/Consume/Freeze/Unfreeze
Examples:
- Manual top-up:
{ amount: 100, change_type: Deposit, reason: "Promotional credit" } - Correction:
{ amount: 50, change_type: Consume, reason: "Duplicate refund correction" } - Hold funds:
{ amount: 200, change_type: Freeze, reason: "Dispute investigation" } - Release hold:
{ amount: 200, change_type: Unfreeze, reason: "Dispute resolved" }
Important: The amount parameter is always a positive number. The operation type determines whether it’s added or subtracted:
- Deposit:
available += amount - Consume:
available -= amount - Freeze:
available -= amount, frozen += amount - Unfreeze:
frozen -= amount, available += amount
List Balance Change Logs
Operation: AdminListUserBalanceLogs
Permissions: All admin roles
Returns paginated balance change history for a specific user, useful for customer support investigations.
Integration Points
Gift Cards
Gift cards are a primary source of balance deposits. When redeemed:
- Card’s
amountfield is added to available balance - Card marked as used with
used_by = user_id,redeem_at = NOW() - Balance change log created with change_type = Deposit
See Gift Card System for card management and generation.
Orders
Balance is consumed when users pay for orders via PayOrderWithBalance:
- Order’s
total_amountis deducted from available balance - Order transitions: Unpaid → Paid
OrderPaidEventemitted for package delivery- Balance change log references the order
See Order System for complete payment flows.
Refunds
When orders are refunded (status: Refunding → Refunded), the original payment amount should be returned to the user’s balance. This is handled by admin operations manually or through automated refund processing.
Current Status: Manual refund workflow requires admin to use AdminChangeUserBalance with change_type: Deposit.
Database Schema
Key tables (see migrations/20250815133800_create_shop_entites.sql):
user_balance:
user_id UUID PRIMARY KEY
available_balance DECIMAL NOT NULL DEFAULT 0
frozen_balance DECIMAL NOT NULL DEFAULT 0
user_balance_change_log:
id BIGSERIAL PRIMARY KEY
user_id UUID NOT NULL
amount DECIMAL NOT NULL
reason TEXT NOT NULL
change_type user_balance_change_type NOT NULL
created_at TIMESTAMP NOT NULL DEFAULT NOW()
Indexes:
user_balance_change_log(user_id, created_at DESC): Efficient pagination of user transaction historyuser_balance(user_id): Fast balance lookups (primary key)
Architecture Notes
Transaction Safety
All balance-modifying operations use database transactions:
- Payment with balance: Locks order and balance rows with
SELECT ... FOR UPDATE - Gift card redemption: Transaction ensures card can’t be double-redeemed
- Admin changes: Atomic balance update + log insertion
Change Log Automation
The UpdateUserBalance entity operation automatically:
- Creates or updates the balance record
- Determines change type from diff signs
- Inserts change log entry with correct amount/type
- All in a single transaction
Developer Note: You should never manually insert into user_balance_change_log. Always use UpdateUserBalance to modify balance, which handles logging automatically.
Decimal Precision
All monetary values use rust_decimal::Decimal for precise arithmetic. This avoids floating-point errors in financial calculations. Decimal serializes as string in protobuf/JSON to preserve precision.
Frozen Balance Use Cases
Currently, frozen balance is supported in the data model but not actively used in the order flow. Potential future use cases:
- Escrow for dispute resolution
- Pre-authorization holds
- Subscription renewals
- Withdrawal processing delays
Best Practices
-
Always Provide Reason: When modifying balance via admin operations, provide clear, descriptive reasons. These appear in user transaction history and audit logs.
-
Check Balance Before Deduction: Always verify sufficient available balance before attempting payment operations to avoid transaction rollbacks.
-
Use Transactions: Any operation involving balance changes and other state updates (orders, gift cards) must be wrapped in a database transaction.
-
Don’t Bypass Change Logs: Never directly update
user_balancetable. Always useUpdateUserBalanceto ensure change logs are created. -
Validate Amounts: All balance operations should validate that amounts are positive and reasonable (not excessively large).
Frontend Integration
Balance Display:
- Show
available_balanceas the user’s wallet balance - Optionally show
frozen_balanceif non-zero (with explanation) - Format decimals appropriately for currency display
Transaction History:
- Display
ListMyBalanceChangeswith infinite scroll or pagination - Color-code change types: green for Deposit/Unfreeze, red for Consume/Freeze
- Show reason string as transaction description
- Format timestamps in user’s local timezone
Payment Method Selection:
- When available_balance ≥ order total, enable “Pay with Balance” option
- Show remaining balance after payment preview
- Handle
NotEnoughBalanceerror gracefully with top-up prompt
Gift Card Redemption:
- Provide input field for gift card secret
- Handle all
RedeemGiftCardResultvariants with appropriate messages - Refresh balance display after successful redemption
See Also:
- Gift Card System - Card generation and management
- Order System - Payment flows and order lifecycle
- Shop Module Introduction - Overall shop architecture