payfrit-api/api/stripe/createPaymentIntent.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

280 lines
12 KiB
PHP

<?php
require_once __DIR__ . '/../helpers.php';
require_once __DIR__ . '/../config/stripe.php';
runAuth();
try {
$data = readJsonBody();
$businessID = (int) ($data['BusinessID'] ?? 0);
$orderID = (int) ($data['OrderID'] ?? 0);
$subtotal = (float) ($data['Subtotal'] ?? 0);
$tax = (float) ($data['Tax'] ?? 0);
$tip = (float) ($data['Tip'] ?? 0);
$customerEmail = $data['CustomerEmail'] ?? '';
if ($businessID === 0) apiAbort(['OK' => false, 'ERROR' => 'BusinessID is required']);
if ($orderID === 0) apiAbort(['OK' => false, 'ERROR' => 'OrderID is required']);
if ($subtotal <= 0) apiAbort(['OK' => false, 'ERROR' => 'Invalid subtotal']);
$config = getStripeConfig();
if (empty($config['secretKey'])) apiAbort(['OK' => false, 'ERROR' => 'Stripe is not configured']);
// Get business Stripe account and fee settings
$qBusiness = queryOne("SELECT StripeAccountID, StripeOnboardingComplete, Name, PayfritFee FROM Businesses WHERE ID = ?", [$businessID]);
if (!$qBusiness) apiAbort(['OK' => false, 'ERROR' => 'Business not found']);
// Get order details
$qOrder = queryOne("
SELECT o.DeliveryFee, o.OrderTypeID, o.GrantID, o.GrantOwnerBusinessID,
o.GrantEconomicsType, o.GrantEconomicsValue, o.StripePaymentIntentID,
o.UserID, o.BalanceApplied AS PrevBalanceApplied,
u.StripeCustomerId, u.EmailAddress, u.FirstName, u.LastName, u.Balance
FROM Orders o
LEFT JOIN Users u ON u.ID = o.UserID
WHERE o.ID = ?
", [$orderID]);
$deliveryFee = 0;
if ($qOrder && (int) $qOrder['OrderTypeID'] === 3) {
$deliveryFee = (float) ($qOrder['DeliveryFee'] ?? 0);
}
// SP-SM: Resolve grant economics
$grantOwnerFeeCents = 0;
$grantOwnerBusinessID = 0;
$grantID = 0;
if ($qOrder && (int) ($qOrder['GrantID'] ?? 0) > 0) {
$grantID = (int) $qOrder['GrantID'];
$grantOwnerBusinessID = (int) ($qOrder['GrantOwnerBusinessID'] ?? 0);
$grantEconType = $qOrder['GrantEconomicsType'] ?? 'none';
$grantEconValue = (float) ($qOrder['GrantEconomicsValue'] ?? 0);
if ($grantEconType === 'flat_fee' && $grantEconValue > 0) {
$grantOwnerFeeCents = round($grantEconValue * 100);
} elseif ($grantEconType === 'percent_of_orders' && $grantEconValue > 0) {
$grantOwnerFeeCents = round($subtotal * ($grantEconValue / 100) * 100);
}
}
$hasStripeConnect = (int) ($qBusiness['StripeOnboardingComplete'] ?? 0) === 1
&& !empty(trim($qBusiness['StripeAccountID'] ?? ''));
// ============================================================
// FEE CALCULATION (from Businesses.PayfritFee)
// ============================================================
$payfritFee = $qBusiness['PayfritFee'] ?? 0;
if (!is_numeric($payfritFee) || $payfritFee <= 0) {
apiAbort(['OK' => false, 'ERROR' => 'Business PayfritFee not configured']);
}
$customerFeePercent = (float) $payfritFee;
$businessFeePercent = $customerFeePercent;
$cardFeePercent = 0.029;
$cardFeeFixed = 0.30;
$payfritCustomerFee = $subtotal * $customerFeePercent;
$payfritBusinessFee = $subtotal * $businessFeePercent;
$totalBeforeCardFee = $subtotal + $tax + $tip + $deliveryFee + $payfritCustomerFee;
// ============================================================
// AUTO-APPLY USER BALANCE
// ============================================================
$balanceApplied = 0;
$userBalance = (float) ($qOrder['Balance'] ?? 0);
$orderUserID = (int) ($qOrder['UserID'] ?? 0);
if ($userBalance > 0 && $orderUserID > 0) {
$balanceApplied = round(min($userBalance, $totalBeforeCardFee) * 100) / 100;
$adjustedTest = (($totalBeforeCardFee - $balanceApplied) + $cardFeeFixed) / (1 - $cardFeePercent);
if ($adjustedTest < 0.50) {
$maxBalance = $totalBeforeCardFee - ((0.50 * (1 - $cardFeePercent)) - $cardFeeFixed);
$balanceApplied = round(max(0, min($userBalance, $maxBalance)) * 100) / 100;
}
if ($balanceApplied > 0) {
queryTimed("UPDATE Orders SET BalanceApplied = ? WHERE ID = ?", [$balanceApplied, $orderID]);
}
}
$adjustedBeforeCardFee = $totalBeforeCardFee - $balanceApplied;
$totalCustomerPays = ($adjustedBeforeCardFee + $cardFeeFixed) / (1 - $cardFeePercent);
$cardFee = $totalCustomerPays - $adjustedBeforeCardFee;
$totalAmountCents = round($totalCustomerPays * 100);
$totalPlatformFeeCents = round(($payfritCustomerFee + $payfritBusinessFee) * 100);
// ============================================================
// CHECK FOR EXISTING PAYMENTINTENT
// ============================================================
$existingPiId = $qOrder['StripePaymentIntentID'] ?? '';
if (!empty(trim($existingPiId))) {
$existingPi = stripeRequest('GET', "https://api.stripe.com/v1/payment_intents/$existingPiId");
if (!isset($existingPi['error'])) {
$piStatus = $existingPi['status'] ?? '';
$reusableStates = ['requires_payment_method', 'requires_confirmation', 'requires_action'];
if (in_array($piStatus, $reusableStates)) {
if ((int) $existingPi['amount'] !== $totalAmountCents) {
$existingPi = stripeRequest('POST', "https://api.stripe.com/v1/payment_intents/$existingPiId", [
'amount' => $totalAmountCents,
]);
}
jsonResponse([
'OK' => true,
'CLIENT_SECRET' => $existingPi['client_secret'],
'PAYMENT_INTENT_ID' => $existingPi['id'],
'PUBLISHABLE_KEY' => $config['publishableKey'],
'REUSED' => true,
'FEE_BREAKDOWN' => [
'SUBTOTAL' => $subtotal, 'TAX' => $tax, 'TIP' => $tip,
'DELIVERY_FEE' => $deliveryFee, 'PAYFRIT_FEE' => $payfritCustomerFee,
'CARD_FEE' => $cardFee, 'BALANCE_APPLIED' => $balanceApplied,
'TOTAL' => $totalCustomerPays,
'TOTAL_BEFORE_BALANCE' => ($balanceApplied > 0) ? $totalCustomerPays + $balanceApplied : 0,
'GRANT_OWNER_FEE_CENTS' => $grantOwnerFeeCents,
],
]);
} elseif ($piStatus === 'succeeded') {
apiAbort(['OK' => false, 'ERROR' => 'already_paid', 'MESSAGE' => 'This order has already been paid.']);
}
}
// Terminal or not found — clear and create new
queryTimed("UPDATE Orders SET StripePaymentIntentID = NULL WHERE ID = ?", [$orderID]);
}
// ============================================================
// STRIPE CUSTOMER (for saving payment methods)
// ============================================================
$stripeCustomerId = '';
$orderUserID = (int) ($qOrder['UserID'] ?? 0);
if ($orderUserID > 0) {
$stripeCustomerId = $qOrder['StripeCustomerId'] ?? '';
// Validate existing customer
$needNewCustomer = empty(trim($stripeCustomerId));
if (!$needNewCustomer) {
$validateData = stripeRequest('GET', "https://api.stripe.com/v1/customers/$stripeCustomerId");
if (isset($validateData['error']) || !isset($validateData['id'])) {
$needNewCustomer = true;
}
}
if ($needNewCustomer) {
$customerParams = ['metadata[user_id]' => $orderUserID];
$customerName = trim(($qOrder['FirstName'] ?? '') . ' ' . ($qOrder['LastName'] ?? ''));
if (!empty($customerName)) $customerParams['name'] = $customerName;
if (!empty(trim($qOrder['EmailAddress'] ?? ''))) $customerParams['email'] = $qOrder['EmailAddress'];
$customerData = stripeRequest('POST', 'https://api.stripe.com/v1/customers', $customerParams);
if (!isset($customerData['error']) && isset($customerData['id'])) {
$stripeCustomerId = $customerData['id'];
queryTimed("UPDATE Users SET StripeCustomerId = ? WHERE ID = ?", [$stripeCustomerId, $orderUserID]);
}
}
}
// ============================================================
// CREATE PAYMENTINTENT
// ============================================================
$piParams = [
'amount' => $totalAmountCents,
'currency' => 'usd',
'automatic_payment_methods[enabled]' => 'true',
'metadata[order_id]' => $orderID,
'metadata[business_id]' => $businessID,
'description' => "Order #$orderID at " . $qBusiness['Name'],
];
if (!empty(trim($stripeCustomerId))) {
$piParams['customer'] = $stripeCustomerId;
$piParams['setup_future_usage'] = 'off_session';
}
if ($hasStripeConnect) {
$effectivePlatformFeeCents = $totalPlatformFeeCents + $grantOwnerFeeCents;
$piParams['application_fee_amount'] = $effectivePlatformFeeCents;
$piParams['transfer_data[destination]'] = $qBusiness['StripeAccountID'];
}
if ($grantOwnerFeeCents > 0) {
$piParams['metadata[grant_id]'] = $grantID;
$piParams['metadata[grant_owner_business_id]'] = $grantOwnerBusinessID;
$piParams['metadata[grant_owner_fee_cents]'] = $grantOwnerFeeCents;
}
if (!empty($customerEmail)) {
$piParams['receipt_email'] = $customerEmail;
}
$piHeaders = ['Idempotency-Key' => "pi-order-$orderID"];
$piData = stripeRequest('POST', 'https://api.stripe.com/v1/payment_intents', $piParams, $piHeaders);
if (isset($piData['error'])) {
apiAbort(['OK' => false, 'ERROR' => $piData['error']['message']]);
}
// Store PaymentIntent ID on order
queryTimed("UPDATE Orders SET StripePaymentIntentID = ? WHERE ID = ?", [$piData['id'], $orderID]);
// Link PaymentIntent to worker payout ledger
try {
queryTimed("
UPDATE WorkPayoutLedgers wpl
INNER JOIN Tasks t ON t.ID = wpl.TaskID AND t.OrderID = ?
SET wpl.StripePaymentIntentID = ?
WHERE wpl.Status = 'pending_charge'
AND wpl.StripePaymentIntentID IS NULL
", [$orderID, $piData['id']]);
} catch (Exception $e) {
// Non-fatal
}
// ============================================================
// CREATE CHECKOUT SESSION (for iOS)
// ============================================================
$checkoutParams = [
'mode' => 'payment',
'line_items[0][price_data][currency]' => 'usd',
'line_items[0][price_data][product_data][name]' => "Order #$orderID at " . $qBusiness['Name'],
'line_items[0][price_data][unit_amount]' => $totalAmountCents,
'line_items[0][quantity]' => '1',
'success_url' => "payfrit://stripe-redirect?success=true&order_id=$orderID",
'cancel_url' => "payfrit://stripe-redirect?success=false&error=cancelled&order_id=$orderID",
'metadata[order_id]' => $orderID,
'metadata[business_id]' => $businessID,
];
if ($hasStripeConnect) {
$effectivePlatformFeeCents = $totalPlatformFeeCents + $grantOwnerFeeCents;
$checkoutParams['payment_intent_data[application_fee_amount]'] = $effectivePlatformFeeCents;
$checkoutParams['payment_intent_data[transfer_data][destination]'] = $qBusiness['StripeAccountID'];
}
$checkoutData = stripeRequest('POST', 'https://api.stripe.com/v1/checkout/sessions', $checkoutParams);
$checkoutUrl = $checkoutData['url'] ?? '';
jsonResponse([
'OK' => true,
'CLIENT_SECRET' => $piData['client_secret'],
'PAYMENT_INTENT_ID' => $piData['id'],
'PUBLISHABLE_KEY' => $config['publishableKey'],
'CHECKOUT_URL' => $checkoutUrl,
'FEE_BREAKDOWN' => [
'SUBTOTAL' => $subtotal,
'TAX' => $tax,
'TIP' => $tip,
'DELIVERY_FEE' => $deliveryFee,
'PAYFRIT_FEE' => $payfritCustomerFee,
'CARD_FEE' => $cardFee,
'BALANCE_APPLIED' => $balanceApplied,
'TOTAL' => $totalCustomerPays,
'TOTAL_BEFORE_BALANCE' => ($balanceApplied > 0) ? $totalCustomerPays + $balanceApplied : 0,
'GRANT_OWNER_FEE_CENTS' => $grantOwnerFeeCents,
],
'STRIPE_CONNECT_ENABLED' => $hasStripeConnect,
]);
} catch (Exception $e) {
jsonResponse(['OK' => false, 'ERROR' => $e->getMessage(), 'DETAIL' => '']);
}