Transaction Types
Every money movement in ProxPanel 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) | internal/handlers/subscriber.go — Create |
renewal | TransactionTypeRenewal | Income (MRR) | internal/handlers/subscriber.go — Renew |
change_service | TransactionTypeChangeService | Income | internal/handlers/subscriber.go — ChangeService |
service_change | (legacy spelling, same constant in some paths) | Income | Legacy code paths pre-v1.0.300 |
static_ip | TransactionTypeStaticIP | Income | internal/handlers/billing.go — Static IP rental |
addon | TransactionTypeAddon | Income | internal/handlers/subscriber.go — Addon |
refill | TransactionTypeRefill | Income | internal/handlers/subscriber.go — Refill quota |
data_topup | (uses refill constant in some flows; emitted by topup endpoint) | Income | internal/handlers/customer_portal.go — Top-Up |
prepaid_card | TransactionTypePrepaidCard | Income | internal/handlers/prepaid.go — Card redemption |
subscriber_topup | TransactionTypeSubscriberTopup | Income | internal/handlers/customer_portal.go — TopUp |
subscriber_purchase | TransactionTypeSubscriberPurchase | Income | internal/handlers/customer_portal.go — Plan change |
reset_fup | TransactionTypeResetFUP | Income | internal/handlers/subscriber.go — Reset FUP (paid) |
rename | TransactionTypeRename | Income | internal/handlers/subscriber.go — Rename (paid) |
transfer | TransactionTypeTransfer | Neutral | internal/handlers/reseller.go — Transfer balance |
withdraw | TransactionTypeWithdraw | Neutral | internal/handlers/reseller.go — Withdraw |
refund | TransactionTypeRefund | Neutral | internal/handlers/subscriber.go — 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 | customer_portal.go TopUp | Yes |
| Subscriber redeems voucher | prepaid_card | prepaid.go 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 handlers/subscriber.go 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
internal/models/billing.go:TransactionTypeYourNewType TransactionType = "your_new_type" - Decide whether it counts as income. If yes, add it to the canonical SQL filter in:
internal/handlers/dashboard.go(Total Income query).internal/handlers/report.go(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.