Transaction Types
Every money movement in ProxRad writes a row to the transactions table with a type string. There are 14 canonical types as of v1.0.552. Of these, 13 count as income (summed into the Dashboard’s “Total Income” cards and the Reports → Revenue page) and 3 are accounting-neutral — internal movements that do not represent revenue.
This page is the canonical reference. Whenever the Dashboard, Reports, the API, or an export disagrees about a number, walk the difference back through this table.
The 14 types at a glance
Section titled “The 14 types at a glance”| Type string | Go constant | Classification | Created in |
|---|---|---|---|
new | TransactionTypeNew | Income (MRR) | the backend — Create |
renewal | TransactionTypeRenewal | Income (MRR) | the backend — Renew |
change_service | TransactionTypeChangeService | Income | the backend — ChangeService |
service_change | (legacy spelling, same constant in some paths) | Income | Legacy code paths pre-v1.0.300 |
static_ip | TransactionTypeStaticIP | Income | the backend — Static IP rental |
addon | TransactionTypeAddon | Income | the backend — Addon |
refill | TransactionTypeRefill | Income | the backend — Refill quota |
data_topup | (uses refill constant in some flows; emitted by topup endpoint) | Income | the backend — Top-Up |
prepaid_card | TransactionTypePrepaidCard | Income | the backend — Card redemption |
subscriber_topup | TransactionTypeSubscriberTopup | Income | the backend — TopUp |
subscriber_purchase | TransactionTypeSubscriberPurchase | Income | the backend — Plan change |
reset_fup | TransactionTypeResetFUP | Income | the backend — Reset FUP (paid) |
rename | TransactionTypeRename | Income | the backend — Rename (paid) |
transfer | TransactionTypeTransfer | Neutral | the backend — Transfer balance |
withdraw | TransactionTypeWithdraw | Neutral | the backend — Withdraw |
refund | TransactionTypeRefund | Neutral | the backend — Delete with refund |
Revenue types (counted in Dashboard Total Income)
Section titled “Revenue types (counted in Dashboard Total Income)”13 transaction types count as income. They appear in:
- Dashboard → Today’s Total Income card.
- Dashboard → Month Total Income card.
- Reports → Revenue.
- Reports → Per-Service revenue.
- Reports → Per-Reseller revenue.
| Type | When emitted | Amount sign | Notes |
|---|---|---|---|
new | A subscriber is created with a paying service | + | Counted in MRR (Subscription Revenue card). |
renewal | An existing subscriber renews their plan | + | Counted in MRR. Auto-renewal also emits this. |
change_service | Subscriber moves from one plan to another mid-cycle | + | Amount may be a prorated charge or credit. |
service_change | Same as change_service — legacy spelling | + | Treated identically. |
static_ip | Static IP rental is started or renewed | + | One row per rental period. |
addon | A paid add-on is attached to the subscriber | + | E.g. premium support, fixed IP add-on. |
refill | Subscriber’s monthly quota is manually refilled (paid) | + | Distinct from free admin refill. |
data_topup | Subscriber buys extra GB via the customer portal | + | Tracked separately from subscriber_topup for legacy reasons. |
prepaid_card | Subscriber redeems a prepaid voucher | + | Revenue is recognised at redemption, not card generation. |
subscriber_topup | Top-up via customer portal | + | Common name “data top-up” in some UI strings. |
subscriber_purchase | Customer-portal self-service plan change | + | Treated as a sale to the subscriber by their reseller. |
reset_fup | Subscriber pays to reset their FUP early | + | Service must have reset_price > 0. |
rename | Subscriber pays to change their PPPoE username | + | Often used in legacy migrations. |
Canonical revenue SQL (v1.0.552)
Section titled “Canonical revenue SQL (v1.0.552)”SELECT COALESCE(SUM(amount), 0) AS total_incomeFROM transactionsWHERE created_at >= $1 AND created_at < $2 AND type IN ( 'new', 'renewal', 'change_service', 'service_change', 'static_ip', 'addon', 'refill', 'data_topup', 'prepaid_card', 'subscriber_topup', 'subscriber_purchase', 'reset_fup', 'rename' );This is the exact list used by both the Dashboard and Reports as of v1.0.552. Older versions may compute differently — see Changelog → v1.0.552.
Neutral / accounting types
Section titled “Neutral / accounting types”3 types are accounting movements. They appear in the Transactions list (for audit) but never count as income.
| Type | When emitted | Amount sign | What it represents |
|---|---|---|---|
transfer | Reseller moves balance to a sub-reseller, or to a peer | varies | A bookkeeping move — money switches accounts but no revenue. |
withdraw | Reseller balance is withdrawn (typically by admin clawing back) | − | Inverse of add_money (which is itself logged via audit, not a transaction). |
refund | Subscriber is deleted with refund, or a charge is reversed | − | Returns money to the reseller’s balance. |
Why these aren’t counted
Section titled “Why these aren’t counted”Including transfers in revenue would double-count: the receiving reseller’s later renewal already counts. Withdrawals are negative; if summed with revenue, they’d subtract from MRR and break dashboards. Refunds are tracked separately on the Reports page under “Refunds” so accountants can see net revenue (income − refunds).
Subscription Revenue (MRR) vs Total Income
Section titled “Subscription Revenue (MRR) vs Total Income”The Dashboard exposes both views via separate cards as of v1.0.552:
| Card | Includes | Excludes |
|---|---|---|
| Today’s Subscriptions / Month Subscriptions | new, renewal only | Everything else |
| Today’s Total Income / Month Total Income | All 13 income types | transfer, withdraw, refund |
The Subscriptions cards are the canonical MRR metric — forecasting / valuation use them. Total Income matches cashflow and is what accountants reconcile against bank statements.
Common amount conventions
Section titled “Common amount conventions”| Field | Meaning |
|---|---|
amount | Decimal(15,2). Positive for charges TO the subscriber / reseller, negative for refunds, withdrawals, internal credits. |
balance_before / balance_after | Reseller balance snapshot before / after the transaction. Used by the Transactions list to render the running balance column. |
reseller_id | The reseller whose balance moved. Always set. |
subscriber_id | Set for subscriber-attached types (new, renewal, refill, etc.); NULL for transfer between resellers. |
target_reseller_id | Set only on transfer rows — points at the receiving reseller. |
service_name | For new / renewal: the service name at the time of the transaction. Captured because the service may be renamed later. |
old_service_name / new_service_name | For change_service / service_change: before / after service names. |
description | Free-form human-readable note. |
Frequently asked questions
Section titled “Frequently asked questions”Why doesn’t Reports → Revenue match my dashboard?
Section titled “Why doesn’t Reports → Revenue match my dashboard?”Three possible causes:
- Time window mismatch — Dashboard uses local timezone midnight (system_preferences.system_timezone); some old API binaries used UTC. Confirm both pages use the same tz.
- Pre-v1.0.552 binary — Older Dashboard versions only counted
new + renewal(MRR), while Reports counted all 13. Upgrade to v1.0.552 or later to align them. - Reseller scope filter — Reports lets you filter by reseller. The Dashboard sums everything within the caller’s permission scope.
Where do add_money transactions appear?
Section titled “Where do add_money transactions appear?”They don’t go in the transactions table. Reseller balance top-ups by admins are logged via the audit_logs table (action = reseller.add_money) and adjust resellers.balance directly. This is intentional — adding money to a reseller’s balance isn’t income, it’s the operator funding their downstream.
Customer-portal top-ups vs admin refills — what’s the difference?
Section titled “Customer-portal top-ups vs admin refills — what’s the difference?”| Action | Type emitted | Source | Counts as income? |
|---|---|---|---|
| Admin clicks “Refill Quota” in subscriber edit | refill | subscribers.refill_quota permission | Yes (paid) or No (free) — depends on whether admin checked “charge reseller” |
| Subscriber buys top-up via customer portal | subscriber_topup | the backend TopUp | Yes |
| Subscriber redeems voucher | prepaid_card | the backend redemption | Yes |
Why do I see service_change rows in old data and change_service in new data?
Section titled “Why do I see service_change rows in old data and change_service in new data?”The constant was renamed in v1.0.300 to standardise on category_action (matching the rest of the type strings). Both spellings are present in the SQL filter so they’re summed correctly. If you need a clean export, alias them in your query:
SELECT CASE type WHEN 'service_change' THEN 'change_service' ELSE type END AS type_normalised, SUM(amount) AS totalFROM transactionsGROUP BY 1;How does a refund actually move money?
Section titled “How does a refund actually move money?”Three rows are written when a subscriber is deleted with refund enabled:
refundtransaction — negative amount, reseller’s balance increases (refund is credit-back).audit_logs.action = 'subscriber.delete'— the deletion record.subscribers.deleted_at— set to now.
The receivable refund value comes from the original new/renewal transaction’s amount minus pro-rata for time already used. The exact formula lives in the backend Delete.
Can transactions be edited or deleted?
Section titled “Can transactions be edited or deleted?”Only deleted, never edited. transactions.delete permission gates the action. Edits would break the running-balance audit chain — to correct an error, create a compensating transaction (typically a manual refund or transfer) instead.
Transaction lifecycle
Section titled “Transaction lifecycle”Most transactions are written synchronously as part of the corresponding handler request:
HTTP POST /api/subscribers/123/renew └── handlers/subscriber.go Renew() ├── Compute charge ├── INSERT transaction (type=renewal) ◄── Transaction row created ├── UPDATE resellers SET balance = balance - charge ├── UPDATE subscribers SET expiry_date = ... └── audit_logs.INSERT (subscriber.renew)Auto-renewal is the exception — it’s driven by a background scheduler that walks subscribers with auto_renew=true AND expiry_date <= now(), emits the same renewal transaction, and updates the same balance.
Schema reference
Section titled “Schema reference”CREATE TABLE transactions ( id SERIAL PRIMARY KEY, type VARCHAR(50) NOT NULL, amount DECIMAL(15,2) NOT NULL, balance_before DECIMAL(15,2), balance_after DECIMAL(15,2), description VARCHAR(500), old_service_name VARCHAR(100), new_service_name VARCHAR(100), service_name VARCHAR(100), reseller_id INTEGER NOT NULL, subscriber_id INTEGER, target_reseller_id INTEGER, ip_address VARCHAR(50), user_agent VARCHAR(255), created_by INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
CREATE INDEX idx_transactions_type ON transactions(type);CREATE INDEX idx_transactions_reseller ON transactions(reseller_id);CREATE INDEX idx_transactions_sub ON transactions(subscriber_id);CREATE INDEX idx_transactions_created ON transactions(created_at);This table is never soft-deleted and never truncated in normal operation — it’s the canonical financial audit trail.
Adding a new transaction type
Section titled “Adding a new transaction type”If you introduce a new type:
- Add the constant in the backend:
TransactionTypeYourNewType TransactionType = "your_new_type"
- Decide whether it counts as income. If yes, add it to the canonical SQL filter in:
- the backend (Total Income query).
- the backend (Revenue report queries).
- Update the Reports per-service / per-reseller aggregations if the new type should appear there.
- Update this reference page.
- Confirm Dashboard + Reports show the same numbers after re-deploy.
Skipping step 2 is the most common cause of “the dashboard says X but Reports says Y” issues post-feature-add.
Related pages
Section titled “Related pages”- Dashboard — where these types are summed into the Total Income card.
- Reports → Revenue — per-type breakdown table.
- Database Schema → transactions — column types.
- Resellers — balance handling and audit trail.