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.
140 lines
6.1 KiB
PHP
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()]);
|
|
}
|