false, 'ERROR' => 'missing_TabID']); if ($userID === 0) apiAbort(['OK' => false, 'ERROR' => 'missing_UserID']); $qTab = queryOne(" SELECT t.*, b.PayfritFee, b.StripeAccountID, b.StripeOnboardingComplete FROM Tabs t JOIN Businesses b ON b.ID = t.BusinessID WHERE t.ID = ? LIMIT 1 ", [$tabID]); if (!$qTab) apiAbort(['OK' => false, 'ERROR' => 'tab_not_found']); if ((int) $qTab['StatusID'] !== 1) apiAbort(['OK' => false, 'ERROR' => 'tab_not_open', 'MESSAGE' => "Tab is not open (status: {$qTab['StatusID']})."]); // Verify: must be tab owner or business employee $isTabOwner = (int) $qTab['OwnerUserID'] === $userID; if (!$isTabOwner) { $qEmp = queryOne("SELECT ID FROM Employees WHERE BusinessID = ? AND UserID = ? AND IsActive = 1 LIMIT 1", [$qTab['BusinessID'], $userID]); if (!$qEmp) apiAbort(['OK' => false, 'ERROR' => 'not_authorized', 'MESSAGE' => 'Only the tab owner or a business employee can close the tab.']); } // Reject pending orders queryTimed("UPDATE TabOrders SET ApprovalStatus = 'rejected' WHERE TabID = ? AND ApprovalStatus = 'pending'", [$tabID]); // Calculate aggregate totals $qTotals = queryOne(" SELECT COALESCE(SUM(SubtotalCents), 0) AS TotalSubtotalCents, COALESCE(SUM(TaxCents), 0) AS TotalTaxCents FROM TabOrders WHERE TabID = ? AND ApprovalStatus = 'approved' ", [$tabID]); $totalSubtotalCents = (int) $qTotals['TotalSubtotalCents']; $totalTaxCents = (int) $qTotals['TotalTaxCents']; // If no orders, just cancel if ($totalSubtotalCents === 0) { stripeRequest('POST', "https://api.stripe.com/v1/payment_intents/{$qTab['StripePaymentIntentID']}/cancel"); queryTimed("UPDATE Tabs SET StatusID = 4, ClosedOn = NOW(), PaymentStatus = 'cancelled' WHERE ID = ?", [$tabID]); jsonResponse(['OK' => true, 'MESSAGE' => 'Tab cancelled (no orders).', 'FINAL_CAPTURE_CENTS' => 0]); } // Calculate tip $tipCents = 0; $tipPct = 0; if ($tipPercent >= 0) { $tipPct = $tipPercent; $tipCents = round($totalSubtotalCents * $tipPercent); } elseif ($tipAmount >= 0) { $tipCents = round($tipAmount * 100); if ($totalSubtotalCents > 0) $tipPct = $tipCents / $totalSubtotalCents; } // Fee calculation $payfritFeeRate = (float) ($qTab['PayfritFee'] ?? 0); if ($payfritFeeRate <= 0) apiAbort(['OK' => false, 'ERROR' => 'no_fee_configured', 'MESSAGE' => 'Business PayfritFee not set.']); $payfritFeeCents = round($totalSubtotalCents * $payfritFeeRate); $totalBeforeCardFeeCents = $totalSubtotalCents + $totalTaxCents + $tipCents + $payfritFeeCents; $cardFeeFixedCents = 30; $cardFeePercent = 0.029; $totalWithCardFeeCents = (int) ceil(($totalBeforeCardFeeCents + $cardFeeFixedCents) / (1 - $cardFeePercent)); $cardFeeCents = $totalWithCardFeeCents - $totalBeforeCardFeeCents; $finalCaptureCents = $totalWithCardFeeCents; $applicationFeeCents = $payfritFeeCents * 2; // Ensure capture doesn't exceed authorization if ($finalCaptureCents > (int) $qTab['AuthAmountCents']) { $finalCaptureCents = (int) $qTab['AuthAmountCents']; if ($totalWithCardFeeCents > 0) { $applicationFeeCents = round($applicationFeeCents * ($finalCaptureCents / $totalWithCardFeeCents)); } } // Mark tab as closing queryTimed("UPDATE Tabs SET StatusID = 2 WHERE ID = ?", [$tabID]); // Capture the PaymentIntent $captureParams = [ 'amount_to_capture' => $finalCaptureCents, 'metadata[type]' => 'tab_close', 'metadata[tab_id]' => $tabID, 'metadata[tip_cents]' => $tipCents, ]; if (!empty(trim($qTab['StripeAccountID'] ?? '')) && (int) ($qTab['StripeOnboardingComplete'] ?? 0) === 1) { $captureParams['application_fee_amount'] = $applicationFeeCents; } $captureData = stripeRequest('POST', "https://api.stripe.com/v1/payment_intents/{$qTab['StripePaymentIntentID']}/capture", $captureParams); if (($captureData['status'] ?? '') === 'succeeded') { queryTimed(" UPDATE Tabs SET StatusID = 3, ClosedOn = NOW(), CapturedOn = NOW(), TipAmountCents = ?, FinalCaptureCents = ?, RunningTotalCents = ?, PaymentStatus = 'captured' WHERE ID = ? ", [$tipCents, $finalCaptureCents, $totalSubtotalCents + $totalTaxCents, $tabID]); queryTimed(" UPDATE Orders o JOIN TabOrders tbo ON tbo.OrderID = o.ID SET o.PaymentStatus = 'paid', o.PaymentCompletedOn = NOW() WHERE tbo.TabID = ? AND tbo.ApprovalStatus = 'approved' ", [$tabID]); queryTimed("UPDATE TabMembers SET StatusID = 3, LeftOn = NOW() WHERE TabID = ? AND StatusID = 1", [$tabID]); jsonResponse([ 'OK' => true, 'FINAL_CAPTURE_CENTS' => $finalCaptureCents, 'TAB_UUID' => $qTab['UUID'], 'FEE_BREAKDOWN' => [ 'SUBTOTAL_CENTS' => $totalSubtotalCents, 'TAX_CENTS' => $totalTaxCents, 'TIP_CENTS' => $tipCents, 'TIP_PERCENT' => $tipPct, 'PAYFRIT_FEE_CENTS' => $payfritFeeCents, 'CARD_FEE_CENTS' => $cardFeeCents, 'TOTAL_CENTS' => $finalCaptureCents, ], ]); } else { $errMsg = $captureData['error']['message'] ?? 'Capture failed'; queryTimed("UPDATE Tabs SET PaymentStatus = 'capture_failed', PaymentError = ? WHERE ID = ?", [$errMsg, $tabID]); apiAbort(['OK' => false, 'ERROR' => 'capture_failed', 'MESSAGE' => $errMsg]); } } catch (Exception $e) { jsonResponse(['OK' => false, 'ERROR' => 'server_error', 'MESSAGE' => $e->getMessage()]); }