yoco updated to use webhook
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Order;
|
||||
use App\Models\CustomOrder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show user account page
|
||||
*/
|
||||
public function show(): View
|
||||
{
|
||||
return view('account.profile', [
|
||||
'user' => auth()->user(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user account
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email,' . auth()->id(),
|
||||
]);
|
||||
|
||||
auth()->user()->update($validated);
|
||||
|
||||
return redirect()->route('my-account')->with('success', 'Profile updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show user's orders (both standard and custom)
|
||||
*/
|
||||
public function orders(): View
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
$standardOrders = Order::where('user_id', $user->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
$customOrders = CustomOrder::where('user_id', $user->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return view('account.orders', [
|
||||
'standardOrders' => $standardOrders,
|
||||
'customOrders' => $customOrders,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show order detail (both standard and custom)
|
||||
*/
|
||||
public function orderDetail(Order $order)
|
||||
{
|
||||
// Check authorization
|
||||
if ($order->user_id !== auth()->id() && !auth()->user()->is_admin) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('account.order-detail', [
|
||||
'order' => $order,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
|
||||
class GoogleAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Redirect to Google OAuth
|
||||
*/
|
||||
public function redirect()
|
||||
{
|
||||
return Socialite::driver('google')->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Google OAuth callback
|
||||
*/
|
||||
public function callback()
|
||||
{
|
||||
try {
|
||||
$googleUser = Socialite::driver('google')->user();
|
||||
|
||||
// Find or create user
|
||||
$user = User::firstOrCreate(
|
||||
['email' => $googleUser->getEmail()],
|
||||
[
|
||||
'name' => $googleUser->getName(),
|
||||
'google_id' => $googleUser->getId(),
|
||||
'email_verified_at' => now(),
|
||||
// Set a random password so the row passes DB constraints; not used for login
|
||||
'password' => Str::random(32),
|
||||
]
|
||||
);
|
||||
|
||||
// Update Google ID if not already set
|
||||
if (!$user->google_id) {
|
||||
$user->update(['google_id' => $googleUser->getId()]);
|
||||
}
|
||||
|
||||
Auth::login($user, remember: true);
|
||||
|
||||
return redirect()->intended('/');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Google OAuth callback failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'exception' => $e,
|
||||
]);
|
||||
return redirect('/login')->with('error', 'Failed to authenticate with Google. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
Log::info('User logging out', [
|
||||
'user_id' => optional(Auth::user())->id,
|
||||
'email' => optional(Auth::user())->email,
|
||||
]);
|
||||
Auth::logout();
|
||||
request()->session()->invalidate();
|
||||
request()->session()->regenerateToken();
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\CustomOrder;
|
||||
use App\Models\CustomOrderFile;
|
||||
use App\Models\CustomOrderSpecification;
|
||||
use App\Models\AppSetting;
|
||||
use App\Models\PrintStock;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CustomOrderController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show custom orders index/list
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$customOrders = CustomOrder::where('user_id', auth()->id())
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return view('custom-orders.index', [
|
||||
'customOrders' => $customOrders,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show custom order creation form
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
$designFee = AppSetting::get('design_fee', 500);
|
||||
|
||||
return view('custom-orders.create', [
|
||||
'printStocks' => PrintStock::all(),
|
||||
'designFee' => $designFee,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store custom order and calculate quote
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
error_log('=== CUSTOM ORDER STORE METHOD CALLED (error_log) ===');
|
||||
error_log('Request method: ' . $request->method());
|
||||
error_log('Request path: ' . $request->path());
|
||||
error_log('Request all data: ' . json_encode($request->all()));
|
||||
$userId = auth()->id();
|
||||
error_log('User ID: ' . ($userId ?? 'NOT AUTHENTICATED'));
|
||||
|
||||
Log::warning('=== CUSTOM ORDER STORE METHOD CALLED ===');
|
||||
Log::warning('Request all data', $request->all());
|
||||
Log::warning('User ID', ['user_id' => auth()->id()]);
|
||||
|
||||
$validated = $request->validate([
|
||||
'type' => 'required|in:wallpaper,mural,fabric',
|
||||
'length' => 'nullable|numeric|min:0.1',
|
||||
'width' => 'nullable|numeric|min:0.1',
|
||||
'height' => 'nullable|numeric|min:0.1',
|
||||
'print_stock_id' => 'required|exists:print_stocks,id',
|
||||
'quantity' => 'required|integer|min:1',
|
||||
'customer_brief' => 'required|string|min:50|max:5000',
|
||||
'special_instructions' => 'nullable|string|max:1000',
|
||||
'library_discount' => 'boolean',
|
||||
'reference_images.*' => 'nullable|file|mimes:jpeg,png,jpg,gif,webp|max:5120',
|
||||
]);
|
||||
|
||||
Log::warning('Validation passed', $validated);
|
||||
|
||||
// Get design fee from settings
|
||||
$designFee = AppSetting::get('design_fee', 500);
|
||||
$libraryDiscountApplied = $request->boolean('library_discount', false);
|
||||
|
||||
// Deduct library discount if selected
|
||||
$finalDesignFee = $libraryDiscountApplied ? $designFee * 0.8 : $designFee;
|
||||
|
||||
// Get print stock for material cost calculation
|
||||
$printStock = PrintStock::find($validated['print_stock_id']);
|
||||
|
||||
// Calculate material cost based on type and dimensions
|
||||
$materialCost = $this->calculateMaterialCost(
|
||||
$validated['type'],
|
||||
$validated['length'] ?? 0,
|
||||
$validated['width'] ?? 0,
|
||||
$validated['height'] ?? 0,
|
||||
$validated['quantity'],
|
||||
$printStock
|
||||
);
|
||||
|
||||
// Total cost = material cost + design fee
|
||||
$totalCost = $materialCost + $finalDesignFee;
|
||||
|
||||
// Deposit is 20% of total, balance is 80%
|
||||
$depositAmount = $totalCost * 0.2;
|
||||
$balanceAmount = $totalCost * 0.8;
|
||||
|
||||
// Create custom order
|
||||
$customOrder = CustomOrder::create([
|
||||
'user_id' => auth()->id(),
|
||||
'type' => $validated['type'],
|
||||
'design_fee' => $designFee,
|
||||
'library_discount_applied' => $libraryDiscountApplied,
|
||||
'material_cost' => $materialCost,
|
||||
'total_cost' => $totalCost,
|
||||
'deposit_amount' => $depositAmount,
|
||||
'balance_amount' => $balanceAmount,
|
||||
'customer_brief' => $validated['customer_brief'],
|
||||
]);
|
||||
|
||||
// Create specifications
|
||||
CustomOrderSpecification::create([
|
||||
'custom_order_id' => $customOrder->id,
|
||||
'length' => $validated['length'] ?? null,
|
||||
'width' => $validated['width'] ?? null,
|
||||
'height' => $validated['height'] ?? null,
|
||||
'print_stock_id' => $validated['print_stock_id'],
|
||||
'quantity' => $validated['quantity'],
|
||||
'special_instructions' => $validated['special_instructions'] ?? null,
|
||||
]);
|
||||
|
||||
// Handle file uploads
|
||||
if ($request->hasFile('reference_images')) {
|
||||
foreach ($request->file('reference_images') as $file) {
|
||||
$path = $file->store('custom-orders/references', 'local');
|
||||
|
||||
CustomOrderFile::create([
|
||||
'custom_order_id' => $customOrder->id,
|
||||
'file_type' => 'reference_image',
|
||||
'file_path' => $path,
|
||||
'original_filename' => $file->getClientOriginalName(),
|
||||
'file_size' => $file->getSize(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'uploaded_by' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('custom-orders.show', $customOrder)->with('success', 'Custom order created successfully. Please review the quote and proceed with deposit payment.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show custom order detail with quote
|
||||
*/
|
||||
public function show(CustomOrder $customOrder): View
|
||||
{
|
||||
// Check authorization
|
||||
if ($customOrder->user_id !== auth()->id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('custom-orders.show', [
|
||||
'customOrder' => $customOrder,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process deposit payment
|
||||
*/
|
||||
public function depositPayment(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'custom_order_id' => 'required|exists:custom_orders,id',
|
||||
]);
|
||||
|
||||
$customOrder = CustomOrder::findOrFail($validated['custom_order_id']);
|
||||
|
||||
// Check authorization
|
||||
if ($customOrder->user_id !== auth()->id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// Check if already paid
|
||||
if ($customOrder->deposit_status === 'paid') {
|
||||
return redirect()->route('custom-orders.show', $customOrder)
|
||||
->with('info', 'Deposit already paid for this order.');
|
||||
}
|
||||
|
||||
// Initiate Yoco payment for deposit
|
||||
$yocoResponse = $this->initiateYocoPayment(
|
||||
amount: (int)($customOrder->deposit_amount * 100), // Convert to cents
|
||||
orderId: $customOrder->uuid,
|
||||
orderType: 'custom_deposit',
|
||||
description: "Deposit for Custom {$customOrder->type} Order #{$customOrder->order_number}"
|
||||
);
|
||||
|
||||
if (!$yocoResponse) {
|
||||
return redirect()->route('custom-orders.show', $customOrder)
|
||||
->with('error', 'Failed to initiate payment. Please try again.');
|
||||
}
|
||||
|
||||
return redirect($yocoResponse['checkout_url']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful deposit payment
|
||||
*/
|
||||
public function depositSuccess(CustomOrder $customOrder)
|
||||
{
|
||||
// Check authorization
|
||||
if ($customOrder->user_id !== auth()->id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// Update order status
|
||||
$customOrder->update([
|
||||
'deposit_status' => 'paid',
|
||||
'status' => 'submitted',
|
||||
]);
|
||||
|
||||
return view('custom-orders.deposit-success', [
|
||||
'customOrder' => $customOrder,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate material cost based on dimensions and print stock
|
||||
*/
|
||||
private function calculateMaterialCost($type, $length, $width, $height, $quantity, $printStock)
|
||||
{
|
||||
$costPerUnit = $printStock->cost_per_meter ?? $printStock->cost_per_m2 ?? 0;
|
||||
|
||||
if (!$costPerUnit) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$cost = 0;
|
||||
|
||||
if ($type === 'wallpaper' && $length > 0 && $width > 0) {
|
||||
// Area-based: length x width in meters
|
||||
$area = $length * $width;
|
||||
$cost = $area * $costPerUnit * $quantity;
|
||||
} elseif ($type === 'mural' && $width > 0 && $height > 0) {
|
||||
// Area-based: width x height in meters
|
||||
$area = $width * $height;
|
||||
$cost = $area * $costPerUnit * $quantity;
|
||||
} elseif ($type === 'fabric' && $length > 0) {
|
||||
// Linear: length in meters
|
||||
$cost = $length * $costPerUnit * $quantity;
|
||||
}
|
||||
|
||||
return round($cost, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate Yoco payment
|
||||
*/
|
||||
private function initiateYocoPayment($amount, $orderId, $orderType, $description)
|
||||
{
|
||||
$yocoSecret = config('services.yoco.secret_key');
|
||||
|
||||
if (!$yocoSecret) {
|
||||
Log::error('Yoco secret key not configured');
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'amount' => $amount,
|
||||
'currency' => 'ZAR',
|
||||
'successUrl' => route('yoco-custom-deposit-success', ['customOrder' => $orderId]),
|
||||
'failureUrl' => route('custom-orders.show', ['customOrder' => $orderId]),
|
||||
'cancelUrl' => route('custom-orders.show', ['customOrder' => $orderId]),
|
||||
'metadata' => [
|
||||
'order_uuid' => $orderId,
|
||||
'order_type' => $orderType,
|
||||
],
|
||||
'description' => $description,
|
||||
];
|
||||
|
||||
try {
|
||||
Log::info('Initiating Yoco payment', [
|
||||
'amount' => $amount,
|
||||
'orderId' => $orderId,
|
||||
'description' => $description
|
||||
]);
|
||||
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $yocoSecret,
|
||||
'Content-Type' => 'application/json',
|
||||
])->post('https://payments.yoco.com/api/checkouts', $payload);
|
||||
|
||||
Log::info('Yoco API response', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body()
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
Log::info('Yoco payment success', [
|
||||
'checkout_url' => $data['redirectUrl'] ?? 'N/A',
|
||||
'checkout_id' => $data['id'] ?? 'N/A'
|
||||
]);
|
||||
return [
|
||||
'checkout_url' => $data['redirectUrl'],
|
||||
'checkout_id' => $data['id'],
|
||||
];
|
||||
} else {
|
||||
Log::error('Yoco payment API error', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body()
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Yoco payment exception: ' . $e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user