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.
142 lines
6 KiB
PHP
142 lines
6 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../helpers.php';
|
|
require_once __DIR__ . '/../config/stripe.php';
|
|
runAuth();
|
|
|
|
try {
|
|
$data = readJsonBody();
|
|
|
|
$userID = (int) ($data['UserID'] ?? 0);
|
|
$businessID = (int) ($data['BusinessID'] ?? 0);
|
|
if (isset($data['AuthAmountCents']) && (int) $data['AuthAmountCents'] > 0) {
|
|
$authAmount = (int) $data['AuthAmountCents'] / 100;
|
|
} else {
|
|
$authAmount = (float) ($data['AuthAmount'] ?? 0);
|
|
}
|
|
$servicePointID = (int) ($data['ServicePointID'] ?? 0);
|
|
$approvalMode = isset($data['ApprovalMode']) ? (int) $data['ApprovalMode'] : null;
|
|
|
|
if ($userID === 0) apiAbort(['OK' => false, 'ERROR' => 'missing_UserID']);
|
|
if ($businessID === 0) apiAbort(['OK' => false, 'ERROR' => 'missing_BusinessID']);
|
|
|
|
$qBiz = queryOne("
|
|
SELECT SessionEnabled, TabMinAuthAmount, TabDefaultAuthAmount, TabMaxAuthAmount,
|
|
StripeAccountID, StripeOnboardingComplete
|
|
FROM Businesses WHERE ID = ? LIMIT 1
|
|
", [$businessID]);
|
|
|
|
if (!$qBiz) apiAbort(['OK' => false, 'ERROR' => 'business_not_found']);
|
|
if (!$qBiz['SessionEnabled']) apiAbort(['OK' => false, 'ERROR' => 'tabs_not_enabled', 'MESSAGE' => 'This business does not accept tabs.']);
|
|
|
|
$minAuth = (float) $qBiz['TabMinAuthAmount'];
|
|
$maxAuth = (float) $qBiz['TabMaxAuthAmount'];
|
|
if ($authAmount <= 0) $authAmount = (float) $qBiz['TabDefaultAuthAmount'];
|
|
if ($authAmount < $minAuth) apiAbort(['OK' => false, 'ERROR' => 'auth_too_low', 'MESSAGE' => "Minimum authorization is \$" . number_format($minAuth, 2), 'MIN' => $minAuth]);
|
|
if ($authAmount > $maxAuth) apiAbort(['OK' => false, 'ERROR' => 'auth_too_high', 'MESSAGE' => "Maximum authorization is \$" . number_format($maxAuth, 2), 'MAX' => $maxAuth]);
|
|
|
|
// Check user not already on a tab
|
|
$qExisting = queryOne("
|
|
SELECT t.ID, t.BusinessID, b.Name AS BusinessName
|
|
FROM TabMembers tm
|
|
JOIN Tabs t ON t.ID = tm.TabID
|
|
JOIN Businesses b ON b.ID = t.BusinessID
|
|
WHERE tm.UserID = ? AND tm.StatusID = 1 AND t.StatusID = 1
|
|
LIMIT 1
|
|
", [$userID]);
|
|
|
|
if ($qExisting) {
|
|
apiAbort([
|
|
'OK' => false, 'ERROR' => 'already_on_tab',
|
|
'MESSAGE' => "You're already on a tab at " . $qExisting['BusinessName'] . ".",
|
|
'EXISTING_TAB_ID' => (int) $qExisting['ID'],
|
|
'EXISTING_BUSINESS_NAME' => $qExisting['BusinessName'],
|
|
]);
|
|
}
|
|
|
|
// Get or create Stripe Customer
|
|
$qUser = queryOne("SELECT StripeCustomerId, EmailAddress, FirstName, LastName FROM Users WHERE ID = ? LIMIT 1", [$userID]);
|
|
if (!$qUser) apiAbort(['OK' => false, 'ERROR' => 'user_not_found']);
|
|
|
|
$stripeCustomerId = $qUser['StripeCustomerId'] ?? '';
|
|
|
|
$needNewCustomer = empty(trim($stripeCustomerId));
|
|
if (!$needNewCustomer) {
|
|
$checkData = stripeRequest('GET', "https://api.stripe.com/v1/customers/$stripeCustomerId");
|
|
if (isset($checkData['error']) || !isset($checkData['id'])) {
|
|
$needNewCustomer = true;
|
|
}
|
|
}
|
|
|
|
if ($needNewCustomer) {
|
|
$custParams = [
|
|
'name' => trim($qUser['FirstName'] . ' ' . $qUser['LastName']),
|
|
'metadata[payfrit_user_id]' => $userID,
|
|
];
|
|
if (!empty(trim($qUser['EmailAddress'] ?? ''))) $custParams['email'] = $qUser['EmailAddress'];
|
|
|
|
$custData = stripeRequest('POST', 'https://api.stripe.com/v1/customers', $custParams);
|
|
if (isset($custData['id'])) {
|
|
$stripeCustomerId = $custData['id'];
|
|
queryTimed("UPDATE Users SET StripeCustomerId = ? WHERE ID = ?", [$stripeCustomerId, $userID]);
|
|
} else {
|
|
apiAbort(['OK' => false, 'ERROR' => 'stripe_customer_failed', 'MESSAGE' => 'Could not create Stripe customer.']);
|
|
}
|
|
}
|
|
|
|
$tabUUID = generateUUID();
|
|
$authAmountCents = round($authAmount * 100);
|
|
|
|
// Create PaymentIntent with manual capture
|
|
$piParams = [
|
|
'amount' => $authAmountCents,
|
|
'currency' => 'usd',
|
|
'capture_method' => 'manual',
|
|
'customer' => $stripeCustomerId,
|
|
'setup_future_usage' => 'off_session',
|
|
'automatic_payment_methods[enabled]' => 'true',
|
|
'metadata[type]' => 'tab_authorization',
|
|
'metadata[tab_uuid]' => $tabUUID,
|
|
'metadata[business_id]' => $businessID,
|
|
'metadata[user_id]' => $userID,
|
|
];
|
|
|
|
if (!empty(trim($qBiz['StripeAccountID'] ?? '')) && (int) ($qBiz['StripeOnboardingComplete'] ?? 0) === 1) {
|
|
$piParams['transfer_data[destination]'] = $qBiz['StripeAccountID'];
|
|
}
|
|
|
|
$now = gmdate('YmdHis');
|
|
$piData = stripeRequest('POST', 'https://api.stripe.com/v1/payment_intents', $piParams,
|
|
['Idempotency-Key' => "tab-open-$userID-$businessID-$now"]);
|
|
|
|
if (!isset($piData['id'])) {
|
|
$errMsg = $piData['error']['message'] ?? 'Stripe error';
|
|
apiAbort(['OK' => false, 'ERROR' => 'stripe_pi_failed', 'MESSAGE' => $errMsg]);
|
|
}
|
|
|
|
// Insert tab
|
|
$spVal = $servicePointID > 0 ? $servicePointID : null;
|
|
queryTimed("
|
|
INSERT INTO Tabs (UUID, BusinessID, OwnerUserID, ServicePointID, StatusID,
|
|
AuthAmountCents, StripePaymentIntentID, StripeCustomerID, ApprovalMode, OpenedOn, LastActivityOn)
|
|
VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, NOW(), NOW())
|
|
", [$tabUUID, $businessID, $userID, $spVal, $authAmountCents, $piData['id'], $stripeCustomerId, $approvalMode]);
|
|
|
|
$tabID = (int) lastInsertId();
|
|
|
|
// Add owner as TabMember
|
|
queryTimed("INSERT INTO TabMembers (TabID, UserID, RoleID, StatusID, JoinedOn) VALUES (?, ?, 1, 1, NOW())", [$tabID, $userID]);
|
|
|
|
$config = getStripeConfig();
|
|
jsonResponse([
|
|
'OK' => true,
|
|
'TAB_ID' => $tabID,
|
|
'TAB_UUID' => $tabUUID,
|
|
'CLIENT_SECRET' => $piData['client_secret'],
|
|
'PAYMENT_INTENT_ID' => $piData['id'],
|
|
'AUTH_AMOUNT_CENTS' => $authAmountCents,
|
|
'PUBLISHABLE_KEY' => $config['publishableKey'],
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'server_error', 'MESSAGE' => $e->getMessage()]);
|
|
}
|