yoco updated to use webhook

This commit is contained in:
twotalesanimation
2025-12-29 14:43:24 +02:00
parent 6de01c13c5
commit 00b8ff77b2
123 changed files with 6959 additions and 12669 deletions
+225 -37
View File
@@ -267,6 +267,7 @@ class OrderController extends Controller
'metadata' => [
'order_uuid' => $order->uuid,
'order_number' => $order->order_number,
'site' => 'additional_design',
],
];
@@ -278,7 +279,55 @@ class OrderController extends Controller
if ($response->successful()) {
$checkout = $response->json();
return redirect($checkout['redirectUrl']);
$checkoutId = $checkout['id'] ?? null;
$redirectUrl = $checkout['redirectUrl'] ?? null;
\Log::info('Yoco checkout created', [
'order_uuid' => $order->uuid,
'checkout_id' => $checkoutId,
'redirect_url' => $redirectUrl,
]);
if (!$checkoutId || !$redirectUrl) {
throw new \Exception('Invalid Yoco checkout response: missing id or redirectUrl');
}
// Persist checkout info to database
try {
$order->update([
'yoco_checkout_id' => $checkoutId,
'yoco_redirect_url' => $redirectUrl,
'yoco_checkout_response' => json_encode($checkout),
]);
// Reload the model to verify update
$order->refresh();
if ($order->yoco_checkout_id) {
//clear cart
session()->forget('cart');
\Log::info('Yoco checkout info saved successfully', [
'order_uuid' => $order->uuid,
'yoco_checkout_id' => $order->yoco_checkout_id,
]);
} else {
\Log::warning('Yoco checkout ID not saved after update', [
'order_uuid' => $order->uuid,
'order_data' => $order->toArray(),
]);
}
} catch (\Exception $dbException) {
\Log::error('Database error while saving Yoco checkout info', [
'order_uuid' => $order->uuid,
'error_message' => $dbException->getMessage(),
'error_code' => $dbException->getCode(),
'checkout_id' => $checkoutId,
'redirect_url' => $redirectUrl,
]);
throw $dbException;
}
return redirect($redirectUrl);
} else {
\Log::error('Yoco API Error', [
'status' => $response->status(),
@@ -300,18 +349,18 @@ class OrderController extends Controller
->with('success', 'Payment was already processed for this order.');
}
// Update order status
$order->update([
'payment_status' => 'paid',
'status' => 'processing',
]);
// // Update order status
// $order->update([
// 'payment_status' => 'paid',
// 'status' => 'processing',
// ]);
// Reduce stock
foreach ($order->items as $item) {
$product = $item->product;
$product->stock -= $item->quantity;
$product->save();
}
// // Reduce stock
// foreach ($order->items as $item) {
// $product = $item->product;
// $product->stock -= $item->quantity;
// $product->save();
// }
// Clear pending order from session
session()->forget('pending_order_uuid');
@@ -339,35 +388,174 @@ class OrderController extends Controller
public function yocoWebhook(Request $request)
{
// Verify webhook signature
$payload = $request->getContent();
$signature = $request->header('X-Yoco-Signature');
// 1. Get Raw Body and Headers
\Log::info('Yoco Webhook: Received webhook');
// Process webhook event
$event = $request->all();
$rawBody = $request->getContent();
$trimmedBody = trim($rawBody);
if (isset($event['type']) && $event['type'] === 'checkout.succeeded') {
$metadata = $event['payload']['metadata'] ?? [];
$orderId = $metadata['order_id'] ?? null;
if ($orderId) {
$order = Order::find($orderId);
if ($order && $order->payment_status !== 'paid') {
$order->update([
'payment_status' => 'paid',
'status' => 'processing',
]);
// Reduce stock
foreach ($order->items as $item) {
$product = $item->product;
$product->stock -= $item->quantity;
$product->save();
}
}
}
$webhookId = $_SERVER['HTTP_WEBHOOK_ID'] ?? null;
$webhookTimestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? null;
$webhookSignatureHeader = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? null;
// Validate headers exist
if (!$webhookId || !$webhookTimestamp || !$webhookSignatureHeader) {
\Log::warning('Yoco Webhook: Missing headers', [
'has_id' => !empty($webhookId),
'has_timestamp' => !empty($webhookTimestamp),
'has_signature' => !empty($webhookSignatureHeader),
]);
return response()->json(['error' => 'Missing headers'], 400);
}
// 2. Parse the Secret Safely
$envSecret = trim(config('services.yoco.webhook_secret'));
if (empty($envSecret)) {
\Log::error('Yoco Webhook: Missing webhook secret configuration');
return response()->json(['error' => 'Webhook secret not configured'], 500);
}
$secretKeyString = strpos($envSecret, 'whsec_') === 0
? substr($envSecret, 6)
: $envSecret;
$secretBytes = base64_decode($secretKeyString);
// 3. Parse Incoming Signature (Standardize)
$incomingSignature = '';
if (preg_match('/(?:v1=|v1,)([^,\s]+)/', $webhookSignatureHeader, $matches)) {
$incomingSignature = $matches[1];
} else {
$incomingSignature = trim($webhookSignatureHeader);
}
// 4. Verification Function
$verifySignature = function($id, $timestamp, $body, $secretBytes, $expectedSig) {
$signedContent = $id . '.' . $timestamp . '.' . $body;
$calculatedHmac = hash_hmac('sha256', $signedContent, $secretBytes, true);
$calculatedSig = base64_encode($calculatedHmac);
return hash_equals($expectedSig, $calculatedSig);
};
// 5. Try Verification (Attempt both Trimmed and Raw)
$isValid = false;
$methodUsed = '';
// Attempt 1: Trimmed Body (Most likely correct for JSON)
if ($verifySignature($webhookId, $webhookTimestamp, trim($rawBody), $secretBytes, $incomingSignature)) {
$isValid = true;
$methodUsed = 'trimmed';
}
// Attempt 2: Raw Body (Fallback if Yoco signed the whitespace)
elseif ($verifySignature($webhookId, $webhookTimestamp, $rawBody, $secretBytes, $incomingSignature)) {
$isValid = true;
$methodUsed = 'raw';
}
if (!$isValid) {
\Log::warning('Yoco Webhook: Signature verification failed', [
'webhook_id' => $webhookId,
'timestamp' => $webhookTimestamp,
'received_signature' => $incomingSignature,
]);
return response()->json(['error' => 'Invalid signature'], 403);
}
\Log::info('Yoco Webhook: Signature verified successfully', ['method' => $methodUsed]);
// 6. Parse Event
$event = json_decode($rawBody, true);
if (!$event) {
\Log::warning('Yoco Webhook: Failed to parse JSON payload');
return response()->json(['error' => 'Invalid JSON'], 400);
}
$type = $event['type'] ?? null;
$payload = $event['payload'] ?? [];
$metadata = $payload['metadata'] ?? [];
$orderUuid = $metadata['order_uuid'] ?? null;
$site = $metadata['site'] ?? null;
$paymentId = $payload['id'] ?? null;
$status = $payload['status'] ?? null;
\Log::info('Yoco Webhook: Payload extracted', [
'type' => $type,
'site' => $site,
'order_uuid' => $orderUuid,
'payment_id' => $paymentId,
'status' => $status,
]);
if ($site !== 'additional_design') {
\Log::warning('Yoco Webhook: Ignored event for different site', [
'expected_site' => 'additional_design',
'received_site' => $site,
]);
return response()->json(['status' => 'ignored'], 200);
}
if (!$orderUuid || !$status) {
\Log::warning('Yoco Webhook: Validation failed', [
'order_uuid' => $orderUuid,
'status' => $status,
'type' => $type,
]);
return response()->json(['error' => 'Invalid payload'], 400);
}
// 7. Update Payment State
$order = Order::where('uuid', $orderUuid)->first();
if (!$order) {
\Log::warning('Yoco Webhook: Order not found', ['order_uuid' => $orderUuid]);
return response()->json(['error' => 'Order not found'], 404);
}
if ($status === 'succeeded') {
\Log::info('Yoco Webhook: Processing successful payment', [
'order_uuid' => $orderUuid,
'payment_id' => $paymentId,
]);
if ($order->payment_status !== 'paid') {
$order->update([
'payment_status' => 'paid',
'status' => 'processing',
'yoco_checkout_response' => json_encode($payload),
]);
// Reduce stock
foreach ($order->items as $item) {
$product = $item->product;
$product->stock -= $item->quantity;
$product->save();
}
\Log::info('Yoco Webhook: Order updated for successful payment', [
'order_uuid' => $orderUuid,
'order_id' => $order->id,
]);
}
} elseif ($status === 'failed' || $status === 'cancelled') {
\Log::info('Yoco Webhook: Processing failed/cancelled payment', [
'order_uuid' => $orderUuid,
'payment_id' => $paymentId,
'status' => $status,
]);
$order->update([
'payment_status' => 'failed',
]);
}
\Log::info('Yoco Webhook: Processed successfully', [
'order_uuid' => $orderUuid,
'status' => $status,
]);
return response()->json(['status' => 'success']);
}
}