get('cart', []); if (empty($cart)) { return redirect()->route('cart')->with('error', 'Your cart is empty!'); } $items = []; $total = 0; foreach ($cart as $itemKey => $cartItem) { // Handle both old and new cart formats if (is_array($cartItem)) { $productId = $cartItem['product_id'] ?? null; $type = $cartItem['type'] ?? 'wallpaper'; $printStockId = $cartItem['print_stock_id'] ?? null; } else { $productId = $itemKey; $type = 'wallpaper'; $printStockId = null; } if ($productId) { $product = Product::find($productId); if ($product) { $quantity = is_array($cartItem) ? ($cartItem['quantity'] ?? 1) : $cartItem; $stockCost = 0; $stock = null; // Get print stock if available if ($printStockId) { $stock = $product->printStocks()->find($printStockId); if ($stock) { $stockCost = ($type === 'wallpaper') ? $stock->cost_per_meter : $stock->cost_per_m2; } } // Calculate price based on stock cost only (no base design cost) $basePrice = $stockCost; if ($type === 'wallpaper') { $length = is_array($cartItem) ? ($cartItem['length'] ?? 0) : 0; $subtotal = $basePrice * $length * $quantity; } elseif ($type === 'mural') { $width = is_array($cartItem) ? ($cartItem['width'] ?? 0) : 0; $height = is_array($cartItem) ? ($cartItem['height'] ?? 0) : 0; $m2 = $width * $height; $subtotal = $basePrice * $m2 * $quantity; } else { $subtotal = $basePrice * $quantity; } $total += $subtotal; $items[] = [ 'key' => $itemKey, 'product' => $product, 'stock' => $stock, 'quantity' => $quantity, 'type' => $type, 'length' => is_array($cartItem) ? ($cartItem['length'] ?? null) : null, 'width' => is_array($cartItem) ? ($cartItem['width'] ?? null) : null, 'height' => is_array($cartItem) ? ($cartItem['height'] ?? null) : null, 'subtotal' => $subtotal ]; } } } return view('checkout', [ 'items' => $items, 'total' => $total, 'itemCount' => count($cart) ]); } public function process(Request $request) { $request->validate([ 'customer_name' => 'required|string|max:255', 'customer_email' => 'required|email', 'customer_phone' => 'required|string|max:20', 'shipping_address' => 'required|string|max:500', 'notes' => 'nullable|string|max:500' ]); $cart = session()->get('cart', []); if (empty($cart)) { return redirect()->route('cart')->with('error', 'Your cart is empty!'); } // Calculate total and validate stock $total = 0; $orderItems = []; foreach ($cart as $itemKey => $cartItem) { // Handle both old and new cart formats if (is_array($cartItem)) { $productId = $cartItem['product_id'] ?? null; $quantity = $cartItem['quantity'] ?? 1; $type = $cartItem['type'] ?? 'wallpaper'; $printStockId = $cartItem['print_stock_id'] ?? null; } else { $productId = $itemKey; $quantity = $cartItem; $type = 'wallpaper'; $printStockId = null; } $product = Product::find($productId); if (!$product) { return redirect()->route('cart')->with('error', 'Product not found!'); } if ($product->stock < $quantity) { return redirect()->route('cart')->with('error', "Insufficient stock for {$product->name}"); } $stockCost = 0; $stock = null; // Get print stock if available if ($printStockId) { $stock = $product->printStocks()->find($printStockId); if ($stock) { $stockCost = ($type === 'wallpaper') ? $stock->cost_per_meter : $stock->cost_per_m2; } } // Calculate price based on stock cost only (no base design cost) $basePrice = $stockCost; if ($type === 'wallpaper') { $length = is_array($cartItem) ? ($cartItem['length'] ?? 0) : 0; $subtotal = $basePrice * $length * $quantity; } elseif ($type === 'mural') { $width = is_array($cartItem) ? ($cartItem['width'] ?? 0) : 0; $height = is_array($cartItem) ? ($cartItem['height'] ?? 0) : 0; $m2 = $width * $height; $subtotal = $basePrice * $m2 * $quantity; } else { $subtotal = $basePrice * $quantity; } $total += $subtotal; $orderItems[$itemKey] = [ 'product_id' => $productId, 'quantity' => $quantity, 'price' => $product->price, 'stock_cost' => $stockCost, 'print_stock_id' => $printStockId, 'type' => $type, 'length' => is_array($cartItem) ? ($cartItem['length'] ?? null) : null, 'width' => is_array($cartItem) ? ($cartItem['width'] ?? null) : null, 'height' => is_array($cartItem) ? ($cartItem['height'] ?? null) : null, 'subtotal' => $subtotal ]; } // Create order $orderNumber = 'ORD-' . date('Ymd') . '-' . strtoupper(uniqid()); $orderData = [ 'user_id' => auth()->check() ? auth()->id() : null, 'order_number' => $orderNumber, 'total' => $total, 'status' => 'pending', 'payment_method' => 'yoco', 'payment_status' => 'pending', 'customer_name' => $request->input('customer_name'), 'customer_email' => $request->input('customer_email'), 'customer_phone' => $request->input('customer_phone'), 'shipping_address' => $request->input('shipping_address'), ]; if ($request->filled('notes')) { $orderData['notes'] = $request->input('notes'); } $order = Order::create($orderData); // Create order items foreach ($orderItems as $itemKey => $data) { $product = Product::find($data['product_id']); OrderItem::create([ 'order_id' => $order->uuid, 'product_id' => $data['product_id'], 'quantity' => $data['quantity'], 'price' => $data['price'], 'print_stock_id' => $data['print_stock_id'], 'type' => $data['type'], 'length' => $data['length'], 'width' => $data['width'], 'height' => $data['height'] ]); } // Store order UUID in session for payment session(['pending_order_uuid' => $order->uuid]); // Redirect to Yoco payment return redirect()->route('yoco-payment', ['order' => $order->uuid]); } public function success(Order $order) { // Authorization: only allow viewing own orders or admin if (auth()->check() && auth()->user()->id !== $order->user_id && !auth()->user()->is_admin) { abort(403, 'Unauthorized access to this order.'); } // For guest orders, verify via session if (!auth()->check() && session('pending_order_uuid') !== $order->uuid) { abort(403, 'Unauthorized access to this order.'); } return view('order-success', ['order' => $order]); } public function history() { $orders = Order::orderBy('created_at', 'desc')->get(); return view('order-history', ['orders' => $orders]); } public function yocoPayment(Order $order) { // Verify this is a pending payment if ($order->payment_status !== 'pending') { abort(400, 'This order has already been paid.'); } // Create Yoco checkout $secretKey = config('services.yoco.secret_key'); $mode = config('services.yoco.mode'); // Check if API key is configured if (empty($secretKey) || $secretKey === 'sk_test_your_key_here') { return redirect()->route('checkout')->with('error', 'Payment gateway not configured. Please contact support.'); } $baseUrl = $mode === 'live' ? 'https://payments.yoco.com/api/checkouts' : 'https://payments.yoco.com/api/checkouts'; $checkoutData = [ 'amount' => (int)($order->total * 100), // Amount in cents 'currency' => 'ZAR', 'successUrl' => route('yoco-success', ['order' => $order->uuid]), 'cancelUrl' => route('yoco-cancel', ['order' => $order->uuid]), 'failureUrl' => route('yoco-failure', ['order' => $order->uuid]), 'metadata' => [ 'order_uuid' => $order->uuid, 'order_number' => $order->order_number, ], ]; try { $response = \Illuminate\Support\Facades\Http::withHeaders([ 'Authorization' => 'Bearer ' . $secretKey, 'Content-Type' => 'application/json', ])->post($baseUrl, $checkoutData); if ($response->successful()) { $checkout = $response->json(); return redirect($checkout['redirectUrl']); } else { \Log::error('Yoco API Error', [ 'status' => $response->status(), 'body' => $response->body() ]); return redirect()->route('checkout')->with('error', 'Unable to initialize payment: ' . $response->body()); } } catch (\Exception $e) { \Log::error('Yoco Payment Exception', ['message' => $e->getMessage()]); return redirect()->route('checkout')->with('error', 'Payment error: ' . $e->getMessage()); } } public function yocoSuccess(Order $order) { // Verify order is pending payment if ($order->payment_status === 'paid') { return redirect()->route('order-success', ['order' => $order]) ->with('success', 'Payment was already processed for this order.'); } // 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(); } // Clear pending order from session session()->forget('pending_order_uuid'); session()->forget('cart'); return redirect()->route('order-success', ['order' => $order]) ->with('success', 'Payment successful! Your order has been confirmed.'); } public function yocoCancel(Order $order) { return redirect()->route('checkout') ->with('error', 'Payment was cancelled. Your order is still pending.'); } public function yocoFailure(Order $order) { $order->update([ 'payment_status' => 'failed', ]); return redirect()->route('checkout') ->with('error', 'Payment failed. Please try again or use a different payment method.'); } public function yocoWebhook(Request $request) { // Verify webhook signature $payload = $request->getContent(); $signature = $request->header('X-Yoco-Signature'); // Process webhook event $event = $request->all(); 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(); } } } } return response()->json(['status' => 'success']); } }