payfrit-api/api/tabs/close.php
John Mizerek 1f81d98c52 Initial PHP API migration from CFML
Complete port of all 163 API endpoints from Lucee/CFML to PHP 8.3.
Shared helpers in api/helpers.php (DB, auth, request/response, security).
PDO prepared statements throughout. Same JSON response shapes as CFML.
2026-03-14 14:26:59 -07:00

140 lines
6.1 KiB
PHP

<?php
require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../config/stripe.php';
runAuth();
try {
$data = readJsonBody();
$tabID = (int) ($data['TabID'] ?? 0);
$userID = (int) ($data['UserID'] ?? 0);
$tipPercent = isset($data['TipPercent']) ? (float) $data['TipPercent'] : -1;
$tipAmount = isset($data['TipAmount']) ? (float) $data['TipAmount'] : -1;
if ($tabID === 0) apiAbort(['OK' => 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()]);
}