

<?php

/**
 * Firetoys
 *
 * 
 * KGH Order processing job 
 * @author Matthew Robertson
 *
 * 
 */
 
require_once("KGHRoutines.php"); 
require_once("ShopifyGraphQLUtilities.php");



// Get required Shopify product details
// sprintf with 'after:' cursor for inventory items continuation, and warehouse location id
define ('GET_SHOPIFY_PRODUCT_DETAILS_QUERY',
	'query {
		inventoryItems(
			first: 250
			%s
		) {
			edges {
				node {
					sku
				    id
				    variants (first:10) {
					  edges {
					    node {
						  id
						  sku
						  barcode
						  requiresComponents
					    }
					  }
				    }
					inventoryLevel(locationId: "%s") {
						id
						quantities(
							names: ["available"]
						) {
							name
							quantity
						}
					}
				}
			}
			pageInfo {
			  endCursor
			  hasNextPage
			}
		}
	}
');


// Activate new products in KGH warehouse
define ('ACTIVATE_QUERY',
	'mutation ActivateInventoryItem($inventoryItemId: ID!, $locationId: ID!, $available: Int) {
    inventoryActivate(inventoryItemId: $inventoryItemId, locationId: $locationId, available: $available) {
      inventoryLevel {
        id
        quantities(names: ["available"]) {
          name
          quantity
        }
        item {
          id
        }
        location {
          id
        }
      }
    }
  }
');


// Get valid shipping codes
define('SELECT_SHIPPING_SERVICE_CODES',
	"SELECT * FROM `ft_kgh_shipping_services`"
);


// Handle errors table
define('INSERT_INTO_ERRORS_TABLE',
	"INSERT INTO `ft_kgh_errors` (`error_message`) VALUES ('%s')"
);

define('CHECK_FOR_RUN_DATE_IN_ERRORS_TABLE',
	"SELECT `entity_id` FROM `ft_kgh_errors` WHERE `error_message` = '%s'"
);

define('UPDATE_RUN_DATE_IN_ERRORS_TABLE',
	"UPDATE `ft_kgh_errors` SET `date_occurred` = CURRENT_TIMESTAMP(3) WHERE `entity_id` = %d"
);

define('INSERT_RUN_DATE_INTO_ERRORS_TABLE',
	"INSERT INTO `ft_kgh_errors` (`error_message`) VALUES ('%s')"
);

define('SELECT_KGH_ORDERS_PROCESSED',
	"SELECT * FROM `ft_kgh_orders_processed` WHERE `order_name` IN %s"
);


// Handle orders processed table. All queries require sprintf
// Requires a comma separated list of: "'order_name'"
define('GET_ORDERS_FROM_KGH_ORDERS_PROCESSED',
	"SELECT * FROM `ft_kgh_orders_processed`
	WHERE `order_name` IN (%s)"
);

// Insert requires a comma separated list of values like: "('order_name', 'YYYY-MM-DD HH:MM:SS')"
define('INSERT_ORDER_INTO_KGH_ORDERS_PROCESSED',
	"INSERT INTO `ft_kgh_orders_processed` (`order_name`, `start_date`)
	VALUES %s"
);

// Requires two strings: "YYYY-MM-DD HH:MM:SS" and "order_name"
define('UPDATE_KGH_ORDER_CREATED_DATE',
	"UPDATE `ft_kgh_orders_processed`
	SET `kgh_order_created_date` = '%s'
    WHERE `order_name` = '%s'"
);

// Requires integer kgh_shipment_count and string "order_name"
define('UPDATE_KGH_SHIPMENT_COUNT',
	"UPDATE `ft_kgh_orders_processed`
	SET `kgh_shipment_count` = %d
    WHERE `order_name` = '%s'"
);

// Requires integer shopify_fulfillment_count and string "order_name"
define('UPDATE_SHOPIFY_FULFILLMENT_COUNT',
	"UPDATE `ft_kgh_orders_processed`
	SET `shopify_fulfillment_count` = %d
    WHERE `order_name` = '%s'"
);

// Requires two strings: "YYYY-MM-DD HH:MM:SS" and "order_name"
define('UPDATE_SHOPIFY_FULFILLED_DATE',
	"UPDATE `ft_kgh_orders_processed`
	SET `shopify_fulfilled_date` = '%s'
    WHERE `order_name` = '%s'"
);




// Update Shopify inventory quantities as required
define ('UPDATE_SHOPIFY_QUANTITIES_STRING',
	'mutation UpdateInventoryQuantities {
	  inventoryAdjustQuantities(
		input: {
		  reason: "correction"
		  name: "available"
		  changes: %s
		}
	  ) {
		userErrors {
		  field
		  message
		}
	  }
	}
');

// Clause for updating Shopify inventory quantity. An array of multiple instances of this will be added to the mutation string aabove
// sprintf with change (+ or -), inventory item id and location id
define ('UPDATE_DELTA',
   '{
      delta: %d
      inventoryItemId: "%s"
      locationId: "%s"
    }
');


// Get unfulfilled, paid Shopify orders. 
// sprintf with 4 strings: Orders continuation cursor, Warehouse id number, Start date (eg "updated_at:>\'2025-12-01\'"), Fulfillment order line items continuation cursor
define ('GET_UNFULFILLED_ORDERS', 
	'query {
  orders (first:100 %s
		query:"financial_status:paid fulfillment_status:UNFULFILLED reference_location_id:%s %s"
  	)    {
    edges {
      node {
        name
		updatedAt
		cancelledAt
        shippingAddress {
          name
          address1
          address2
          city
          provinceCode
          zip
          countryCodeV2
          phone
        }
        displayFinancialStatus
        displayFulfillmentStatus
        id
        shippingLine {
          carrierIdentifier
          code
          source
          title
        }
        fulfillmentOrders (first: 5) {
          edges {
            node {
			  assignedLocation {
			    location {
				  id
			    }
			  }
			  id
              lineItems (first: 100 %s) {
                edges {
                  node {
                    id
					sku
                    productTitle
                    remainingQuantity
                  }
                }
			    pageInfo {
				  endCursor
				  hasNextPage
			    }
              }
            }
          }
        }
        fulfillments (first: 5) {
		  location {
			id
		  }
		  displayStatus
          trackingInfo {
            company
            number
            url
          }
        }
      }
    }
	pageInfo {
	  endCursor
	  hasNextPage
	}
  }
}');


// Get unfulfilled, paid Shopify orders. 
// sprintf with 4 strings: Orders continuation cursor, Warehouse id number, Start date (eg "updated_at:>\'2025-12-01\'"), Fulfillment order line items continuation cursor
define ('GET_ORDER_FULFILLMENTSTATUS', 
	'query {
  order (id:\"$s\")    {
	  name
	  displayFulfillmentStatus
  }
}');


// Create Shopify fulfillment 
define('CREATE_FULFILLMENTS',
	'mutation fulfillmentCreate($fulfillment: FulfillmentInput!, $message: String) {
  fulfillmentCreate(fulfillment: $fulfillment, message: $message) {
    fulfillment {
      id
      order {
		name
        displayFulfillmentStatus
      }
      status
      trackingInfo {
        company
        number
        url
      }
    }
    userErrors {
      field
      message
    }
  }
}');


// Helper definitions for building fulfillment input for above
define('FULFILLMENT_ORDER_lINE_ITEM', Array(
  "id" => "",
  "quantity" => 0
));

define('FULFILLMENT_ORDER_FULFILLMENT_lINE_ITEMS', Array(
  "fulfillmentOrderId" => "",
  "fulfillmentOrderLineItems" => Array()
));

define('FULFILLMENT_TRACKING_INFO', Array(
  "number" => "",
  "company" => ""
));

define('FULFILLMENT_LINE_ITEMS_BY_FULFILLMENT_ORDER', Array(
));

define('CREATE_SHOPIFY_FULFILLMENT_ARRAY', Array(
  "trackingInfo" => FULFILLMENT_TRACKING_INFO,
  "notifyCustomer" => true,
  "lineItemsByFulfillmentOrder" => Array()
));


 
/***** Settings for running in test mode *****/

$runningInTestMode = false;
 
if ($runningInTestMode)
{
	echo "***** Running in test mode *****\r\n\n";
	$thisShopifyAccessToken = MATTHEW_TEST_ACCESS_TOKEN;
	$thisStoreName = MATTHEW_TEST_STORE_NAME;
	$thisWarehouse = MATTHEW_TEST_WAREHOUSE_BKA;
}
else
{
	$thisShopifyAccessToken = FIRETOYS_US_ACCESS_TOKEN;
	$thisStoreName = FIRETOYS_US_STORE_NAME;
	$thisWarehouse = FIRETOYS_US_WAREHOUSE_KGH;
}
 

	// Connect to database
	try
	{
		// Connect to database
		$con = mysqli_connect(DATABASE_SERVER,DATABASE_USER,DATABASE_PASSWORD,DATABASE_NAME);
	}
	catch(Exception $e)
	{
		echo "Unable to connect to SQL: " . mysqli_connect_error() . "\r\nMessage was: " . $errorMessage . "\r\n\n";
		return;
	}

	// Write start timeestamp to errors table
	writeRunDateToErrorsTable($con, "LATEST START DATE");

	echo date("H:i:s") . " Start\r\n";
	
	// Get Shipping Service Code table
	if (($shippingServiceCodeArray = getShippingServiceCodes($con)) === false)
	{
		writeToErrorsTable($con, "");
		return;
	}
	
	echo "\r\n" . date("H:i:s") . " Getting Shopify order and fulfillment details\r\n";
	
	// Get details of unfulfilled, paid orders
	// $shopifyOrdersArray contains the data required to create a KGH order
	// $fulfillmentOrdersDetails contains fulfillment order id and fulfillment line item ids, necessary for creating fulfillments
	// $fulfilledTrackingNumbers contains tracking numbers of KGH shipments that have already been fulfilled in Shopify
	
	$returnArray = getShopifyOrderDetails($con, $shippingServiceCodeArray);
	
	if (array_key_exists('Error', $returnArray))
	{
		// Fatal error retrieving Shopify orders - proceed to inventory quantity synchronisation
		writeToErrorsTable($con, "Error getting Shopify orders - only inventory quantities synchronisation will be attempted: " . $returnArray['Error']);
	}
	else
	{
		writeRunDateToErrorsTable($con, "LATEST SHOPIFY ORDERS RETURNED");
	
		// Shopify orders retrieved OK
		$shopifyOrdersArray = $returnArray['Data']['ordersDataArray'];
		$fulfillmentOrdersDetails = $returnArray['Data']['fulfillmentOrdersDetails'];
		$fulfilledTrackingNumbers = $returnArray['Data']['fulfilledTrackingNumbers'];
//echo "***** shopifyOrdersArray:\r\n" . print_r($shopifyOrdersArray, true) . "\r\n\n";
//echo "***** fulfillmentOrdersDetails:\r\n" . print_r($fulfillmentOrdersDetails, true) . "\r\n\n";
//return;
		
		// Get existing order details from orders prccessed table
		$returnedOrdersList = "'" . implode("','", array_keys($shopifyOrdersArray)) . "'";
		$ordersAlreadyInsertedArray = getOrdersAlreadyInserted($con, $returnedOrdersList);

		// Generate values for new order insertion
		$valuesArray = Array();
		foreach (array_keys($shopifyOrdersArray) as $orderName)
		{
			// Ignore orders already inserted
			if (array_key_exists($orderName, $ordersAlreadyInsertedArray))
				continue;
			
			// Generate values for this order
			$entryDate = date("Y/m/d H:i:s");
			$valuesArray[] = "('" . $orderName . "','" . $entryDate . "')";
		}
		
		$ordersValuesList = implode(",", $valuesArray);
	
		// Insert new shopify orders into orders processed table
		if (!($errorMessage = insertOrdersIntoOrdersProcessedTable($con, $ordersValuesList)) === "")
		{
			writeToErrorsTable($con, $errorMessage);
			return;
		}

		// Create KGH order creation queries
		// These queries are created for all the Shopify orders, but only some of them will be used
		$kghOrderQueriesArray = createKghOrderQueries($con, $shopifyOrdersArray, $ordersAlreadyInsertedArray);
//echo "***** kghOrderQueriesArray:\r\n" . print_r($kghOrderQueriesArray, true) . "\r\n\n";

		echo "\r\n" . date("H:i:s") . " Getting corresponding existing KGH order and fulfillment details\r\n";
		
		// Get details of KGH orders and any associated shipments
		$ordersShipmentsByTrackingNumber = Array();
		foreach($kghOrderQueriesArray as $orderName => $orderDetails)
		{
			$kghOrderError = false;
			$orderStatusReturn = getOrderStatus(KGH_ACCESS_TOKEN, $orderName, $runningInTestMode);
			if ($orderStatusReturn['httpCode'] == 400)
			{
				// Order not found - create it
				$returnArray = createOrder(KGH_ACCESS_TOKEN, json_encode($orderDetails), $runningInTestMode);
				
				if ($returnArray['httpCode'] == 200)
				{
					if (($returnMessage = updateKghOrderCreated($con, $orderName)) !== "")
						writeToErrorsTable($con, "Error writing KGH order date to processed table " 
								. $orderName . ": " . $returnedMessage);
				}
				else
				{
					// Failed to create KGH order
					$kghOrderError = true;
					
					if (array_key_exists('Error', $returnArray))
						$returnedErrorMessage = $returnArray['Error'];
					elseif (array_key_exists('Data', $returnArray) && array_key_exists('message', $returnArray['Data']))
						$returnedErrorMessage = $returnArray['Data']['message'];
					else
						$returnedErrorMessage = json_encode($returbArray);
					writeToErrorsTable($con, "Error writing KGH order for " . $orderName . ": " . $returnedErrorMessage);
				}
			}
			elseif ($orderStatusReturn['httpCode'] == 200)
			{
				// Check creation date is already there
				if (!array_key_exists($orderName, $ordersAlreadyInsertedArray) 
						|| $ordersAlreadyInsertedArray[$orderName]['kgh_order_created_date'] == null)
					if (($returnMessage = updateKghOrderCreated($con, $orderName)) !== "")
						writeToErrorsTable($con, "Error writing KGH order date to processed table " 
								. $orderName . ": " . $returnMessage);
					
				// Order exists - get any shipment details
				// Tracking numbers will be used to relate KGH shipments to Shopify fulfillments
				$orderShipments = getShipmentDetailsByOrderNo(KGH_ACCESS_TOKEN, urlencode($orderName), $runningInTestMode);

				$ordersShipmentsByTrackingNumber[$orderName] = Array();
				$shipmentPackages = $orderShipments['Data']['packages'];
				$shipmentCount = sizeof($shipmentPackages);
				for ($packageCount = 0; $packageCount < $shipmentCount; $packageCount++)
				{
					$thisTrackingNumber = $shipmentPackages[$packageCount]['trackingNo'];
					$ordersShipmentsByTrackingNumber[$orderName][$thisTrackingNumber] = $shipmentPackages[$packageCount];
				}

				if (($returnMessage = updateKghShipmentCount($con, $orderName, $shipmentCount)) !== "")
					writeToErrorsTable($con, "Error writing shipment count to processed table " 
							. $orderName . ": " . $returnMessage);
			}
			else
			{
				// Failure. Write to errors table and continue with remaining orders
				writeToErrorsTable($con, "Error getting " . $orderName . ". Order not processed. Order status: " . json_encode($orderStatusReturn, true));
				$kghOrderError = true;
			}
		}

		writeRunDateToErrorsTable($con, "LATEST KGH ORDERS AND SHIPMENTS RETURNED");

		echo "\r\n" . date("H:i:s") . " Creating Shopify fulfillments for new KGH shipments\r\n";

		// Convert new KGH shipments to Shopify fulfillments
		foreach($ordersShipmentsByTrackingNumber as $thisOrderName => $thisOrderShipmentDetails)
		{
			foreach($thisOrderShipmentDetails as $thisTrackingNumber => $thisShipmentDetails)
			{
				// Check whether fulfillment already exists with this tracking number
				if (array_key_exists($thisTrackingNumber, $fulfilledTrackingNumbers) && ($fulfilledTrackingNumbers[$thisTrackingNumber] == $thisOrderName))
					continue;
				
				// Create and run fulfillment query
				$createFulfillmentQuery = setUpCreateFulfillmentQuery($fulfillmentOrdersDetails[$thisOrderName], $thisShipmentDetails);
				$currentFulfillmentCount = $ordersAlreadyInsertedArray[$thisOrderName]['shopify_fulfillment_count'];
				$returnMessage = runCreateFulfillmentQuery($createFulfillmentQuery, $currentFulfillmentCount, $thisOrderName);
				if (!($returnMessage == ""))
					writeToErrorsTable($con, "Error creating Shopify fulfillment for KGH shipment with tracking number " 
						. $thisTrackingNumber . ": " . $returnMessage);
			}
		}
		
		writeRunDateToErrorsTable($con, "LATEST SHOPIFY FULFILLMENTS WRITTEN");
	}

	echo "\r\n" . date("H:i:s") . " Getting KGH product details\r\n";

	// Get KGH product Details
	$pathParameters = "";
	$method = "GET";
	$category = "Inventory";
	$contentParameters = "";
	$queryParameters = "";		
	$kghQuantities = Array();
	
	$kghProductsReturnArray = runKGHCall(KGH_ACCESS_TOKEN, $pathParameters, $method, $contentParameters, $category, $queryParameters, $runningInTestMode);
	if (array_key_exists("Error", $kghProductsReturnArray))
	{
		// On failure we end the program
		$kghProductsReturnArray['Error']['AppLocation'] = basename(__FILE__) . '/line ' . __LINE__;
		writeToErrorsTable($con, "Error getting KGH product details: " . json_encode($kghProductsReturnArray));
		return;
	}
	
	writeRunDateToErrorsTable($con, "LATEST KGH QUANTITIES RETURNED");

	// Write quantities into array
	$productsArray = $kghProductsReturnArray['Data']['products'];
	for ($productIndex = 0; $productIndex < sizeof($productsArray); $productIndex++)
	{
		$thisSku = $productsArray[$productIndex]['productId'];
		$thisQuantity = $productsArray[$productIndex]['quantityOnHand'];
		$kghQuantities[$thisSku] = $thisQuantity;
	}

	echo "\r\n" . date("H:i:s") . " Getting Shopify inventory quantities\r\n";

	// Get Shopify inventory quantities for all US store products
	$returnArray = getShopifyInventoryQuantities($kghQuantities);
	if (array_key_exists('Error', $returnArray))
	{
		// On failure we end the program
		writeToErrorsTable($con, "Error getting Shopify inventory quantities: " . $returnArray['Error']);
		return;
	}
	$quantityChangesStringArray = $returnArray['quantityChangesStringArray'];

	writeRunDateToErrorsTable($con, "LATEST SHOPIFY QUANTITIES RETURNED");

	echo "\r\n" . date("H:i:s") . " Updating Shopify inventory quantities\r\n";

	// Update any changed quantities
	if (!($errorMessage = changeShopifyQuantities($quantityChangesStringArray)) == "")
	{
		writeToErrorsTable($con, "Error writing Shopify inventory details: " . $errorMessage);
		return;
	}

	writeRunDateToErrorsTable($con, "LATEST SHOPIFY QUANTITIES UPDATED");

	echo "\r\n" . date("H:i:s") . " End\r\n";
	
	writeRunDateToErrorsTable($con, "LATEST END DATE");
	

	return;



// Get table to convert Shopify shipping carrier info to KGH constants
function getShippingServiceCodes($con)
{
	$shippingServiceCodeArray = Array();
	
	try
	{
		$result = mysqli_query($con, SELECT_SHIPPING_SERVICE_CODES);
		if (!$result)
		{
			echo "Failed to read service codes: " . mysqli_error($con) . "\r\n\n";
			return false;
		}
		
		while ($row = mysqli_fetch_array($result))
		{
			$shippingServiceCodeArray[$row[0]] = Array();
			$shippingServiceCodeArray[$row[0]]['serviceCarrier'] = $row[1];
			$shippingServiceCodeArray[$row[0]]['kghServiceCode'] = $row[2];
		}
	}
	catch(Exception $e)
	{
		echo "Failed to read service codes: " . mysqli_error($con) . "\r\n\n";
		return false;
	}
	
	return $shippingServiceCodeArray;
}


// Write error messages
function writeToErrorsTable($con, $errorMessage)
{
	return writeRunDateToErrorsTable($con, $errorMessage);

	try
	{
		$insertQuery = sprintf(INSERT_INTO_ERRORS_TABLE, $errorMessage);
		$result = mysqli_query($con, $insertQuery);
		if (!$result)
			echo "Failed to write error message to table: " . mysqli_error($con) . "\r\n\n";
	}
	catch(Exception $e)
	{
		echo "Failed to write error message to table: " . mysqli_error($con) . "\r\n\n";
	}
	
}


// Write latest run markers into errors table
function writeRunDateToErrorsTable($con, $text)
{
	// Make sure we have a connection
	if ($con == null)
	{
		try
		{
			// Connect to database
			$con = mysqli_connect(DATABASE_SERVER,DATABASE_USER,DATABASE_PASSWORD,DATABASE_NAME);
		}
		catch(Exception $e)
		{
			echo "Unable to connect to SQL: " . mysqli_connect_error() . "\r\n\n";
			return false;
		}
	}

	try
	{
		// Check for given text row
		$checkQuery = sprintf(CHECK_FOR_RUN_DATE_IN_ERRORS_TABLE, $text);
		$checkResult = mysqli_query($con, $checkQuery);
		if (!$checkResult)
		{
			echo "Failed to read last update date: " . mysqli_error($con) . "\r\n\n";
			return false;
		}
		else
		{
			if (mysqli_num_rows($checkResult) > 0)
			{
				$row = mysqli_fetch_array($checkResult);
				$runDateEntityId = $row[0];
				
				// Update existing row
				$updateQuery = sprintf(UPDATE_RUN_DATE_IN_ERRORS_TABLE, $runDateEntityId);
				$updateResult = mysqli_query($con, $updateQuery);
				if (!$updateResult)
				{
					echo "Failed to update last update date: " . mysqli_error($con) . "\r\n\n";
					return false;
				}
			}
			else
			{
				// Insert new row
				$insertQuery = sprintf(INSERT_RUN_DATE_INTO_ERRORS_TABLE, $text);
				$insertResult = mysqli_query($con, $insertQuery);
				if (!$insertResult)
				{
					echo "Failed to insert last update date: " . mysqli_error($con) . "\r\n\n";
					return false;
				}
			}
		}
	}
	catch(Exception $e)
	{
		echo "Failed to write new run date to table: " . mysqli_error($con) . "\r\n\n";
	}
	
	return true;
}


// Check which of the returned orders already have a row in the processed orders table
function getOrdersAlreadyInserted($con, $orderList)
{
	$ordersAlreadyInsertedArray = Array();
	$query = sprintf(GET_ORDERS_FROM_KGH_ORDERS_PROCESSED, $orderList);
	
	try
	{
		$result = mysqli_query($con, $query);
		if (!$result)
		{
			echo "Failed to read processed orders table: " . mysqli_error($con) . "\r\n\n";
			return false;
		}
		
		while ($row = mysqli_fetch_array($result))
		{
			$ordersAlreadyInsertedArray[$row[0]] = Array();
			$ordersAlreadyInsertedArray[$row[0]]['start_date'] = $row[1];
			$ordersAlreadyInsertedArray[$row[0]]['kgh_order_created_date'] = $row[2];
			$ordersAlreadyInsertedArray[$row[0]]['kgh_shipment_count'] = $row[3];
			$ordersAlreadyInsertedArray[$row[0]]['shopify_fulfillment_count'] = $row[4];
			$ordersAlreadyInsertedArray[$row[0]]['shopify_fulfilled_date'] = $row[5];
		}
	}
	catch(Exception $e)
	{
		echo "Failed to read processed orders table: " . mysqli_error($con) . "\r\n\n";
		return false;
	}
	
	return $ordersAlreadyInsertedArray;
}


//Insert new orders into processed orders table
function insertOrdersIntoOrdersProcessedTable($con, $ordersValuesList)
{
	$query = sprintf(INSERT_ORDER_INTO_KGH_ORDERS_PROCESSED, $ordersValuesList);
	
	try
	{
		$result = mysqli_query($con, $query);
		if (!$result)
		{
			return "Failed to insert new orders into processed list: " . mysqli_error($con) . "\r\n\n";
		}
	}
	catch(Exception $e)
	{
		return "Failed to insert new orders into processed list:  " . mysqli_error($con) . "\r\n\n";
	}
	
	return "";
}


// Set order first processed date
function updateKghOrderCreated($con, $orderName)
{
	$query = sprintf(UPDATE_KGH_ORDER_CREATED_DATE, date("Y/m/d H:i:s"), $orderName);
//echo $query . "\r\n\n";
	
	try
	{
		$result = mysqli_query($con, $query);
		if (!$result)
		{
			return "Failed to update KGH order creation date: " . mysqli_error($con) . "\r\n\n";
		}
	}
	catch(Exception $e)
	{
		return "Failed to update KGH order creation date: " . mysqli_error($con) . "\r\n\n";
	}
	
	return "";
}


// Update KGH shipment count
function updateKghShipmentCount($con, $orderName, $shipmentCount)
{
	$query = sprintf(UPDATE_KGH_SHIPMENT_COUNT, $shipmentCount, $orderName);
	
	try
	{
		$result = mysqli_query($con, $query);
		if (!$result)
		{
			return "Failed to update KGH shipment count: " . mysqli_error($con) . "\r\n\n";
		}
	}
	catch(Exception $e)
	{
		return "Failed to update KGH order shipment count: " . mysqli_error($con) . "\r\n\n";
	}
	
	return "";
}


// Update Shopify fulfillment count
function updateShopifyFulfillmentCount($con, $orderName, $currentFulfillmentCount)
{
	$query = sprintf(UPDATE_SHOPIFY_FULFILLMENT_COUNT, $currentFulfillmentCount + 1, $orderName);
	
	try
	{
		$result = mysqli_query($con, $query);
		if (!$result)
		{
			return "Failed to update Shopify order fulfillment count: " . mysqli_error($con) . "\r\n\n";
		}
	}
	catch(Exception $e)
	{
		return "Failed to update Shopify order fulfillment count: " . mysqli_error($con) . "\r\n\n";
	}
	
	return "";
}


// Update Shopify fulfilledDate
function updateShopifyFulfilledDate($con, $orderName, $fulfilledDate)
{
	$query = sprintf(UPDATE_SHOPIFY_FULFILLED_DATE, $fulfilledDate, $orderName);
	
	try
	{
		$result = mysqli_query($con, $query);
		if (!$result)
		{
			return "Failed to update Shopify order fulfilled date: " . mysqli_error($con) . "\r\n\n";
		}
	}
	catch(Exception $e)
	{
		return "Failed to update Shopify order fulfilled date: " . mysqli_error($con) . "\r\n\n";
	}
	
	return "";
}



function getFormattedDateWithMilliseconds()
{
	// Get current Unix timestamp with microseconds
	$microTime = microtime(true);

	// Extract milliseconds
	$milliseconds = sprintf("%03d", ($microTime - floor($microTime)) * 1000);

	// Create DateTime object from timestamp
	$dateTime = new DateTime(date('Y-m-d H:i:s', (int)$microTime));

	// Format with milliseconds
	$formatted = $dateTime->format("Y-m-d H:i:s") . ".$milliseconds";
	
	return $formatted;
}




// Get Shopfy inventory quantities of all products in the KGH warehouse defined to Shopify
// Returns false if there is a problem, or inventory details otherwise
function getShopifyInventoryQuantities($kghQuantities)
{
	global $thisShopifyAccessToken;
	global $thisStoreName;
	global $thisWarehouse;
	
	$error = false;
	$returnArray = Array();
	
	// Multiple calls will be required
	$continuation = true;
	$afterClause = "";

	$changesResultsString = '';
	$firstNewSku = true;
	$updateDetailsQuery = "";
//	$newSkuCount = 0;
	$changedInventoryQuantity = 0;
	
	$quantityChangesStringArray = Array();

	while ($continuation)
	{
		// $continuation will be set back to true if required
		$continuation = false;
		
		// Set up and run the query
		$ch = curl_init("https://" . $thisStoreName . ".myshopify.com/admin/api/2026-01/graphql.json");
		$paramArray ["query"] = sprintf(GET_SHOPIFY_PRODUCT_DETAILS_QUERY, $afterClause, $thisWarehouse);
		$productDetailsParamString = json_encode($paramArray, JSON_FORCE_OBJECT);
		$productDetailsResultsString = runMyGraphQLCall($ch, $thisShopifyAccessToken, $productDetailsParamString);
		
		if (substr($productDetailsResultsString, 0, 6) == "Error:")
		{
			$errorMessage = $productDetailsResultsString;
			$error = true;
			break;
		}
		
		// Convert return string into object
		$productDetailsResultsArray = json_decode($productDetailsResultsString, true);

		// Extract required data
		$inventoryItemsEdges = $productDetailsResultsArray['data']['inventoryItems']['edges'];

		// Build an array of UPDATE_DELTA objects
		$quantityChangesString = "[";
		$firstQuantityChange = true;
		for ($inventoryItemIndex = 0; $inventoryItemIndex < sizeof($inventoryItemsEdges); $inventoryItemIndex++)
		{
			$thisSku = $inventoryItemsEdges[$inventoryItemIndex]['node']['sku'];
			$thisInventoryItemId = $inventoryItemsEdges[$inventoryItemIndex]['node']['id'];
			$thisInventoryLevel = $inventoryItemsEdges[$inventoryItemIndex]['node']['inventoryLevel'];
			$thisRequiresComponents = false;
			
			$variantsEdges = $inventoryItemsEdges[$inventoryItemIndex]['node']['variants']['edges'];
			$returnVariantsCount = sizeof($variantsEdges);
			for ($variantIndex =0; $variantIndex < $returnVariantsCount; $variantIndex++)
			{
				if ($variantsEdges[$variantIndex]['node']['sku'] == $thisSku)
				{
//					$thisBarcode = $variantsEdges[$variantIndex]['node']['barcode'];
					$thisVariantId = $variantsEdges[$variantIndex]['node']['id'];
					$thisRequiresComponents = $variantsEdges[$variantIndex]['node']['requiresComponents'];
					break;
				}
			}
			
//			if (!is_null($thisSku) && (strlen($thisSku)) == 5 && !$thisRequiresComponents)
			if (!is_null($thisSku) && !$thisRequiresComponents)
			{
				if (is_null($thisInventoryLevel))
				{
					// If this product doesn't have an inventory level, then it needs to be activated in the Shopify KGH warehouse
					activateKghInventoryForProduct ($thisInventoryItemId);
//					$newSkuCount += 1;
					continue;
				}

				// Get inventory available quantity for this product
				$currentAvailableQuantity = 0;
				$inventoryLevelQuantities = $inventoryItemsEdges[$inventoryItemIndex]['node']['inventoryLevel']['quantities'];
				$returnQuantitiesCount = sizeof($inventoryLevelQuantities);
				for ($quantitiesIndex = 0; $quantitiesIndex < $returnQuantitiesCount; $quantitiesIndex++)
				{
					if ($inventoryLevelQuantities[$quantitiesIndex]['name'] == 'available')
						$currentAvailableQuantity = $inventoryLevelQuantities[$quantitiesIndex]['quantity'];
				}
				
				// Shopify and KGH both hold an available quantity which is reduced when an order is made,
				// so the shopify inventory quantities should just reflect the KGH quantities exactly. 
				// Shopify only allows inventory quantities to be set with a delta, not an absolute value.
				if (array_key_exists($thisSku, $kghQuantities))
					$thisDelta = $kghQuantities[$thisSku] - $currentAvailableQuantity;
				else
					$thisDelta = 0 - $currentAvailableQuantity;

				// Append this delta to the changes string if it is non zero
				if ($thisDelta != 0)
				{
					$changedInventoryQuantity += 1;
					if (!$firstQuantityChange)
						$quantityChangesString .= ",";
					$firstQuantityChange = false;
					$quantityChangesString .= sprintf(UPDATE_DELTA, $thisDelta, $thisInventoryItemId, $thisWarehouse);
				}
				
			}

		}

		// Terminate the changes string, and add it to the array of changes strings
		$quantityChangesString .= "]";
		$quantityChangesStringArray[] = $quantityChangesString;
		
		// Check whether there are more products to retrieve
		if ($continuation = $productDetailsResultsArray['data']['inventoryItems']['pageInfo']['hasNextPage'])
			$afterClause = 'after: "' . $productDetailsResultsArray['data']['inventoryItems']['pageInfo']['endCursor'] . '"';
	}
	
	if (!$error)
	{
		// All OK, return the changes string array
		$returnArray['Data'] = Array();
		$returnArray['quantityChangesStringArray'] = $quantityChangesStringArray;
	}
	else
	{
		// Error has occurred
		$returnArray['Error'] = $errorMessage;
	}

	return $returnArray;
		
}


// Build array of KGH create order queries
function createKghOrderQueries($con, $shopifyOrdersArray, $ordersAlreadyInsertedArray)
{
	$kghOrdersArray = Array();
	
	foreach($shopifyOrdersArray as $thisOrder => $thisOrderShopifyDetails)
	{
		// Restructure Shopify order data into KGH format
		$thisOrderKghDetails = CREATE_ORDER_PAYLOAD_ARRAY;
		$thisOrderKghDetails["purchaseOrderNo"] = $thisOrder;
		$thisOrderKghDetails["options"]["backOrdersAllowed"] = false;
		
		// Check for shipping address data
		if (!array_key_exists('shippingAddress', $thisOrderShopifyDetails) || !is_array($thisOrderShopifyDetails['shippingAddress']))
		{
			writeToErrorsTable($con, "No shipping address for order: " . $thisOrder . ". Order not created in KGH");
			continue;
		}
		
		// Check required shipping fields are filled 
		if ($thisOrderShopifyDetails['shippingAddress']['address1'] == "" ||
			$thisOrderShopifyDetails['shippingAddress']['city'] == "" ||
			$thisOrderShopifyDetails['shippingAddress']['countryCodeV2'] == "")
		{
			writeToErrorsTable($con, "Shipping address1, city or country are blank for order: " . $thisOrder . ". Order not created in KGH");
			continue;
		}
		
		// Get shipping line details
		$thisOrderKghDetails["shippingCarrier"] = $thisOrderShopifyDetails["shippingCarrier"];
		$thisOrderKghDetails["shippingService"] = $thisOrderShopifyDetails["shippingService"];
		
		// Fill KGH shipping details
		$thisOrderKghDetails["shipTo"]['name'] = $thisOrderShopifyDetails['shippingAddress']['name'];
		$thisOrderKghDetails["shipTo"]['address1'] = $thisOrderShopifyDetails['shippingAddress']['address1'];
		$thisOrderKghDetails["shipTo"]['address2'] = $thisOrderShopifyDetails['shippingAddress']['address2'];
		$thisOrderKghDetails["shipTo"]['city'] = $thisOrderShopifyDetails['shippingAddress']['city'];
		$thisOrderKghDetails["shipTo"]['state'] = $thisOrderShopifyDetails['shippingAddress']['provinceCode'];
		$thisOrderKghDetails["shipTo"]['zipCode'] = $thisOrderShopifyDetails['shippingAddress']['zip'];
		$thisOrderKghDetails["shipTo"]['country'] = $thisOrderShopifyDetails['shippingAddress']['countryCodeV2'];
		$thisOrderKghDetails["shipTo"]['phone'] = $thisOrderShopifyDetails['shippingAddress']['phone'];
		$thisOrderKghDetails["shipTo"]["options"]["adultSignature"] = false;
		$thisOrderKghSalesLine = CREATE_SALES_LINE_ARRAY;

		// Add line items as long as the remaining quantity is > 0
		$thisOrderLineItemsDetails = $thisOrderShopifyDetails['lineItems'];
		foreach($thisOrderLineItemsDetails as $thisFulfillmentOrderId => $thisSkuDetails)	
		{
			if (($thisQuantity = $thisSkuDetails['remainingQuantity']) > 0)
			{
				$thisSku = $thisSkuDetails["sku"];
				
				// Check whether this sku has been entered under an earlier fulfillmentLineItemId
				$thisSkuIndex = -1;
				if (is_array($thisOrderKghDetails) && array_key_exists("salesLines", $thisOrderKghDetails))
				{
					for ($skuCount = 0; $skuCount < sizeof($thisOrderKghDetails["salesLines"]); $skuCount++)
					{
						if ($thisOrderKghDetails["salesLines"][$skuCount]["productId"] == $thisSku)
						{
							$thisSkuIndex = $skuCount;
							break;
						}
					}
				}
				
				if ($thisSkuIndex > -1)
					$thisOrderKghDetails["salesLines"][$thisSkuIndex]["quantity"] += $thisQuantity;
				else
				{
					$thisOrderKghSalesLine["productId"] = "" . $thisSku;
					$thisOrderKghSalesLine["quantity"] = $thisQuantity;
					$thisOrderKghDetails["salesLines"][] = $thisOrderKghSalesLine;
				}
			}
		}
	
		$kghOrdersArray[$thisOrder] = $thisOrderKghDetails;
	}

	return $kghOrdersArray;
	
}


// Build Shopify create fulfillment query
function setUpCreateFulfillmentQuery($fulfillmentOrdersDetails, $shipmentDetails)
{
	// Restructure KGH shipment details into Shopify fulfillment format
	$createFulfillmentQuery = CREATE_SHOPIFY_FULFILLMENT_ARRAY;
	$trackingInfo = FULFILLMENT_TRACKING_INFO;
	$trackingInfo['number'] = $shipmentDetails['trackingNo'];
	$trackingInfo['company'] = $shipmentDetails['carrierCode'];
	$createFulfillmentQuery['trackingInfo'] = $trackingInfo;
	$fulfillmentOrderFulfillmentLineItems = FULFILLMENT_ORDER_FULFILLMENT_lINE_ITEMS;
	$fulfillmentOrderFulfillmentLineItems['fulfillmentOrderId'] = $fulfillmentOrdersDetails['fulfillmentOrderId'];
	foreach ($shipmentDetails['products'] as $productsArray)
	{
		$productId = $productsArray['productId'];
		$quantityToFulfill = $productsArray['quantity'];

		// Handle one product appearing under more than one line item
		$productLineItemArray = $fulfillmentOrdersDetails[$productId];
		for ($lineItemCount = 0; $lineItemCount < sizeof($productLineItemArray); $lineItemCount++)
		{
			$fulfillmentLineItemId = $productLineItemArray[$lineItemCount]['lineItemId'];
			$thisRemainingQuantity = $productLineItemArray[$lineItemCount]['remainingQuantity'];
			$fulfillmentOrderLineItem = Array();
			$fulfillmentOrderLineItem['id'] = $fulfillmentLineItemId;
			
			if ($quantityToFulfill <= $thisRemainingQuantity)
			{
				// All the shipped quantity can be fulfilled under this line item id
				$fulfillmentOrderLineItem['quantity'] = $quantityToFulfill;
				$quantityToFulfill = 0;
			}
			else
			{
				// The remaining quantity under this line item can be fulfilled
				$fulfillmentOrderLineItem['quantity'] = $thisRemainingQuantity;
				$quantityToFulfill -= $thisRemainingQuantity;
			}

			$fulfillmentOrderFulfillmentLineItems['fulfillmentOrderLineItems'][] = $fulfillmentOrderLineItem;
			
			// If all the shipped quantity has been fulfilled, we don't need to look at any further line items for this product
			if ($quantityToFulfill == 0)
				break;
		}
	}
	$createFulfillmentQuery['lineItemsByFulfillmentOrder'] = $fulfillmentOrderFulfillmentLineItems;
	
	return $createFulfillmentQuery;
}


// Create Shopify fulfillment
function runCreateFulfillmentQuery ($createFulfillmentQuery, $currentFulfillmentCount, $orderName)
{
	global $thisShopifyAccessToken;
	global $thisStoreName;
	global $ordersAlreadyInsertedArray;
	global $con;
	
	$errorMessage = "";
	
	// Run Shopify create fulfillment query
	$fulfillmentParamArray ["query"] = CREATE_FULFILLMENTS;
	$fulfillmentParamArray ["variables"] = Array();
	$fulfillmentParamArray ["variables"]["fulfillment"] = $createFulfillmentQuery;
	$fulfillmentParamArray ["variables"]["message"] = "Creating fulfillment for KGH shipment";
	$ch = curl_init("https://" . $thisStoreName . ".myshopify.com/admin/api/2026-01/graphql.json");
	$fulfillmentParamString = json_encode($fulfillmentParamArray);
	$fulfillmentResultsString = runMyGraphQLCall($ch, $thisShopifyAccessToken, $fulfillmentParamString);
	if (substr($fulfillmentResultsString, 0, 6) == "Error:")
	{
		$errorMessage = $fulfillmentResultsString;
	}
	else
	{
		// Fulfillment has been created. Update processed order table
		$currentFulfillmentCount = $ordersAlreadyInsertedArray[$orderName]['shopify_fulfillment_count'];
		if (($returnMessage = updateShopifyFulfillmentCount($con, $orderName, $currentFulfillmentCount)) !== "")
			writeToErrorsTable($con, "Error writing fulfillment count to processed table " 
					. $orderName . ": " . $returnMessage);
		
	}
	
	// If fulfillment has been successfully created, and the order fulfillment status is "FULFILLED" thenwrite date to table
	if ($errorMessage === "")
	{
		$fulfillmentResultArray = json_decode($fulfillmentResultsString, true);
		$displayFulfillmentStatus = $fulfillmentResultArray['data']['fulfillmentCreate']['fulfillment']['order']['displayFulfillmentStatus'];
		
		if ($displayFulfillmentStatus == "FULFILLED")
		{
			// Order is now fulfilled in Shopify
			$fulfilledDate = date("Y/m/d H:i:s");
			if (($returnMessage = updateShopifyFulfilledDate($con, $orderName, $fulfilledDate)) !== "")
				writeToErrorsTable($con, "Error writing fulfillment date to processed table " 
						. $orderName . ": " . $returnMessage);
			
		}
	}
	
	return $errorMessage;
}


// Update Shopify inventory quantities
function changeShopifyQuantities($quantityChangesStringArray)
{
	global $thisShopifyAccessToken;
	global $thisStoreName;
	$errorMessage = false;

	foreach($quantityChangesStringArray as $quantityChangesString)
	{
		if (strlen($quantityChangesString) > 2)
		{
			$ch = curl_init("https://" . $thisStoreName . ".myshopify.com/admin/api/2026-01/graphql.json");
			$changesParamArray ["query"] = sprintf(UPDATE_SHOPIFY_QUANTITIES_STRING, $quantityChangesString);
			$changesParamString = json_encode($changesParamArray);

			$changesResultsString = runMyGraphQLCall($ch, $thisShopifyAccessToken, $changesParamString);
			if (substr($changesResultsString, 0, 6) == "Error:")
			{
				$errorMessage = $changesResultsString;
				break;
			}
		}
	}
	
	return $errorMessage;
}


function getShopifyIdNumber($fullId)
{
	if (strrpos($fullId, '/') === false)
		return $fullId;
	else
		return substr($fullId, strrpos($fullId, '/') + 1);
}


// Activate new products in Shopify KGH warehouse
function activateKghInventoryForProduct ($thisInventoryItemId)
{
	global $thisShopifyAccessToken;
	global $thisStoreName;
	
	$errorMessage = "";
	
	echo "Activating KGH Warehouse Inventory for Id: " . $thisInventoryItemId . "\r\n";
	$activateParamArray ["query"] = ACTIVATE_QUERY;
	$activateParamArray ["variables"] = Array();
	$activateParamArray ["variables"]["inventoryItemId"] = $thisInventoryItemId;
	$activateParamArray ["variables"]["locationId"] = "gid://shopify/Location/112831561909";
	$activateParamArray ["variables"]["available"] = 0;
	$ch = curl_init("https://" . $thisStoreName . ".myshopify.com/admin/api/2026-01/graphql.json");
	$activateParamString = json_encode($activateParamArray);
	$activateResultsString = runMyGraphQLCall($ch, $thisShopifyAccessToken, $activateParamString);
	if (substr($activateResultsString, 0, 6) == "Error:")
	{
		$errorMessage = "Error activting product " . $thisInventoryItemId . ": " . $activateResultsString;
	}
	
	return $errorMessage;
}


// Get Shopify order details (2-level continuation possible)
function getShopifyOrderDetails($con, $shippingServiceCodeArray)
{
	global $thisShopifyAccessToken;
	global $thisStoreName;
	global $thisWarehouse;
	global $runningInTestMode;
	
	$returnArray = Array();
	$fatalError = false;

	$orderContinuation = true;
	$lineItemContinuation = false;
	$orderAfterClause = "";
	$lineItemAfterClause = "";
	$ordersDataArray = Array();
	$fulfillmentOrdersDetails = Array();
	$fulfilledTrackingNumbers = Array();

	while ($orderContinuation || $lineItemContinuation)
	{

		$orderContinuation = false;
		
		// Set up query string
		if ($runningInTestMode)
			$orderStartDate = "updated_at:>2026-03-01";
		else
			$orderStartDate = "";
		$queryString = sprintf(GET_UNFULFILLED_ORDERS, $orderAfterClause, getShopifyIdNumber($thisWarehouse), $orderStartDate, $lineItemAfterClause);
		$paramArray ["query"] = $queryString;
		$paramString = json_encode($paramArray);

		// Run GraphQL call
		$resultArray = runGraphQLCall($thisStoreName, $thisShopifyAccessToken, $paramString);
//echo print_r($resultArray, true) . "\r\n\n";

		if (array_key_exists('Error', $resultArray))
		{
			$returnArray['Error'] = "Error getting Shopify order details: " . $resultArray['Error'];
			$fatalError = true;
			break;
		}
		
		// Get required details of all orders returned
		$ordersEdgesArray = $resultArray['Data']['orders']['edges'];
		for ($orderIndex = 0; $orderIndex < sizeof($ordersEdgesArray); $orderIndex++)
		{
			$orderError = false;

			$thisOrderName = $ordersEdgesArray[$orderIndex]['node']['name'];
			
			if (!array_key_exists($thisOrderName, $ordersDataArray))
			{
				// Get necessary shipping info
				$ordersDataArray[$thisOrderName] = Array();
				$ordersDataArray[$thisOrderName]['shippingAddress'] = $ordersEdgesArray[$orderIndex]['node']['shippingAddress'];
				$ordersDataArray[$thisOrderName]['displayFinancialStatus'] = $ordersEdgesArray[$orderIndex]['node']['displayFinancialStatus'];
				$ordersDataArray[$thisOrderName]['displayFulfillmentStatus'] = $ordersEdgesArray[$orderIndex]['node']['displayFulfillmentStatus'];
				$ordersDataArray[$thisOrderName]['id'] = $ordersEdgesArray[$orderIndex]['node']['id'];
				$ordersDataArray[$thisOrderName]['lineItems'] = Array();
				
				// Check we have shipping line information
				$shippingCode = null;
				if (!is_null($ordersEdgesArray[$orderIndex]['node']['shippingLine']))
				{
					$shippingCode = $ordersEdgesArray[$orderIndex]['node']['shippingLine']['code'];
				
					// Special rules for USPS shipments
					// 		All USPS codes are 15 characters. The ones we use begin DU (USPS Ground Advantage)
					//																DP (Priority Mail)
					//																DE (Priority Mail Express)
					// Our table keys contain an example of each of these - here we convert actual code to the corresponding example
					
					if (strlen(trim($shippingCode)) == 15 && !strpos(" ", $shippingCode) && (strpos("D", $shippingCode) == 0))
					{
						// This is a 15 char string with no spaces, starting with 'D', so we assume it is USPS
						// Set it to the example key we have
						switch(substr($shippingCode, 1, 1))
						{
							case 'U':
								$shippingCode = "DUXP0XXXUC01060";
								break;
							case 'P':
								$shippingCode = "DPXX0XXXXC01005";
								break;
							case 'E':
								$shippingCode = "DEXX0XXXXC01005";
								break;
							default:
								break;
						}
					}
				}
				
				if (!is_null($shippingCode) && (array_key_exists($shippingCode, $shippingServiceCodeArray)))
				{
					$ordersDataArray[$thisOrderName]['shippingCarrier'] = $shippingServiceCodeArray[$shippingCode]['serviceCarrier'];
					$ordersDataArray[$thisOrderName]['shippingService'] = $shippingServiceCodeArray[$shippingCode]['kghServiceCode'];

					// Check all existing fulfillments. 
					// Tracking numbers will be used to find which KGH shipments have already had Shopify fulfillments created
					if (($fulfillments = $ordersEdgesArray[$orderIndex]['node']['fulfillments']) !== null)
					{
						for ($fulfillmentIndex = 0; $fulfillmentIndex < sizeof($fulfillments); $fulfillmentIndex++)
						{
							$locationId = $fulfillments[$fulfillmentIndex]['location']['id'];
							$fulfillmentStatus = $fulfillments[$fulfillmentIndex]['displayStatus'];
							if (($locationId == $thisWarehouse) && ($fulfillmentStatus != "CANCELED"))
							{
								for ($trackingInfoIndex = 0; $trackingInfoIndex < sizeof($fulfillments[$fulfillmentIndex]['trackingInfo']); $trackingInfoIndex++)
								{
									$thisTrackingNumber = $fulfillments[$fulfillmentIndex]['trackingInfo'][$trackingInfoIndex]['number'];
									$fulfilledTrackingNumbers[$thisTrackingNumber] = $thisOrderName;
								}
							}
						}
					}
				}
				else
				{
					if (is_null($shippingCode))
						$codeFound = "*null*";
					elseif ($shippingCode == "")
						$codeFound = "*empty*";
					else 
						$codeFound = $shippingCode;
					writeToErrorsTable($con, "Order " . $thisOrderName . " Shipping code " . $codeFound . " not found. Order not processed.");
					$orderError = true;
				}	
			}

			$fulfillmentOrderId = null;
			if (!$orderError)
			{
				// Get fulfillment order for the KGH warehouse location
				$fulfillmentOrdersEdgesArray = $ordersEdgesArray[$orderIndex]['node']['fulfillmentOrders']['edges'];
				$returnFulfillmentOrderArray = checkForCorrectFulfillmentOrder($thisWarehouse, $fulfillmentOrdersEdgesArray);
				$fulfillmentOrderId = $returnFulfillmentOrderArray['fulfillmentOrderId'];
				
				if ($fulfillmentOrderId == null)
				{
					writeToErrorsTable($con, "Order " . $thisOrderName . " No valid fulfillmentOrder at this location. Order not processed.");
					$orderError = true;
				}
				else
				{
					$fulfillmentOrdersDetails[$thisOrderName] = Array();
					$fulfillmentOrdersDetails[$thisOrderName]['fulfillmentOrderId'] = $fulfillmentOrderId;
					$lineItemsArray = $returnFulfillmentOrderArray['lineItemsArray'];

					// Get all line items in the fulfillment order
					if (($lineItemCount = sizeof($lineItemsArray['edges'])) == 0)
					{
						writeToErrorsTable($con, "Order: " . $thisOrderName . " Fulfillment order contains no items. Order not processed.");
						$orderError = true;
					}
					else
					{
						for ($lineItemIndex = 0; $lineItemIndex < $lineItemCount; $lineItemIndex++)
						{
							$thisSku = $lineItemsArray['edges'][$lineItemIndex]['node']['sku'];
							if ($thisSku == "")
							{
								writeToErrorsTable($con, "Order: " . $thisOrderName . " Fulfillment order contains blank sku. Order not processed.");
								$orderError = true;
								break;
							}

							$thisRemainingQuantity = $lineItemsArray['edges'][$lineItemIndex]['node']['remainingQuantity'];
							$fulfillmentLineItemId = $lineItemsArray['edges'][$lineItemIndex]['node']['id'];
							$ordersDataArray[$thisOrderName]['lineItems'][$fulfillmentLineItemId] = $lineItemsArray['edges'][$lineItemIndex]['node'];

							if (!array_key_exists($thisSku, $fulfillmentOrdersDetails[$thisOrderName]))
								$fulfillmentOrdersDetails[$thisOrderName][$thisSku] = Array();
							$skuLineItemArray = Array();
							$skuLineItemArray['lineItemId'] = $fulfillmentLineItemId;
							$skuLineItemArray['remainingQuantity'] = $thisRemainingQuantity;
							
							$fulfillmentOrdersDetails[$thisOrderName][$thisSku][] = $skuLineItemArray;
							
						}
					}
				}
			}
			
			// If there is a problem with this order, remove details from return arrays then continue to next order
			if ($orderError)
			{
				unset($ordersDataArray[$thisOrderName]);
				unset($fulfillmentOrdersDetails[$thisOrderName]);
				foreach($fulfilledTrackingNumbers As $trackingNumber => $orderName)
				{
					if ($orderName === $thisOrderName)
						unset($fulfilledTrackingNumbers[$trackingNumber]);
				}
				continue;
			}
			
			// Check whether there are more line items to come
			$lineItemContinuation = $lineItemsArray['pageInfo']['hasNextPage'];
			if ($lineItemContinuation)
			{
				$lineItemAfterClause = 'after:"' . $lineItemsArray['pageInfo']['endCursor'] . '"';
				break;
			}
			else
				$lineItemAfterClause = "";
		}
		if ($lineItemContinuation)
			continue;
		
		// Check whether there are more orders to come
		$orderContinuation = $resultArray['Data']['orders']['pageInfo']['hasNextPage'];
		if ($orderContinuation)
		{
			$orderAfterClause = 'after:"' . $resultArray['Data']['orders']['pageInfo']['endCursor'] . '"';
		}

	}

	if (!$fatalError)
	{
		// Populate return data
		$returnArray['Data'] = Array();
		$returnArray['Data']['ordersDataArray'] = $ordersDataArray;
		$returnArray['Data']['fulfillmentOrdersDetails'] = $fulfillmentOrdersDetails;
		$returnArray['Data']['fulfilledTrackingNumbers'] = $fulfilledTrackingNumbers;
	}

	return $returnArray;

}

function checkForCorrectFulfillmentOrder ($thisWarehouse, $fulfillmentOrdersEdgesArray)
{
	$fulfillmentOrderId = null;
	$lineItemsArray = null;
	
	for ($fulfillmentOrderIndex = 0; $fulfillmentOrderIndex < sizeof($fulfillmentOrdersEdgesArray); $fulfillmentOrderIndex++)
	{
		$locationId = $fulfillmentOrdersEdgesArray[$fulfillmentOrderIndex]['node']['assignedLocation']['location']['id'];
		if ($locationId == $thisWarehouse)
		{
			$lineItemsArray = $fulfillmentOrdersEdgesArray[$fulfillmentOrderIndex]['node']['lineItems'];
			$lineItemCount = sizeof($lineItemsArray['edges']);
			$itemsFound = false;
			for ($lineItemIndex = 0; $lineItemIndex < $lineItemCount; $lineItemIndex++)
			{
				$thisRemainingQuantity = $lineItemsArray['edges'][$lineItemIndex]['node']['remainingQuantity'];
				if ($thisRemainingQuantity > 0)
				{
					$itemsFound = true;
					break;
				}
			}

			if ($itemsFound)
			{
				$fulfillmentOrderId = $fulfillmentOrdersEdgesArray[$fulfillmentOrderIndex]['node']['id'];
				break;
			}
		}
	}
	
	$returnArray = Array();
	$returnArray['lineItemsArray'] = $lineItemsArray;
	$returnArray['fulfillmentOrderId'] = $fulfillmentOrderId;
	
	return $returnArray;
}


// Create KGH order
function createOrder($accessKey, $contentParameters, $test = false)
{
	// Constant values for createOrder
	$pathParameters = "";
	$method = "POST";
	$category = "SalesOrder/Create";
	$queryParameters = "";
	

	// Make the call 
	$returnArray = runKGHCall($accessKey, $pathParameters, $method, $contentParameters, $category, $queryParameters, $test);
	
	return $returnArray;
}


// Get KGH product details
function getProductDetails($accessKey, $productList = "", $test = false)
{
	$returnArray = Array();
	
	// Constant values for getProductDetails
	$pathParameters = "";
	$method = "GET";
	$category = "Inventory";
	$contentParameters = "";

	// Product list is a comma separated list of products
	$normalisedList = str_replace(" ", "", $productList);
	if ($normalisedList == "")
		$queryParameters = "";
	else
		$queryParameters = "?products=" . $normalisedList;	
	
	// Make the call 
	$returnArray = runKGHCall($accessKey, $pathParameters, $method, $contentParameters, $category, $queryParameters, $test);
	
	return $returnArray;
	
}


// Get KGH order status
function getOrderStatus ($accessKey, $purchaseOrderNo, $test = false)
{
	$returnArray = Array();
	
	// Constant values for getOrderStatus
	$pathParameters = "";
	$method = "POST";
	$category = "SalesOrder/Status";
	$queryParameters = "";

	// Normally purchaseOrderNo will be filled, otherwise salesOrdeHo can be used
	if ($purchaseOrderNo == "")
	{
		// Order number not filled
		$returnArray ["Error"] = "PurchaseOrderNo must be passed";
		return $returnArray;
	}
	else
		$contentParameters = '{
		  "purchaseOrderNo": "' . $purchaseOrderNo . '"
		}';
	
	// Make the call 
	$returnArray = runKGHCall($accessKey, $pathParameters, $method, $contentParameters, $category, $queryParameters, $test);
	
	return $returnArray;
	
}

// Get KGH shipment details by purchase order number	
function getShipmentDetailsByOrderNo($accessKey, $purchaseOrderNo, $test = false)
{

	$returnArray = Array();

	// Constant values for getShipmentDetailsByOrderNo
	$method = "GET";
	$category = "Shipments";
	$contentParameters = "";
	$queryParameters = "";		

	// Required order number is part of the path
	$pathParameters = "/" . $purchaseOrderNo;
	
	// Make the call 
	$returnArray = runKGHCall($accessKey, $pathParameters, $method, $contentParameters, $category, $queryParameters, $test);
	
	return $returnArray;

}


// Get KGH shipment details by date range			
function getShipmentDetailsByDate($accessKey, $startDate, $endDate, $test = false)
{		
	$returnArray = Array();
	
	// Constant values for getShipmentDetailsByDate
	$pathParameters = "";
	$method = "GET";
	$category = "Shipments";
	$contentParameters = "";
	
	// Start and end dates must be no more than 5 days apart
	// They might be specified as DateTime objects, or as strings (Format "YYYY-MM-DDTHH:mm:ssZ")
	if (is_string($startDate))
		$queryParameters = "?startDate=" . $startDate . "&endDate=" . $endDate;	
	else
		$queryParameters = "?startDate=" . dateToString($startDate) . "&endDate=" . dateToString($endDate);

	// Make the call 
	$returnArray = runKGHCall($accessKey, $pathParameters, $method, $contentParameters, $category, $queryParameters, $test);
	
	return $returnArray;

}


// Run Shopify GraphQl call. Returns a string
function runMyGraphQLCall($ch, $shopifyToken, $paramString)
{
	// Set options
	curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
	curl_setopt($ch, CURLOPT_POSTFIELDS, $paramString);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: application/json", 
					"X-Shopify-Access-Token:  " . $shopifyToken,
							"Content-Length: " . strlen($paramString)));

	// Get types
	$result = curl_exec($ch);
	
	// Check result - might be Magento error message or curl error
	if (curl_error($ch) != "")
	{
		// Curl error
		return "Error: " . curl_error($ch);
	}

	$resultArray = json_decode($result, true);
	if (array_key_exists("errors", $resultArray))
	{
		return "Error: Shopify error" . json_encode($resultArray["errors"], true);
	}

	if ($result == "")
	{
		// Empty string returned
		return "Error: No data returned";
	}
	else
	{
		// Success!
		return $result;
	}
}






?>
