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);
}
}
}