<?php
declare(strict_types=1);
if (!defined('STORE_ROOT')) { http_response_code(403); exit; }
function callbackHandler(string $action, array $in): void { if ($action !== 'payment') App::err('Unknown callback action'); $rawBody = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_BRIDGE_SIG'] ?? ''; $timestamp = $_SERVER['HTTP_X_BRIDGE_TS'] ?? ''; $nonce = $_SERVER['HTTP_X_BRIDGE_NONCE'] ?? ''; if (!PaymentBridge::verifyCallback($rawBody, $signature, $timestamp, $nonce)) { error_log('[CALLBACK] Signature failed from ' . ($_SERVER['REMOTE_ADDR'] ?? 'unknown')); App::err('Signature invalid', 403); } $data = json_decode($rawBody, true); if (!$data || !isset($data['order_ref'], $data['event'])) App::err('Missing fields', 400); $orderRef = $data['order_ref']; $event = $data['event']; $order = DB::one("SELECT id, status_code, subtotal FROM orders WHERE order_ref = :ref", [':ref' => $orderRef]); if (!$order) App::err('Order not found', 404); $orderId = (int) $order['id']; $status = (int) $order['status_code']; switch ($event) { case 'payment.succeeded': if ($status !== 2001) App::err('Order not pending'); if (isset($data['amount_received'], $data['currency'])) { $expected = (float) $order['subtotal']; $received = (float) $data['amount_received']; if (strtoupper($data['currency'] ?? '') !== 'CAD' || abs($received - $expected) > 0.01) { error_log("[CALLBACK] Amount mismatch: expected={$expected}, got={$received}"); App::err('Amount/currency mismatch', 400); } } DB::run("UPDATE orders SET status_code = 2002, updated_at = NOW() WHERE id = :id", [':id' => $orderId]); $stripeRef = $data['stripe_ref'] ?? $data['stripe_payment_intent'] ?? null; if ($stripeRef) { DB::ins("INSERT INTO payment_logs (order_id, method, transaction_id, amount_cad, status, created_at) VALUES (:oid, 'stripe', :tid, :amt, 'confirmed', NOW())", [':oid' => $orderId, ':tid' => $stripeRef, ':amt' => (float) $order['subtotal']]); } DB::ins("INSERT INTO events (category, action, target_type, target_id, meta, ip_address, created_at) VALUES (4, 'payment.confirmed', 'order', :oid, :meta, :ip, NOW())", [':oid' => $orderId, ':meta' => json_encode(['method' => $data['payment_method'] ?? '', 'ref' => $orderRef]), ':ip' => $_SERVER['REMOTE_ADDR'] ?? '172.28.41.2']); App::ok(['status' => 'paid', 'order_id' => $orderId]); case 'payment.failed': DB::run("UPDATE orders SET status_code = 2007, notes = :n, updated_at = NOW() WHERE id = :id", [':n' => $data['failure_reason'] ?? 'Payment failed', ':id' => $orderId]); App::ok(['status' => 'cancelled']); case 'crypto.confirmed': if ($status !== 2001) App::err('Not pending'); DB::run("UPDATE orders SET status_code = 2002, updated_at = NOW() WHERE id = :id", [':id' => $orderId]); if (isset($data['tx_hash'])) { DB::ins("INSERT INTO payment_logs (order_id, method, transaction_id, amount_xmr, status, created_at) VALUES (:oid, 'crypto', :tx, :amt, 'confirmed', NOW())", [':oid' => $orderId, ':tx' => $data['tx_hash'], ':amt' => $data['amount'] ?? '0']); } App::ok(['status' => 'paid']); case 'etransfer.confirmed': if ($status !== 2001) App::err('Not pending'); DB::run("UPDATE orders SET status_code = 2002, updated_at = NOW() WHERE id = :id", [':id' => $orderId]); App::ok(['status' => 'paid']); default: App::err('Unknown event', 400); } }
