File: /var/www/javago-portal-updates/app/Http/Controllers/API/SquareController.php
<?php
namespace App\Http\Controllers\api;
use App\Http\Controllers\Controller;
use App\Models\AddonSizeCafeItem;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use App\Models\Cafe;
use App\Models\Size as Sizes;
use Illuminate\Support\Facades\DB;
use App\Models\CafeMenuItemSize;
use App\Models\CafeMenuItem;
use App\Models\AddonSize;
use App\Models\CafeMenu;
class SquareController extends Controller
{
// Step 1: Redirect to Square OAuth
public function redirectToSquare(Request $request)
{
$cafe = Cafe::find($request->user->id);
if (!$cafe) {
return response()->json(['error' => 'Cafe not found'], 404);
}
$clientId = config('services.square.client_id');
$redirectUri = route('square.callback');
$scope = 'MERCHANT_PROFILE_READ PAYMENTS_READ PAYMENTS_WRITE ORDERS_WRITE ORDERS_READ ITEMS_READ ITEMS_WRITE CUSTOMERS_READ CUSTOMERS_WRITE';
// Encode state with cafe_id
$state = base64_encode(json_encode(['cafe_id' => $cafe->id]));
$squareEnv = config('services.square.environment', 'sandbox');
$authorizeUrl = $squareEnv == 'sandbox'
? 'https://connect.squareupsandbox.com/oauth2/authorize'
: 'https://connect.squareup.com/oauth2/authorize';
$url = $authorizeUrl . '?' . http_build_query([
'client_id' => $clientId,
'scope' => $scope,
'session' => 'false',
'state' => $state,
'redirect_uri' => $redirectUri,
]);
// Make scope space-encoded
$url = str_replace('+', '%20', $url);
return response()->json(['url' => $url], 200);
}
public function handleCallback(Request $request)
{
// Decode the state param to get cafe_id
$stateData = json_decode(base64_decode($request->state), true);
if (!$stateData || !isset($stateData['cafe_id'])) {
return response()->json(['error' => 'Invalid state'], 400);
}
$cafe = Cafe::find($stateData['cafe_id']);
if (!$cafe) {
return response()->json(['error' => 'Cafe not found'], 404);
}
$squareEnv = config('services.square.environment', 'sandbox');
$tokenUrl = $squareEnv == 'sandbox'
? 'https://connect.squareupsandbox.com/oauth2/token'
: 'https://connect.squareup.com/oauth2/token';
$response = Http::withHeaders([
'Content-Type' => 'application/json',
])->post($tokenUrl, [
'client_id' => config('services.square.client_id'),
'client_secret' => config('services.square.client_secret'),
'code' => $request->code,
'grant_type' => 'authorization_code',
'redirect_uri' => route('square.callback'),
]);
if ($response->failed()) {
\Log::error('Square OAuth token exchange failed', [
'status' => $response->status(),
'body' => $response->body(),
]);
return response()->json([
'error' => 'Square connection failed. Please try again.',
'details' => $response->json(),
], 500);
}
$data = $response->json();
// Now fetch merchant locations using the access token
$locationsResponse = Http::withHeaders([
'Authorization' => 'Bearer ' . $data['access_token'],
'Accept' => 'application/json',
])->get(
$squareEnv == 'sandbox'
? 'https://connect.squareupsandbox.com/v2/locations'
: 'https://connect.squareup.com/v2/locations'
);
if ($locationsResponse->failed()) {
\Log::error('Failed to fetch Square locations', [
'status' => $locationsResponse->status(),
'body' => $locationsResponse->body(),
]);
return response()->json([
'error' => 'Failed to retrieve locations for merchant.',
], 500);
}
$locations = $locationsResponse->json()['locations'] ?? [];
// Save the first location ID or all as JSON — adjust as per your DB schema
$locationIds = collect($locations)->pluck('id')->toArray();
// Check if this Square merchant is already linked to another cafe
$existingCafe = Cafe::where('square_merchant_id', $data['merchant_id'])
->first();
if ($existingCafe) {
return response()->json(
'This Square account is already connected to another cafe!'
,
409
); // Conflict
}
$cafe->update([
'square_access_token' => $data['access_token'],
'square_refresh_token' => $data['refresh_token'],
'square_merchant_id' => $data['merchant_id'],
'square_location_id' => json_encode($locationIds),
'square_onboarding_completed' => 1
]);
return response()->json([
'message' => 'Thanks! Your Square account connected successfully.',
]);
}
public function syncMenuToSquare(Request $request)
{
try {
$cafe = Cafe::find($request->user->id);
if (!$cafe) {
return response()->json(['error' => 'Cafe not found'], 404);
}
if (empty($cafe->square_access_token)) {
return response()->json(['error' => 'Square account not connected'], 400);
}
$menuItemIds = DB::table('cafe_menu_items')
->where('cafe_id', $cafe->id)
->where('item_deleted_at', 0)
->pluck('id');
if ($menuItemIds->isEmpty()) {
return response()->json(['error' => 'No menu items to sync'], 404);
}
$itemObjects = [];
$objectIdMap = [];
$modifierListIdMap = [];
foreach ($menuItemIds as $itemId) {
$itemData = $this->getItemDetail($itemId);
if (!$itemData) {
continue;
}
// Get Square Category ID
$menu = DB::table('cafe_menus')->where('id', $itemData['cafe_menu_id'])->first();
$squareCategoryId = $menu->square_category_id ?? null;
// Validate category exists
if (!empty($squareCategoryId)) {
$catResponse = Http::withToken($cafe->square_access_token)
->get("https://connect.squareup.com/v2/catalog/object/{$squareCategoryId}");
if (!$catResponse->successful() || isset($catResponse->json()['errors'])) {
\Log::warning("Invalid Square category for menu ID {$menu->id}");
$squareCategoryId = null;
} else {
$catData = $catResponse->json()['object'] ?? null;
if (!isset($catData['type']) || $catData['type'] !== 'CATEGORY') {
\Log::warning("Fetched object is not a CATEGORY", ['object' => $catData]);
$squareCategoryId = null;
}
}
}
$squareItemId = $itemData['square_item_id'] ?? null;
$validSquareId = null;
$itemVersion = null;
if ($squareItemId) {
$existingSquare = $this->getSquareObjectNew($squareItemId, $cafe->square_access_token);
if ($existingSquare && empty($existingSquare['errors'])) {
$validSquareId = $squareItemId;
$itemVersion = $existingSquare['object']['version'] ?? null;
}
}
$itemObjectId = "#ITEM_{$itemId}_" . uniqid();
$variationObjects = [];
if (!empty($itemData['item_size_prices'])) {
foreach ($itemData['item_size_prices'] as $size) {
$price = floatval($size->item_size_price ?? 0);
if ($price <= 0)
continue;
$variationId = "#VARIATION_{$itemId}_{$size->size_id}_" . uniqid();
$sizeData = Sizes::find($size->size_id);
$sizeName = $sizeData->size_name ?? 'Regular';
$variationObjects[] = [
'type' => 'ITEM_VARIATION',
'id' => $variationId,
'item_variation_data' => [
'item_id' => $validSquareId ?: $itemObjectId,
'name' => $sizeName,
'pricing_type' => 'FIXED_PRICING',
'price_money' => [
'amount' => (int) ($price * 100),
'currency' => 'GBP',
],
'present_at_all_locations' => true,
],
];
}
} else {
$defaultPrice = floatval($itemData['item_price'] ?? 0);
if ($defaultPrice <= 0)
continue;
$variationObjects[] = [
'type' => 'ITEM_VARIATION',
'id' => "#VARIATION_{$itemId}_default_" . uniqid(),
'item_variation_data' => [
'item_id' => $validSquareId ?: $itemObjectId,
'name' => 'Regular',
'pricing_type' => 'FIXED_PRICING',
'price_money' => [
'amount' => (int) ($defaultPrice * 100),
'currency' => 'GBP',
],
'present_at_all_locations' => true,
],
];
}
// Modifier setup
$existingModListId = $itemData['square_modifier_list_id'] ?? null;
$itemModifierListId = $existingModListId ?: "#MODLIST_{$itemId}_" . uniqid();
$modifierListVersion = null;
if ($existingModListId) {
$modObj = $this->getSquareObjectNew($existingModListId, $cafe->square_access_token);
if ($modObj && empty($modObj['errors'])) {
$modifierListVersion = $modObj['object']['version'] ?? null;
} else {
$itemModifierListId = "#MODLIST_{$itemId}_" . uniqid();
}
}
$modifiers = [];
foreach ($itemData['optionSizeCafeItems'] as $addon) {
$addonSizeId = $addon['addon_size_id'];
$addonPrice = floatval($addon['addon_size_price'] ?? 0);
$modifiers[] = [
'type' => 'MODIFIER',
'id' => "#OPTION_{$addonSizeId}_" . uniqid(),
'modifier_data' => [
'name' => $addon['addon_size_name'] ?? "Addon {$addonSizeId}",
'price_money' => [
'amount' => (int) ($addonPrice * 100),
'currency' => 'GBP',
],
],
];
}
$modifierListInfo = [];
if (!empty($modifiers)) {
$modifierListObject = [
'type' => 'MODIFIER_LIST',
'id' => $itemModifierListId,
'modifier_list_data' => [
'name' => $itemData['item_name'] . " Addons",
'modifiers' => $modifiers,
'present_at_all_locations' => true,
],
];
if ($modifierListVersion) {
$modifierListObject['version'] = $modifierListVersion;
}
$modifierListInfo[] = [
'modifier_list_id' => $itemModifierListId,
'enabled' => true,
];
$itemObjects[] = $modifierListObject;
$modifierListIdMap[$itemId] = $itemModifierListId;
}
// Preserve existing description if missing locally
$finalDescription = $itemData['item_description'];
if (!$finalDescription && $validSquareId) {
$existingItem = $this->getSquareObjectNew($validSquareId, $cafe->square_access_token);
$finalDescription = $existingItem['object']['item_data']['description'] ?? '';
}
$itemPayload = [
'type' => 'ITEM',
'id' => $validSquareId ?: $itemObjectId,
'item_data' => [
'name' => $itemData['item_name'],
'description' => $finalDescription,
'abbreviation' => strtoupper(substr($itemData['item_name'], 0, 3)),
'product_type' => 'REGULAR',
'present_at_all_locations' => true,
'modifier_list_info' => $modifierListInfo,
],
];
// NEW: Set categories using the complete format Square expects
if (!empty($squareCategoryId)) {
$itemPayload['item_data']['categories'] = [
[
'id' => $squareCategoryId
]
];
// Also set the reporting category
$itemPayload['item_data']['reporting_category'] = [
'id' => $squareCategoryId
];
\Log::info('Setting categories array for item', [
'item_id' => $itemId,
'item_name' => $itemData['item_name'],
'category_id' => $squareCategoryId
]);
}
if ($itemVersion) {
$itemPayload['version'] = $itemVersion;
}
$itemObjects = array_merge($itemObjects, $variationObjects);
$itemObjects[] = $itemPayload;
$objectIdMap[$itemId] = $validSquareId ?: $itemObjectId;
}
// Chunked Square upsert
$chunks = array_chunk($itemObjects, 500);
$allMappings = [];
foreach ($chunks as $chunkIndex => $chunk) {
$payload = [
'idempotency_key' => uniqid('menu_sync_', true),
'batches' => [['objects' => $chunk]],
];
\Log::info('Sending batch to Square', [
'chunk_index' => $chunkIndex,
'objects_count' => count($chunk),
'idempotency_key' => $payload['idempotency_key']
]);
$response = Http::withToken($cafe->square_access_token)
->post("https://connect.squareup.com/v2/catalog/batch-upsert", $payload);
if (!$response->successful()) {
$errorMessage = "Square sync failed for chunk {$chunkIndex}: " . $response->body();
\Log::error($errorMessage, [
'response' => $response->json(),
'status' => $response->status()
]);
throw new \Exception($errorMessage);
}
$responseData = $response->json();
\Log::info('Square batch response', [
'chunk_index' => $chunkIndex,
'id_mappings_count' => count($responseData['id_mappings'] ?? []),
'has_errors' => isset($responseData['errors'])
]);
if (isset($responseData['errors'])) {
\Log::error('Square API returned errors', ['errors' => $responseData['errors']]);
throw new \Exception("Square API errors: " . json_encode($responseData['errors']));
}
$allMappings = array_merge($allMappings, $responseData['id_mappings'] ?? []);
}
// Update local database with Square IDs
foreach ($allMappings as $map) {
if (!empty($map['client_object_id']) && !empty($map['object_id'])) {
foreach ($objectIdMap as $localId => $clientId) {
if ($clientId === $map['client_object_id']) {
DB::table('cafe_menu_items')->where('id', $localId)->update([
'square_item_id' => $map['object_id'],
]);
\Log::info('Updated item Square ID', [
'local_id' => $localId,
'square_id' => $map['object_id']
]);
}
}
foreach ($modifierListIdMap as $localId => $modClientId) {
if ($modClientId === $map['client_object_id']) {
DB::table('cafe_menu_items')->where('id', $localId)->update([
'square_modifier_list_id' => $map['object_id'],
]);
\Log::info('Updated modifier list Square ID', [
'local_id' => $localId,
'square_modifier_id' => $map['object_id']
]);
}
}
}
}
return response()->json([
'success' => true,
'message' => 'Menu synced to Square successfully',
'items_processed' => count($objectIdMap),
'id_mappings' => count($allMappings)
]);
} catch (\Exception $e) {
\Log::error('Sync to Square failed', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'cafe_id' => $cafe->id ?? null
]);
return response()->json(['error' => 'Failed to sync menu: ' . $e->getMessage()], 500);
}
}
public function oldsyncMenuToSquare(Request $request)
{
try {
$cafe = Cafe::find($request->user->id);
if (!$cafe) {
return response()->json(['error' => 'Cafe not found'], 404);
}
if (empty($cafe->square_access_token)) {
return response()->json(['error' => 'Square account not connected'], 400);
}
$menuItemIds = DB::table('cafe_menu_items')
->where('cafe_id', $cafe->id)
->where('item_deleted_at', 0)
->pluck('id');
if ($menuItemIds->isEmpty()) {
return response()->json(['error' => 'No menu items to sync'], 404);
}
$itemObjects = [];
$objectIdMap = [];
$modifierListIdMap = [];
foreach ($menuItemIds as $itemId) {
$itemData = $this->getItemDetail($itemId);
if (!$itemData) {
continue;
}
$menu = DB::table('cafe_menus')->where('id', $itemData['cafe_menu_id'])->first();
$squareCategoryId = $menu->square_category_id ?? null;
// $squareCategoryId = $itemData['square_category_id'] ?? null;
if (!empty($squareCategoryId)) {
$squareCatResponse = Http::withToken($cafe->square_access_token)
->get("https://connect.squareup.com/v2/catalog/object/{$squareCategoryId}");
if (!$squareCatResponse->successful() || isset($squareCatResponse->json()['errors'])) {
\Log::warning("Category not found on Square for menu ID {$menu->id}, ignoring category for this item.");
$squareCategoryId = null;
}
\Log::warning("Category found on Square for menu ID {$menu->id}, {$squareCatResponse->body()}");
}
$squareItemId = $itemData['square_item_id'] ?? null;
$validSquareId = null;
$itemVersion = null;
if ($squareItemId) {
$squareObj = $this->getSquareObjectNew($squareItemId, $cafe->square_access_token);
if ($squareObj && empty($squareObj['errors'])) {
$validSquareId = $squareItemId;
$itemVersion = $squareObj['object']['version'] ?? null;
}
}
$itemObjectId = "#ITEM_{$itemId}_" . uniqid();
$variationObjects = [];
if (!empty($itemData['item_size_prices'])) {
foreach ($itemData['item_size_prices'] as $size) {
$price = floatval($size->item_size_price ?? 0);
if ($price <= 0)
continue;
$variationId = "#VARIATION_{$itemId}_{$size->size_id}_" . uniqid();
$sizeData = Sizes::find($size->size_id);
if (!$sizeData) {
\Log::warning("Size not found for ID {$size->size_id}, skipping variation.");
$sizeName = 'Regular';
} else {
$sizeName = $sizeData->size_name ?? "Size {$size->size_id}";
}
$variationObjects[] = [
'type' => 'ITEM_VARIATION',
'id' => $variationId,
'item_variation_data' => [
'item_id' => $validSquareId ?: $itemObjectId,
'name' => $sizeName,
'pricing_type' => 'FIXED_PRICING',
'price_money' => [
'amount' => (int) ($price * 100),
'currency' => 'GBP',
],
'present_at_all_locations' => true,
],
];
}
} else {
$defaultPrice = floatval($itemData['item_price'] ?? 0);
if ($defaultPrice <= 0)
continue;
$variationId = "#VARIATION_{$itemId}_default_" . uniqid();
$variationObjects[] = [
'type' => 'ITEM_VARIATION',
'id' => $variationId,
'item_variation_data' => [
'item_id' => $validSquareId ?: $itemObjectId,
'name' => 'Regular',
'pricing_type' => 'FIXED_PRICING',
'price_money' => [
'amount' => (int) ($defaultPrice * 100),
'currency' => 'GBP',
],
'present_at_all_locations' => true,
],
];
}
// ✅ Create separate modifier list per item
$existingModifierListId = $itemData['square_modifier_list_id'] ?? null;
\Log::info("existingModifierListId", ['existingModifierListId' => $existingModifierListId]);
$modifierListVersion = null;
if (!empty($existingModifierListId)) {
$squareObj = $this->getSquareObjectNew($existingModifierListId, $cafe->square_access_token);
if ($squareObj && empty($squareObj['errors'])) {
$modifierListVersion = $squareObj['object']['version'] ?? null;
$itemModifierListId = $existingModifierListId;
} else {
$itemModifierListId = "#MODLIST_{$itemId}_" . uniqid();
}
} else {
$itemModifierListId = "#MODLIST_{$itemId}_" . uniqid();
}
$modifiers = [];
if (!empty($itemData['optionSizeCafeItems'])) {
foreach ($itemData['optionSizeCafeItems'] as $addon) {
$addonSizeId = $addon['addon_size_id'];
$addonPrice = floatval($addon['addon_size_price'] ?? 0);
$modifierOptionId = "#OPTION_{$addonSizeId}_" . uniqid();
$modifiers[] = [
'type' => 'MODIFIER',
'id' => $modifierOptionId,
'modifier_data' => [
'name' => $addon['addon_size_name'] ?? "Addon {$addonSizeId}",
'price_money' => [
'amount' => (int) ($addonPrice * 100),
'currency' => 'GBP',
],
],
];
}
}
$modifierListObject = null;
$modifierListInfo = [];
if (!empty($modifiers)) {
$modifierListObject = [
'type' => 'MODIFIER_LIST',
'id' => $itemModifierListId,
'modifier_list_data' => [
'name' => $itemData['item_name'] . " Addons",
'modifiers' => $modifiers,
'present_at_all_locations' => true,
],
];
if (!empty($modifierListVersion)) {
$modifierListObject['version'] = $modifierListVersion;
}
$modifierListInfo[] = [
'modifier_list_id' => $itemModifierListId,
'enabled' => true,
];
$modifierListIdMap[$itemId] = $itemModifierListId;
}
// Try to preserve the existing Square description if local one is null
$finalDescription = '';
if (isset($itemData['item_description']) && $itemData['item_description'] !== null) {
$finalDescription = $itemData['item_description'];
} elseif ($validSquareId) {
// fetch existing description from Square
$existingSquareItem = $this->getSquareObjectNew($validSquareId, $cafe->square_access_token);
if (!empty($existingSquareItem['object']['item_data']['description'])) {
$finalDescription = $existingSquareItem['object']['item_data']['description'];
}
}
$itemObject = [
'type' => 'ITEM',
'id' => $validSquareId ?: $itemObjectId,
'version' => $itemVersion ?? null,
'item_data' => [
'name' => $itemData['item_name'],
'description' => $finalDescription,
'abbreviation' => strtoupper(substr($itemData['item_name'], 0, 3)),
'product_type' => 'REGULAR',
'present_at_all_locations' => true,
'modifier_list_info' => $modifierListInfo,
'category_id' => $squareCategoryId,
],
];
if ($modifierListObject) {
$itemObjects[] = $modifierListObject;
}
$itemObjects = array_merge($itemObjects, $variationObjects);
$itemObjects[] = $itemObject;
$objectIdMap[$itemId] = $validSquareId ?: $itemObjectId;
}
\Log::info("Final catalog object count before chunking", ['count' => count($itemObjects)]);
$chunks = array_chunk($itemObjects, 500);
$allMappings = [];
foreach ($chunks as $chunk) {
$payload = [
'idempotency_key' => uniqid('menu_sync_', true),
'batches' => [
['objects' => $chunk],
],
];
\Log::info("Sending chunk", ['count' => count($chunk)]);
$response = Http::withToken($cafe->square_access_token)
->post("https://connect.squareup.com/v2/catalog/batch-upsert", $payload);
\Log::info("Square API Response", [
'status' => $response->status(),
'successful' => $response->successful(),
'body' => $response->body(),
]);
if (!$response->successful()) {
throw new \Exception("Square batch upsert failed: " . $response->body());
}
$mappings = $response->json()['id_mappings'] ?? [];
$allMappings = array_merge($allMappings, $mappings);
}
foreach ($allMappings as $map) {
if (!empty($map['client_object_id']) && !empty($map['object_id'])) {
foreach ($objectIdMap as $localId => $clientId) {
if ($clientId == $map['client_object_id']) {
DB::table('cafe_menu_items')->where('id', $localId)->update([
'square_item_id' => $map['object_id'],
]);
}
}
foreach ($modifierListIdMap as $localId => $modClientId) {
if ($modClientId == $map['client_object_id']) {
DB::table('cafe_menu_items')->where('id', $localId)->update([
'square_modifier_list_id' => $map['object_id'],
]);
}
}
}
}
return response()->json(['success' => true, 'message' => 'Menu synced to Square successfully']);
} catch (\Exception $e) {
return response()->json(['error' => 'Failed to sync menu: ' . $e->getMessage()], 500);
}
}
public function deleteItemAndModifiersOnSquare(Request $request, $itemId)
{
$cafe = Cafe::find($request->user->id);
if (!$cafe) {
return response()->json(['error' => 'Cafe not found'], 404);
}
if (empty($cafe->square_access_token)) {
return response()->json(['error' => 'Square account not connected'], 400);
}
$itemDetails = $this->getItemDetail($itemId);
if (!$itemDetails || !$itemDetails['square_item_id']) {
return ['success' => false, 'message' => 'Item or Square item ID not found'];
}
$squareItemId = $itemDetails['square_item_id'];
$squareToken = $cafe->square_access_token;
// --- Use your logic for modifier list ID ---
$existingModifierListId = $itemDetails['square_modifier_list_id'] ?? null;
\Log::info("existingModifierListId", ['existingModifierListId' => $existingModifierListId]);
$modifierListVersion = null;
$itemModifierListId = null;
if (!empty($existingModifierListId)) {
$squareObj = $this->getSquareObjectNew($existingModifierListId, $cafe->square_access_token);
if ($squareObj && empty($squareObj['errors'])) {
$modifierListVersion = $squareObj['object']['version'] ?? null;
$itemModifierListId = $existingModifierListId;
}
$deleteModifierResponse = Http::withToken($squareToken)
->delete("https://connect.squareup.com/v2/catalog/object/{$itemModifierListId}");
if (!$deleteModifierResponse->successful()) {
\Log::error("Failed to delete modifier list {$itemModifierListId}", ['response' => $deleteModifierResponse->body()]);
} else {
\Log::info("Deleted modifier list {$itemModifierListId}");
}
}
// Delete item
$deleteItemResponse = Http::withToken($squareToken)
->delete("https://connect.squareup.com/v2/catalog/object/{$squareItemId}");
if (!$deleteItemResponse->successful()) {
\Log::error("Failed to delete item {$squareItemId}", ['response' => $deleteItemResponse->body()]);
return ['success' => false, 'message' => 'Failed to delete item on Square'];
}
return ['success' => true, 'message' => 'Item and modifier list deleted successfully from Square'];
}
private function getSquareObject($objectId, $accessToken)
{
$response = Http::withToken($accessToken)
->get("https://connect.squareup.com/v2/catalog/object/{$objectId}");
if ($response->successful()) {
return $response->json();
}
return null;
}
private function sendSquareBatchUpsert(array $catalogObjects, $cafe)
{
$payload = [
'idempotency_key' => uniqid('sync_', true),
'batches' => [['objects' => $catalogObjects]],
];
$response = Http::withToken($cafe->square_access_token)
->post("https://connect.squareup.com/v2/catalog/batch-upsert", $payload);
\Log::info("Square API Response", [
'status' => $response->status(),
'successful' => $response->successful(),
'body' => $response->body(),
]);
if (!$response->successful()) {
throw new \Exception("Square batch upsert failed: " . $response->body());
}
return $response;
}
public function syncModifiersToSquare(Request $request)
{
$cafe = Cafe::find($request->user->id);
if (!$cafe) {
return response()->json(['error' => 'Cafe not found'], 404);
}
if (empty($cafe->square_access_token)) {
return response()->json(['error' => 'Square account not connected'], 400);
}
$allAddonSizes = DB::table('addon_sizes')
->where('cafe_id', $cafe->id)
->get(['id', 'addon_size_name', 'square_modifier_id']);
$modifierObjects = [];
$modifierIdMap = [];
foreach ($allAddonSizes as $addon) {
$addonPrice = AddonSizeCafeItem::where('addon_size_id', $addon->id)->value('addon_size_price');
$modifierOptionId = "#OPTION_{$addon->id}_" . uniqid();
$existingSquareId = $addon->square_modifier_id;
$version = null;
// ✅ Check if modifier exists on Square
if (!empty($existingSquareId)) {
$squareObjResponse = Http::withToken($cafe->square_access_token)
->get("https://connect.squareup.com/v2/catalog/object/{$existingSquareId}");
if ($squareObjResponse->successful() && empty($squareObjResponse->json()['errors'])) {
$version = $squareObjResponse->json()['object']['version'] ?? null;
} else {
// Object not found → force create new
$existingSquareId = null;
}
}
// ✅ Build modifier option
$modifierOption = [
'type' => 'MODIFIER',
'id' => $modifierOptionId,
'modifier_data' => [
'name' => $addon->addon_size_name,
'price_money' => [
'amount' => (int) ($addonPrice * 100),
'currency' => 'GBP',
],
],
];
// ✅ Build modifier list
$modifierListObject = [
'type' => 'MODIFIER_LIST',
'id' => $existingSquareId ?: "#MODLIST_{$addon->id}_" . uniqid(),
'modifier_list_data' => [
'name' => $addon->addon_size_name . " Addons",
'modifiers' => [$modifierOption],
'present_at_all_locations' => true,
],
];
if ($version) {
$modifierListObject['version'] = $version;
}
$modifierObjects[] = $modifierListObject;
$modifierIdMap[$addon->id] = $modifierListObject['id'];
}
$chunks = array_chunk($modifierObjects, 500);
foreach ($chunks as $chunk) {
$payload = [
'idempotency_key' => uniqid('mod_sync_', true),
'batches' => [
['objects' => $chunk],
],
];
$response = Http::withToken($cafe->square_access_token)
->post("https://connect.squareup.com/v2/catalog/batch-upsert", $payload);
\Log::info("Square Modifier API Response", [
'status' => $response->status(),
'successful' => $response->successful(),
'body' => $response->body(),
]);
if (!$response->successful()) {
throw new \Exception("Square modifier batch upsert failed: " . $response->body());
}
// ✅ Update DB with returned Square IDs
$mappings = $response->json()['id_mappings'] ?? [];
foreach ($mappings as $map) {
foreach ($modifierIdMap as $addonId => $clientModifierId) {
if ($clientModifierId === $map['client_object_id']) {
DB::table('addon_sizes')->where('id', $addonId)->update([
'square_modifier_id' => $map['object_id'],
]);
}
}
}
}
\Log::info("Modifier sync completed successfully (update or create).");
return response()->json(['success' => true, 'message' => 'Modifiers synced to Square successfully']);
}
public function syncCategoriesToSquare(Request $request)
{
$cafe = Cafe::find($request->user->id);
if (!$cafe) {
return response()->json(['error' => 'Cafe not found'], 404);
}
if (empty($cafe->square_access_token)) {
return response()->json(['error' => 'Square account not connected'], 400);
}
$menus = DB::table('cafe_menus')
->where('cafe_id', $cafe->id)
->get(['id', 'menu_name', 'square_category_id']);
// 🟡 Step 1: Fetch all existing Square categories
$squareCategories = [];
$cursor = null;
do {
$url = "https://connect.squareup.com/v2/catalog/list?types=CATEGORY";
if ($cursor) {
$url .= "&cursor={$cursor}";
}
$res = Http::withToken($cafe->square_access_token)->get($url);
if (!$res->successful()) {
throw new \Exception("Failed to fetch categories from Square: " . $res->body());
}
$data = $res->json();
foreach ($data['objects'] ?? [] as $obj) {
$name = strtolower(trim($obj['category_data']['name']));
$squareCategories[$name] = [
'id' => $obj['id'],
'version' => $obj['version'] ?? null
];
}
$cursor = $data['cursor'] ?? null;
} while ($cursor);
$categoryObjects = [];
$categoryIdMap = [];
foreach ($menus as $menu) {
$nameKey = strtolower(trim($menu->menu_name));
$existingSquareId = $menu->square_category_id;
$version = null;
// 🟢 Try to match by name if ID is missing
if (empty($existingSquareId) && isset($squareCategories[$nameKey])) {
$existingSquareId = $squareCategories[$nameKey]['id'];
$version = $squareCategories[$nameKey]['version'];
// 🔁 Update local DB with matched ID
DB::table('cafe_menus')->where('id', $menu->id)->update([
'square_category_id' => $existingSquareId,
]);
continue; // No need to recreate
}
// 🟠 If still no ID, generate temporary ID for upsert
$categoryId = $existingSquareId ?: "#CAT_{$menu->id}_" . uniqid();
// 🟡 If existing, fetch version (only if not already set above)
if ($existingSquareId && !$version) {
$squareObjResponse = Http::withToken($cafe->square_access_token)
->get("https://connect.squareup.com/v2/catalog/object/{$existingSquareId}");
if ($squareObjResponse->successful() && empty($squareObjResponse->json()['errors'])) {
$version = $squareObjResponse->json()['object']['version'] ?? null;
}
}
$categoryObject = [
'type' => 'CATEGORY',
'id' => $categoryId,
'category_data' => [
'name' => $menu->menu_name,
],
];
if ($version) {
$categoryObject['version'] = $version;
}
$categoryObjects[] = $categoryObject;
$categoryIdMap[$menu->id] = $categoryId;
}
// 🔄 Batch upsert only if needed
if (count($categoryObjects)) {
$chunks = array_chunk($categoryObjects, 500);
foreach ($chunks as $chunk) {
$payload = [
'idempotency_key' => uniqid('cat_sync_', true),
'batches' => [['objects' => $chunk]],
];
\Log::info("Final category chunk payload", $payload);
$response = Http::withToken($cafe->square_access_token)
->post("https://connect.squareup.com/v2/catalog/batch-upsert", $payload);
\Log::info("Square Category API Response", [
'status' => $response->status(),
'successful' => $response->successful(),
'body' => $response->body(),
]);
if (!$response->successful()) {
throw new \Exception("Square category batch upsert failed: " . $response->body());
}
// 🔁 Update local DB with new object IDs
$mappings = $response->json()['id_mappings'] ?? [];
foreach ($mappings as $map) {
foreach ($categoryIdMap as $menuId => $clientCategoryId) {
if ($clientCategoryId === $map['client_object_id']) {
DB::table('cafe_menus')->where('id', $menuId)->update([
'square_category_id' => $map['object_id'],
]);
}
}
}
}
}
\Log::info("Category sync completed successfully.");
return response()->json(['success' => true, 'message' => 'Categories synced to Square successfully']);
}
public function oldsyncCategoriesToSquare(Request $request)
{
$cafe = Cafe::find($request->user->id);
if (!$cafe) {
return response()->json(['error' => 'Cafe not found'], 404);
}
if (empty($cafe->square_access_token)) {
return response()->json(['error' => 'Square account not connected'], 400);
}
$menus = DB::table('cafe_menus')
->where('cafe_id', $cafe->id)
->get(['id', 'menu_name', 'square_category_id']);
$categoryObjects = [];
$categoryIdMap = [];
foreach ($menus as $menu) {
$existingSquareId = $menu->square_category_id;
$version = null;
if (!empty($existingSquareId)) {
// ✅ Fetch latest version from Square
$squareObjResponse = Http::withToken($cafe->square_access_token)
->get("https://connect.squareup.com/v2/catalog/object/{$existingSquareId}");
if ($squareObjResponse->successful() && empty($squareObjResponse->json()['errors'])) {
$version = $squareObjResponse->json()['object']['version'] ?? null;
}
}
if (!empty($existingSquareId)) {
$categoryId = $existingSquareId;
} else {
$categoryId = "#CAT_{$menu->id}_" . uniqid();
}
$categoryObject = [
'type' => 'CATEGORY',
'id' => $categoryId,
'category_data' => [
'name' => $menu->menu_name,
],
];
if ($version) {
$categoryObject['version'] = $version;
}
$categoryObjects[] = $categoryObject;
$categoryIdMap[$menu->id] = $categoryId;
}
$chunks = array_chunk($categoryObjects, 500);
foreach ($chunks as $chunk) {
$payload = [
'idempotency_key' => uniqid('cat_sync_', true),
'batches' => [
['objects' => $chunk],
],
];
\Log::info("Final category chunk payload", $payload);
$response = Http::withToken($cafe->square_access_token)
->post("https://connect.squareup.com/v2/catalog/batch-upsert", $payload);
\Log::info("Square Category API Response", [
'status' => $response->status(),
'successful' => $response->successful(),
'body' => $response->body(),
]);
if (!$response->successful()) {
throw new \Exception("Square category batch upsert failed: " . $response->body());
}
$mappings = $response->json()['id_mappings'] ?? [];
foreach ($mappings as $map) {
foreach ($categoryIdMap as $menuId => $clientCategoryId) {
if ($clientCategoryId === $map['client_object_id']) {
DB::table('cafe_menus')->where('id', $menuId)->update([
'square_category_id' => $map['object_id'],
]);
}
}
}
}
\Log::info("Category sync completed successfully.");
return response()->json(['success' => true, 'message' => 'Categories synced to Square successfully']);
}
public function syncMenuToSquareNew(Request $request)
{
try {
$cafe = Cafe::find($request->user->id);
if (!$cafe) {
return response()->json(['error' => 'Cafe not found'], 404);
}
if (empty($cafe->square_access_token)) {
return response()->json(['error' => 'Square account not connected'], 400);
}
$addonSizes = DB::table('addon_sizes')
->where('status', 1)
->whereNotNull('square_modifier_id')
->get(['id', 'square_modifier_id']);
$modifierIdMap = [];
foreach ($addonSizes as $addon) {
$modifierIdMap[$addon->id] = $addon->square_modifier_id;
}
$menuItemIds = DB::table('cafe_menu_items')
->where('cafe_id', $cafe->id)
->where('item_deleted_at', 0)
->pluck('id');
if ($menuItemIds->isEmpty()) {
return response()->json(['error' => 'No menu items to sync'], 404);
}
$itemObjects = [];
$objectIdMap = [];
foreach ($menuItemIds as $itemId) {
$itemData = $this->getItemDetail($itemId);
if (!$itemData) {
continue;
}
// ✅ Refresh category ID from DB
$menu = DB::table('cafe_menus')->where('id', $itemData['cafe_menu_id'])->first();
$squareCategoryId = $menu->square_category_id ?? null;
// ✅ Check if category exists on Square
if (!empty($squareCategoryId)) {
$squareCatResponse = Http::withToken($cafe->square_access_token)
->get("https://connect.squareup.com/v2/catalog/object/{$squareCategoryId}");
if (!$squareCatResponse->successful() || isset($squareCatResponse->json()['errors'])) {
\Log::warning("Category not found on Square for menu ID {$menu->id}, ignoring category for this item.");
$squareCategoryId = null; // fallback to no category
}
}
\Log::info("Using category ID for item {$itemData['item_name']}", [
'category_id' => $squareCategoryId
]);
$squareItemId = $itemData['square_item_id'] ?? null;
$validSquareId = null;
$itemVersion = null;
if ($squareItemId) {
$squareObj = $this->getSquareObjectNew($squareItemId, $cafe->square_access_token);
if ($squareObj && empty($squareObj['errors'])) {
$validSquareId = $squareItemId;
$itemVersion = $squareObj['object']['version'] ?? null;
}
}
$itemObjectId = $validSquareId ?: "#ITEM_{$itemId}_" . uniqid();
$variationObjects = [];
if (!empty($itemData['item_size_prices'])) {
foreach ($itemData['item_size_prices'] as $size) {
$price = floatval($size->item_size_price ?? 0);
if ($price <= 0)
continue;
$variationId = "#VARIATION_{$itemId}_{$size->size_id}_" . uniqid();
$variationObjects[] = [
'type' => 'ITEM_VARIATION',
'id' => $variationId,
'item_variation_data' => [
'item_id' => $itemObjectId,
'name' => "Size {$size->size_id}",
'pricing_type' => 'FIXED_PRICING',
'price_money' => [
'amount' => (int) ($price * 100),
'currency' => 'GBP',
],
'present_at_all_locations' => true,
],
];
}
} else {
$defaultPrice = floatval($itemData['item_price'] ?? 0);
if ($defaultPrice <= 0)
continue;
$variationId = "#VARIATION_{$itemId}_default_" . uniqid();
$variationObjects[] = [
'type' => 'ITEM_VARIATION',
'id' => $variationId,
'item_variation_data' => [
'item_id' => $itemObjectId,
'name' => 'Regular',
'pricing_type' => 'FIXED_PRICING',
'price_money' => [
'amount' => (int) ($defaultPrice * 100),
'currency' => 'GBP',
],
'present_at_all_locations' => true,
],
];
}
$assignedModifierLists = [];
if (!empty($itemData['optionSizeCafeItems'])) {
foreach ($itemData['optionSizeCafeItems'] as $addon) {
$addonSizeId = $addon['addon_size_id'];
if (!empty($modifierIdMap[$addonSizeId])) {
$assignedModifierLists[] = [
'modifier_list_id' => $modifierIdMap[$addonSizeId],
'enabled' => true,
];
}
}
}
$itemObject = [
'type' => 'ITEM',
'id' => $validSquareId ?: $itemObjectId,
'version' => $itemVersion ?? null,
'item_data' => [
'name' => $itemData['item_name'],
'description' => $itemData['item_description'] ?? '',
'abbreviation' => strtoupper(substr($itemData['item_name'], 0, 3)),
'present_at_all_locations' => true,
'modifier_list_info' => $assignedModifierLists,
'category_id' => $squareCategoryId,
],
];
$itemObjects = array_merge($itemObjects, $variationObjects);
$itemObjects[] = $itemObject;
$objectIdMap[$itemId] = $validSquareId ?: $itemObjectId;
}
\Log::info("Final catalog object count before chunking", ['count' => count($itemObjects)]);
$chunks = array_chunk($itemObjects, 500);
$allMappings = [];
foreach ($chunks as $chunk) {
$payload = [
'idempotency_key' => uniqid('menu_sync_', true),
'batches' => [
['objects' => $chunk],
],
];
\Log::info("Sending chunk", ['count' => count($chunk)]);
$response = Http::withToken($cafe->square_access_token)
->post("https://connect.squareup.com/v2/catalog/batch-upsert", $payload);
\Log::info("Square API Response", [
'status' => $response->status(),
'successful' => $response->successful(),
'body' => $response->body(),
]);
if (!$response->successful()) {
throw new \Exception("Square batch upsert failed: " . $response->body());
}
$mappings = $response->json()['id_mappings'] ?? [];
$allMappings = array_merge($allMappings, $mappings);
}
foreach ($allMappings as $map) {
if (!empty($map['client_object_id']) && !empty($map['object_id'])) {
foreach ($objectIdMap as $localId => $clientId) {
if ($clientId == $map['client_object_id']) {
DB::table('cafe_menu_items')->where('id', $localId)->update([
'square_item_id' => $map['object_id'],
]);
}
}
}
}
return response()->json(['success' => true, 'message' => 'Menu synced to Square successfully']);
} catch (\Exception $e) {
return response()->json(['error' => 'Failed to sync menu: ' . $e->getMessage()], 500);
}
}
private function sendSquareBatchUpsertNew(array $chunk, $cafe)
{
if (count($chunk) > 1000) {
throw new \Exception("Chunk too large before sending: " . count($chunk));
}
$payload = [
'idempotency_key' => uniqid('sync_', true),
'batches' => [
[
'objects' => $chunk,
],
],
];
\Log::info("Sending SINGLE API REQUEST with chunk", ['count' => count($chunk)]);
$response = Http::withToken($cafe->square_access_token)
->post("https://connect.squareup.com/v2/catalog/batch-upsert", $payload);
\Log::info("Square API Response", [
'status' => $response->status(),
'successful' => $response->successful(),
'body' => $response->body(),
]);
if (!$response->successful()) {
throw new \Exception("Square batch upsert failed: " . $response->body());
}
return $response->json()['id_mappings'] ?? [];
}
private function getItemDetail($itemId)
{
$model = DB::table('cafe_menu_items')
->join('pre_defined_item_images', 'pre_defined_item_images.id', '=', 'cafe_menu_items.item_image_id')
->join('cafe_menus', 'cafe_menus.id', '=', 'cafe_menu_items.cafe_menu_id')
->where('cafe_menu_items.id', $itemId)
->where('cafe_menu_items.item_deleted_at', 0)
->select(
'cafe_menu_items.*',
'pre_defined_item_images.item_image',
'cafe_menus.menu_name as item_category',
'cafe_menus.square_category_id',
'cafe_menu_items.item_description', // ✅ explicitly select
'cafe_menu_items.cafe_menu_id' // ✅ ensure this is available
)
->first();
if (!$model) {
return null;
}
$optionSizeCafeItems = DB::table('addon_size_cafe_items')
->join('addon_sizes', 'addon_sizes.id', '=', 'addon_size_cafe_items.addon_size_id')
->where('item_id', $model->id)
->get([
'addon_size_cafe_items.addon_size_id',
'addon_size_cafe_items.addon_size_price',
'addon_sizes.addon_size_name',
'addon_sizes.status',
])
->map(function ($item) {
return [
'addon_size_id' => $item->addon_size_id,
'addon_size_name' => $item->addon_size_name,
'addon_size_price' => $item->addon_size_price,
'status' => $item->status,
];
})->toArray();
$addonItems = DB::table('suggested_items')
->where('item_id', $model->id)
->pluck('suggested_item_id');
$itemPriceSize = DB::table('cafe_menu_item_sizes')
->where('item_id', $model->id)
->get();
return [
'square_item_id' => $model->square_item_id,
'square_modifier_list_id' => $model->square_modifier_list_id,
'item_image_id' => $model->item_image_id ?? 1,
'item_name' => $model->item_name,
'item_category' => $model->item_category, // From cafe_menus.menu_name
'item_type' => $model->item_type,
'item_description' => $model->item_description, // ✅ now available
'item_price' => $model->item_price,
'cafe_menu_id' => $model->cafe_menu_id, // ✅ now available
'square_category_id' => $model->square_category_id,
'item_size_prices' => $itemPriceSize,
'optionSizeCafeItems' => $optionSizeCafeItems,
'addon_items' => $addonItems,
];
}
private function oldgetItemDetail($itemId)
{
$model = DB::table('cafe_menu_items')
->join('pre_defined_item_images', 'pre_defined_item_images.id', '=', 'cafe_menu_items.item_image_id')
->join('cafe_menus', 'cafe_menus.id', '=', 'cafe_menu_items.cafe_menu_id')
->where('cafe_menu_items.id', $itemId)
->where('cafe_menu_items.item_deleted_at', 0)
->select('cafe_menu_items.*', 'pre_defined_item_images.item_image', 'cafe_menus.menu_name as item_category', 'cafe_menus.square_category_id')
->first();
if (!$model)
return null;
$optionSizeCafeItems = DB::table('addon_size_cafe_items')
->join('addon_sizes', 'addon_sizes.id', '=', 'addon_size_cafe_items.addon_size_id')
->where('item_id', $model->id)
->get(['addon_size_cafe_items.addon_size_id', 'addon_size_cafe_items.addon_size_price', 'addon_sizes.addon_size_name', 'addon_sizes.status'])
->map(function ($item) {
return [
'addon_size_id' => $item->addon_size_id,
'addon_size_name' => $item->addon_size_name,
'addon_size_price' => $item->addon_size_price,
'status' => $item->status,
];
})->toArray();
$addonItems = DB::table('suggested_items')
->where('item_id', $model->id)
->pluck('suggested_item_id');
$itemPriceSize = DB::table('cafe_menu_item_sizes')
->where('item_id', $model->id)
->get();
return [
'square_item_id' => $model->square_item_id,
'square_modifier_list_id' => $model->square_modifier_list_id,
'item_image_id' => $model->item_image_id ?? 1,
'item_name' => $model->item_name,
'item_category' => $model->cafe_menu_id,
'item_type' => $model->item_type,
'item_description' => $model->item_description,
'item_price' => $model->item_price,
'cafe_menu_id' => $model->cafe_menu_id,
'square_category_id' => $model->square_category_id,
'item_size_prices' => $itemPriceSize,
'optionSizeCafeItems' => $optionSizeCafeItems,
'addon_items' => $addonItems,
];
}
private function getSquareObjectNew($objectId, $accessToken)
{
$response = Http::withToken($accessToken)
->get("https://connect.squareup.com/v2/catalog/object/{$objectId}");
if ($response->successful()) {
return $response->json();
}
return null;
}
public function syncMenuFromSquare(Request $request)
{
$cafe = Cafe::find($request->user->id);
if (!$cafe || empty($cafe->square_access_token)) {
return response()->json(['error' => 'Cafe not found or Square not connected'], 404);
}
// 1. Fetch Items from Square
$squareItemsResp = Http::withToken($cafe->square_access_token)
->get('https://connect.squareup.com/v2/catalog/list?types=ITEM');
//dd($squareItemsResp);
if (!$squareItemsResp->successful()) {
return response()->json(['error' => 'Failed to fetch Square items'], 500);
}
$squareItems = $squareItemsResp->json()['objects'] ?? [];
// 2. Fetch all Square categories (for name mapping)
$squareCatalogResp = Http::withToken($cafe->square_access_token)
->get('https://connect.squareup.com/v2/catalog/list?types=CATEGORY');
$squareFetchedData = $squareCatalogResp->json();
$squareCategories = [];
foreach ($squareFetchedData['objects'] ?? [] as $obj) {
if ($obj['type'] === 'CATEGORY') {
$id = $obj['id'];
$name = $obj['category_data']['name'] ?? null;
if ($name) {
$squareCategories[$id] = [
'id' => $id,
'name' => $name,
];
}
}
}
// 3. Local reference maps
$categoryMap = CafeMenu::where('cafe_id', $cafe->id)
->pluck('id', 'square_category_id')
->toArray();
$categoryNameMap = CafeMenu::where('cafe_id', $cafe->id)
->get()
->mapWithKeys(function ($menu) {
return [strtolower(trim($menu->menu_name)) => $menu->id];
})->toArray();
$othersCategory = CafeMenu::where('menu_name', 'Other')
->where('cafe_id', $cafe->id)
->first();
$othersCategoryId = $othersCategory ? $othersCategory->id : null;
// 4. Prepare size and addon references
$sizes = Sizes::all();
$sizeMap = [];
foreach ($sizes as $sz)
$sizeMap[strtolower($sz->size_name)] = $sz->id;
$mediumSizeId = $sizeMap['medium'] ?? null;
$addons = AddonSize::where('cafe_id', $cafe->id)->get();
$addonMap = [];
foreach ($addons as $ad)
$addonMap[$ad->square_modifier_id] = $ad->id;
// 5. Process each item
foreach ($squareItems as $item) {
$itemData = $item['item_data'] ?? [];
$squareItemId = $item['id'];
$itemName = $itemData['name'] ?? 'Unnamed';
$itemDescription = $itemData['description'] ?? $itemData['name'];
$categoryId = $itemData['category_id'] ?? ($itemData['categories'][0]['id'] ?? null);
// Get category name from Square
$categoryName = null;
if (!empty($categoryId) && isset($squareCategories[$categoryId])) {
$categoryName = $squareCategories[$categoryId]['name'];
}
// Match to local menu category
$cafeMenuId = null;
// Step 1: Match by square_category_id
if (!empty($categoryId) && isset($categoryMap[$categoryId])) {
$cafeMenuId = $categoryMap[$categoryId];
}
// Step 2: Match by name if ID fails
elseif (!empty($categoryName)) {
$normalizedCategoryName = strtolower(trim($categoryName));
if (isset($categoryNameMap[$normalizedCategoryName])) {
$cafeMenuId = $categoryNameMap[$normalizedCategoryName];
// Save Square category ID to local menu if missing
DB::table('cafe_menus')
->where('id', $cafeMenuId)
->whereNull('square_category_id')
->update(['square_category_id' => $categoryId]);
}
}
// Step 3: Fallback to "Other" if no match
if (empty($cafeMenuId)) {
$cafeMenuId = $othersCategoryId;
}
// Get main price (first variation)
$firstVariationPrice = 0;
if (!empty($itemData['variations'][0]['item_variation_data']['price_money']['amount'])) {
$firstVariationPrice = $itemData['variations'][0]['item_variation_data']['price_money']['amount'] / 100;
}
// Upsert item
$localItem = CafeMenuItem::where('square_item_id', $squareItemId)
->where('cafe_id', $cafe->id)
->first();
$itemAttrs = [
'item_name' => $itemName,
'item_description' => $itemDescription,
'cafe_id' => $cafe->id,
'item_image_id' => 1,
'cafe_menu_id' => $cafeMenuId,
'item_price' => $firstVariationPrice,
'item_type' => 1,
'status' => 1,
'updated_at' => now(),
];
if (!$localItem) {
$itemAttrs['created_at'] = now();
$itemAttrs['square_item_id'] = $squareItemId;
$localItem = CafeMenuItem::create($itemAttrs);
} else {
$localItem->update($itemAttrs);
}
$itemId = $localItem->id;
// Sync sizes
if (!empty($itemData['variations'])) {
foreach ($itemData['variations'] as $var) {
$varData = $var['item_variation_data'] ?? [];
$sizeName = strtolower($varData['name'] ?? 'medium');
$sizeId = $sizeMap[$sizeName] ?? $mediumSizeId;
if (!$sizeId)
continue;
CafeMenuItemSize::updateOrCreate(
['item_id' => $itemId, 'size_id' => $sizeId],
[
'item_size_price' => ($varData['price_money']['amount'] ?? 0) / 100,
'updated_at' => now(),
]
);
}
}
// Sync addons
if (!empty($itemData['modifier_list_info'])) {
foreach ($itemData['modifier_list_info'] as $modInfo) {
$modifierListId = $modInfo['modifier_list_id'] ?? null;
if ($modifierListId && isset($addonMap[$modifierListId])) {
AddonSizeCafeItem::updateOrCreate(
['addon_size_id' => $addonMap[$modifierListId], 'item_id' => $itemId],
[
'addon_size_price' => 0,
'created_at' => now(),
'updated_at' => now(),
]
);
}
}
}
}
return response()->json(['message' => 'Menu items synced from Square.']);
}
}