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

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:

  1. Deposit: Adds to available balance (gift card redemption, admin top-up, refunds)
  2. Consume: Deducts from available balance (order payment, admin deduction)
  3. Freeze: Moves available balance to frozen balance (hold funds)
  4. 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 controls
  • asc: 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:

  1. Validate gift card exists and is not used/expired
  2. Verify user exists
  3. Add card amount to user’s available balance (transaction)
  4. Log balance change with reason “Redeem Gift Card”
  5. Mark gift card as redeemed with user ID and timestamp

Result Types:

  • Success: Balance credited, card redeemed
  • CardNotFound: Invalid secret code
  • AlreadyUsed: Card already redeemed by someone
  • Expired: Card past valid_until date
  • UserNotFound: 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:

  1. Verify order exists, belongs to user, and is unpaid
  2. Check user has sufficient available balance
  3. Transaction begins:
    • Deduct order amount from available balance
    • Log balance change with order reference
    • Update order status to Paid
    • Record paid_at timestamp
  4. Emit OrderPaidEvent for downstream processing

Result Types:

  • Success: Order paid, balance deducted
  • OrderNotFound: Invalid order or already paid
  • NotEnoughBalance: 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 = 0 and frozen_balance = 0
  • Uses UpdateUserBalance with 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 user
  • amount: 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:

  1. Card’s amount field is added to available balance
  2. Card marked as used with used_by = user_id, redeem_at = NOW()
  3. 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:

  1. Order’s total_amount is deducted from available balance
  2. Order transitions: Unpaid → Paid
  3. OrderPaidEvent emitted for package delivery
  4. 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 history
  • user_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:

  1. Creates or updates the balance record
  2. Determines change type from diff signs
  3. Inserts change log entry with correct amount/type
  4. 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

  1. Always Provide Reason: When modifying balance via admin operations, provide clear, descriptive reasons. These appear in user transaction history and audit logs.

  2. Check Balance Before Deduction: Always verify sufficient available balance before attempting payment operations to avoid transaction rollbacks.

  3. Use Transactions: Any operation involving balance changes and other state updates (orders, gift cards) must be wrapped in a database transaction.

  4. Don’t Bypass Change Logs: Never directly update user_balance table. Always use UpdateUserBalance to ensure change logs are created.

  5. Validate Amounts: All balance operations should validate that amounts are positive and reasonable (not excessively large).

Frontend Integration

Balance Display:

  • Show available_balance as the user’s wallet balance
  • Optionally show frozen_balance if non-zero (with explanation)
  • Format decimals appropriately for currency display

Transaction History:

  • Display ListMyBalanceChanges with 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 NotEnoughBalance error gracefully with top-up prompt

Gift Card Redemption:

  • Provide input field for gift card secret
  • Handle all RedeemGiftCardResult variants with appropriate messages
  • Refresh balance display after successful redemption

See Also: