--- before/public_html/app/checkout-v40.php
+++ after/public_html/app/checkout-v40.php
@@ -9,9 +9,10 @@
}
function av40_items(PDO $pdo,bool $lock=false):array {
$result=[]; $total=0.;$weight=0;$cart=$_SESSION['cart']??[];
+ ksort($cart,SORT_STRING); // Stable lock order reduces concurrent checkout deadlocks.
foreach($cart as $key=>$quantity){
- if(!preg_match('/^[0-9]+:[0-9]+$/',(string)$key))throw new RuntimeException('Invalid cart entry.');
- [$id,$vid]=array_map('intval',explode(':',$key));$qty=(int)$quantity;
+ if(!preg_match('/^[1-9][0-9]*:(?:0|[1-9][0-9]*)$/D',(string)$key))throw new RuntimeException('Invalid cart entry.');
+ [$id,$vid]=array_map('intval',explode(':',$key));$validQty=filter_var($quantity,FILTER_VALIDATE_INT);$qty=$validQty===false?0:(int)$validQty;
if($qty<1||$qty>99)throw new RuntimeException('Quantity must be between 1 and 99.');
$s=$pdo->prepare("SELECT * FROM products WHERE id=? AND status='published' AND is_active=1".($lock?' FOR UPDATE':''));$s->execute([$id]);$p=$s->fetch();if(!$p)throw new RuntimeException('A product is unavailable.');
$v=null;if($p['type']!=='single'){$s=$pdo->prepare('SELECT * FROM variants WHERE id=? AND product_id=? AND is_active=1'.($lock?' FOR UPDATE':''));$s->execute([$vid,$id]);$v=$s->fetch();if(!$v)throw new RuntimeException('Please select a valid product variant.');}elseif($vid!==0)throw new RuntimeException('Invalid item option.');
@@ -28,7 +29,7 @@
$code=strtoupper(trim($code));if($code==='')return 0.;
$s=$pdo->prepare('SELECT * FROM av_coupons WHERE code=? AND active=1 AND (expires_at IS NULL OR expires_at>NOW())');$s->execute([$code]);$c=$s->fetch();
if(!$c||$subtotal<(float)$c['min_subtotal'])throw new RuntimeException('Coupon is invalid or requirements are not met.');
- return round(min($subtotal, $c['type']==='fixed'?(float)$c['value']:$subtotal*min(100,(float)$c['value'])/100),2);
+ return round(max(0,min($subtotal, $c['type']==='fixed'?(float)$c['value']:$subtotal*min(100,(float)$c['value'])/100)),2);
}
function av40_delivery(PDO $pdo,int $courierId,string $district,string $method,int $weight):array {
$district=trim($district);if($district==='')return ['known'=>false,'base'=>null,'extra'=>null,'eta'=>'','cod'=>false];if($method!=='home'&&$method!=='branch')throw new RuntimeException('Invalid delivery method.');
--- before/public_html/app/confirmation-v52.php
+++ after/public_html/app/confirmation-v52.php
@@ -12,7 +12,7 @@
$title=htmlspecialchars($headline,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');
$message=htmlspecialchars($detail,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');
$reference=$ref?'
Support reference: '.htmlspecialchars($ref,ENT_QUOTES,'UTF-8').'
':'';
- echo ''.$title.' | AVOLEEN
'.$title.'
'.$message.'
'.$reference.'Back to Store';
+ echo ''.$title.' | AVOLEEN
';exit;
}
function av53_read(PDO $pdo,string $sql,array $params,string $ref,string $stage,int $orderId,bool $optional=false):array {
@@ -32,7 +32,7 @@
function av53_order_status(array $o):string {
$status=strtolower((string)($o['status']??''));if(in_array($status,['cancelled','canceled','returned','refunded'],true))return ucfirst($status);
$f=strtolower((string)($o['fulfillment_status']??''));
- return match($f){'confirmed'=>'Confirmed','processing'=>'Processing','shipped'=>'Shipped','out_for_delivery'=>'Out for Delivery','delivered'=>'Delivered',default=>'Order Received'};
+ return match($f){'confirmed'=>'Confirmed','processing'=>'Processing','ready_to_ship'=>'Ready to Ship','on_hold'=>'On Hold','delivery_failed'=>'Delivery Failed','shipped'=>'Shipped','out_for_delivery'=>'Out for Delivery','delivered'=>'Delivered',default=>'Order Received'};
}
function av53_public_id(array $o,int $id):string {$v=trim((string)($o['public_order_id']??''));return $v!==''?$v:'avo-'.str_pad((string)$id,5,'0',STR_PAD_LEFT);}
function av53_date($time):string {try {if(!$time)return 'Unavailable';return (new DateTimeImmutable((string)$time,new DateTimeZone('Asia/Dhaka')))->setTimezone(new DateTimeZone('Asia/Dhaka'))->format('d M Y, h:i A').' (BDT)';}catch(Throwable $e){return 'Unavailable';}}
--- before/public_html/app/order-v41.php
+++ after/public_html/app/order-v41.php
@@ -0,0 +1,19 @@
+prepare('UPDATE av54_order_workflow SET workflow_status=? WHERE order_id=?')->execute([$next,$id]);
// Keep existing storefront tracker in sync with normal workflow updates.
if(av54_col($db,'orders','fulfillment_status'))$db->prepare('UPDATE orders SET fulfillment_status=? WHERE id=?')->execute([$next,$id]);
--- before/public_html/app/payment-v38.php
+++ after/public_html/app/payment-v38.php
@@ -77,10 +77,14 @@
if(strtoupper((string)($remote['currency']??''))!=='BDT')return 'unverified';
$pdo->beginTransaction();
try {
+ // Lock the order first, matching staff cancellation and retry lock order.
+ $lookup=$pdo->prepare('SELECT order_id FROM av_payment_attempts WHERE transaction_id=? UNION SELECT order_id FROM order_payments WHERE transaction_id=? LIMIT 1');$lookup->execute([$tranId,$tranId]);$oid=$lookup->fetchColumn();
+ if(!$oid){$pdo->rollBack();return 'unverified';}
+ $lock=$pdo->prepare('SELECT status,fulfillment_status FROM orders WHERE id=? FOR UPDATE');$lock->execute([$oid]);$order=$lock->fetch();
$s=$pdo->prepare('SELECT * FROM order_payments WHERE transaction_id=? FOR UPDATE');$s->execute([$tranId]);$pay=$s->fetch();$older=false;
if(!$pay){$t=$pdo->prepare('SELECT order_id FROM av_payment_attempts WHERE transaction_id=? FOR UPDATE');$t->execute([$tranId]);$oldOrder=$t->fetchColumn();if($oldOrder){$s=$pdo->prepare('SELECT * FROM order_payments WHERE order_id=? FOR UPDATE');$s->execute([$oldOrder]);$pay=$s->fetch();$older=true;}}
if(!$pay || $pay['method']!=='sslcommerz'){$pdo->rollBack();return 'unverified';}
- if(abs((float)$remote['amount']-(float)$pay['amount'])>0.009){$pdo->rollBack();return 'unverified';}
+ if(!isset($remote['amount']) || !is_scalar($remote['amount']) || !preg_match('/^[0-9]+(?:\.[0-9]{1,2})?$/D',(string)$remote['amount']) || abs((float)$remote['amount']-(float)$pay['amount'])>0.009){$pdo->rollBack();return 'unverified';}
if($older){
// A previously failed transaction was later charged after a retry began.
// Keep all details for staff review; never silently mark the new transaction paid.
@@ -90,11 +94,15 @@
}
if($pay['status']==='paid'){$pdo->commit();return 'paid';}
if(in_array($pay['status'],['cancelled','refunded'],true)){$pdo->rollBack();return 'manual_review';}
- $risk=((int)($remote['risk_level']??0)===1)?'review':'paid';
+ // A late payment must never reactivate a cancelled order or released stock.
+ $reservation=$pdo->prepare('SELECT state FROM av_order_reservations WHERE order_id=? FOR UPDATE');$reservation->execute([$pay['order_id']]);$reservationState=$reservation->fetchColumn();
+ $risk=(($remote['risk_level']??null) === '0' || ($remote['risk_level']??null) === 0)?'paid':'review';
+ if(!$order || in_array($order['status'],['cancelled','refunded'],true) || in_array($order['fulfillment_status'],['cancelled','returned'],true) || !in_array($reservationState,['reserved','consumed'],true))$risk='review';
$s=$pdo->prepare('UPDATE order_payments SET status=?, gateway_val_id=?, gateway_bank_ref=?, gateway_response=?, updated_at=NOW() WHERE id=?');
$pdo->prepare('UPDATE av_payment_attempts SET status=? WHERE transaction_id=?')->execute([$risk,$tranId]);
$s->execute([$risk,substr($valId,0,80),substr((string)($remote['bank_tran_id']??''),0,100),json_encode(['status'=>$remote['status'],'risk_level'=>$remote['risk_level']??null],JSON_UNESCAPED_UNICODE),$pay['id']]);
- if($risk==='paid'){$s=$pdo->prepare("UPDATE orders SET status='processing' WHERE id=? AND status='pending'");$s->execute([$pay['order_id']]);$pdo->prepare("UPDATE av_order_reservations SET state='consumed' WHERE order_id=? AND state='reserved'")->execute([$pay['order_id']]);}
+ if($risk==='paid'){// Payment verification does not confirm the staff fulfillment workflow.
+ $pdo->prepare("UPDATE av_order_reservations SET state='consumed' WHERE order_id=? AND state='reserved'")->execute([$pay['order_id']]);}
$pdo->commit();return $risk;
}catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
}
--- before/public_html/checkout.php
+++ after/public_html/checkout.php
@@ -4,7 +4,7 @@
if(empty($_SESSION['cart'])){ $previous=av51_recover($pdo); if($previous)av51_redirect($previous[0],$previous[1]); go('cart.php'); }
$u=me();$allowCod=av40_payment_enabled($pdo,'cod');$allowOnline=av40_payment_enabled($pdo,'sslcommerz')&&av_online_enabled();$values=$_SESSION['av40_checkout']??[];$errors=[];$message='';$failureRef='';$couriers=$pdo->query("SELECT * FROM av_couriers WHERE active=1 ORDER BY display_order ASC, name ASC")->fetchAll();
if(!isset($_SESSION['av40_checkout_key']))$_SESSION['av40_checkout_key']=bin2hex(random_bytes(32));
-try {[$items,$subtotal,$weight]=av40_items($pdo);}catch(Throwable $e){flash($e->getMessage());go('cart.php');}
+try {[$items,$subtotal,$weight]=av40_items($pdo);}catch(Throwable $e){error_log('[AVOLEEN Phase 1] Cart validation: '.get_class($e));flash(av50_checkout_error($e)??'Unable to load your cart. Please try again or contact support.');go('cart.php');}
if($_SERVER['REQUEST_METHOD']==='POST'){
check_csrf();
foreach(['recipient','phone','district','upazila','address','courier_other','delivery_option','coupon'] as $k)$values[$k]=trim((string)($_POST[$k]??''));
@@ -30,9 +30,10 @@
$attempt=(string)($_POST['attempt_key']??'');
if(!preg_match('/^[a-f0-9]{64}$/',$attempt) || ($attempt!==($_SESSION['av40_checkout_key']??'') && empty($_SESSION['av42_completed_keys'][$attempt])))throw new RuntimeException('This checkout session expired. Refresh the page to continue.');
$s=$pdo->prepare('SELECT order_id FROM av_checkout_attempts WHERE attempt_key=? FOR UPDATE');$s->execute([$attempt]);$existing=$s->fetchColumn();
- if($existing){$pdo->commit();$orderId=(int)$existing;$token=$_SESSION['av40_order_tokens'][$orderId]??'';if(!$token && !$u)throw new RuntimeException('Order already submitted. Use your private tracking link.');av51_store_recent($orderId,$token);av51_redirect($orderId,$token);}
+ if($existing){$pdo->commit();$orderId=(int)$existing;$token=$_SESSION['av40_order_tokens'][$orderId]??'';if(!av40_order_access($pdo,$orderId,$token))throw new RuntimeException('Order already submitted. Use your private tracking link.');av51_store_recent($orderId,$token);av51_redirect($orderId,$token);}
$s=$pdo->prepare('INSERT INTO av_checkout_attempts(attempt_key) VALUES(?)');$s->execute([$attempt]);
$stage='cart-lock';[$items,$subtotal,$weight]=av40_items($pdo,true);
+ if(!$items)throw new RuntimeException('Your cart is empty.');
$discount=av40_coupon($pdo,$values['coupon'],$subtotal);
$d=['known'=>true,'base'=>av42_delivery($values['delivery_option']),'extra'=>0.0,'eta'=>'','cod'=>true];
$pending=false;
@@ -49,7 +50,7 @@
$s->execute([$u['id']??null,$storedTotal,$values['recipient'],$phone,$fullAddress,$trackingHash,$u['email']??null,$values['district'],$values['upazila'],'',$values['courier_id']>0?$values['courier_id']:null,$values['courier_id']===-1?$values['courier_other']:null,'home',null,null,$pending?'pending_confirmation':'charge_known_assignment_pending',$subtotal,$discount,$d['base'],$d['extra'],$weight,$values['coupon']?:null,$attempt,$values['delivery_option']]);
$orderId=(int)$pdo->lastInsertId();
// V41: deterministic public reference from the database-assigned primary key.
- $pdo->prepare("UPDATE orders SET public_order_id=CONCAT('avo-',LPAD(id,5,'0')) WHERE id=?")->execute([$orderId]);
+ $pdo->prepare("UPDATE orders SET public_order_id=CONCAT('avo-',LPAD(id,GREATEST(5,CHAR_LENGTH(id)),'0')) WHERE id=?")->execute([$orderId]);
$pdo->prepare("INSERT IGNORE INTO av41_order_events(order_id,event_type,status,source,public_note,event_key) VALUES(?, 'order_status', 'placed', 'system', 'Order received', ?)")->execute([$orderId,'placed:'.$orderId]);
$stage='order-items-and-stock';foreach($items as $r){$p=$r['product'];$v=$r['variant'];$s=$pdo->prepare('INSERT INTO order_items(order_id,product_id,variant_id,product_name,variant_label,unit_price,quantity,sku_snapshot,options_snapshot,weight_g_snapshot,image_snapshot) VALUES(?,?,?,?,?,?,?,?,?,?,?)');$s->execute([$orderId,$p['id'],$v['id']??null,$p['name'],$r['label'],$r['price'],$r['qty'],$r['sku'],$r['label'],$r['weight'],($v['image']??$p['image']??null)]);
$s=$pdo->prepare($v?'UPDATE variants SET stock=stock-? WHERE id=? AND stock>=?':'UPDATE products SET stock=stock-? WHERE id=? AND stock>=?');$s->execute([$r['qty'],$v['id']??$p['id'],$r['qty']]);if($s->rowCount()!==1)throw new RuntimeException('Stock changed. Please review your cart.');
--- before/public_html/confirm-delivery.php
+++ after/public_html/confirm-delivery.php
@@ -1,6 +1,7 @@
beginTransaction();$s=$pdo->prepare('SELECT * FROM orders WHERE id=? FOR UPDATE');$s->execute([$id]);$o=$s->fetch();if(!$o || $o['delivery_status']!=='awaiting_customer_acceptance')throw new RuntimeException('Quote is not awaiting acceptance.');
@@ -9,5 +10,5 @@
$pdo->commit();go('order-complete.php?id='.$id.'&token='.$token);
}catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();$error=$e->getMessage();}}
$s=$pdo->prepare('SELECT * FROM orders WHERE id=?');$s->execute([$id]);$o=$s->fetch();header_page('Confirm delivery quote');echo '
';
--- before/public_html/payment/retry.php
+++ after/public_html/payment/retry.php
@@ -1,6 +1,7 @@
beginTransaction();
+ $lock=$pdo->prepare('SELECT status,fulfillment_status FROM orders WHERE id=? FOR UPDATE');$lock->execute([$id]);$lockedOrder=$lock->fetch();
+ $lock=$pdo->prepare('SELECT state FROM av_order_reservations WHERE order_id=? FOR UPDATE');$lock->execute([$id]);$lockedReservation=$lock->fetchColumn();
+ if(!$lockedOrder || $lockedOrder['status']==='cancelled' || in_array($lockedOrder['fulfillment_status'],['cancelled','returned','delivered'],true) || $lockedReservation!=='reserved')throw new RuntimeException('This order is not eligible for payment retry.');
$s=$pdo->prepare('SELECT p.*,o.total,o.delivery_status FROM order_payments p JOIN orders o ON o.id=p.order_id WHERE p.order_id=? FOR UPDATE');$s->execute([$id]);$p=$s->fetch();
if(!$p||$p['status']==='paid'||$p['transaction_id']!==$order['transaction_id']||$p['method']!=='sslcommerz'||$p['delivery_status']!=='charge_known_assignment_pending'&&$p['delivery_status']!=='courier_assigned')throw new RuntimeException('Payment state changed. Reload order.');
$pdo->prepare("UPDATE av_payment_attempts SET status='failed' WHERE transaction_id=?")->execute([$order['transaction_id']]);
$s=$pdo->prepare("UPDATE order_payments SET transaction_id=?,status='initiating',amount=? WHERE order_id=?");$s->execute([$newTran,$p['total'],$id]);
$pdo->prepare("INSERT INTO av_payment_attempts(order_id,transaction_id,status) VALUES(?,?,'initiating')")->execute([$id,$newTran]);$pdo->commit();
- try{$gateway=av_create_gateway_session(['order_id'=>$id,'transaction_id'=>$newTran,'amount'=>(float)$order['total']],['name'=>$order['recipient'],'email'=>$order['contact_email'],'phone'=>$order['phone'],'address'=>$order['address']]);
+ try{$gateway=av_create_gateway_session(['order_id'=>$id,'transaction_id'=>$newTran,'amount'=>(float)$p['total']],['name'=>$order['recipient'],'email'=>filter_var($order['contact_email'],FILTER_VALIDATE_EMAIL)?$order['contact_email']:'no-reply@avoleen.com','phone'=>$order['phone'],'address'=>$order['address']]);
$pdo->prepare("UPDATE order_payments SET status='pending' WHERE order_id=? AND status='initiating'")->execute([$id]);header('Location: '.$gateway,true,303);exit;
}catch(Throwable $e){error_log('[AVOLEEN V40] Retry initiation failed for order '.$id.': '.$e->getMessage());$pdo->prepare("UPDATE order_payments SET status='review' WHERE order_id=? AND status='initiating'")->execute([$id]);throw new RuntimeException('Payment could not be started; contact support with your order number.');}
- }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();$error=$e->getMessage();}}
-header_page('Retry payment');echo '
Retry payment · Order #'.$id.'
Before retrying, AVOLEEN will verify that the previous gateway transaction was definitely failed or cancelled. Pending or uncertain payments cannot be retried automatically.
Current payment status: '.h($order['payment_status']).'
';footer_page();
+ }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();error_log('[AVOLEEN Phase 1] Retry: '.get_class($e));$error=$e instanceof PDOException?'Payment is temporarily unavailable. Contact support before retrying.':$e->getMessage();}}
+header_page('Retry payment');echo '
Retry payment · Order #'.$id.'
Before retrying, AVOLEEN will verify that the previous gateway transaction was definitely failed or cancelled. Pending or uncertain payments cannot be retried automatically.
Current payment status: '.h($order['payment_status']).'