mirror of
https://github.com/myronblair/tomtomgames
synced 2026-07-27 17:02:27 -05:00
Add in-admin refund processing for Square token purchases; commit earlier pending security fixes
Refund feature: token_purchases had no way to refund a completed Square payment - the existing resolve_purchase action only approves/rejects manual (Zelle etc) payments still in pending status. Adds a real refund_purchase action (api/admin.php) that calls Squares actual Refunds API via a new SquarePayment::refund() method, updates status to the new refunded enum value, logs the refund ID, and claws back the tokens credited on purchase (clamped at 0 if the customer already spent some, reported back as a shortfall so staff know). UI: admin/index.php shows a Refund button on completed card purchases with a square_payment_id. Tested in sandbox: full refund with full token clawback, and a shortfall scenario (user already spent below the credited amount) to confirm clamping works correctly rather than going negative. Also committing two earlier pending fixes from this session that were never committed: .htaccess .py/!install!! blocking hardening and moving bump_version.PHPs BUMP_KEY out of the web-reachable file into includes/config.php.
This commit is contained in:
+21
-3
@@ -1303,8 +1303,8 @@ function purchaseCard(p, showActions=true) {
|
||||
const isManual = p.payment_method !== 'card';
|
||||
const amt = (p.amount_cents/100).toFixed(2);
|
||||
const date = (p.created_at||'').substring(0,16);
|
||||
const statusColors = { pending:'var(--gold)', completed:'var(--green)', failed:'var(--red)' };
|
||||
const statusLabels = { pending:'⏳ Pending Approval', completed:'✅ Completed', failed:'❌ Failed' };
|
||||
const statusColors = { pending:'var(--gold)', completed:'var(--green)', failed:'var(--red)', refunded:'var(--cyan)' };
|
||||
const statusLabels = { pending:'⏳ Pending Approval', completed:'✅ Completed', failed:'❌ Failed', refunded:'↩ Refunded' };
|
||||
const statusColor = statusColors[p.status] || 'var(--text2)';
|
||||
const statusLabel = statusLabels[p.status] || p.status;
|
||||
const mIcons = { card:'💳', venmo:'💙', cashapp:'💚', zelle:'💜', chime:'🟢', manual:'💵' };
|
||||
@@ -1314,12 +1314,17 @@ function purchaseCard(p, showActions=true) {
|
||||
const platformRow = p.platform_id
|
||||
? '<div style="font-size:14px;color:var(--text2);margin-top:4px">Platform: <strong style="color:var(--cyan)">'+pname(p.platform_id)+'</strong> · Alias: <strong style="color:var(--gold2)">'+escHtmlA(p.game_alias||'—')+'</strong></div>'
|
||||
: '';
|
||||
const canRefund = showActions && p.status === 'completed' && p.payment_method === 'card' && p.square_payment_id;
|
||||
const actions = showActions && isPending
|
||||
? '<div style="display:flex;flex-direction:column;gap:8px;flex-shrink:0;min-width:160px">'+
|
||||
'<input class="fi-sm" type="text" id="pnote-'+p.id+'" placeholder="Note to player (optional)" style="width:100%;font-size:14px;padding:8px 10px">'+
|
||||
'<button class="btn btn-green" data-pid="'+p.id+'" onclick="resolvePurchase(this.dataset.pid,"completed")" style="width:100%;font-size:15px;padding:10px;font-weight:700">✓ Approve & Credit</button>'+
|
||||
'<button class="btn btn-red" data-pid="'+p.id+'" onclick="resolvePurchase(this.dataset.pid,"failed")" 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>' : '');
|
||||
: (canRefund
|
||||
? '<div style="display:flex;flex-direction:column;gap:8px;flex-shrink:0;min-width:160px">'+
|
||||
'<input class="fi-sm" type="text" id="rnote-'+p.id+'" placeholder="Refund reason (optional)" style="width:100%;font-size:14px;padding:8px 10px">'+
|
||||
'<button class="btn btn-red" data-pid="'+p.id+'" data-amt="'+amt+'" onclick="refundPurchase(this.dataset.pid,this.dataset.amt)" style="width:100%;font-size:14px;padding:9px">↩ Refund $'+amt+'</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" 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">'+
|
||||
@@ -1362,6 +1367,19 @@ async function resolvePurchase(id, status) {
|
||||
} else toast(d.error||'Error','err');
|
||||
}
|
||||
|
||||
async function refundPurchase(id, maxAmount) {
|
||||
if (!confirm('Refund $'+maxAmount+' to this customer via Square? This also claws back the tokens credited for this purchase. This cannot be undone.')) return;
|
||||
const reason = (document.getElementById('rnote-'+id)?.value||'').trim();
|
||||
const d = await apiFetch('refund_purchase','POST',{id,amount:maxAmount,reason});
|
||||
if (d.success) {
|
||||
let msg = '✓ Refunded! ' + d.tokens_clawed_back + ' tokens clawed back.';
|
||||
if (d.token_shortfall > 0) msg += ' (' + d.token_shortfall + ' tokens already spent, could not claw back)';
|
||||
toast(msg, 'ok');
|
||||
loadPurchases('all');
|
||||
loadStats();
|
||||
} else toast(d.error||'Error','err');
|
||||
}
|
||||
|
||||
// ─── CASHOUTS ──────────────────────────────────────────────
|
||||
const PAYOUT_ICONS_A = { venmo:'💙', cashapp:'💚', zelle:'💜', chime:'🟢', bank:'🏦', other:'💰' };
|
||||
const PAYOUT_LABELS_A = { venmo:'Venmo', cashapp:'Cash App', zelle:'Zelle', chime:'Chime', bank:'Bank Transfer', other:'Other' };
|
||||
|
||||
Reference in New Issue
Block a user