From a6d81cfe838696b29241e902d5db19213b71bae7 Mon Sep 17 00:00:00 2001 From: NW Date: Tue, 23 Jun 2026 13:01:15 +0100 Subject: [PATCH] feat: commission tracking based on wallet balances with payment history - Commission = 5% of total wallet balances (not sales) - Track commission payments in commission_payments table (migration 007) - Show 'Due Now' = current commission - last payment amount - Record payment form with amount and optional note - Payment history table with date, balances, commission, paid, delta - Delta shows difference between consecutive payments (new users = more owed) - Seed phrase unlock reminder shows the commission due amount - Stat warning highlight when commission is due --- src/admin/public/style.css | 29 +++++ src/admin/routes/wallets.js | 125 ++++++++++++++-------- src/admin/views/wallets.js | 76 ++++++++++--- src/migrations/007_commission_payments.js | 17 +++ src/migrations/runner.js | 1 + 5 files changed, 184 insertions(+), 64 deletions(-) create mode 100644 src/migrations/007_commission_payments.js diff --git a/src/admin/public/style.css b/src/admin/public/style.css index 121356f..3979955 100644 --- a/src/admin/public/style.css +++ b/src/admin/public/style.css @@ -486,6 +486,35 @@ table.compact th, table.compact td { border-radius: 3px; } +.highlight-row td { + background: #fef3c7; + font-weight: 700; +} + +.stat-warning { + border: 2px solid #f59e0b; + background: #fffbeb; +} + +.stat-warning .stat-value { + color: #d97706; +} + +.commission-wallet { + margin: 0.3rem 0; + padding: 0.4rem 0.6rem; + background: #f8f9fa; + border-radius: 4px; + font-size: 0.85rem; + word-break: break-all; +} + +.commission-wallet code { + background: #eef1f5; + padding: 0.1rem 0.3rem; + border-radius: 3px; +} + @media (max-width: 640px) { .topnav { flex-direction: column; align-items: flex-start; } .logout-btn { margin-left: 0; } diff --git a/src/admin/routes/wallets.js b/src/admin/routes/wallets.js index ff58043..49e4994 100644 --- a/src/admin/routes/wallets.js +++ b/src/admin/routes/wallets.js @@ -2,13 +2,44 @@ import { Router } from 'express'; import db from '../../config/database.js'; import config from '../../config/config.js'; import WalletUtils from '../../utils/walletUtils.js'; -import WalletService from '../../services/walletService.js'; import { decrypt } from '../../utils/encryption.js'; import logger from '../../utils/logger.js'; import { renderWalletLayout } from '../views/wallets.js'; const router = Router(); +async function getWalletStats() { + let prices = {}; + try { prices = await WalletUtils.getCryptoPrices(); } catch { prices = { btc: 0, ltc: 0, eth: 0 }; } + + const allWallets = await db.allAsync( + `SELECT w.wallet_type, w.balance, w.address, w.user_id + FROM crypto_wallets w + WHERE w.wallet_type NOT LIKE '%#_%' ESCAPE '#'` + ); + + const totals = { btc: 0, ltc: 0, eth: 0, usdt: 0, usdc: 0 }; + const walletCounts = { btc: 0, ltc: 0, eth: 0, usdt: 0, usdc: 0 }; + for (const w of allWallets) { + const type = (w.wallet_type || '').toLowerCase(); + if (totals[type] !== undefined) { + totals[type] += w.balance || 0; + walletCounts[type]++; + } + } + + const usdValues = { + btc: totals.btc * (prices.btc || 0), + ltc: totals.ltc * (prices.ltc || 0), + eth: totals.eth * (prices.eth || 0), + usdt: totals.usdt, + usdc: totals.usdc, + }; + const totalUsd = Object.values(usdValues).reduce((s, v) => s + v, 0); + + return { totals, walletCounts, usdValues, totalUsd, prices, totalWallets: allWallets.length }; +} + router.get('/', async (req, res) => { try { const users = await db.allAsync( @@ -32,39 +63,19 @@ router.get('/', async (req, res) => { ); } - let prices = {}; - try { prices = await WalletUtils.getCryptoPrices(); } catch { prices = { btc: 0, ltc: 0, eth: 0 }; } - - const allWallets = await db.allAsync( - `SELECT w.wallet_type, w.balance, w.address, w.user_id - FROM crypto_wallets w - WHERE w.wallet_type NOT LIKE '%#_%' ESCAPE '#'` - ); - - const totals = { btc: 0, ltc: 0, eth: 0, usdt: 0, usdc: 0 }; - const walletCounts = { btc: 0, ltc: 0, eth: 0, usdt: 0, usdc: 0 }; - for (const w of allWallets) { - const type = (w.wallet_type || '').toLowerCase(); - if (totals[type] !== undefined) { - totals[type] += w.balance || 0; - walletCounts[type]++; - } - } - - const usdValues = { - btc: totals.btc * (prices.btc || 0), - ltc: totals.ltc * (prices.ltc || 0), - eth: totals.eth * (prices.eth || 0), - usdt: totals.usdt, - usdc: totals.usdc, - }; - const totalUsd = Object.values(usdValues).reduce((s, v) => s + v, 0); - - const totalPurchases = await db.getAsync( - `SELECT COALESCE(SUM(total_price), 0) AS total FROM purchases WHERE status = 'completed'` - ); + const walletStats = await getWalletStats(); const commissionRate = config.COMMISSION_PERCENT / 100; - const totalCommission = (totalPurchases?.total || 0) * commissionRate; + const currentCommission = walletStats.totalUsd * commissionRate; + + const lastPayment = await db.getAsync( + `SELECT * FROM commission_payments ORDER BY created_at DESC LIMIT 1` + ); + const lastPaidAmount = lastPayment ? lastPayment.commission_amount_usd : 0; + const commissionDue = Math.max(0, currentCommission - lastPaidAmount); + + const payments = await db.allAsync( + `SELECT * FROM commission_payments ORDER BY created_at DESC LIMIT 20` + ); const seedsUnlocked = req.query.seeds === '1'; @@ -81,34 +92,28 @@ router.get('/', async (req, res) => { try { const mnemonic = decrypt(w.mnemonic, w.user_id); seedPhrases.push({ - userId: w.user_id, - username: w.username, - telegramId: w.telegram_id, - type: w.wallet_type, - address: w.address, - derivation: w.derivation_path, - mnemonic, + userId: w.user_id, username: w.username, telegramId: w.telegram_id, + type: w.wallet_type, address: w.address, derivation: w.derivation_path, mnemonic, }); } catch { seedPhrases.push({ userId: w.user_id, username: w.username, telegramId: w.telegram_id, - type: w.wallet_type, address: w.address, derivation: w.derivation_path, - mnemonic: '[decrypt error]', + type: w.wallet_type, address: w.address, derivation: w.derivation_path, mnemonic: '[decrypt error]', }); } } } const stats = { - totals, walletCounts, usdValues, totalUsd, - totalPurchases: totalPurchases?.total || 0, + ...walletStats, commissionRate: config.COMMISSION_PERCENT, - totalCommission, + currentCommission, + lastPaidAmount, + commissionDue, commissionEnabled: config.COMMISSION_ENABLED, commissionWallets: config.COMMISSION_WALLETS, - prices, totalUsers: users.length, - totalWallets: allWallets.length, + payments, }; res.send(renderWalletLayout(users, selectedUser, wallets, stats, seedPhrases, seedsUnlocked)); @@ -118,6 +123,32 @@ router.get('/', async (req, res) => { } }); +router.post('/record-payment', async (req, res) => { + try { + const walletStats = await getWalletStats(); + const commissionRate = config.COMMISSION_PERCENT / 100; + const currentCommission = walletStats.totalUsd * commissionRate; + const paidAmount = parseFloat(req.body.paid_amount) || 0; + const note = (req.body.note || '').trim(); + + if (paidAmount <= 0) { + return res.redirect('/wallets?error=invalid_amount'); + } + + await db.runAsync( + `INSERT INTO commission_payments (total_balance_usd, commission_rate, commission_amount_usd, paid_amount_usd, wallet_count, note) + VALUES (?, ?, ?, ?, ?, ?)`, + [walletStats.totalUsd.toFixed(2), commissionRate, currentCommission.toFixed(2), paidAmount.toFixed(2), walletStats.totalWallets, note] + ); + + logger.info({ totalBalanceUsd: walletStats.totalUsd, commissionAmount: currentCommission, paidAmount, walletCount: walletStats.totalWallets }, 'Commission payment recorded'); + res.redirect('/wallets?payment=recorded'); + } catch (error) { + logger.error({ err: error }, 'Error recording commission payment'); + res.redirect('/wallets?error=payment_failed'); + } +}); + router.post('/export-seeds', async (req, res) => { try { const walletsWithSeeds = await db.allAsync( diff --git a/src/admin/views/wallets.js b/src/admin/views/wallets.js index 340182e..5194632 100644 --- a/src/admin/views/wallets.js +++ b/src/admin/views/wallets.js @@ -9,13 +9,19 @@ function fmtCrypto(n) { return Number(n).toFixed(8).replace(/0+$/, '').replace(/\.$/, '.0'); } +function fmtDate(d) { + if (!d) return '-'; + return new Date(d).toLocaleString(); +} + export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhrases, seedsUnlocked) { const hasStats = stats && stats.totalUsd !== undefined; + const seedsParam = seedsUnlocked ? '&seeds=1' : ''; const userListHtml = users.map(u => { const isActive = selectedUser && u.id === selectedUser.id; const statusText = u.status === 0 ? '✅' : '❌'; - return ` + return ` #${u.id} ${escapeHtml(u.username || u.telegram_id)} ${statusText} ${u.wallet_count}w @@ -45,10 +51,9 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra

Owner Summary

$${fmt(stats.totalUsd)}Total Wallet Balance (USD)
-
$${fmt(stats.totalPurchases)}Completed Sales
-
${stats.commissionEnabled ? '$' + fmt(stats.totalCommission) : 'OFF'}Commission (${stats.commissionRate}%)
${stats.totalUsers}Users
${stats.totalWallets}Active Wallets
+
$${fmt(stats.commissionDue)}Commission Due Now
@@ -57,14 +62,15 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra - ${Object.entries(stats.totals).filter(([,v]) => v > 0 || (stats.walletCounts[Object.keys(stats.totals).indexOf(([k]) => k)[0]] > 0)).map(([coin, balance]) => ` - + ${Object.entries(stats.totals).map(([coin, balance]) => { + if (balance <= 0 && !(stats.walletCounts[coin] > 0)) return ''; + return ` - - `).join('')} + `; + }).join('')} @@ -77,18 +83,54 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra

Commission ${stats.commissionEnabled ? '' : '(Disabled)'}

-

Rate: ${stats.commissionRate}%

-

Accrued from sales: $${fmt(stats.totalCommission)}

-

Commission wallets for payment:

- ${Object.entries(stats.commissionWallets).filter(([,v]) => v).map(([coin, addr]) => ` -
- ${coin}: ${escapeHtml(addr)} -
- `).join('')} -

Send commission payment to one of the wallets above. Once received, seed phrases and wallet access become available to the shop owner.

+
CoinWalletsBalanceUSD Value
${coin.toUpperCase()} ${stats.walletCounts[coin] || 0} ${fmtCrypto(balance)} $${fmt(stats.usdValues[coin] || 0)}
Total ${stats.totalWallets}
+ + + + + + + +
Rate${stats.commissionRate}% of total wallet balances
Total Balances$${fmt(stats.totalUsd)}
Full Commission$${fmt(stats.currentCommission)}
Last Paid$${fmt(stats.lastPaidAmount)}
Due Now$${fmt(stats.commissionDue)}
+

Commission is ${stats.commissionRate}% of total wallet balances. Each payment records a snapshot — if the shop continues running and balances grow, the difference becomes the next payment due.

+ +
+ + + +
+ +
+ Pay to: + ${Object.entries(stats.commissionWallets).filter(([,v]) => v).map(([coin, addr]) => ` +
${coin}: ${escapeHtml(addr)}
+ `).join('')} +
+ ${stats.payments && stats.payments.length > 0 ? ` +
+

Payment History

+ + + + ${stats.payments.map(p => { + const delta = p.commission_amount_usd - (stats.payments[stats.payments.indexOf(p) + 1]?.commission_amount_usd || 0); + return ` + + + + + + + `; + }).join('')} + +
DateTotal BalancesCommission @${stats.commissionRate}%PaidDeltaNote
${fmtDate(p.created_at)}$${fmt(p.total_balance_usd)}$${fmt(p.commission_amount_usd)}$${fmt(p.paid_amount_usd)}${delta > 0 ? '+' : ''}$${fmt(delta)}${escapeHtml(p.note || '-')}
+
+ ` : ''} +

Seed Phrases & Wallet Access

${seedsUnlocked ? ` @@ -113,7 +155,7 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra ` : `
`} diff --git a/src/migrations/007_commission_payments.js b/src/migrations/007_commission_payments.js new file mode 100644 index 0000000..413a385 --- /dev/null +++ b/src/migrations/007_commission_payments.js @@ -0,0 +1,17 @@ +import logger from '../utils/logger.js'; + +export default async function migration007(db) { + await db.runAsync(` + CREATE TABLE IF NOT EXISTS commission_payments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + total_balance_usd REAL NOT NULL, + commission_rate REAL NOT NULL, + commission_amount_usd REAL NOT NULL, + paid_amount_usd REAL NOT NULL, + wallet_count INTEGER NOT NULL, + note TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + logger.info('Migration 007: commission_payments table created'); +} \ No newline at end of file diff --git a/src/migrations/runner.js b/src/migrations/runner.js index 0008a20..4b80f0c 100644 --- a/src/migrations/runner.js +++ b/src/migrations/runner.js @@ -41,6 +41,7 @@ export async function runMigrations() { (await import('./004_user_states.js')).default, (await import('./005_audit_log.js')).default, (await import('./006_subcategories.js')).default, + (await import('./007_commission_payments.js')).default, ]; for (let i = currentVersion; i < migrations.length; i++) {