HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux ip-10-0-8-47 6.8.0-1021-aws #23~22.04.1-Ubuntu SMP Tue Dec 10 16:31:58 UTC 2024 aarch64
User: ubuntu (1000)
PHP: 8.1.2-1ubuntu2.22
Disabled: NONE
Upload Files
File: /var/www/javago-portal-updates/app/Http/Controllers/API/SquareWebHookController.php
<?php

namespace App\Http\Controllers\API;


use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Models\Cafe;
use App\Models\GroupCoffeeRun;
use App\Models\CafeMenu;
use App\Models\CafeMenuItem;
use App\Models\AddonSize;
use App\Models\AddonSizeCafeItem;
use App\Models\Size;
use App\Models\CafeMenuItemSize;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use App\Models\User;
use App\Models\Notification as NotificationModel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Kreait\Firebase\Factory;
use Kreait\Firebase\Messaging\CloudMessage;
use Kreait\Firebase\Messaging\Notification as FirebaseNotification;
use Carbon\Carbon;

class SquareWebHookController extends Controller
{

    public function neworderEvent(Request $request)
    {
        try {
            Log::info('Square Webhook Received:', $request->all());

            $type = $request->input('type');
            $object = $request->input('data.object');

            if ($type !== 'order.fulfillment.updated') {
                Log::info("Ignoring webhook type: {$type}");
                return response()->json(['message' => 'Webhook type ignored'], 200);
            }

            $orderData = $object['order_fulfillment_updated'] ?? null;
            $squareOrderId = $orderData['order_id'] ?? null;

            if (!$squareOrderId) {
                Log::warning('No Square order ID found in webhook.');
                return response()->json(['message' => 'No order_id found'], 400);
            }

            // Get all local orders for grouped Square order
            $orders = Order::where('square_order_id', $squareOrderId)->get();
            if ($orders->isEmpty()) {
                Log::warning("No local orders found for Square order ID: {$squareOrderId}");
                return response()->json(['message' => 'Orders not found'], 404);
            }

            $cafe = Cafe::find($orders->first()->cafe_id);
            if (!$cafe || !$cafe->square_access_token) {
                Log::warning("Cafe or Square Access Token not found for cafe_id: {$orders->first()->cafe_id}");
                return response()->json(['message' => 'Cafe or access token not found'], 404);
            }

            // Fetch order detail from Square
            $response = Http::withToken($cafe->square_access_token)
                ->get("https://connect.squareup.com/v2/orders/{$squareOrderId}");

            if (!$response->successful()) {
                Log::error("Failed to retrieve Square order. Status: {$response->status()}", $response->json());
                return response()->json(['message' => 'Failed to retrieve Square order'], 500);
            }

            $squareOrder = $response->json('order');
            Log::info("Square Order Detail Response:", $response->json());

            $orderLineItems = collect($squareOrder['line_items'] ?? []);
            $orderFulfillments = collect($squareOrder['fulfillments'] ?? []);
            $totalLineItems = $orderLineItems->count();

            // --- REFUND HANDLING ---
            $canceledFulfillments = $orderFulfillments->where('state', 'CANCELED');
            $matchedItems = [];

            foreach ($canceledFulfillments as $fulfillment) {
                foreach ($fulfillment['entries'] ?? [] as $entry) {
                    $lineItemUid = $entry['line_item_uid'] ?? null;
                    $quantity = (int) ($entry['quantity'] ?? 1);

                    $lineItem = $orderLineItems->firstWhere('uid', $lineItemUid);
                    if ($lineItem) {
                        $price = ($lineItem['base_price_money']['amount'] ?? 0) / 100;
                        $matchedItems[] = [
                            'item_name' => $lineItem['name'] ?? 'Unknown',
                            'item_amount' => $price,
                            'item_quantity' => $quantity,
                            'refund_amount' => $price * $quantity,
                            'fulfillment_uid' => $fulfillment['uid'],
                            'state' => 'CANCELED',
                        ];
                    }
                }
            }

            if (empty($matchedItems) && $canceledFulfillments->count() === 1 && $totalLineItems > 0) {
                foreach ($orderLineItems as $item) {
                    $quantity = (int) ($item['quantity'] ?? 1);
                    $price = ($item['base_price_money']['amount'] ?? 0) / 100;
                    $matchedItems[] = [
                        'item_name' => $item['name'] ?? 'Unknown',
                        'item_amount' => $price,
                        'item_quantity' => $quantity,
                        'refund_amount' => $price * $quantity,
                        'fulfillment_uid' => $canceledFulfillments->first()['uid'],
                        'state' => 'CANCELED',
                    ];
                }
            }

            // Apply matched refund items to each order
            foreach ($orders as $order) {
                $existingRefunded = json_decode($order->refunded_order_items_array ?? '[]', true);
                $existingKeys = array_map(fn($i) => $i['item_name'] . '|' . ($i['fulfillment_uid'] ?? ''), $existingRefunded);

                $newRefundedItems = [];
                $newRefundAmount = 0;

                foreach ($matchedItems as $item) {
                    $key = $item['item_name'] . '|' . ($item['fulfillment_uid'] ?? '');
                    if (!in_array($key, $existingKeys)) {
                        $newRefundedItems[] = $item;
                        $newRefundAmount += $item['refund_amount'];
                    }
                }

                if (!empty($newRefundedItems)) {
                    $order->refunded_order_items_array = json_encode(array_merge($existingRefunded, $newRefundedItems));
                    $order->refunded_amount = ($order->refunded_amount ?? 0) + $newRefundAmount;

                    $allRefundedCount = count($newRefundedItems) + count($existingRefunded);
                    if ($allRefundedCount === $totalLineItems) {
                        $order->refund_status = 'full_refund';
                        $order->is_full_order_cancelled = 1;
                        $order->save();
                        $this->sendFullRefundNotification($order, $newRefundedItems, $newRefundAmount);
                    } else {
                        $order->refund_status = 'partial_refund';
                        $order->save();
                        $this->sendPartialRefundNotification($order, $newRefundedItems, $newRefundAmount);
                    }
                }
            }

            // --- PICKUP HANDLING ---
            $completedPickups = $orderFulfillments->where('state', 'COMPLETED')->where('type', 'PICKUP');

            $processedItems = [];
            foreach ($completedPickups as $fulfillment) {
                foreach ($orderLineItems as $item) {
                    $price = ($item['base_price_money']['amount'] ?? 0) / 100;
                    $processedItems[] = [
                        'item_name' => $item['name'] ?? 'Unknown',
                        'item_amount' => $price,
                        'item_quantity' => (int) ($item['quantity'] ?? 1),
                        'fulfillment_uid' => $fulfillment['uid'],
                        'state' => 'COMPLETED',
                    ];
                }
            }

            foreach ($orders as $order) {
                $existingProcessed = json_decode($order->processed_order_items_array ?? '[]', true);
                $existingKeys = array_map(fn($i) => $i['item_name'] . '|' . ($i['fulfillment_uid'] ?? ''), $existingProcessed);

                $newProcessedItems = [];
                foreach ($processedItems as $item) {
                    $key = $item['item_name'] . '|' . ($item['fulfillment_uid'] ?? '');
                    if (!in_array($key, $existingKeys)) {
                        $newProcessedItems[] = $item;
                    }
                }

                if (!empty($newProcessedItems)) {
                    $order->processed_order_items_array = json_encode(array_merge($existingProcessed, $newProcessedItems));
                    $order->save();
                    $this->sendOrderCompletedNotification($order, $newProcessedItems);
                    Log::info("✅ Pickup items processed", ['order_id' => $order->id, 'items' => $newProcessedItems]);
                }
            }

            return response()->json(['message' => 'Webhook processed'], 200);
        } catch (\Exception $e) {
            Log::error('Error processing Square webhook:', ['error' => $e->getMessage()]);
            return response()->json(['message' => 'Internal Server Error'], 500);
        }
    }

    public function orderEvent(Request $request)
    {
        try {
            Log::info('Square Webhook Received:', $request->all());

            $type = $request->input('type');
            $object = $request->input('data.object');

            if ($type !== 'order.fulfillment.updated') {
                Log::info("Ignoring webhook type: {$type}");
                return response()->json(['message' => 'Webhook type ignored'], 200);
            }

            $orderData = $object['order_fulfillment_updated'] ?? null;
            $squareOrderId = $orderData['order_id'] ?? null;

            if (!$squareOrderId) {
                Log::warning('No Square order ID found in webhook.');
                return response()->json(['message' => 'No order_id found'], 400);
            }

            // Get all local orders for grouped Square order
            $orders = Order::where('square_order_id', $squareOrderId)->get();
            if ($orders->isEmpty()) {
                Log::warning("No local orders found for Square order ID: {$squareOrderId}");
                return response()->json(['message' => 'Orders not found'], 404);
            }

            $cafe = Cafe::find($orders->first()->cafe_id);
            if (!$cafe || !$cafe->square_access_token) {
                Log::warning("Cafe or Square Access Token not found for cafe_id: {$orders->first()->cafe_id}");
                return response()->json(['message' => 'Cafe or access token not found'], 404);
            }

            // Fetch order detail from Square
            $response = Http::withToken($cafe->square_access_token)
                ->get("https://connect.squareup.com/v2/orders/{$squareOrderId}");

            if (!$response->successful()) {
                Log::error("Failed to retrieve Square order. Status: {$response->status()}", $response->json());
                return response()->json(['message' => 'Failed to retrieve Square order'], 500);
            }

            $squareOrder = $response->json('order');
            Log::info("Square Order Detail Response:", $response->json());

            $orderLineItems = collect($squareOrder['line_items'] ?? []);
            $orderFulfillments = collect($squareOrder['fulfillments'] ?? []);

            // --- REFUND HANDLING ---
            $canceledFulfillments = $orderFulfillments->where('state', 'CANCELED');
            Log::info("cancelledFulfilments", $canceledFulfillments->toArray());
            // Process refunds for each order individually
            foreach ($orders as $order) {
                $matchedItems = [];

                foreach ($canceledFulfillments as $fulfillment) {
                    foreach ($fulfillment['entries'] ?? [] as $entry) {
                        $lineItemUid = $entry['line_item_uid'] ?? null;
                        $quantity = (int) ($entry['quantity'] ?? 1);

                        $lineItem = $orderLineItems->firstWhere('uid', $lineItemUid);
                        if ($lineItem) {
                            $itemNote = $lineItem['note'] ?? '';

                            // Extract Order ID from note (e.g., "User: Nikhil | Order ID: 1352")
                            if (preg_match('/Order ID:\s*(\d+)/', $itemNote, $matches)) {
                                $noteOrderId = $matches[1];

                                // Check if this item belongs to the current order
                                if ($noteOrderId == $order->id) {
                                    $price = ($lineItem['base_price_money']['amount'] ?? 0) / 100;
                                    $matchedItems[] = [
                                        'item_name' => $lineItem['name'] ?? 'Unknown',
                                        'item_amount' => $price,
                                        'item_quantity' => $quantity,
                                        'refund_amount' => $price * $quantity,
                                        'fulfillment_uid' => $fulfillment['uid'],
                                        'state' => 'CANCELED',
                                    ];
                                }
                            }
                        }
                    }
                }

                // Handle case where no specific entries but fulfillment is canceled
                if (empty($matchedItems) && $canceledFulfillments->count() === 1) {
                    foreach ($orderLineItems as $item) {
                        $itemNote = $item['note'] ?? '';

                        // Extract Order ID from note
                        if (preg_match('/Order ID:\s*(\d+)/', $itemNote, $matches)) {
                            $noteOrderId = $matches[1];

                            // Check if this item belongs to the current order
                            if ($noteOrderId == $order->id) {
                                $quantity = (int) ($item['quantity'] ?? 1);
                                $price = ($item['base_price_money']['amount'] ?? 0) / 100;
                                $matchedItems[] = [
                                    'item_name' => $item['name'] ?? 'Unknown',
                                    'item_amount' => $price,
                                    'item_quantity' => $quantity,
                                    'refund_amount' => $price * $quantity,
                                    'fulfillment_uid' => $canceledFulfillments->first()['uid'],
                                    'state' => 'CANCELED',
                                ];
                            }
                        }
                    }
                }

                // Apply matched refund items to this specific order
                if (!empty($matchedItems)) {
                    $existingRefunded = json_decode($order->refunded_order_items_array ?? '[]', true);
                    $existingKeys = array_map(fn($i) => $i['item_name'] . '|' . ($i['fulfillment_uid'] ?? ''), $existingRefunded);

                    $newRefundedItems = [];
                    $newRefundAmount = 0;

                    foreach ($matchedItems as $item) {
                        $key = $item['item_name'] . '|' . ($item['fulfillment_uid'] ?? '');
                        if (!in_array($key, $existingKeys)) {
                            $newRefundedItems[] = $item;
                            $newRefundAmount += $item['refund_amount'];
                        }
                    }

                    if (!empty($newRefundedItems)) {
                        $order->refunded_order_items_array = json_encode(array_merge($existingRefunded, $newRefundedItems));
                        $order->refunded_amount = ($order->refunded_amount ?? 0) + $newRefundAmount;

                        // Count items in this specific order that are refunded
                        $orderItems = json_decode($order->order_items_array ?? '[]', true);
                        $allRefundedCount = count($newRefundedItems) + count($existingRefunded);
                        $orderItemCount = count($orderItems);

                        if ($allRefundedCount >= $orderItemCount) {
                            $order->refund_status = 'full_refund';
                            $order->is_full_order_cancelled = 1;
                            $order->save();
                            $this->sendFullRefundNotification($order, $newRefundedItems, $newRefundAmount);
                        } else {
                            $order->refund_status = 'partial_refund';
                            $order->save();
                            $this->sendPartialRefundNotification($order, $newRefundedItems, $newRefundAmount);
                        }

                        Log::info("Refund processed for order", [
                            'order_id' => $order->id,
                            'user_id' => $order->user_id,
                            'refunded_items' => $newRefundedItems,
                            'refund_amount' => $newRefundAmount
                        ]);
                    }
                }
            }

            // -- Mark order as Ready --

            $readyPickups = $orderFulfillments->where('state', 'PREPARED')->where('type', 'PICKUP');
            Log::info("ReadyPickups ", $readyPickups->toArray());
            // Process pickups for each order individually
            foreach ($orders as $order) {
                $processedItems = [];

                foreach ($readyPickups as $fulfillment) {
                    foreach ($orderLineItems as $item) {
                        $itemNote = $item['note'] ?? '';

                        // Extract Order ID from note
                        if (preg_match('/Order ID:\s*(\d+)/', $itemNote, $matches)) {
                            $noteOrderId = $matches[1];

                            // Check if this item belongs to the current order
                            if ($noteOrderId == $order->id) {
                                $price = ($item['base_price_money']['amount'] ?? 0) / 100;
                                $processedItems[] = [
                                    'item_name' => $item['name'] ?? 'Unknown',
                                    'item_amount' => $price,
                                    'item_quantity' => (int) ($item['quantity'] ?? 1),
                                    'fulfillment_uid' => $fulfillment['uid'],
                                    'state' => 'PREPARED',
                                ];
                            }
                        }
                    }
                }

                if (!empty($processedItems)) {
                    $existingProcessed = json_decode($order->processed_order_items_array ?? '[]', true);
                    $existingKeys = array_map(fn($i) => $i['item_name'] . '|' . ($i['fulfillment_uid'] ?? ''), $existingProcessed);

                    $newProcessedItems = [];
                    foreach ($processedItems as $item) {
                        $key = $item['item_name'] . '|' . ($item['fulfillment_uid'] ?? '');
                        if (!in_array($key, $existingKeys)) {
                            $newProcessedItems[] = $item;
                        }
                    }

                    if (!empty($newProcessedItems)) {
                        $order->processed_order_items_array = json_encode(array_merge($existingProcessed, $newProcessedItems));
                        $order->save();
                        $this->sendOrderReadyNotification($order, $newProcessedItems);
                        Log::info("✅ Pickup items ready", [
                            'order_id' => $order->id,
                            'user_id' => $order->user_id,
                            'items' => $newProcessedItems
                        ]);
                    }
                }
            }

            // --- PICKUP HANDLING ---
            $completedPickups = $orderFulfillments->where('state', 'COMPLETED')->where('type', 'PICKUP');
            Log::info("completedPickups ", $completedPickups->toArray());

            // Process pickups for each order individually
            foreach ($orders as $order) {
                $processedItems = [];

                foreach ($completedPickups as $fulfillment) {
                    foreach ($orderLineItems as $item) {
                        $itemNote = $item['note'] ?? '';

                        // Extract Order ID from note
                        if (preg_match('/Order ID:\s*(\d+)/', $itemNote, $matches)) {
                            $noteOrderId = $matches[1];

                            // Check if this item belongs to the current order
                            if ($noteOrderId == $order->id) {

                                $price = ($item['base_price_money']['amount'] ?? 0) / 100;
                                $processedItems[] = [
                                    'item_name' => $item['name'] ?? 'Unknown',
                                    'item_amount' => $price,
                                    'item_quantity' => (int) ($item['quantity'] ?? 1),
                                    'fulfillment_uid' => $fulfillment['uid'],
                                    'state' => 'COMPLETED',
                                ];

                            }
                        }
                    }
                }


                Log::info("completed state processed items", ['order' => $processedItems]);
                if (!empty($processedItems)) {
                    $existingProcessed = json_decode('[]', true);
                    // $existingProcessed = json_decode($order->processed_order_items_array ?? '[]', true);
                    // $existingKeys = array_map(fn($i) => $i['item_name'] . '|' . ($i['fulfillment_uid'] ?? ''), $existingProcessed);

                    // $newProcessedItems = [];
                    // foreach ($processedItems as $item) {
                    //     $key = $item['item_name'] . '|' . ($item['fulfillment_uid'] ?? '');
                    //     if (!in_array($key, $existingKeys)) {
                    //         $newProcessedItems[] = $item;
                    //     }
                    // }

                    // if (!empty($newProcessedItems)) {
                    $order->processed_order_items_array = json_encode(array_merge($existingProcessed, $processedItems));
                    $order->save();
                    $this->sendOrderCompletedNotification($order, $processedItems);
                    Log::info("✅ Pickup items processed", [
                        'order_id' => $order->id,
                        'user_id' => $order->user_id,
                        'items' => $processedItems
                    ]);
                    // }
                }
            }

            return response()->json(['message' => 'Webhook processed'], 200);
        } catch (\Exception $e) {
            Log::error('Error processing Square webhook:', ['error' => $e->getMessage()]);
            return response()->json(['message' => 'Internal Server Error'], 500);
        }
    }

    public function oldorderEvent(Request $request)
    {
        try {
            Log::info('Square Webhook Received:', $request->all());

            $type = $request->input('type');
            $object = $request->input('data.object');

            // Only handle order.fulfillment.updated
            if ($type !== 'order.fulfillment.updated') {
                Log::info("Ignoring webhook type: {$type}");
                return response()->json(['message' => 'Webhook type ignored'], 200);
            }

            $orderData = $object['order_fulfillment_updated'] ?? null;
            $squareOrderId = $orderData['order_id'] ?? null;

            if (!$squareOrderId) {
                Log::warning('No Square order ID found in webhook.');
                return response()->json(['message' => 'No order_id found'], 400);
            }

            // Fetch local order
            $order = Order::where('square_order_id', $squareOrderId)->first();
            if (!$order) {
                Log::warning("No order found for Square order ID: {$squareOrderId}");
                return response()->json(['message' => 'Order not found'], 404);
            }

            $cafe = Cafe::find($order->cafe_id);
            if (!$cafe || !$cafe->square_access_token) {
                Log::warning("Cafe or Square Access Token not found for cafe_id: {$order->cafe_id}");
                return response()->json(['message' => 'Cafe or access token not found'], 404);
            }

            // Get full order details from Square
            $response = Http::withToken($cafe->square_access_token)
                ->get("https://connect.squareup.com/v2/orders/{$squareOrderId}");

            if (!$response->successful()) {
                Log::error("Failed to retrieve Square order. Status: {$response->status()}", $response->json());
                return response()->json(['message' => 'Failed to retrieve Square order'], 500);
            }

            $squareOrder = $response->json('order');
            Log::info("Square Order Detail Response:", $response->json());

            $orderLineItems = collect($squareOrder['line_items'] ?? []);
            $orderFulfillments = collect($squareOrder['fulfillments'] ?? []);
            $totalLineItems = $orderLineItems->count();

            // ------------------- REFUND HANDLING -------------------
            $canceledFulfillments = $orderFulfillments->filter(fn($f) => $f['state'] === 'CANCELED');
            $matchedItems = [];

            foreach ($canceledFulfillments as $fulfillment) {
                $entries = $fulfillment['entries'] ?? [];

                foreach ($entries as $entry) {
                    $lineItemUid = $entry['line_item_uid'] ?? null;
                    $quantity = (int) ($entry['quantity'] ?? 1);

                    $lineItem = $orderLineItems->firstWhere('uid', $lineItemUid);
                    if ($lineItem) {
                        $price = ($lineItem['base_price_money']['amount'] ?? 0) / 100;
                        $matchedItems[] = [
                            'item_name' => $lineItem['name'] ?? 'Unknown',
                            'item_amount' => $price,
                            'item_quantity' => $quantity,
                            'refund_amount' => $price * $quantity,
                            'fulfillment_uid' => $fulfillment['uid'],
                            'state' => $fulfillment['state'],
                        ];
                    }
                }
            }

            if (empty($matchedItems) && $canceledFulfillments->count() === 1 && $totalLineItems > 0) {
                foreach ($orderLineItems as $item) {
                    $quantity = (int) ($item['quantity'] ?? 1);
                    $price = ($item['base_price_money']['amount'] ?? 0) / 100;
                    $matchedItems[] = [
                        'item_name' => $item['name'] ?? 'Unknown',
                        'item_amount' => $price,
                        'item_quantity' => $quantity,
                        'refund_amount' => $price * $quantity,
                        'fulfillment_uid' => $canceledFulfillments->first()['uid'] ?? null,
                        'state' => 'CANCELED',
                    ];
                }
            }

            if (!empty($matchedItems)) {
                $existingRefunded = json_decode($order->refunded_order_items_array ?? '[]', true);
                $existingKeys = array_map(fn($i) => $i['item_name'] . '|' . ($i['fulfillment_uid'] ?? ''), $existingRefunded);

                $newRefundedItems = [];
                $newRefundAmount = 0;

                foreach ($matchedItems as $item) {
                    $key = $item['item_name'] . '|' . ($item['fulfillment_uid'] ?? '');
                    if (!in_array($key, $existingKeys)) {
                        $newRefundedItems[] = $item;
                        $newRefundAmount += $item['refund_amount'];
                    }
                }

                if (!empty($newRefundedItems)) {
                    $order->refunded_order_items_array = json_encode(array_merge($existingRefunded, $newRefundedItems));
                    $order->refunded_amount = ($order->refunded_amount ?? 0) + $newRefundAmount;

                    if (count($newRefundedItems) + count($existingRefunded) === $totalLineItems) {
                        $order->refund_status = 'full_refund';
                        $order->is_full_order_cancelled = 1;
                        $order->save();
                        $this->sendFullRefundNotification($order, $newRefundedItems, $newRefundAmount);
                    } else {
                        $order->refund_status = 'partial_refund';
                        $order->save();
                        $this->sendPartialRefundNotification($order, $newRefundedItems, $newRefundAmount);
                    }
                }
            }

            // ------------------- PICKUP HANDLING -------------------
            $completedPickups = $orderFulfillments->filter(function ($f) {
                return $f['state'] === 'COMPLETED' && ($f['type'] ?? '') === 'PICKUP';
            });

            $processedItems = [];
            foreach ($completedPickups as $fulfillment) {
                $uid = $fulfillment['uid'] ?? null;

                // Square often omits entries, fallback to full item list
                foreach ($orderLineItems as $item) {
                    $price = ($item['base_price_money']['amount'] ?? 0) / 100;
                    $processedItems[] = [
                        'item_name' => $item['name'] ?? 'Unknown',
                        'item_amount' => $price,
                        'item_quantity' => (int) ($item['quantity'] ?? 1),
                        'fulfillment_uid' => $uid,
                        'state' => 'COMPLETED',
                    ];
                }
            }

            if (!empty($processedItems)) {
                $existingProcessed = json_decode($order->processed_order_items_array ?? '[]', true);
                $existingKeys = array_map(fn($i) => $i['item_name'] . '|' . ($i['fulfillment_uid'] ?? ''), $existingProcessed);

                $newProcessedItems = [];
                foreach ($processedItems as $item) {
                    $key = $item['item_name'] . '|' . ($item['fulfillment_uid'] ?? '');
                    if (!in_array($key, $existingKeys)) {
                        $newProcessedItems[] = $item;
                    }
                }

                if (!empty($newProcessedItems)) {
                    $order->processed_order_items_array = json_encode(array_merge($existingProcessed, $newProcessedItems));
                    $order->save();

                    $this->sendOrderCompletedNotification($order, $newProcessedItems);
                    Log::info("✅ Pickup items processed", ['order_id' => $order->id, 'items' => $newProcessedItems]);
                }
            }

            return response()->json(['message' => 'Webhook processed'], 200);
        } catch (\Exception $e) {
            Log::error('Error processing Square webhook:', ['error' => $e->getMessage()]);
            return response()->json(['message' => 'Internal Server Error'], 500);
        }
    }


    private function sendFullRefundNotification($order, $refundedItems, $totalRefund)
    {
        try {
            $user = $order->user;
            if (!$user) {
                Log::warning("User not found for full refund notification", ['order_id' => $order->id]);
                return;
            }

            $title = 'Order Cancelled & Refunded';
            $body = "Your order has been cancelled and a refund of £" . number_format($totalRefund, 2) . " has been processed.";

            // Create notification in database
            NotificationModel::create([
                'sender_id' => $order->cafe_id,
                'receiver_id' => $user->id,
                'reference_id' => $order->id,
                'notification_type' => 9, // refund
                'is_read' => 0,
                'message' => $body,
                'created_at' => now()->timestamp,
                'updated_at' => now()->timestamp
            ]);

            // Send FCM notification
            $this->sendFCMNotification($user, $title, $body, [
                'order_id' => (string) $order->id,
                'type' => 'full_refund',
                'refund_amount' => $totalRefund
            ]);

            Log::info("📱 Full refund notification sent", [
                'user_id' => $user->id,
                'order_id' => $order->id,
                'refund_amount' => $totalRefund
            ]);

        } catch (\Exception $e) {
            Log::error("Full refund notification failed", [
                'order_id' => $order->id,
                'error' => $e->getMessage()
            ]);
        }
    }

    private function sendPartialRefundNotification($order, $refundedItems, $totalRefund)
    {
        try {
            $user = $order->user;
            if (!$user) {
                Log::warning("User not found for partial refund notification", ['order_id' => $order->id]);
                return;
            }
            $itemNames = collect($refundedItems)->pluck('item_name')->toArray();

            // Implode into comma-separated string
            $itemsList = implode(', ', $itemNames);
            $title = 'Partial Refund Processed';
            $body = "{$itemsList} " . (count($itemNames) > 1 ? 'are' : 'is') . " not available. Your refund of £" . number_format($totalRefund, 2) . " has been processed.";

            // Create notification in database
            NotificationModel::create([
                'sender_id' => $order->cafe_id,
                'receiver_id' => $user->id,
                'reference_id' => $order->id,
                'notification_type' => 9, // refund
                'is_read' => 0,
                'message' => $body,
                'created_at' => now()->timestamp,
                'updated_at' => now()->timestamp
            ]);

            // Send FCM notification
            $this->sendFCMNotification($user, $title, $body, [
                'order_id' => (string) $order->id,
                'type' => 'partial_refund',
                'refund_amount' => $totalRefund,
                'refunded_items' => implode(', ', $itemNames)
            ]);

            Log::info("📱 Partial refund notification sent", [
                'user_id' => $user->id,
                'order_id' => $order->id,
                'refund_amount' => $totalRefund,
                'refunded_items' => $itemsList
            ]);

        } catch (\Exception $e) {
            Log::error("Partial refund notification failed", [
                'order_id' => $order->id,
                'error' => $e->getMessage()
            ]);
        }
    }

    private function sendOrderCompletedNotification($order, $processedItems = [])
    {
        try {
            // CRITICAL FIX: Double-check that this is not a fully refunded order
            $orderItems = json_decode($order->order_item_array, true) ?? [];
            $refundedItems = array_filter($orderItems, fn($item) => !empty($item['refunded']));

            if (count($refundedItems) === count($orderItems) && count($orderItems) > 0) {
                Log::info("🚫 Preventing completion notification for fully refunded order", ['order_id' => $order->id]);
                return;
            }

            $user = $order->user;
            if (!$user) {
                Log::warning("User not found for order completion", ['order_id' => $order->id]);
                return;
            }

            $title = 'Order Completed';
            $body = 'Your order has been completed!';

            // Mark order as completed
            $order->order_completed = 2;
            $order->status = 2;
            $order->save();
            $groupCoffeeRun = GroupCoffeeRun::where('request_unique_id', $order->group_coffee_run_id)
                ->where('request_created_by', $user->id)
                ->first();

            if ($order->is_individual_order == 0 && $groupCoffeeRun) {

                // Create notification in database
                NotificationModel::create([
                    'sender_id' => $order->cafe_id,
                    'receiver_id' => $user->id,
                    'reference_id' => $order->id,
                    'notification_type' => 101, // order completed
                    'is_read' => 0,
                    'message' => $body,
                    'created_at' => now()->timestamp,
                    'updated_at' => now()->timestamp
                ]);

                // Send FCM notification
                $this->sendFCMNotification($user, $title, $body, [
                    'order_id' => (string) $order->id,
                    'group_coffee_run_id' => $groupCoffeeRun->id,
                    'request_unique_id' => $groupCoffeeRun->request_unique_id,
                    'group_id' => $groupCoffeeRun->group_id,
                    'type' => 'order_completed'
                ]);

                Log::info("✅ Order completion notification sent", [
                    'user_id' => $user->id,
                    'order_id' => $order->id,
                    'processed_items_count' => count($processedItems)
                ]);
            } else {
                // Create notification in database
                NotificationModel::create([
                    'sender_id' => $order->cafe_id,
                    'receiver_id' => $user->id,
                    'reference_id' => $order->id,
                    'notification_type' => 101, // order completed
                    'is_read' => 0,
                    'message' => $body,
                    'created_at' => now()->timestamp,
                    'updated_at' => now()->timestamp
                ]);

                // Send FCM notification
                $this->sendFCMNotification($user, $title, $body, [
                    'order_id' => (string) $order->id,
                    'type' => 'order_completed'
                ]);

                Log::info("✅ Order completion notification sent", [
                    'user_id' => $user->id,
                    'order_id' => $order->id,
                    'processed_items_count' => count($processedItems)
                ]);
            }

            // Handle referral rewards for first-time users
            $this->handleFirstOrderRewards($order, $user);

        } catch (\Exception $e) {
            Log::error("Order completion notification failed", [
                'order_id' => $order->id,
                'error' => $e->getMessage()
            ]);
        }
    }

    private function sendOrderReadyNotification($order, $processedItems = [])
    {
        try {
            // CRITICAL FIX: Double-check that this is not a fully refunded order
            $orderItems = json_decode($order->order_item_array, true) ?? [];
            $refundedItems = array_filter($orderItems, fn($item) => !empty($item['refunded']));

            if (count($refundedItems) === count($orderItems) && count($orderItems) > 0) {
                Log::info("🚫 Preventing completion notification for fully refunded order", ['order_id' => $order->id]);
                return;
            }

            $user = $order->user;
            if (!$user) {
                Log::warning("User not found for order completion", ['order_id' => $order->id]);
                return;
            }

            $title = 'Order Ready';
            $body = 'Your order is Ready! Please collect it.';

            // Mark order as completed
            $order->order_completed = 1;
            $order->save();
            $groupCoffeeRun = GroupCoffeeRun::where('request_unique_id', $order->group_coffee_run_id)
                ->where('request_created_by', $user->id)
                ->first();

            if ($order->is_individual_order == 0 && $groupCoffeeRun) {

                // Create notification in database
                NotificationModel::create([
                    'sender_id' => $order->cafe_id,
                    'receiver_id' => $user->id,
                    'reference_id' => $order->id,
                    'notification_type' => 10, // order completed
                    'is_read' => 0,
                    'message' => $body,
                    'created_at' => now()->timestamp,
                    'updated_at' => now()->timestamp
                ]);

                // Send FCM notification
                $this->sendFCMNotification($user, $title, $body, [
                    'order_id' => (string) $order->id,
                    'group_coffee_run_id' => $groupCoffeeRun->id,
                    'request_unique_id' => $groupCoffeeRun->request_unique_id,
                    'group_id' => $groupCoffeeRun->group_id,
                    'type' => 'order_completed'
                ]);

                Log::info("✅ Order ready notification sent", [
                    'user_id' => $user->id,
                    'order_id' => $order->id,
                    'processed_items_count' => count($processedItems)
                ]);
            } else {
                // Create notification in database
                NotificationModel::create([
                    'sender_id' => $order->cafe_id,
                    'receiver_id' => $user->id,
                    'reference_id' => $order->id,
                    'notification_type' => 10, // order completed
                    'is_read' => 0,
                    'message' => $body,
                    'created_at' => now()->timestamp,
                    'updated_at' => now()->timestamp
                ]);

                // Send FCM notification
                $this->sendFCMNotification($user, $title, $body, [
                    'order_id' => (string) $order->id,
                    'type' => 'order_ready'
                ]);

                Log::info("✅ Order ready notification sent", [
                    'user_id' => $user->id,
                    'order_id' => $order->id,
                    'processed_items_count' => count($processedItems)
                ]);
            }

            // Handle referral rewards for first-time users
            // $this->handleFirstOrderRewards($order, $user);

        } catch (\Exception $e) {
            Log::error("Order ready notification failed", [
                'order_id' => $order->id,
                'error' => $e->getMessage()
            ]);
        }
    }

    private function handleFirstOrderRewards($order, $user)
    {
        try {
            $existingOrders = Order::where('user_id', $user->id)
                ->where('id', '!=', $order->id)
                ->where('order_completed', 2)
                ->count();

            if ($existingOrders == 0) {
                $user->remaining_awarded_stamps = ($user->remaining_awarded_stamps ?? 0) + 2;
                $user->save();

                Log::info("First order rewards given", [
                    'user_id' => $user->id,
                    'order_id' => $order->id,
                    'stamps_awarded' => 2
                ]);

                if (!empty($user->referral_code_used)) {
                    $referrer = User::where('referral_code', $user->referral_code_used)->first();
                    if ($referrer) {
                        $referrer->remaining_awarded_stamps = ($referrer->remaining_awarded_stamps ?? 0) + 2;
                        $referrer->save();

                        Log::info("Referral rewards given", [
                            'referrer_id' => $referrer->id,
                            'referred_user_id' => $user->id,
                            'order_id' => $order->id,
                            'stamps_awarded' => 2
                        ]);
                    }
                }
            }
        } catch (\Exception $e) {
            Log::error("First order rewards failed", [
                'order_id' => $order->id,
                'user_id' => $user->id,
                'error' => $e->getMessage()
            ]);
        }
    }

    private function sendFCMNotification($user, $title, $body, $data = [])
    {
        try {
            if (!$user->fcm_token) {
                Log::info("No FCM token for user", ['user_id' => $user->id]);
                return;
            }

            $firebaseConfig = config('firebase.credentials.file') ?: storage_path('app/java-go-380509-firebase-adminsdk-236oo-44f6190a2d.json');

            $messaging = (new Factory)->withServiceAccount($firebaseConfig)->createMessaging();

            $messageBuilder = CloudMessage::withTarget('token', $user->fcm_token)
                ->withNotification(FirebaseNotification::create($title, $body));

            if (!empty($data)) {
                $messageBuilder = $messageBuilder->withData($data);
            }

            $repsomes = $messaging->send($messageBuilder);
            log::info("FCM RESPONSE", [$repsomes]);
            Log::info("FCM notification sent successfully", [
                'title' => $title,
                'user_id' => $user->id,
                'data' => $data
            ]);

        } catch (\Throwable $e) {
            Log::error("FCM notification failed", [
                'title' => $title,
                'user_id' => $user->id ?? 'unknown',
                'error' => $e->getMessage()
            ]);
        }
    }

    public function refundEvent(Request $request)
    {
        // This method is currently empty, but you can implement refund handling logic here
        Log::info('Square Refund Event Received:', $request->all());
        return response()->json(['message' => 'Refund event received'], 200);

    }

    //handle Square webhook for item events
    public function handleSquareWebhook(Request $request)
    {
        \Log::info("Square webhook payload", ['payload' => $request->all()]);

        $merchantId = $request->input('merchant_id');

        if (empty($merchantId)) {
            \Log::error("Merchant ID not found in webhook payload");
            return response()->json(['message' => 'Merchant ID missing'], 400);
        }

        $cafe = Cafe::where('square_merchant_id', $merchantId)->first();
        if (!$cafe || empty($cafe->square_access_token)) {
            \Log::error("Cafe or Square access token not found for merchant ID: {$merchantId}");
            return response()->json(['message' => 'Cafe or Square token not found'], 404);
        }

        $squareToken = $cafe->square_access_token;

        // ✅ Fetch all catalog objects
        $response = Http::withToken($squareToken)
            ->get('https://connect.squareup.com/v2/catalog/list', [
                'types' => 'ITEM,MODIFIER_LIST,CATEGORY',
            ]);

        if (!$response->successful()) {
            \Log::error("Failed to fetch catalog", ['response' => $response->body()]);
            return response()->json(['message' => 'Failed to fetch catalog'], 500);
        }

        $objects = $response->json('objects') ?? [];

        // ✅ Collect all Square item IDs
        $squareItemIds = collect($objects)
            ->where('type', 'ITEM')
            ->pluck('id')
            ->toArray();

        // ✅ Process objects for add/update
        foreach ($objects as $object) {
            switch ($object['type']) {
                case 'ITEM':
                    $this->addOrUpdateItemFromSquare($object, $cafe);
                    break;
                case 'MODIFIER_LIST':
                    $this->addOrUpdateModifierFromSquare($object, $cafe);
                    break;
                case 'CATEGORY':
                    $this->addOrUpdateCategoryFromSquare($object, $cafe);
                    break;
                default:
                    break;
            }
        }

        // ✅ Mark local items as deleted if no longer in Square
        $localItems = CafeMenuItem::where('cafe_id', $cafe->id)
            ->where('item_deleted_at', 0)
            ->get();

        foreach ($localItems as $localItem) {
            if (!in_array($localItem->square_item_id, $squareItemIds)) {
                \Log::info("Marking item as deleted in local DB", ['item_id' => $localItem->id]);

                $localItem->update(['item_deleted_at' => 1]);

                // Delete modifier list from Square if exists
                if (!empty($localItem->square_modifier_list_id)) {
                    $deleteResp = Http::withToken($squareToken)
                        ->post('https://connect.squareup.com/v2/catalog/batch-delete', [
                            'object_ids' => [$localItem->square_modifier_list_id],
                        ]);

                    \Log::info("Deleted modifier list from Square", [
                        'modifier_list_id' => $localItem->square_modifier_list_id,
                        'response' => $deleteResp->body()
                    ]);

                    $localItem->update(['square_modifier_list_id' => null]);
                }
            }
        }

        return response()->json(['message' => 'Catalog processed successfully'], 200);
    }




    public function addOrUpdateItemFromSquare($object, $cafe)
    {
        $itemData = $object['item_data'];
        \Log::info("Processing Square item", ['item_data' => $itemData]);
        $squareItemId = $object['id'];

        $localItem = CafeMenuItem::where('square_item_id', $squareItemId)->first();
        // $categoryId = $itemData['categories'][0]['id'] ?? null;
        $categoryId = null;
        if (!empty($itemData['categories']) && is_array($itemData['categories'])) {
            $categoryId = $itemData['categories'][0]['id'] ?? null;
        } else {
            $categoryFound = CafeMenu::where('id', $localItem->cafe_menu_id)->first();
            $categoryId = $categoryFound ? $categoryFound->square_category_id : null;
        }

        $dbcategory = CafeMenu::where('square_category_id', $categoryId)->where('cafe_id', $cafe->id)->first();
        \Log::info("Found category for item", ['category_id' => $categoryId, 'dbcategory' => $dbcategory]);

        // Get first variation price if available
        if (!$categoryId) {
            $cafeMenuId = $localItem->cafe_menu_id;
        } else {
            // Else try to find category from Square
            $dbcategory = CafeMenu::where('square_category_id', $categoryId)
                ->where('cafe_id', $cafe->id)
                ->first();
            \Log::info("Found category for item", ['category_id' => $categoryId, 'dbcategory' => $dbcategory]);
            $cafeMenuId = $dbcategory ? $dbcategory->id : null;
        }
        $itemName = $itemData['name'] ?? $localItem->item_name;
        if (isset($itemData['description']) && !empty($itemData['description'])) {
            $itemDescription = $itemData['description'];
        } elseif ($localItem) {
            $itemDescription = $localItem->item_description; // retain existing
        } else {
            $itemDescription = '';
        }
        $firstVariationPrice = 0;
        if (!empty($itemData['variations'])) {
            $firstVariation = $itemData['variations'][0]['item_variation_data'] ?? null;
            if ($firstVariation && !empty($firstVariation['price_money']['amount'])) {
                $firstVariationPrice = ($firstVariation['price_money']['amount'] ?? 0) / 100;
            }
        }
        $data = [
            'item_name' => $itemName,
            'item_description' => $itemDescription,
            'cafe_id' => $cafe->id,
            'item_image_id' => 1,
            'cafe_menu_id' => $cafeMenuId, // You might need to map category IDs separately
            'item_price' => $firstVariationPrice, //here update the first price from variations
            'item_type' => 1,
            'status' => 1,
            'created_at' => Carbon::now()->timestamp,
            'updated_at' => Carbon::now()->timestamp,
        ];

        if (!$localItem) {
            $data['square_item_id'] = $squareItemId;
            $itemId = CafeMenuItem::insertGetId($data);
        } else {
            $localItem->update($data);
            $itemId = $localItem->id;
        }

        // Handle variations (sizes and prices)
        if (!empty($itemData['variations'])) {
            foreach ($itemData['variations'] as $variation) {
                $variationData = $variation['item_variation_data'];
                $sizeName = strtolower($variationData['name'] ?? 'medium');

                $size = Size::whereRaw('LOWER(size_name) = ?', [$sizeName])->first();
                if (!$size) {
                    // You can optionally create a new size record
                    $size = Size::whereRaw('LOWER(size_name) = ?', 'medium')->first();
                }

                CafeMenuItemSize::updateOrCreate(
                    ['item_id' => $itemId, 'size_id' => $size->id],
                    [
                        'item_size_price' => ($variationData['price_money']['amount'] ?? 0) / 100,
                        'updated_at' => Carbon::now()->timestamp,
                    ]
                );
            }
        }

        // Handle modifiers if assigned
        if (!empty($itemData['modifier_list_info'])) {
            foreach ($itemData['modifier_list_info'] as $modInfo) {
                $modifierListId = $modInfo['modifier_list_id'];
                // Fetch full modifier list from Square
                $modifierResponse = Http::withToken($cafe->square_access_token)
                    ->get("https://connect.squareup.com/v2/catalog/object/{$modifierListId}");

                if ($modifierResponse->successful()) {
                    $modifierObject = $modifierResponse->json('object');
                    $this->addOrUpdateModifierFromSquare($modifierObject, $cafe, $itemId);
                } else {
                    \Log::error("Failed to fetch modifier list", ['modifierListId' => $modifierListId]);
                }

                // AddonSizeCafeItem::updateOrCreate(
                //     ['item_id' => $itemId, 'square_modifier_id' => $modifierListId],
                //     [
                //         'addon_size_price' => 0,
                //         'created_at' => now(),
                //         'updated_at' => now(),
                //     ]
                // );
            }
        }
    }

    public function addOrUpdateModifierFromSquare($object, $cafe, $itemId = null)
    {
        $modifierListData = $object['modifier_list_data'];
        $squareModifierListId = $object['id'];

        \Log::info("Processing Square modifier list", ['modifier_list_data' => $modifierListData]);

        // Update the item with modifier_list_id if itemId provided
        if ($itemId) {
            $localItem = CafeMenuItem::find($itemId);
            if ($localItem) {
                $localItem->update(['square_modifier_list_id' => $squareModifierListId]);
            }
        }

        if (!empty($modifierListData['modifiers'])) {
            foreach ($modifierListData['modifiers'] as $modifier) {
                $modifierData = $modifier['modifier_data'];
                $addonName = $modifierData['name'] ?? 'Unnamed';
                $squareAddonId = $modifier['id'];
                $addonPrice = ($modifierData['price_money']['amount'] ?? 0) / 100;

                // Find or create AddonSize
                $addonItem = AddonSize::whereRaw('LOWER(addon_size_name) = ?', [strtolower($addonName)])->where('cafe_id', $cafe->id)->first();
                if ($addonItem) {
                    // $addonItem = AddonSize::create([
                    //     'addon_size_name' => ucfirst(strtolower($addonName)),
                    // ]);
                    // Create or update in AddonSizeCafeItem
                    AddonSizeCafeItem::updateOrCreate(
                        [
                            'addon_size_id' => $addonItem->id,
                            'item_id' => $itemId,
                        ],
                        [
                            'addon_size_price' => $addonPrice,
                            'created_at' => Carbon::now()->timestamp,
                            'updated_at' => Carbon::now()->timestamp,
                        ]
                    );
                }



            }
        }
    }

    public function addOrUpdateCategoryFromSquare($object, $cafe)
    {
        $categoryData = $object['category_data'];
        $squareCategoryId = $object['id'];
        $name = $categoryData['name'] ?? 'Unnamed Category';

        $category = CafeMenu::whereRaw('LOWER(menu_name) = ?', [$name])->first();
        if (!$category) {
            $category = CafeMenu::whereRaw('LOWER(menu_name) = ?', 'other')->first();
        }
        $data = [
            'menu_name' => $name,
            'updated_at' => now(),
        ];

        if ($category) {
            $category->update($data);
        }
    }
}