false, 'ERROR' => 'missing_orderid', 'MESSAGE' => 'OrderID is required.']); } if ($CashAmount <= 0) { apiAbort(['OK' => false, 'ERROR' => 'missing_amount', 'MESSAGE' => 'CashAmount is required.']); } try { $qOrder = queryOne(" SELECT o.ID, o.StatusID, o.UserID, o.BusinessID, o.ServicePointID, o.PaymentID, o.DeliveryFee, o.OrderTypeID, b.PayfritFee, b.TaxRate FROM Orders o INNER JOIN Businesses b ON b.ID = o.BusinessID WHERE o.ID = ? LIMIT 1 ", [$OrderID]); if (!$qOrder) { apiAbort(['OK' => false, 'ERROR' => 'not_found', 'MESSAGE' => 'Order not found.']); } if ((int) $qOrder['StatusID'] !== 0) { apiAbort(['OK' => false, 'ERROR' => 'bad_state', 'MESSAGE' => 'Order is not in cart state.']); } // Calculate platform fee $feeRate = (is_numeric($qOrder['PayfritFee']) && (float) $qOrder['PayfritFee'] > 0) ? (float) $qOrder['PayfritFee'] : 0; $qSubtotal = queryOne( "SELECT COALESCE(SUM(Price * Quantity), 0) AS Subtotal FROM OrderLineItems WHERE OrderID = ? AND IsDeleted = 0", [$OrderID] ); $subtotal = (float) ($qSubtotal['Subtotal'] ?? 0); $platformFee = $subtotal * $feeRate; // Calculate full order total $taxRate = (is_numeric($qOrder['TaxRate']) && (float) $qOrder['TaxRate'] > 0) ? (float) $qOrder['TaxRate'] : 0; $taxAmount = $subtotal * $taxRate; $deliveryFee = ((int) $qOrder['OrderTypeID'] === 3) ? (float) $qOrder['DeliveryFee'] : 0; $orderTotal = round(($subtotal + $taxAmount + $platformFee + $Tip + $deliveryFee) * 100) / 100; // Auto-apply user balance $balanceApplied = 0; $cashNeeded = $orderTotal; $userID = (int) $qOrder['UserID']; if ($userID > 0) { $qBalance = queryOne("SELECT Balance FROM Users WHERE ID = ?", [$userID]); $userBalance = (float) ($qBalance['Balance'] ?? 0); if ($userBalance > 0) { $balanceToApply = round(min($userBalance, $orderTotal) * 100) / 100; // Atomic deduct $stmt = queryTimed( "UPDATE Users SET Balance = Balance - ? WHERE ID = ? AND Balance >= ?", [$balanceToApply, $userID, $balanceToApply] ); // queryTimed returns PDOStatement for UPDATE; check rowCount if ($stmt instanceof \PDOStatement && $stmt->rowCount() > 0) { $balanceApplied = $balanceToApply; $cashNeeded = max(0, $orderTotal - $balanceApplied); } } } // Create Payment record $CashAmountCents = (int) round($cashNeeded * 100); queryTimed( "INSERT INTO Payments (PaymentPaidInCash, PaymentFromPayfritBalance, PaymentFromCreditCard, PaymentSentByUserID, PaymentReceivedByUserID, PaymentOrderID, PaymentPayfritsCut, PaymentAddedOn) VALUES (?, ?, 0, ?, 0, ?, ?, NOW())", [$cashNeeded, $balanceApplied, $userID, $OrderID, $platformFee] ); $PaymentID = lastInsertId(); // Update order $fullyPaidByBalance = ($cashNeeded <= 0); $paymentStatus = $fullyPaidByBalance ? 'paid' : 'pending'; queryTimed(" UPDATE Orders SET StatusID = 1, PaymentID = ?, PaymentStatus = ?, PlatformFee = ?, TipAmount = ?, BalanceApplied = ?, SubmittedOn = NOW(), LastEditedOn = NOW(), PaymentCompletedOn = CASE WHEN ? = 1 THEN NOW() ELSE PaymentCompletedOn END WHERE ID = ? ", [$PaymentID, $paymentStatus, $platformFee, $Tip, $balanceApplied, $fullyPaidByBalance ? 1 : 0, $OrderID]); // Create kitchen task for KDS display (same as webhook.php does for card payments) $spName = ''; $spID = (int) ($qOrder['ServicePointID'] ?? 0); if ($spID > 0) { $qSP = queryOne("SELECT Name FROM ServicePoints WHERE ID = ?", [$spID]); $spName = !empty(trim($qSP['Name'] ?? '')) ? $qSP['Name'] : 'Table'; } else { $spName = 'Table'; } $taskTitle = "Prepare Order #{$OrderID} for {$spName}"; queryTimed(" INSERT INTO Tasks (BusinessID, OrderID, ServicePointID, Title, CreatedOn, ClaimedByUserID) VALUES (?, ?, ?, ?, NOW(), 0) ", [$qOrder['BusinessID'], $OrderID, $spID > 0 ? $spID : null, $taskTitle]); $response = [ 'OK' => true, 'OrderID' => $OrderID, 'PaymentID' => (int) $PaymentID, 'CashAmountCents' => $CashAmountCents, 'MESSAGE' => 'Order submitted with cash payment.', ]; if ($balanceApplied > 0) { $response['BalanceApplied'] = round($balanceApplied * 100) / 100; $response['BalanceAppliedCents'] = (int) round($balanceApplied * 100); $response['FullyPaidByBalance'] = ($cashNeeded <= 0); } jsonResponse($response); } catch (Exception $e) { jsonResponse(['OK' => false, 'ERROR' => 'server_error', 'MESSAGE' => 'Failed to submit cash order', 'DETAIL' => $e->getMessage()]); }