65536) reply(413, 'Too large'); $ts = $_SERVER['HTTP_X_DIDBAN_TIMESTAMP'] ?? ''; $sig = $_SERVER['HTTP_X_DIDBAN_SIGNATURE'] ?? ''; if (!preg_match('/^\d{1,12}$/D', $ts) || abs(time()-(int)$ts)>300) reply(401, 'Expired signature'); if (!hash_equals('v1='.hash_hmac('sha256', $ts.'.'.$raw, $secret), $sig)) reply(401, 'Invalid signature'); try { $e=json_decode($raw,true,32,JSON_THROW_ON_ERROR); } catch (Throwable $error) { reply(400,'Invalid JSON'); } $d=$e['data']??[];$event=$e['event_id']??null; if (is_string($event)&&strlen($event)>0&&strlen($event)<=100&&$event===($_SERVER['HTTP_X_DIDBAN_EVENT_ID']??'')&&($e['type']??'')==='webhook.test'&&($d['mode']??'')==='test'&&($d['paid']??null)===false) reply(200,'Test acknowledged'); if (!is_string($event)||strlen($event)<1||strlen($event)>100||$event!==($_SERVER['HTTP_X_DIDBAN_EVENT_ID']??'')||($e['type']??'')!=='invoice.paid'||($d['mode']??'')!=='live'||($d['status']??'')!=='PAID'||($d['paid']??false)!==true||!is_string($d['amount_rial']??null)||!preg_match('/^[1-9]\d{0,23}$/D',$d['amount_rial'])) reply(400,'Invalid event'); try { $dbPath=getenv('BLUAUTO_WEBHOOK_DB');if(!$dbPath) reply(503,'Set database path outside web root'); $db=new PDO('sqlite:'.$dbPath);$db->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION); $db->exec('PRAGMA busy_timeout=10000; PRAGMA journal_mode=WAL; CREATE TABLE IF NOT EXISTS orders(order_id TEXT PRIMARY KEY,invoice_id TEXT UNIQUE NOT NULL,amount_rial TEXT NOT NULL,paid INTEGER NOT NULL DEFAULT 0); CREATE TABLE IF NOT EXISTS webhook_receipts(event_id TEXT PRIMARY KEY,invoice_id TEXT NOT NULL); CREATE TABLE IF NOT EXISTS fulfillment_jobs(order_id TEXT PRIMARY KEY,status TEXT NOT NULL DEFAULT 0);'); $db->exec('BEGIN IMMEDIATE'); $q=$db->prepare('SELECT invoice_id,amount_rial FROM orders WHERE order_id=?');$q->execute([$d['order_id']??'']);$row=$q->fetch(PDO::FETCH_ASSOC); if (!$row||$row['invoice_id']!==($d['id']??'')||$row['amount_rial']!==$d['amount_rial']) { $db->exec('ROLLBACK');reply(409,'Order mismatch'); } $q=$db->prepare('SELECT invoice_id FROM webhook_receipts WHERE event_id=?');$q->execute([$event]);$old=$q->fetchColumn(); if ($old!==false&&$old!==$d['id']) { $db->exec('ROLLBACK');reply(409,'Event conflict'); } $db->prepare('INSERT OR IGNORE INTO webhook_receipts VALUES(?,?)')->execute([$event,$d['id']]); $db->prepare('UPDATE orders SET paid=1 WHERE order_id=? AND paid=0')->execute([$d['order_id']]); $db->prepare('INSERT OR IGNORE INTO fulfillment_jobs(order_id) VALUES(?)')->execute([$d['order_id']]); // Add an idempotent fulfillment job here, in this DB transaction. $db->exec('COMMIT');reply(200,'OK'); } catch (Throwable $error) { if(isset($db))try{$db->exec('ROLLBACK');}catch(Throwable $ignored){} reply(503,'Retry later'); }