false,'error'=>'Not authenticated']); exit; } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; } $data = json_decode(file_get_contents('php://input'), true); $sourceId = trim($data['source_id'] ?? ''); $tokens = (int)($data['tokens'] ?? 0); $priceCents = (int)($data['price_cents'] ?? 0); $method = trim($data['method'] ?? 'card'); $platformId = trim($data['platform_id'] ?? ''); $gameAlias = trim($data['game_alias'] ?? ''); $playerName = trim($data['player_name'] ?? ''); $isCustom = (bool)($data['is_custom'] ?? false); $billing = $data['billing'] ?? []; $userId = $_SESSION['user_id']; // ─── Validate ───────────────────────────────────────────── if ($tokens < 1 || $priceCents < 100) { echo json_encode(['success'=>false,'error'=>'Minimum purchase is $1 (1 token).']); exit; } // Validate preset packages (custom bypasses preset check) if (!$isCustom) { $packages = json_decode(TOKEN_PACKAGES, true); $validPkg = false; foreach ($packages as $pkg) { if ($pkg['tokens'] === $tokens && ($pkg['price'] * 100) === $priceCents) { $validPkg = true; break; } } if (!$validPkg) { echo json_encode(['success'=>false,'error'=>'Invalid token package.']); exit; } } else { // Custom: tokens must equal dollars (1:1 ratio), cap at $500 if ($tokens !== ($priceCents / 100) || $tokens > 500) { echo json_encode(['success'=>false,'error'=>'Invalid custom amount.']); exit; } } // Sanitize billing fields $billingFirst = substr(trim($billing['first_name'] ?? ''), 0, 80); $billingLast = substr(trim($billing['last_name'] ?? ''), 0, 80); $billingAddress = substr(trim($billing['address'] ?? ''), 0, 200); $billingCity = substr(trim($billing['city'] ?? ''), 0, 80); $billingState = strtoupper(substr(trim($billing['state'] ?? ''), 0, 2)); $billingZip = substr(trim($billing['zip'] ?? ''), 0, 10); $billingEmail = substr(strtolower(trim($billing['email'] ?? '')), 0, 150); $cardholderName = trim("$billingFirst $billingLast"); $isManual = in_array($method, ['venmo','chime','cashapp','zelle']); // ─── Manual payment ──────────────────────────────────────── if ($isManual) { $stmt = db()->prepare(" INSERT INTO token_purchases (user_id, tokens, amount_cents, payment_method, platform_id, game_alias, player_name, billing_name, billing_email, is_custom, status) VALUES (?,?,?,?,?,?,?,?,?,?,'pending') "); $stmt->execute([ $userId, $tokens, $priceCents, $method, $platformId, $gameAlias, $playerName, $cardholderName, $billingEmail, $isCustom ? 1 : 0 ]); $pid = db()->lastInsertId(); logActivity('manual_payment_pending', $userId, null, 'purchase', 0, "Manual payment pending: {$paymentMethod} \${$amountDollars}"); echo json_encode(['success'=>true,'manual'=>true,'purchase_id'=>$pid, 'message'=>"Request #{$pid} submitted! Tokens credited after payment verification."]); exit; } // ─── Card payment via Square ─────────────────────────────── if (empty($sourceId)) { echo json_encode(['success'=>false,'error'=>'Card payment token required.']); exit; } $square = new SquarePayment(); $note = "TomGames {$tokens}tok | {$platformId} | {$gameAlias} | user#{$userId}" . ($isCustom ? ' [CUSTOM]' : ''); // Build buyer info for Square $buyerInfo = []; if ($cardholderName) $buyerInfo['buyer_email_address'] = $billingEmail ?: null; $billingAddr = []; if ($billingAddress) $billingAddr['address_line_1'] = $billingAddress; if ($billingCity) $billingAddr['locality'] = $billingCity; if ($billingState) $billingAddr['administrative_district_level_1'] = $billingState; if ($billingZip) $billingAddr['postal_code'] = $billingZip; $billingAddr['country'] = 'US'; $result = $square->charge($sourceId, $priceCents, $note, $cardholderName, $billingAddr, $billingEmail); if (!$result['success']) { // Log failed attempt db()->prepare("INSERT INTO token_purchases (user_id,tokens,amount_cents,payment_method,platform_id,game_alias,player_name,billing_name,billing_email,is_custom,status,failure_reason) VALUES (?,?,?,'card',?,?,?,?,?,?,'failed',?)") ->execute([$userId,$tokens,$priceCents,$platformId,$gameAlias,$playerName,$cardholderName,$billingEmail,$isCustom?1:0,$result['error']]); echo json_encode(['success'=>false,'error'=>$result['error']]); exit; } // ─── Credit tokens ───────────────────────────────────────── db()->beginTransaction(); try { db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$tokens,$userId]); $cardBrand = $result['card_brand'] ?? null; $cardLast4 = $result['last_4'] ?? null; $receiptUrl= $result['receipt_url'] ?? null; db()->prepare(" INSERT INTO token_purchases (user_id, tokens, amount_cents, payment_method, square_payment_id, platform_id, game_alias, player_name, billing_name, billing_address, billing_city, billing_state, billing_zip, billing_email, is_custom, card_brand, card_last4, receipt_url, status) VALUES (?,?,?,'card',?,?,?,?,?,?,?,?,?,?,?,?,?,?,'completed') ")->execute([ $userId, $tokens, $priceCents, $result['payment_id'], $platformId, $gameAlias, $playerName, $cardholderName, $billingAddress, $billingCity, $billingState, $billingZip, $billingEmail, $isCustom ? 1 : 0, $cardBrand, $cardLast4, $receiptUrl ]); db()->commit(); } catch (Exception $e) { db()->rollBack(); echo json_encode(['success'=>false,'error'=>'Token credit failed. Payment ID: '.$result['payment_id']]); exit; } // ─── Update saved billing (separate try — must NOT roll back token credit) ── try { db()->prepare(" INSERT INTO saved_billing (user_id,first_name,last_name,email,address,city,state,zip,card_brand,card_last4) VALUES (?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE card_brand=VALUES(card_brand), card_last4=VALUES(card_last4), first_name=COALESCE(NULLIF(VALUES(first_name),''),first_name), last_name=COALESCE(NULLIF(VALUES(last_name),''),last_name), email=COALESCE(NULLIF(VALUES(email),''),email), address=COALESCE(NULLIF(VALUES(address),''),address), city=COALESCE(NULLIF(VALUES(city),''),city), state=COALESCE(NULLIF(VALUES(state),''),state), zip=COALESCE(NULLIF(VALUES(zip),''),zip) ")->execute([ $userId, $billingFirst, $billingLast, $billingEmail, $billingAddress, $billingCity, $billingState, $billingZip, $cardBrand, $cardLast4 ]); } catch (Exception $e) { /* non-critical — tokens already credited */ } $bal = db()->prepare("SELECT tokens FROM users WHERE id=?"); $bal->execute([$userId]); $newBal = (float)$bal->fetchColumn(); logActivity('token_purchase', $userId, null, 'purchase', (int)db()->lastInsertId(), "Bought {$tokens} tokens via {$paymentMethod} for \${$amountDollars}"); echo json_encode([ 'success' => true, 'manual' => false, 'tokens_added' => (int)$tokens, 'new_balance' => $newBal, 'payment_id' => $result['payment_id'], 'card_brand' => $cardBrand, 'card_last4' => $cardLast4, 'receipt_url' => $receiptUrl, ]);