Auto-debit platform credits when purchase is approved

When a pending purchase is resolved as completed:
- Inserts a debit row into platform_credits for the matching platform
  (joins token_purchases.platform_id slug → platforms.id)
- Debit notes include purchase #, player name, username, token count, amount, method
- Total shown in credit modal now subtracts debits from credits (net balance)

Credit history table updates:
- CREDIT/DEBIT type badges, debit rows tinted red with − prefix
- Debit rows show "Purchase #X ↗" button that closes modal, jumps to
  the Purchases section (all tab), and highlights that purchase row
- Edit/delete buttons hidden on auto-generated debit rows

Also fixes: resolve_purchase was echoing $sent (undefined variable bug)
Also fixes: purchaseCard div now has id="pr-N" so jump-highlight works

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 21:38:03 +00:00
parent 1367fa334b
commit f54cdb11db
4 changed files with 85 additions and 17 deletions
+43 -12
View File
@@ -1228,7 +1228,7 @@ function purchaseCard(p, showActions=true) {
'<button class="btn btn-red" data-pid="'+p.id+'" onclick="resolvePurchase(this.dataset.pid,&quot;failed&quot;)" style="width:100%;font-size:15px;padding:10px">✗ Reject</button></div>'
: (p.admin_note ? '<div style="font-size:14px;color:var(--gold);padding:6px 8px;background:rgba(240,192,64,.07);border-radius:6px;margin-top:4px">📝 '+escHtmlA(p.admin_note)+'</div>' : '');
return '<div class="card" style="margin-bottom:10px;'+(isPending?'border-color:rgba(240,192,64,.25);background:rgba(240,192,64,.02)':'')+'">'+
return '<div class="card" id="pr-'+p.id+'" style="margin-bottom:10px;'+(isPending?'border-color:rgba(240,192,64,.25);background:rgba(240,192,64,.02)':'')+'">'+
'<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap">'+
'<div style="flex:1;min-width:180px">'+
'<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:4px">'+
@@ -3173,22 +3173,37 @@ async function loadCreditEntries() {
list.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:14px">
<thead><tr style="border-bottom:1px solid var(--border)">
<th style="padding:8px 6px;text-align:left;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">DATE</th>
<th style="padding:8px 6px;text-align:right;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">CREDITS</th>
<th style="padding:8px 6px;text-align:left;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">TYPE</th>
<th style="padding:8px 6px;text-align:right;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">AMOUNT</th>
<th style="padding:8px 6px;text-align:left;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">METHOD</th>
<th style="padding:8px 6px;text-align:left;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">NOTES</th>
<th style="padding:8px 6px;width:80px"></th>
</tr></thead>
<tbody>
${d.credits.map(c=>`<tr style="border-bottom:1px solid rgba(255,255,255,0.05)">
<td style="padding:9px 6px;color:var(--text)">${escHtmlA(c.credit_date)}</td>
<td style="padding:9px 6px;text-align:right;font-family:'Exo 2',sans-serif;font-weight:700;color:var(--cyan)">${parseFloat(c.credits_purchased).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}</td>
<td style="padding:9px 6px;color:var(--text2)">${escHtmlA(c.payment_method||'—')}</td>
<td style="padding:9px 6px;color:var(--text2);font-size:13px;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtmlA(c.notes||'')}</td>
<td style="padding:9px 6px;text-align:right;white-space:nowrap">
<button onclick="editCreditEntry(${c.id})" style="background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);color:var(--cyan);border-radius:5px;padding:3px 8px;font-size:13px;cursor:pointer;margin-right:4px">✏️</button>
<button onclick="deleteCreditEntry(${c.id})" style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:5px;padding:3px 8px;font-size:13px;cursor:pointer">🗑</button>
</td>
</tr>`).join('')}
${d.credits.map(c=>{
const isDebit = c.type === 'debit';
const amtColor = isDebit ? 'var(--red)' : 'var(--cyan)';
const amtPrefix = isDebit ? '' : '+';
const rowBg = isDebit ? 'background:rgba(255,68,68,.03)' : '';
const typeBadge = isDebit
? '<span style="background:rgba(255,68,68,.15);color:var(--red);font-size:11px;font-weight:700;padding:2px 7px;border-radius:10px;letter-spacing:.3px">DEBIT</span>'
: '<span style="background:rgba(0,229,255,.12);color:var(--cyan);font-size:11px;font-weight:700;padding:2px 7px;border-radius:10px;letter-spacing:.3px">CREDIT</span>';
const refLink = (isDebit && c.purchase_ref_id)
? `<button onclick="jumpToPurchase(${c.purchase_ref_id})" style="background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.25);color:var(--gold);border-radius:5px;padding:3px 8px;font-size:12px;cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700;white-space:nowrap">Purchase #${c.purchase_ref_id} ↗</button>`
: '';
const editDel = !isDebit
? `<button onclick="editCreditEntry(${c.id})" style="background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);color:var(--cyan);border-radius:5px;padding:3px 8px;font-size:13px;cursor:pointer;margin-right:4px">✏️</button>
<button onclick="deleteCreditEntry(${c.id})" style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:5px;padding:3px 8px;font-size:13px;cursor:pointer">🗑</button>`
: '';
return `<tr style="border-bottom:1px solid rgba(255,255,255,0.05);${rowBg}">
<td style="padding:9px 6px;color:var(--text)">${escHtmlA(c.credit_date)}</td>
<td style="padding:9px 6px">${typeBadge}</td>
<td style="padding:9px 6px;text-align:right;font-family:'Exo 2',sans-serif;font-weight:700;color:${amtColor}">${amtPrefix}${parseFloat(c.credits_purchased).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}</td>
<td style="padding:9px 6px;color:var(--text2)">${escHtmlA(c.payment_method||'—')}</td>
<td style="padding:9px 6px;color:var(--text2);font-size:13px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtmlA(c.notes||'')}">${escHtmlA(c.notes||'')}</td>
<td style="padding:9px 6px;text-align:right;white-space:nowrap">${refLink}${editDel}</td>
</tr>`;
}).join('')}
</tbody></table>`;
}
@@ -3249,6 +3264,22 @@ async function deleteCreditEntry(id) {
else toast(d.error||'Error','err');
}
async function jumpToPurchase(purchaseId) {
closeCreditModal();
showSec('purchases');
document.querySelectorAll('#section-purchases .ftab').forEach(b=>b.classList.remove('active'));
const allTab = document.querySelector('#section-purchases .ftab[onclick*="\'all\'"]');
if (allTab) allTab.classList.add('active');
await loadPurchases('all');
const el = document.getElementById('pr-' + purchaseId);
if (el) {
el.scrollIntoView({behavior:'smooth', block:'center'});
el.style.outline = '2px solid var(--gold)';
el.style.borderRadius = '12px';
setTimeout(() => { el.style.outline = ''; el.style.borderRadius = ''; }, 2500);
}
}
// Sync hex input with color picker
document.addEventListener('DOMContentLoaded', function() {
const picker = document.getElementById('gf-color');
+20 -4
View File
@@ -104,17 +104,33 @@ switch ($action) {
db()->beginTransaction();
try {
if ($status === 'completed') {
// Credit tokens to user
logAdminAction('TOKENS_ADJUSTED', $adminId, 'user', isset($targetId)?(int)$targetId:0, 'Manual token adjustment: '.($data['tokens']??0).' tokens', '', ($data['tokens']??''), 'critical');
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$purchase['tokens'], $purchase['user_id']]);
}
db()->prepare("UPDATE token_purchases SET status=?,admin_note=? WHERE id=?")->execute([$status, $note, $id]);
db()->commit();
echo json_encode($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success'=>false,'error'=>'DB error']);
echo json_encode(['success'=>false,'error'=>'DB error']); exit;
}
// Insert debit entry into platform_credits when approved
if ($status === 'completed' && !empty($purchase['platform_id'])) {
$platRow = db()->prepare("SELECT id FROM platforms WHERE slug=?");
$platRow->execute([$purchase['platform_id']]);
$platNumId = (int)$platRow->fetchColumn();
if ($platNumId) {
$userRow = db()->prepare("SELECT username FROM users WHERE id=?");
$userRow->execute([$purchase['user_id']]);
$username = $userRow->fetchColumn() ?: 'User#'.$purchase['user_id'];
$amtDollars = number_format($purchase['amount_cents'] / 100, 2);
$playerLabel = trim($purchase['player_name'] ?: $purchase['game_alias'] ?: $username);
$debitNotes = "Purchase #{$id} · {$playerLabel} ({$username}) · {$purchase['tokens']} tokens · \${$amtDollars} via {$purchase['payment_method']}";
db()->prepare("INSERT INTO platform_credits (platform_id, credits_purchased, credit_date, payment_method, notes, type, purchase_ref_id) VALUES (?,?,CURDATE(),?,?,?,?)")
->execute([$platNumId, $purchase['tokens'], $purchase['payment_method'], $debitNotes, 'debit', $id]);
}
}
echo json_encode(['success'=>true]);
break;
// ─── CASHOUTS ─────────────────────────────────────────────
+1 -1
View File
@@ -120,7 +120,7 @@ switch ($action) {
$rows = db()->prepare("SELECT * FROM platform_credits WHERE platform_id=? ORDER BY credit_date DESC, id DESC");
$rows->execute([$pid]);
$credits = $rows->fetchAll();
$total = db()->prepare("SELECT COALESCE(SUM(credits_purchased),0) FROM platform_credits WHERE platform_id=?");
$total = db()->prepare("SELECT COALESCE(SUM(CASE WHEN type='debit' THEN -credits_purchased ELSE credits_purchased END),0) FROM platform_credits WHERE platform_id=?");
$total->execute([$pid]);
echo json_encode(['success'=>true,'credits'=>$credits,'total'=>(float)$total->fetchColumn()]);
break;
+21
View File
@@ -282,6 +282,27 @@ CREATE TABLE `platforms` (
UNIQUE KEY `slug` (`slug`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `platform_credits`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `platform_credits` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`platform_id` int(11) NOT NULL,
`credits_purchased` decimal(12,2) NOT NULL DEFAULT 0.00,
`credit_date` date NOT NULL,
`payment_method` varchar(100) DEFAULT NULL,
`notes` text DEFAULT NULL,
`type` enum('credit','debit') NOT NULL DEFAULT 'credit',
`purchase_ref_id` int(11) DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
KEY `platform_id` (`platform_id`),
KEY `credit_date` (`credit_date`),
KEY `idx_pc_type` (`type`),
KEY `idx_pc_ref` (`purchase_ref_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `referral_social_shares`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;