submitCash.php, submit.php, and webhook.php were creating kitchen tasks without TaskTypeID, which is NOT NULL with no default. This caused cash order submission to fail with a SQL error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
293 lines
14 KiB
PHP
293 lines
14 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../helpers.php';
|
|
require_once __DIR__ . '/../config/stripe.php';
|
|
// NO runAuth() — webhook is called by Stripe, not a user
|
|
|
|
try {
|
|
$payload = file_get_contents('php://input');
|
|
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
|
|
$config = getStripeConfig();
|
|
$webhookSecret = $config['webhookSecret'];
|
|
|
|
// Verify webhook signature (Stripe HMAC-SHA256)
|
|
if (!empty($webhookSecret) && !empty($sigHeader)) {
|
|
$sigParts = [];
|
|
foreach (explode(',', $sigHeader) as $part) {
|
|
$kv = explode('=', trim($part), 2);
|
|
if (count($kv) === 2) $sigParts[$kv[0]] = $kv[1];
|
|
}
|
|
|
|
$sigTimestamp = $sigParts['t'] ?? '';
|
|
$sigV1 = $sigParts['v1'] ?? '';
|
|
|
|
if (empty($sigTimestamp) || empty($sigV1)) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'invalid_signature']);
|
|
}
|
|
|
|
// Check timestamp tolerance (5 minutes)
|
|
if (abs(time() - (int) $sigTimestamp) > 300) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'timestamp_expired']);
|
|
}
|
|
|
|
$signedPayload = $sigTimestamp . '.' . $payload;
|
|
$expectedSig = hash_hmac('sha256', $signedPayload, $webhookSecret);
|
|
|
|
if (!hash_equals($expectedSig, strtolower($sigV1))) {
|
|
jsonResponse(['OK' => false, 'ERROR' => 'signature_mismatch']);
|
|
}
|
|
}
|
|
|
|
$event = json_decode($payload, true);
|
|
$eventType = $event['type'] ?? '';
|
|
$eventData = $event['data']['object'] ?? [];
|
|
|
|
switch ($eventType) {
|
|
|
|
case 'payment_intent.succeeded':
|
|
$paymentIntentID = $eventData['id'];
|
|
$orderID = (int) ($eventData['metadata']['order_id'] ?? 0);
|
|
$metaType = $eventData['metadata']['type'] ?? '';
|
|
|
|
// === TAB CLOSE CAPTURE ===
|
|
if ($metaType === 'tab_close') {
|
|
$tabID = (int) ($eventData['metadata']['tab_id'] ?? 0);
|
|
if ($tabID > 0) {
|
|
queryTimed("
|
|
UPDATE Tabs SET StatusID = 3, PaymentStatus = 'captured', CapturedOn = NOW(), ClosedOn = NOW()
|
|
WHERE ID = ? AND StatusID = 2
|
|
", [$tabID]);
|
|
|
|
$qTabOrders = queryTimed("SELECT OrderID FROM TabOrders WHERE TabID = ? AND ApprovalStatus = 'approved'", [$tabID]);
|
|
foreach ($qTabOrders as $row) {
|
|
queryTimed("UPDATE Orders SET PaymentStatus = 'paid', PaymentCompletedOn = NOW() WHERE ID = ?", [$row['OrderID']]);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ($orderID > 0) {
|
|
// Mark order as paid
|
|
queryTimed("
|
|
UPDATE Orders
|
|
SET PaymentStatus = 'paid', PaymentCompletedOn = NOW(),
|
|
SubmittedOn = COALESCE(SubmittedOn, NOW()),
|
|
StatusID = CASE WHEN StatusID = 0 THEN 1 ELSE StatusID END
|
|
WHERE ID = ?
|
|
", [$orderID]);
|
|
|
|
// Deduct user balance if applied at checkout
|
|
$qBalOrder = queryOne("SELECT BalanceApplied, UserID FROM Orders WHERE ID = ?", [$orderID]);
|
|
if ($qBalOrder && (float) ($qBalOrder['BalanceApplied'] ?? 0) > 0) {
|
|
queryTimed("UPDATE Users SET Balance = GREATEST(Balance - ?, 0) WHERE ID = ?",
|
|
[(float) $qBalOrder['BalanceApplied'], (int) $qBalOrder['UserID']]);
|
|
}
|
|
|
|
// Create kitchen task
|
|
$qOrder = queryOne("
|
|
SELECT o.BusinessID, o.ServicePointID, sp.Name AS ServicePointName
|
|
FROM Orders o
|
|
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
|
|
WHERE o.ID = ?
|
|
", [$orderID]);
|
|
|
|
if ($qOrder) {
|
|
$tableName = !empty(trim($qOrder['ServicePointName'] ?? '')) ? $qOrder['ServicePointName'] : 'Table';
|
|
$taskTitle = "Prepare Order #$orderID for $tableName";
|
|
|
|
queryTimed("
|
|
INSERT INTO Tasks (BusinessID, OrderID, ServicePointID, TaskTypeID, Title, CreatedOn, ClaimedByUserID)
|
|
VALUES (?, ?, ?, 0, ?, NOW(), 0)
|
|
", [$qOrder['BusinessID'], $orderID, (int) ($qOrder['ServicePointID'] ?? 0), $taskTitle]);
|
|
}
|
|
}
|
|
|
|
// === WORKER PAYOUT TRANSFER ===
|
|
try {
|
|
$qLedger = queryOne("
|
|
SELECT ID, UserID, TaskID, NetTransferCents, StripeTransferID, Status
|
|
FROM WorkPayoutLedgers
|
|
WHERE StripePaymentIntentID = ? LIMIT 1
|
|
", [$paymentIntentID]);
|
|
|
|
if ($qLedger && empty($qLedger['StripeTransferID']) && $qLedger['Status'] === 'pending_charge') {
|
|
queryTimed("UPDATE WorkPayoutLedgers SET Status = 'charged', UpdatedAt = NOW() WHERE ID = ?",
|
|
[$qLedger['ID']]);
|
|
|
|
$qWorker = queryOne("SELECT StripeConnectedAccountID FROM Users WHERE ID = ?", [$qLedger['UserID']]);
|
|
$workerAccountID = $qWorker['StripeConnectedAccountID'] ?? '';
|
|
|
|
if (!empty(trim($workerAccountID)) && (int) $qLedger['NetTransferCents'] > 0) {
|
|
$transferData = stripeRequest('POST', 'https://api.stripe.com/v1/transfers', [
|
|
'amount' => $qLedger['NetTransferCents'],
|
|
'currency' => 'usd',
|
|
'destination' => $workerAccountID,
|
|
'metadata[user_id]' => $qLedger['UserID'],
|
|
'metadata[task_id]' => $qLedger['TaskID'],
|
|
'metadata[ledger_id]' => $qLedger['ID'],
|
|
'metadata[activation_withheld_cents]' => 0,
|
|
], ['Idempotency-Key' => 'transfer-ledger-' . $qLedger['ID']]);
|
|
|
|
if (isset($transferData['id'])) {
|
|
queryTimed("UPDATE WorkPayoutLedgers SET StripeTransferID = ?, Status = 'transferred', UpdatedAt = NOW() WHERE ID = ?",
|
|
[$transferData['id'], $qLedger['ID']]);
|
|
}
|
|
} elseif ((int) $qLedger['NetTransferCents'] === 0) {
|
|
queryTimed("UPDATE WorkPayoutLedgers SET Status = 'transferred', UpdatedAt = NOW() WHERE ID = ?",
|
|
[$qLedger['ID']]);
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// Non-fatal
|
|
}
|
|
|
|
// === SP-SM: GRANT OWNER TRANSFER ===
|
|
try {
|
|
$grantOwnerFeeCents = (int) ($eventData['metadata']['grant_owner_fee_cents'] ?? 0);
|
|
$grantOwnerBizID = (int) ($eventData['metadata']['grant_owner_business_id'] ?? 0);
|
|
$grantMetaID = (int) ($eventData['metadata']['grant_id'] ?? 0);
|
|
|
|
if ($grantOwnerFeeCents > 0 && $grantOwnerBizID > 0) {
|
|
$qOwnerBiz = queryOne("SELECT StripeAccountID FROM Businesses WHERE ID = ?", [$grantOwnerBizID]);
|
|
$ownerStripeAcct = $qOwnerBiz['StripeAccountID'] ?? '';
|
|
|
|
if (!empty(trim($ownerStripeAcct))) {
|
|
stripeRequest('POST', 'https://api.stripe.com/v1/transfers', [
|
|
'amount' => $grantOwnerFeeCents,
|
|
'currency' => 'usd',
|
|
'destination' => $ownerStripeAcct,
|
|
'metadata[grant_id]' => $grantMetaID,
|
|
'metadata[order_id]' => $orderID,
|
|
'metadata[type]' => 'grant_owner_fee',
|
|
], ['Idempotency-Key' => "grant-transfer-$orderID-$grantMetaID"]);
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// Non-fatal
|
|
}
|
|
break;
|
|
|
|
case 'payment_intent.payment_failed':
|
|
$orderID = (int) ($eventData['metadata']['order_id'] ?? 0);
|
|
$failureMessage = $eventData['last_payment_error']['message'] ?? 'Payment failed';
|
|
|
|
if ($orderID > 0) {
|
|
queryTimed("UPDATE Orders SET PaymentStatus = 'failed', PaymentError = ? WHERE ID = ?",
|
|
[$failureMessage, $orderID]);
|
|
}
|
|
break;
|
|
|
|
case 'charge.refunded':
|
|
$paymentIntentID = $eventData['payment_intent'] ?? '';
|
|
$refundAmount = ($eventData['amount_refunded'] ?? 0) / 100;
|
|
|
|
if (!empty($paymentIntentID)) {
|
|
queryTimed("
|
|
UPDATE Orders SET PaymentStatus = 'refunded', RefundAmount = ?, RefundedOn = NOW()
|
|
WHERE StripePaymentIntentID = ?
|
|
", [$refundAmount, $paymentIntentID]);
|
|
}
|
|
break;
|
|
|
|
case 'charge.dispute.created':
|
|
$paymentIntentID = $eventData['payment_intent'] ?? '';
|
|
|
|
if (!empty($paymentIntentID)) {
|
|
$qOrder = queryOne("SELECT ID, BusinessID FROM Orders WHERE StripePaymentIntentID = ?", [$paymentIntentID]);
|
|
|
|
if ($qOrder) {
|
|
queryTimed("UPDATE Orders SET PaymentStatus = 'disputed' WHERE StripePaymentIntentID = ?",
|
|
[$paymentIntentID]);
|
|
|
|
queryTimed("
|
|
INSERT INTO Tasks (BusinessID, CategoryID, Title, Details, CreatedOn, StatusID, SourceType, SourceID)
|
|
VALUES (?, 4, 'Payment Dispute', ?, NOW(), 0, 'dispute', ?)
|
|
", [
|
|
$qOrder['BusinessID'],
|
|
'Order #' . $qOrder['ID'] . ' has been disputed. Review immediately.',
|
|
$qOrder['ID'],
|
|
]);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'account.updated':
|
|
$accountID = $eventData['id'];
|
|
$chargesEnabled = $eventData['charges_enabled'] ?? false;
|
|
$payoutsEnabled = $eventData['payouts_enabled'] ?? false;
|
|
|
|
// Business accounts
|
|
if ($chargesEnabled && $payoutsEnabled) {
|
|
queryTimed("UPDATE Businesses SET StripeOnboardingComplete = 1 WHERE StripeAccountID = ?", [$accountID]);
|
|
}
|
|
|
|
// Worker accounts
|
|
try {
|
|
$qWorkerAcct = queryOne("SELECT ID FROM Users WHERE StripeConnectedAccountID = ? LIMIT 1", [$accountID]);
|
|
if ($qWorkerAcct) {
|
|
queryTimed("UPDATE Users SET StripePayoutsEnabled = ? WHERE StripeConnectedAccountID = ?",
|
|
[$payoutsEnabled ? 1 : 0, $accountID]);
|
|
}
|
|
} catch (Exception $e) {
|
|
// Non-fatal
|
|
}
|
|
break;
|
|
|
|
case 'checkout.session.completed':
|
|
try {
|
|
$sessionMetadata = $eventData['metadata'] ?? [];
|
|
$metaType = $sessionMetadata['type'] ?? '';
|
|
$metaUserID = (int) ($sessionMetadata['user_id'] ?? 0);
|
|
|
|
// Activation early unlock
|
|
if ($metaType === 'activation_unlock' && $metaUserID > 0) {
|
|
queryTimed("UPDATE Users SET ActivationBalanceCents = ActivationCapCents WHERE ID = ?", [$metaUserID]);
|
|
}
|
|
|
|
// Tip payment
|
|
if ($metaType === 'tip') {
|
|
$metaTipID = (int) ($sessionMetadata['tip_id'] ?? 0);
|
|
$metaWorkerID = (int) ($sessionMetadata['worker_user_id'] ?? 0);
|
|
$tipPaymentIntent = $eventData['payment_intent'] ?? '';
|
|
|
|
if ($metaTipID > 0) {
|
|
queryTimed("UPDATE Tips SET Status = 'paid', PaidOn = NOW(), StripePaymentIntentID = ? WHERE ID = ? AND Status = 'pending'",
|
|
[$tipPaymentIntent, $metaTipID]);
|
|
|
|
// Transfer tip to worker
|
|
if ($metaWorkerID > 0) {
|
|
$qTipWorker = queryOne("SELECT StripeConnectedAccountID FROM Users WHERE ID = ?", [$metaWorkerID]);
|
|
$tipWorkerAcct = $qTipWorker['StripeConnectedAccountID'] ?? '';
|
|
|
|
if (!empty(trim($tipWorkerAcct))) {
|
|
$qTipAmt = queryOne("SELECT AmountCents FROM Tips WHERE ID = ?", [$metaTipID]);
|
|
|
|
if ($qTipAmt && (int) $qTipAmt['AmountCents'] > 0) {
|
|
$tipTransferData = stripeRequest('POST', 'https://api.stripe.com/v1/transfers', [
|
|
'amount' => $qTipAmt['AmountCents'],
|
|
'currency' => 'usd',
|
|
'destination' => $tipWorkerAcct,
|
|
'metadata[type]' => 'tip',
|
|
'metadata[tip_id]' => $metaTipID,
|
|
'metadata[worker_user_id]' => $metaWorkerID,
|
|
], ['Idempotency-Key' => "tip-transfer-$metaTipID"]);
|
|
|
|
if (isset($tipTransferData['id'])) {
|
|
queryTimed("UPDATE Tips SET Status = 'transferred', StripeTransferID = ? WHERE ID = ?",
|
|
[$tipTransferData['id'], $metaTipID]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// Non-fatal
|
|
}
|
|
break;
|
|
}
|
|
|
|
jsonResponse(['OK' => true, 'RECEIVED' => true]);
|
|
|
|
} catch (Exception $e) {
|
|
jsonResponse(['OK' => false, 'ERROR' => $e->getMessage()]);
|
|
}
|