Skip to content

Balance & Transactions

Every reseller has a wallet (balance) and a credit limit (credit). Every operational action — renew, top-up, plan change, refund — debits or credits the wallet and writes a row to the transactions table. This page explains the wallet mechanics, the 14 transaction types, and which ones count as revenue vs which are pure accounting movements.

PageURLWhat it shows
My balance (reseller’s own wallet)/profile or sidebar header chipYour live balance, last 10 transactions.
Reseller detail (admin or parent)/resellers/{id} → Transactions tabFull transaction history for one reseller.
Transactions (global)/transactionsAll transactions across your scope (filterable by type, date, reseller, subscriber).

The current balance is also shown in the top-right header next to the clock — a reseller sees their own balance auto-refreshing every 60 seconds (v1.0.258).

Wallet vs credit — two numbers, one rule

Section titled “Wallet vs credit — two numbers, one rule”
FieldMeaning
balanceLive money in the wallet. Goes up on Transfer-in, top-up, refund. Goes down on renewal, plan change, withdraw.
creditHow far below zero balance is allowed to go. Default 0 means renewals fail the moment the wallet hits zero. A credit of 500 means you can stay at -500 before renewals start failing.

The rule the API enforces:

if (action.cost > balance + credit) → reject with "Insufficient balance"

Only an admin can change a reseller’s credit — see Onboarding.

Transactions live in the transactions table with a type column. They split into three groups:

Revenue (11 types)

These count toward “Total Income” on the Dashboard and Reports → Revenue. They’re real money the ISP collected.

TypeWhen it’s writtenSign
newFirst charge when a subscriber is created.Positive
renewalRecurring charge when a subscriber renews their plan.Positive
change_serviceProrated charge / refund when a subscriber changes plan.Positive or negative (refund is on downgrade).
static_ipOne-time fee for assigning a static IP.Positive
addonOptional add-on fee (configured per service).Positive
refillOperator manually tops up a subscriber’s quota outside a renewal.Positive
data_topupCustomer or operator buys extra GB.Positive
prepaid_cardSubscriber redeems a prepaid voucher.Positive
subscriber_topupSubscriber adds money to their own wallet (cash receipt).Positive
subscriber_purchaseSubscriber spends from their wallet on the portal.Positive (revenue to ISP, debit on subscriber wallet).
reset_fupOptional charge when an operator forces a FUP reset (configured per reseller).Positive
renameOptional fee for renaming a subscriber’s PPPoE username.Positive

Neutral (3 types)

These are accounting movements between two wallets. They do not count as revenue. The Dashboard’s Total Income card excludes them.

TypeWhat movesSign
transferMoney moves parent → sub-reseller. One row on each side.+ on receiver, − on sender
withdrawMoney moves sub-reseller → parent. One row on each side.+ on parent, − on sub
refundReverses a prior charge. One row debiting wallet, optionally a matching refund on the subscriber side.Negative

Every transaction row has:

ColumnUse
typeOne of the 14 above.
amountSigned — positive for credit, negative for debit on this wallet.
balance_before / balance_afterThe wallet’s balance immediately before and after the row was written. Used for audit reconciliation.
reseller_idWhose wallet this row affects.
subscriber_idOptional — set when the transaction relates to a specific subscriber.
target_reseller_idOptional — set on transfer / withdraw to point to the other party.
descriptionHuman-readable note shown in the UI.
created_byThe user (admin or reseller) who triggered the action.
ip_addressThe originating IP — useful for audit.
created_atTimestamp.

Topping up a reseller — how it works end-to-end

Section titled “Topping up a reseller — how it works end-to-end”

When an admin or parent reseller hits the Transfer button on a reseller’s row:

  1. The API checks the caller is either admin or the direct parent of the target.
  2. If the caller is a reseller, it checks the caller’s wallet has enough (balance + credit >= amount).
  3. In one DB transaction: subtract amount from source wallet, add to target wallet.
  4. Insert one transfer row on the source (amount=-X, target=target_id), one on the target (amount=+X).
  5. Insert an audit log entry: Transferred $X.XX to <target_name>.
  6. Return success. Both balances update live in the UI on next refresh.

If step 1 fails → 403. If step 2 fails → 400. No DB writes happen.

Withdrawing from a reseller (parent or admin)

Section titled “Withdrawing from a reseller (parent or admin)”

Same flow as Transfer, but reversed direction. Most operators use Withdraw at month-end as a settlement — pull back money the sub didn’t burn, before the next month’s grant.

A refund is created by:

  • A subscriber-initiated downgrade (change_service flagged as a downgrade).
  • An admin manually creating a refund from the subscriber’s billing tab.

Refunds always credit the subscriber’s wallet, never the reseller’s. The reseller side of a refund is recorded as a change_service or manual adjustment with a negative amount.

  1. Resellers → click the sub-reseller → Transfer.
  2. Enter amount (e.g. 300) and a note ("Mid-month top-up").
  3. Confirm. Two rows appear: one on your wallet (transfer, −300), one on theirs (transfer, +300).

Reconciling a month — “where did my balance go?”

Section titled “Reconciling a month — “where did my balance go?””
  1. Transactions → filter date >= 1st of last month, date <= last day of last month.
  2. Group mentally by type:
    • new + renewal + change_service + static_ip + addon + refill + data_topup + prepaid_card + subscriber_purchase + reset_fup + rename = revenue earned
    • transfer (negative) = money you sent to subs
    • transfer (positive) = top-ups received from admin
    • withdraw (positive) = money you pulled back from subs
  3. Net change in balance should equal start_balance + sum(amount) = end_balance. If it doesn’t, look at balance_before / balance_after columns to find the offending row.
  1. Transactions → filter type = transfer OR type = withdraw.
  2. Sort by amount DESC.
  3. Look for unusual large movements between resellers — particularly ones that happen at odd hours.
  4. The ip_address column tells you where the action was triggered from. If a Transfer happened from an IP that isn’t your normal office IP, audit the reseller account.
  5. To lock the reseller down going forward, set their allowed_ips to your office’s public IP — see IP Restriction.
PermissionEffect
transactions.viewSee the Transactions page (your own scope).
transactions.view_allSee all transactions across the system (system-wide audit / partner role).
transactions.transferUse the Transfer button.
transactions.withdrawUse the Withdraw button.
transactions.refundIssue a manual refund to a subscriber.
  • Float precision. All amounts are stored as NUMERIC(12,2) and rounded to two decimals on every write. The frontend never does float math.
  • No undo. A wrong Transfer cannot be undone with a single click — you must Withdraw the same amount back. Both rows stay in the history for audit.
  • Cross-customer transfers. Not possible — you can only Transfer to a direct child. The original M6 audit fix removed an exploit where a reseller could move balance between any two accounts.