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()]); }