<?php 
$time1 = microtime(true);
$cli = php_sapi_name() === 'cli';
if($cli) { 
	fwrite(STDERR,"Command line mode started ". date("F j, Y, g:i a") . ".\n"); 
	$headers = [];
}
else { 
	ob_start(); 
	$headers = getallheaders();
}

// test mode
$testMode = false;
if ( ($headers['test'] ?? '0')  == '1') {
	error_reporting(E_ALL);
	ini_set('display_errors', 1);
	$testMode = true;
}

// for the cloud environment (and future maybe our dev database)
$sandbox = false;
if ( ($headers['sandbox'] ?? '0')  == '1') {
	$sandbox = true;
}
$env = preg_match('/staging/',__DIR__) ? 'staging' : 'prod';
$wsApi = ($env == 'staging') ? 'http://localhost:8080' : 'http://localhost:8080'; // for now the same websocket server

require_once __DIR__.'/../vendor/autoload.php';
use StripeService\StripeService;
use CloudKitService\CloudKitService;
use Db\Db;
use Db\Godb;
use ApplePay\ApplePay;
use SquareService\SquareService;

$noSubscriptionMessage = 'No subscription found. If you feel you have reached this in error please call us at (385) 204-6301';
$response = array();
$logSuffix = '';
$logFile = __DIR__ . '/../logs/log';




/// Function for logging to our flat file and the database
function logThis($action, $inputs, $response) {
	global $time1, $_SERVER, $testMode, $inputBody, $output, $logSuffix, $logFile;
	$time2 = microtime(true);
	$times = sprintf("%.2f,%.2f", $time1-$_SERVER["REQUEST_TIME_FLOAT"],$time2-$time1);
	$logAddition = date("m/d H:i:s",time()-6*3600) . 
		($testMode ? ' [TEST MODE]' : '') . 
		" REQUEST($action)[$times]: $inputBody\n\t" . 
		$response .  $logSuffix . "\n";
	file_put_contents($logFile, $logAddition, FILE_APPEND);				// Log to file
	Db::shared()->log($action, $inputs, $response); 						// Log to the database
}

// Gather inputs
{
	if(!$cli) {
		if($_SERVER['REQUEST_METHOD'] === 'GET') { 
			parse_str($_SERVER['QUERY_STRING'], $inputs); 
			$inputBody = json_encode( $inputs );	// for the log
		}
		else {
			$inputBody = file_get_contents('php://input');
			$inputs = json_decode( $inputBody, true);
		}
	}
	else {
		$body = getopt("",["body:"]);
		if(isset($body['body'])) {
			$inputBody = $body['body'];
		}
		else {
			$stdin = fopen('php://stdin', 'r');
			$inputBody = "";
			while(!feof($stdin)){
				$inputBody .= fgets($stdin, 4096);
			}
			$inputBody = trim($inputBody);
			if(strlen($inputBody)==0) {
				echo "WARNING: no inputs read from stdin...\n";
			}
		}
		$inputs = json_decode( $inputBody, true);
	}

	if(!isset($inputs)) { 
		//header("Location: /noInputs");
		error_log('Invalid request: no inputs');
		die("The time is " . date("m/d H:i:s"));
	}
}

// Determine the method to use
{
	$action = 'none';
	if($cli) {
		$action = getopt("f:");
		if(!isset($action['f'])) {
			die("ERROR. No function specified. Usage: XXXXX -f=function\n");
		}
		$action = $action['f'];
		fwrite(STDERR,"INPUTS: $action(\"$inputBody\")\nOUTPUT: ");
	}
	else if(isset($_REQUEST['action'])) {
		$action = $_REQUEST['action'];
	}
}

function forwardToSwift($vars) {
	//forwardToSwiftServer($vars,'https://c2fc-136-36-61-151.ngrok.io/v1PaymentUpdated');
	forwardToSwiftServer($vars,'https://dev.flashordr.com/flashorder/v1PaymentUpdated');
	// disabled for now
}

function forwardToSwiftServer($vars,$server) {
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $server);
	//curl_setopt($ch, CURLOPT_URL, 'https://d306686a8eca.ngrok.io/v1PaymentUpdated');
	$query = json_encode($vars);
	$headers = Array();
	$headers[] = 'Content-Length: ' . strlen($query);
	$headers[] = 'Content-Type: application/json';
	curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	$result = curl_exec($ch);
	curl_close($ch);
}

try {


	// God Mode stuff Administrative Stuff
	if($env == 'staging' && ($headers['dog'] ?? '')=='98ub23fa9vaiu3nqipufasijdfvni-98j2n3' ) {
		$db = Db::shared();
		switch($action) {
		case 'gmList':	 
			$response['vendors'] = $db->getVendors();
			break;

		case 'gmLogin':
			$response['session'] = $db->createSession(null,$inputs['appId'],null);
			$response['appId'] = $inputs['appId'];
			break;
		
		case 'getSession':
			$db = Db::shared();
			$stmt = $db->prepare('SELECT sessionId,expires,locationId,appId,userId,permission FROM sessions WHERE sessionId=?');
			$stmt->execute([$inputs['sessionId']]);
			$response = $stmt->fetch(PDO::FETCH_ASSOC);
			$response['expires'] = intVal($response['expires']);
			$response['permissions'] = intVal($response['permissions']);
			break;

		case 'godEcho':
			$response['echo'] = 'Hello world';
			break;
		}

		header('Content-type: application/json');
		echo json_encode($response);
		exit;
	}


	// Main API 
	switch($action) {
//		case "crypto":
//			$crypto = new Crypto();
//			$response['test'] = $crypto->test($inputs);
//			break;

		case 'dbExport':
			Db::shared()->exportSqlite($inputs['locationId'], isset($inputs['silent']));
			logThis($action, $inputs, "<<gzipped file: {$inputs['locationId']}>>");
			exit;
		
		case 'env':
			echo $env;
			exit;

		case 'extensions':
			$response['extensions'] = get_loaded_extensions();
			break;

		case 'applepayTest':
			$test = new ApplePay();
			if(isset($inputs['applepay'])) {
				$response['decrypted'] = $test->decrypt($inputs['applepay']);
			}
			else {
				$test->decrypt("asdf");
			}
			break;

		case 'orderNumberTest':
			$db = Db::shared();
			$response['n'] = $db->getOrderNumber($inputs['vendorId']);
			break;

		case 'god':
			if ($env != 'staging') {
				error_log('UNAUTHORIZED GOD: a God tried to issue command not on staging');
				die("bad env");
			}
			$db = Db::shared();
			$stmt = $db->prepare('INSERT INTO gods(name,pushToken) VALUES (?,?)');
			$stmt->execute([$inputs['name'],base64_decode($inputs['token'])]);
			$response['god'] = true;
			break;

		case 'gmSessions':
			if ($env != 'staging') {die("bad env");}
			$db = Db::shared();
			$stmt = $db->prepare('SELECT * FROM smallSessions WHERE appId=?');
			//$stmt = $db->prepare('SELECT * FROM smallSessions WHERE appId=? AND build NOT REGEXP "debug"');
			$stmt->execute([$inputs['appId']]);
			$response['sessions'] = $stmt->fetchAll();
			break;

		case 'onboardSquare':
			$to = 'westonhafen@gmail.com';
			$subject = 'Square Customer Onboard';
			$body= "<html><table>
			<tr><td>Business</td><td>{$inputs['name']}</td></tr>
			<tr><td>Email</td><td>{$inputs['email']}</td></tr>
			<tr><td>Phone</td><td>{$inputs['phone']}</td></tr>
			</table></html>";
			$headers =  "From: mobileorderingsolutions@gmail.com\r\n";
			$headers .= "Reply-To: Flash Order<kiosko@kioskomobile.com>\r\n";
			$headers .= "MIME-Version: 1.0\r\n";
			$headers .= "Content-type: text/html\r\n";
			$response['mailed'] = mail($to,$subject,$body,$headers);
			break;

		case 'appauth':	// Square OAuth for a customer's own mobile app
			$db = Db::shared();

			// Read their app credentials (need to be set up first--taken from their developer portal)
			//   manual insert id : UPDATE apps SET sqAppId='xxxxxx' WHERE appId='sqQZW24F9RZ8F38' LIMIT 1;
			//   manual insert key: UPDATE apps SET appSecretAES=AES_ENCRYPT('xxxxx', UNHEX('xxxxxx')) WHERE appId='sqQZW24F9RZ8F38' LIMIT 1;
			$merchantId = 'sq' . $_REQUEST['state'];	// We are getting a warning here that it's not there
			$getStmt = $db->prepare('SELECT sqAppId, AES_DECRYPT(appSecretAES,UNHEX(?)) FROM apps WHERE appId=?'); 
			$getStmt->execute( [ Db::AES_KEY, $merchantId ] );
			$row = $getStmt->fetch(PDO::FETCH_NUM);
			if($row[0] == null || $row[1] == null) {
				echo json_encode($getStmt->errorInfo());
				throw new Exception('Your app id & secret are not configured yet- contact us' . '<pre>'.json_encode($_REQUEST).'</pre>');
			}

			// get their token
			$redirect = 'kioskosq://oauthdone';
			$result = SquareService::getAccessToken( $_REQUEST['code'], $row[0], $row[1], $redirect );
			if(isset($result['access_token'])) {
				$token = $result['access_token'];
				$rToken = $result['refresh_token'];
				$expire = strtotime($result['expires_at']);
				$stmt = $db->prepare('UPDATE apps SET appTokenAES=AES_ENCRYPT(?,UNHEX(?)),appRefreshAES=AES_ENCRYPT(?,UNHEX(?)),appTokenExp=? WHERE appId=? LIMIT 1');
				$response['result'] = $stmt->execute( [$token, Db::AES_KEY, $rToken, Db::AES_KEY, $expire, $merchantId] );

				header("Location: $redirect");
			}
			else {
				echo 'Something went wrong';
				error_log('SQ MOBILE APP TOKEN FETCH FAIL: ' . json_encode($result));
			}
			break;

		case 'loyaltyStatus':
			$stmt = Db::shared()->prepare('SELECT loyalty FROM apps WHERE appId=?');
			$stmt->execute([ $inputs['appId'] ]);
			$response['loyalty'] = intval($stmt->fetch(PDO::FETCH_NUM)[0]);
			break;

		case 'loyaltyAdmin':
			$stmt = Db::shared()->prepare('UPDATE apps SET loyaltyBankPts=?,loyaltyBankAmt=? WHERE appId=? AND loyalty=1');
			$response['saved'] = $stmt->execute([ $inputs['loyaltyBankPts'], $inputs['loyaltyBankAmt'], $inputs['appId'] ]);
			break;

		case 'mobileAppInfo':		// information about their mobile app setup  input: appId (merchantId)
			$db = Db::shared();
			$session = $db->getSession();
			$response = $db->getMobileAppStatus($session['appId']);
			break;

		case 'setMobAppKey':
			$db = Db::shared();
			$session = $db->getSession();
			$key = $inputs['key'] ?? '';
			$value = $inputs['value'];
			if($key == 'sqAppId') {
				$stmt = $db->prepare('UPDATE apps SET sqAppId=? WHERE appId=? LIMIT 1');
				$stmt->execute([ $inputs['value'], $session['appId'] ]);
			}
			else if ($key == 'secret') {
				$stmt = $db->prepare('UPDATE apps SET appSecretAES=AES_ENCRYPT(?,UNHEX(?)) WHERE appId=? LIMIT 1');
				$stmt->execute([ $inputs['value'], Db::AES_KEY, $session['appId'] ]);
			}
			else {
				throw new \Exception('invalid input');
			}
			$response = $db->getMobileAppStatus($session['appId']);
			break;

		case 'promoCode':
			$db = Db::shared();
			if(strtolower($inputs['code']) == 'devin') {
				$stmt = $db->prepare('INSERT INTO promoCodes (id,appId,product,duration,title) VALUES (?,?,?,?,?)');
				$code = Db::getGuid();
				$stmt->execute([ $code , $inputs['appId'], 'Kiosk_1', 7*24*3600, $inputs['code']]);
				$to = 'devin.cash@gmail.com';
				//$to = 'jon.lund@gmail.com';
				$subject = 'Promo Code ' . $code;
				$body = "The promo code is $code";
				$headers =  "From: Flash Order<mobileorderingsolutions@gmail.com>\r\n";
				$headers .= "Reply-To: Flash Order<kiosko@kioskomobile.com>\r\n";
				$headers .= "MIME-Version: 1.0\r\n";
				$headers .= "Content-type: text/html\r\n";
				$response['mailed'] = mail($to,$subject,$body,$headers);
				$response['to'] = $to;
			}
			else {
				$stmt = $db->prepare('SELECT appId,product,duration,title FROM promoCodes WHERE id=?');
				$stmt->execute([  strtolower($inputs['code'])  ]);
				if($arr = $stmt->fetch(PDO::FETCH_ASSOC)) {
					$stmt = $db->prepare('INSERT INTO subscriptions (subscriptionId,productId,appId,expires) VALUES (?,?,?,?)');
					$stmt->execute([ 
						'PROMO_' . strtoupper($arr['title']) . '_' . $inputs['code'], 
						$arr['product'], 
						$arr['appId'], 
						time() + $arr['duration'] ]);
					$response['success'] = true;
				}
				else {
					throw new \Exception('invalid code');
				}
			}
			break;

		case 'getCustomer':
			$byId = isset($inputs['id']);
			$query = 'SELECT id,name,email,phone,balance,credit FROM customers WHERE appId=? AND ';
			$query .= ($byId ? 'id=?' : 'phone=?');
			$param = ($byId ? $inputs['id'] : $inputs['phone']);
			$stmt = Db::shared()->prepare($query);
			$stmt->execute([$inputs['appId'], $param]);
			if($arr = $stmt->fetch(PDO::FETCH_ASSOC)) {
				$response = $arr;
				$response['balance'] = intVal($arr['balance']);
				$response['credit'] = intVal($arr['credit']);
				
				if(isset($inputs['orderId'])) {
					$response['balance'] = (int)$db->assignCustomerToOrder($arr['id'], $inputs['orderId']);
				}
			}
			else {
				$response['id'] = null;
			}
			break;

		case 'mkCustomer':
			$db = Db::shared();
			$cleaned = array_filter($inputs, function($k) {		// only allow these
				   return in_array($k,Array('appId','name','email','phone','custom0','custom1'));
			}, ARRAY_FILTER_USE_KEY);
			$newCustomerId = $db->createCustomer($cleaned);
			$response['id'] = $newCustomerId;
			$response['phone'] = $inputs['phone'];
			$response['appId'] = $inputs['appId'];


			// If they sent an order then give them the credit for it and merge them with the previous entry
			if(isset($inputs['orderId'])) {
				$response['balance'] = (int)$db->assignCustomerToOrder($newCustomerId, $inputs['orderId']);
			}
			else {
				$arr = $db->addPointsToCustomer(0,$newCustomerId,null);
				array_merge($response, $arr);
			}
			break;

		case 'tax':
			$db = Db::shared();
			$tax = doubleVal($inputs['tax']);
			$stmt = $db->prepare('UPDATE locations SET tax=? WHERE locationId=? LIMIT 1');
			$stmt->execute([$tax,$inputs['locationId']]);

			// Save to cloudkit as well
			$ck = new CloudKitService($sandbox);
			$record = new \CloudKit\Record('Vendor',$inputs['locationId']);
			$record->setField('tax',$tax);
			$ck->saveRecords([$record],true);
			$response['tax'] = "" . $tax;		// purposely make string
			break;

		case 'updateLocations':
			$db = Db::shared();
			$merchant = $inputs['appId'];
			$sq = SquareService::fromStored( $inputs['appId'] );
			$response['inputs'] = $inputs;
			$locations = $sq->getLocations();
			$response['locationsFromSquare'] = $locations['locations'];
			$n = count($locations['locations']);
			if($n < 1) {
				throw new \Exception('Error: no locations are set up yet in Square for this account');
			}
			$stmt = $db->prepare('INSERT INTO locations (locationId,appId,name) VALUES (?,?,?) ON DUPLICATE KEY UPDATE name=?');
			foreach($locations['locations'] as $location) {
				$lid = 'sq' . $location['id'];
				$stmt->execute([ $lid, $merchant, $location['name'], $location['name'] ]);
			}
			$response['nUpdated'] = $n;

			$ck = new CloudKitService($sandbox);
			$locationRecords = Array();
			$locationIds = Array();
			for($i=0;$i<$n;$i++){
				$location = $locations['locations'][$i];
				$locationRecords[] = $ck->newVendorRecord($location, $merchant);	// the 'sq' gets added to location
				$locationIds[] = 'sq' . $location['id'];
			}
			$results = $ck->saveRecords($locationRecords,true);
			$response['results'] = $results;

			break;

		case 'rmRecords':
			// TODO: check for admin token
			$ck = new CloudKitService($sandbox);				// input: {appId:'XXXXX',style:base64}
			$chunkSize = 20;
			$records = $inputs['recordIds'];
			$ckResults = Array();
			$numCloudKitChunks = 0;
			for($i=0;$i<count($records);$i+=$chunkSize){
				$chunk = array_slice($records,$i,$chunkSize);
				$ckResults[] = $ck->deleteRecords($chunk);
			}
			$response['done'] = count($ckResults);
			break;

		case 'sqRevoke':
			$sq = SquareService::fromStored( $inputs['appId'] );
			$response['revoked'] = $sq->revoke();
			if($response['revoked'] == true) {
				$stmt = Db::shared()->prepare('UPDATE apps SET squareTokenAES=NULL,tokenExpires=NULL WHERE appId=? LIMIT 1');
				$stmt->execute([ $inputs['appId'] ]);
			}
			break;

		case 'getsqToken':
			if($headers['MASTERKEY'] ?? 'a' != 'asdinaoivnaoiefawinfaiwoefnawfawefawefnvd23') {}
			else if(!$cli) { throw new \Exception('must be local client'); }
			$db = Db::shared();
			$whichToken = ( isset($inputs['app']) ? 'appTokenAES' : 'squareTokenAES' );
			$stmt = $db->prepare("SELECT AES_DECRYPT($whichToken,UNHEX(?)) as `squareToken` FROM apps WHERE appId=?");
			$stmt->execute([Db::AES_KEY, $inputs['appId']]);
			$response['result'] = $stmt->fetch(PDO::FETCH_ASSOC);
			break;

		case 'locationInfo':
			$sq = SquareService::fromStored( $inputs['appId'] );
			$locations = $sq->getLocations();
			$response['locations'] = Array();//$locations;
			foreach($locations['locations'] as $l) {
				$ll = Array();
				$ll['id'] = $l->getId();
				$ll['name'] = $l->getName();
				$ll['address'] = $l->getAddress();
				$ll['timezone'] = $l->getTimezone();
				$ll['capabilities'] = $l->getCapabilities();
				$ll['status'] = $l->getStatus();
				$ll['created_at'] = $l->getCreatedAt();
				$ll['merchant_id'] = $l->getMerchantId();
				$ll['country'] = $l->getCountry();
				$ll['language_code'] = $l->getLanguageCode();
				$ll['currency'] = $l->getCurrency();
				$ll['phone_number'] = $l->getPhoneNumber();
				$ll['business_name'] = $l->getBusinessName();
				$ll['type'] = $l->getType();
				$ll['website_url'] = $l->getWebsiteUrl();
				$ll['business_hours'] = $l->getBusinessHours();
				$ll['business_email'] = $l->getBusinessEmail();
				$ll['description'] = $l->getDescription();
				$ll['twitter_username'] = $l->getTwitterUsername();
				$ll['instagram_username'] = $l->getInstagramUsername();
				$ll['facebook_url'] = $l->getFacebookUrl();
				$ll['coordinates'] = $l->getCoordinates();
				$response['locations'][$ll['id']] = $ll;
				//echo "Location: " . $l->getName();
				//echo $l->getStatus();
			}

			//$response['locatoins'] = json_encode($locations);
			//print_r($locations);
			//exit;
			//echo $locations[0]->getStatus();//['status'];
			break;

		case 'tokenStatus':
			$db = Db::shared();
			$session = $db->getSession();

			try {
				$sq = SquareService::fromStored( $session['appId'] );
				$response['confirmed'] = $sq->checkToken();
				if($response['confirmed'] == false || isset($response['confirmed']['type'])) {
					$response['isNull'] = "1";
				}
				else {
					$stmt = $db->prepare('SELECT squareTokenAES IS NULL as `isNull`, tokenExpires FROM apps WHERE appId=?');
					$stmt->execute([$session['appId']]);
					$response = array_merge($response,$stmt->fetch(PDO::FETCH_ASSOC));
				}
			} catch (\Exception $e) {
				$response['isNull'] = "1";
			}

			
			//$response['tokenStatus'] = $stmt->fetch(PDO::FETCH_ASSOC);
			$stmt = $db->prepare('SELECT productId, expires, type FROM subscriptions WHERE appId=?');
			$stmt->execute([$session['appId']]);
			$response['subscriptions'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
			break;

		case 'updateCurrency':
			if(!$cli) { die("forbidden"); }
			$sq = SquareService::fromStored( $inputs['appId'] );
			$ck = new CloudKitService($sandbox);
			echo "working on {$inputs['appId']}...";
			$locations = $sq->getLocations();
			$i = 0;
			$records = Array();
			foreach($locations['locations'] as $loc) {
				$records[] = $ck->newVendorRecord($loc, 'sq' . $inputs['appId']);
				//$id = $loc['id'];
				//$cur = $loc['currency'];
				//echo "\nsetting {$id} to {$cur}...";
				//$ck->setRecordValue('Vendor','sq' . $id,'currency',$cur);
			}
			$results = $ck->saveRecords($records,true);
			header('Content-type: application/json');
			echo substr(json_encode($results),0,80);
			exit;

		case 'squareStatus':
			$sq = SquareService::fromStored( $inputs['merchant_id'] );		// send without the 'sq'
			$response['expires'] = $sq->expires($inputs['merchant_id']);			
			break;

		case 'updateTip':
			$sq = SquareService::fromStored( $inputs['merchant'] );
			$transaction = $sq->getTransaction( $inputs['locationId'], $inputs['transactionId'] );
			//$response['transaction'] = $transaction;
			$tip = $transaction['tenders'][0]['tip_money']['amount'] ?? 0;
			if($tip>0) {
				$db = Db::shared();
				$stmt = $db->prepare('UPDATE orders set tip=?/100 WHERE orderId=?');
				$stmt->execute([$tip,$inputs['transactionId']]);
				$response['upated'] = true;
			}
			$response['tip'] = $tip;
			break;

		case 'sqReaderCode':					// Square mobile authorization code
			$db = Db::shared();
			$db->validateSession();
			$sq = SquareService::fromStored( $inputs['merchant'] );
			$response = $sq->getMobileAuthCode( $inputs['locationId'] );
			if(isset($response['type']) && $response['type'] == 'oauth.revoked') {
				$db->rmToken( $inputs['merchant'] );
				throw new \Exception('Your Square account access has been revoked. To re-grant access, please sign all the way out and sign in again.');
			}
			break;

		case 'getCodes':										// input:  ["sq26K9MSNCXX76H","sq20IVE9VNEW",...]
			if(!isset($inputs) || !isset($inputs[0])) {
				throw new \Exception($noSubscriptionMessage);
			}

			// First get subscription, check number of locations		 (look up appId, subscription from join off locations table)
			$db = Db::shared();
			$stmt = $db->prepare('select productId, s.appId, s.subscriptionId from locations join subscriptions as `s` on s.appId=locations.appId WHERE locationId=? LIMIT 1');
			$stmt->execute([ $inputs[0] ]);
			$arr = $stmt->fetch(PDO::FETCH_ASSOC);
			if(!$arr) { throw new \Exception($noSubscriptionMessage); }
			$n = intval(substr($arr['productId'],-1));								// The trailing number of the subscription tells how many locations they purchased
			if($n < count($inputs)) { throw new \Exception('Insufficient subscription. Please upgrade your account'); }

			// Clear existing codes on all locations
			$appId = $arr['appId'];
			$stmt = $db->prepare('UPDATE locations SET launchCode=NULL WHERE appId=?');
			$stmt->execute([$appId]);

			// Generate new codes for the specified locations
			$newCodes = array();
			$stmt = $db->prepare('UPDATE locations SET launchCode=?,subscriptionId=? WHERE locationId=?');
			foreach($inputs as $location) {
				$code = Db::genCode();
				$stmt->execute([$code,$arr['subscriptionId'],$location]);
				$newCodes[] = $code;
			}

			$response['codes'] = $newCodes;
			break;

		case 'itunesInfo':
			if(isset($inputs['latest_receipt_info'])) {
				$last = $inputs['latest_receipt_info'];
				$appSecret = 'ba2a9cd3747d4e7e8f884b737fbe6697';
				$masterSecret = 'e9971c394afc4d69b294f061674087ba';
				//if($last['password'] != $appSecret && $last['password'] != $masterSecret) {
				//	throw new \Exception('Password error');
				//}
				$a = array( 
						'purchaseId' => $last['original_transaction_id'], 
						'productId' => $last['product_id'], 
						'expiresTimestamp' => $last['expires_date']/1000);
				$db = Db::shared();
				$stmt = $db->prepare('INSERT INTO subscriptions (subscriptionId,productId,expires) VALUES (?,?,?) ON DUPLICATE KEY UPDATE productId=?,expires=?');
				$stmt->execute([ $a['purchaseId'], $a['productId'], $a['expiresTimestamp'], $a['productId'], $a['expiresTimestamp'] ]);
				$response['saved'] = true;
			}
			else if(isset($inputs['latest_expired_receipt_info'])) {		// handle cancellation
				$info = $inputs['latest_expired_receipt_info'];
				$a = array(
					'id' => $info['original_transaction_id'],
					'ts' => $info['expires_date']/1000
				);
				$db = Db::shared();
				$db->killSubscription( $a['id'], $a['ts'] );
				$response['cancelled'] = true;
			}
			break;

		case 'locationSettings':
			$ck = new CloudKitService($sandbox);				// input: {appId:'XXXXX',style:base64}
			$record = new \CloudKit\Record('Vendor',$inputs['locationId']);
			$record->setField('stripeAccountId',$inputs['settings']);
			$response['result'] = $ck->saveRecords([$record],true);
			break;

		case 'geoCodeLocation':										// input: {locationId:'XXXXX',latitude:XX,longitude:XX}
			$ck = new CloudKitService($sandbox);
			$record = new \CloudKit\Record('Vendor',$inputs['locationId']);
			$record->setField('location', new \CloudKit\Location($inputs['latitude'], $inputs['longitude']));
			$response['result'] = $ck->saveRecords([$record],true);
			break;

		case 'saveSettings':
			$ck = new CloudKitService($sandbox);				// input: {appId:'XXXXX',style:base64}
			$record = new \CloudKit\Record('App',$inputs['appId']);
			if(is_array($inputs['settings'])) {
				$inputs['settings'] = base64_encode(json_encode($inputs['settings']));
			}
			$record->setField('settings',$inputs['settings']);
			$response['result'] = $ck->saveRecords([$record],true);
			break;

		//case 'iap':		// save in-app-purchase
		//	$db = Db::shared();
		//	$stmt = $db->prepare('INSERT INTO subscriptions (subscriptionId,productId,expires) VALUES (?,?,?) ON DUPLICATE KEY UPDATE productId=?,expires=?');
		//	$a = $inputs['info'];
		//	$stmt->execute([ $a['purchaseId'], $a['productId'], $a['expiresTimestamp'], $a['productId'], $a['expiresTimestamp'] ]);
		//	$response['subscriptionId'] = $a['purchaseId'];
		//	break;
	
		case 'getUrls':
			if(false) {		// obscurity server (TODO: should keep staging?)
				$response['v1api'] 	= 'https://flashordr.com/api';
				$response['api'] 		= 'https://staging.flashordr.com/v2';
				$response['wsapi'] 	= 'wss://ws.staging.flashordr.com';
			}
			else {
				$response['v1api'] 	= 'https://flashordr.com/api';
				$response['api'] 		= 'https://flashordr.com/v2';
				$response['wsapi'] 	= 'wss://ws.flashordr.com';
			}
			break;

		case 'importCatalog':
			$merchant = $inputs['merchant'];
			$catalogPath = __DIR__ . '/../catalogs/' . $merchant;	
			//if($cli) {
			//	$sq = new SquareService($inputs['token'] ?? die('no token specified'));
			//}
			//else {
				$sq = SquareService::fromStored( $merchant );	// no 'sq' prefix
			//}

			//if(file_exists($catalogPath)) {
			//	echo file_get_contents($
			//}

			$sharedCatalog = \SquareService\SquareCatalog::shared();
			$cursor = null;
			$squareObjects = Array();
			//$types = Array('CATEGORY', 'ITEM,ITEM_VARIATION,DISCOUNT,TAX,MODIFIER,MODIFIER_LIST');	// Do CATEGORY first, by itself
			$types = Array('CATEGORY', 'ITEM,DISCOUNT,TAX,MODIFIER_LIST,IMAGE');	// Do CATEGORY first, by itself
			$types = Array('CATEGORY,ITEM,DISCOUNT,TAX,MODIFIER_LIST,IMAGE');	// Do CATEGORY first, by itself
			//$types = Array('CATEGORY', 'MODIFIER,MODIFIER_LIST');	// Do CATEGORY first, by itself
			//$types = Array('CATEGORY', 'ITEM');	// Do CATEGORY first, by itself
			if(isset($inputs['types'])) {
				$types = Array($inputs['types']);
			}
			$sourceObjects = Array();

			foreach($types as $type) {
				do {
					$catalog = $sq->getCatalog($cursor,$type);
					$n = count($catalog['objects'] ?? Array());
					if ($n==0) { continue; }
					$sourceObjects[$type] = count($catalog['objects'] ?? Array());
					$squareObjects = array_merge($squareObjects,$catalog['objects']);
					if(isset($catalog['cursor'])) {
						$cursor = $catalog['cursor'];
					}
					else {
						$cursor = null;
					}
					$response["fetch_{$i}"] = $n;
				}
				while( $cursor != null );
			}

			if($inputs['download'] ?? false) {
				header('Content-Type: application/json');
				header('Content-Disposition: attachment; filename="catalog.json"');
				echo json_encode(Array('objects' => $squareObjects));
				file_put_contents($catalogPath, json_encode(Array('objects'=>$squareObjects),JSON_PRETTY_PRINT));	// save a copy
				exit(0);
			}
		
			file_put_contents($catalogPath, json_encode(Array('objects'=>$squareObjects),JSON_PRETTY_PRINT));
			$output = shell_exec(__DIR__ . "/../bin/SquareCatalogImport '{$merchant}' '{$catalogPath}'"); 
			file_put_contents($catalogPath . '.output',$output);
			$records = json_decode($output,true);
			if(json_last_error() !== JSON_ERROR_NONE) {
				error_log('JSON Deocde error: #' . json_last_error() . ': ' . json_last_error_msg());
				//throw new \Exception('JSON Deocde error: #' . json_last_error() . ': ' . json_last_error_msg());
			}
			if($records == null) {
				throw new \Exception('Conversion Error' . $output);
			}
			$records = $records['operations'];
			$response['count'] = count($records);


			//$catalog = json_decode(file_get_contents('testdata.json'),true);
			//echo json_encode($catalog,JSON_PRETTY_PRINT);
			
			//$merchant = 'sq' . $merchant;
			//$locations = $sq->getLocations();	
			//$n = count($locations);
			//$locationIds = Array();
			//for($i=0;$i<$n;$i++){
			//	$location = $locations['locations'][$i];
			//	$locationIds[] = 'sq' . $location['id'];
			//}
			//$sharedCatalog->setJSON($catalog,$merchant,$locationIds);

			//$records = $sharedCatalog->allRecords();
			//$response['records'] = $records;
			//$response['counts'] = $sharedCatalog->itemCounts();
			if($cli) {
				echo json_encode($records,JSON_PRETTY_PRINT);
				exit;
			}

			//$ck = new CloudKitService($sandbox);
			//$chunkSize = 20;
			//$ckResults = Array();
			//$numCloudKitChunks = 0;
			//for($i=0;$i<count($records);$i+=$chunkSize){
			//	$chunk = array_slice($records,$i,$chunkSize);
			//	$numCloudKitChunks++;
			//	$ckResults[] = $ck->rawRequest('modify', Array('operations' => $chunk), true);
			//	if(isset(end($ckResults)['serverErrorCode'])) {
			//		$response['lastRequests'] = $chunk;
			//		break;
			//	}
			//}
			//if($numCloudKitChunks>1) {
			//	$response['numChunks'] = $numCloudKitChunks;
			//	$response['chunkSize'] = $chunkSize;
			//}
			//$response['isTestMode'] = $testMode;
			//$response['sourceObjects'] = $sourceObjects;



			//if(count($ckResults)>10) {
			//	$response['ckResults'] = array_slice($ckResults,0,10);
			//} else {
			//	$response['ckResults'] = $ckResults;
			//}
			break;

		case 'publishCatalog':
			// TODO: verify login
			$records = $inputs;
			$operations = Array();
			foreach($records as $record) {
				$operations[] = Array("operationType"=>"forceUpdate","record"=>$record);
			}
			$records = $operations;
			$ck = new CloudKitService($sandbox);
			$chunkSize = 20;
			$ckResults = Array();
			$numCloudKitChunks = 0;
			for($i=0;$i<count($records);$i+=$chunkSize){
				$chunk = array_slice($records,$i,$chunkSize);
				$numCloudKitChunks++;
				$ckResults[] = $ck->rawRequest('modify', Array('operations' => $chunk), true);
				if(isset(end($ckResults)['serverErrorCode'])) {
					throw new \Exception(end($ckResults)['serverErrorCode']);
					$response['lastRequests'] = $chunk;
					break;
				}
			}
			$response['saved'] = true;
			break;

		case 'getSubId':	// validate receipt
			$vars = array();
			$vars['receipt-data'] = $inputs['receipt-data'];
			$vars['password'] = 'ba2a9cd3747d4e7e8f884b737fbe6697';
			$vars['exclude-old-transactions'] = true;
			$ch = curl_init();
			$storeApi = $testMode ? "sandbox" : "buy";
			curl_setopt($ch, CURLOPT_URL, "https://{$storeApi}.itunes.apple.com/verifyReceipt");
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($vars));
			$data = curl_exec($ch);
			$result = json_decode($data,true);
			if(isset($result['latest_receipt_info'])) {	
				$response['error'] = $noSubscriptionMessage;// will remove if found
				foreach( $result['latest_receipt_info'] as $last ) {
					if(isset($last['original_transaction_id']) && isset($last['product_id']) && isset($last['expires_date_ms']) && $last['expires_date_ms']/1000 > time()) {
						$arr = array( 'purchaseId' => $last['original_transaction_id'], 'productId' => $last['product_id'], 'expiresTimestamp' => $last['expires_date_ms']/1000);

						// Sometimes a blank appId is being sent. Let's just look it up in that case
						if( strlen($inputs['appId'] ?? "") < 2 ) {
							$response['subscriptionId'] = $arr['purchaseId'];
							$response['productId'] = $arr['productId'];
							break;
						}


						// Otherwise save updated data to database
						$db = Db::shared();
						$stmt = $db->prepare('INSERT INTO subscriptions (subscriptionId,appId,expires,productId) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE appId=?,expires=?,productId=?');
						$a = array($arr['purchaseId'], $inputs['appId'], $arr['expiresTimestamp'], $arr['productId']);
						$stmt->execute([ $a[0], $a[1], $a[2], $a[3], $a[1], $a[2], $a[3] ]);
						$response['subscriptionId'] = $a[0];
						$response['productId'] = $arr['productId'];
						unset($response['error']);
						break;
					}
				}
			}
			else {
				$response['error'] = 'Unable to retrieve a valid subscription receipt' . $data;
			}
			break;

		case 'refreshSession':
			$db = Db::shared();
			$response['vendor'] = $db->validateSession($true);		// force subscription check
			break;

		case 'extend':					// extend someone's subscription
			$db = Db::shared();
			$stmt = $db->prepare('UPDATE subscriptions SET expires=expires+? WHERE subscriptionId=? LIMIT 1');
			$response['success'] = $stmt->execute( [$inputs['interval'],$inputs['subscriptionId'] ]);
			break;

		case 'assignCodes':
			$db = DB::shared();
			$appId = $inputs['appId'][0];		// TODO: get this from session

			// Determine number of products
			$stmt = $db->prepare('SELECT productId, subscriptionId FROM subscriptions WHERE appId=?');
			$stmt->execute($inputs['appId']);
			$nKiosk = 0;
			$nKDS = 0;
			$subscriptions = $stmt->fetchAll(PDO::FETCH_ASSOC);
			$kioskSubscription = null;
			$kdsSubscription = null;
			foreach ($subscriptions as $s) {
				$p = $s['productId'];
				$n = intval(substr($p,-1,1));				// TODO: someday we will need to support > 9 
				if($p[1] == 'D') { 
					$nKDS = $n; 
					$kdsSubscription = $s['subscriptionId'];
				}
				else { 
					$nKiosk = $n;
					$kioskSubscription = $s['subscriptionId'];
				}
			}

			// Validate numbers
			if (count($inputs['kiosk']) > $nKiosk) {
				throw new \Exception("Subscription only supports {$nKiosk} kiosks.");
			}
			if (count($inputs['kds']) > $nKDS) {
				throw new \Exception("Subscription only supports {$nKDS} kds locations.");
			}

			// set up a generic query
			$query = 'SELECT locationId, l.subscriptionId FROM locations AS l
				JOIN subscriptions AS s on s.subscriptionID = l.subscriptionID
				    WHERE l.appId=?
					 AND launchCode IS NOT NULL
					 AND LEFT(s.productID,2) LIKE ?';
			$existingType = $db->prepare($query);


			// Do the Kiosks
			$existingType->execute([$appId,'Ki']);
			$existingKiosks = $existingType->fetchAll(PDO::FETCH_COLUMN);
			$alreadyKiosks = array_values(array_intersect($existingKiosks,$inputs['kiosk']));
			$newKiosks = array_values(array_diff($inputs['kiosk'],$alreadyKiosks));
			$remove = array_values(array_diff($existingKiosks,$inputs['kiosk']));

			$updateStmt = $db->prepare('UPDATE locations SET launchCode=?,subscriptionId=?,webhook=1 WHERE locationId=?');
			foreach($newKiosks as $location) {
				$vals = Array( Db::genCode(),$kioskSubscription,$location );
				if ($updateStmt->execute($vals) == false) {
					throw new \Exception(json_encode($updateStmt->errorInfo(),true));
				}
				SquareService::setWebhookAsync($inputs['appId'],$location,$logFile);
			}

			$rmStmt = $db->prepare('UPDATE locations SET launchCode=NULL,subscriptionId=NULL,webhook=0 WHERE locationId=?');
			$logout = $db->prepare('DELETE FROM sessions WHERE locationId=?');
			foreach($remove as $location) {
				$rmStmt->execute([$location]);
				$logout->execute([$location]);
				SquareService::setWebhookAsync($inputs['appId'],$location,$logFile,'unset');
			}
		
			// Now do the KDS	
			$existingType->execute([$appId,'KD']);
			$existingKDSs = $existingType->fetchAll(PDO::FETCH_COLUMN);
			$response['existingKDSs'] = $existingKDSs;
			$alreadyKDSs = array_values(array_intersect($existingKDSs,$inputs['kds']));
			$newKDSs = array_values(array_diff($inputs['kds'],$alreadyKDSs));
			$remove = array_values(array_diff($existingKDSs,$inputs['kds']));

			$updateStmt = $db->prepare('UPDATE locations SET launchCode=?,subscriptionId=?,webhook=1 WHERE locationId=?');
			foreach($newKDSs as $location) {
				$vals = Array( Db::genCode(),$kdsSubscription,$location );
				if ($updateStmt->execute($vals) == false) {
					throw new \Exception(json_encode($updateStmt->errorInfo(),true));
				}
				SquareService::setWebhookAsync($inputs['appId'],$location,$logFile);
			}

			$rmStmt = $db->prepare('UPDATE locations SET launchCode=NULL,subscriptionId=NULL,webhook=0 WHERE locationId=?');
			foreach($remove as $location) {
				$rmStmt->execute([$location]);
				$logout->execute([$location]);
				SquareService::setWebhookAsync($inputs['appId'],$location,$logFile,'unset');
			}
			
			// break; // NO BREAK!! Fall through to the session

		case 'logIntoSession':												// inputs: {sessionId,{deviceId, ckUserId, pushToken}}
			$db = DB::shared();
			$stmt = $db->prepare('SELECT * FROM sessions WHERE sessionId=?');
			$sessionId = $headers['Authorization'] ?? $inputs['sessionId'] ?? "noSession";
			$result = $stmt->execute([ $sessionId ]); 
			if($stmt->rowCount() == 0 || $result == false) {
				throw new \Exception('Session not found');
			}
			$arr = $stmt->fetch(PDO::FETCH_ASSOC);
			$appId = $arr['appId'];
			if(true || $arr['locationId'] == null) {	// indicates admin: give them
				$response['usages'] = Array();
				$s1 = $db->prepare('SELECT * FROM subscriptions WHERE appId=?');
				$s1->execute([ $appId ]);
				while($row = $s1->fetch(PDO::FETCH_ASSOC)) {
					$usage = Array();
					$usage['product'] = $row['productId'];
					$s2 = $db->prepare('SELECT locationId, launchCode as code FROM locations WHERE subscriptionId=? AND launchCode IS NOT NULL');
					$s2->execute([ $row['subscriptionId'] ]);
					//$usage['locations'] = $s2->fetchAll(PDO::FETCH_COLUMN | PDO::FETCH_GROUP);
					$usage['locations'] = $s2->fetchAll(PDO::FETCH_CLASS);
					$response['usages'][] = $usage;
				}
			}
			$expires = $db->sessionUpdated($inputs['deviceInfo'] ?? null,$sessionId);
			$response['usedId'] = $sessionId;
			$response['session'] = Array( 'sessionId'=>$sessionId, 'expires'=>$expires );
			if(isset($arr['locationId'])) { $response['session']['locationId'] = $arr['locationId']; }
			$response['appId'] = $appId;

			// // TEMPORARY: update current locations so they reference their subscriptionId
			// $locationsWithCode = $db->prepare('
			// SELECT locations.appId, s.subscriptionId FROM locations LEFT JOIN subscriptions as s 
			// 	on s.appId=locations.appId
			// 	WHERE launchCode IS NOT NULL AND locations.subscriptionId IS NULL');
			// $locationsWithCode->execute([]);
			// $s = $db->prepare('UPDATE locations set subscriptionId=? WHERE appId=?');
			// while($arr = $locationsWithCode->fetch(PDO::FETCH_ASSOC)) {
			// 	$s->execute([ $arr['subscriptionId'], $arr['appId'] ]);
			// }

			break;

		case 'loginWithCode':
			$db = Db::shared();
			try {
				$db->expireTokens();
				$stmt = $db->prepare("SELECT locationId,autoAck,appId,webhook from locations WHERE launchCode=?");
				$result = $stmt->execute([ strtoupper($inputs['code']) ]);
				if($result == false) { throw new \Exception('code error'); }
				$arr = $stmt->fetch(PDO::FETCH_ASSOC);
				$sub = $db->validateSubscription($arr['locationId']);
				$response['session'] = $db->createSession($arr['locationId'],$arr['appId'],$inputs['info'] ?? null);
				$response['appId'] = $arr['appId'];
				$response['usages'] = Array(Array('product'=>$sub['productId'],'locations'=>Array(Array('locationId'=>$arr['locationId'],'code'=>$inputs['code']))));
				$webhook = intval($arr['webhook']) > 0 ? 'set' : 'unset';

				SquareService::setWebhookAsync($arr['appId'],$arr['locationId'],$logFile,$webhook);		// Future: This might be excessive
			} catch (\Exception $e) {
				$logSuffix .= "\nLogin failed because " . $e->getMessage();
				throw new \Exception('Unable to log in'); 
			}
			break;

		case 'webhook':
			$sq = SquareService::fromStored( $inputs['merchant_id'] );		// send without the 'sq'
			$response['webhook'] = $sq->ezWebhook( $inputs );
			break;

		case 'autoack':
			$db = Db::shared();
			$stmt = $db->prepare('UPDATE locations SET autoAck=? WHERE locationId=?');
			$response['location_result'] = $stmt->execute( [ $inputs['autoAck'], $inputs['location_id'] ] );
			$stmt = $db->prepare('UPDATE apps SET autoAck=? WHERE appId=?');
			$response['app_result'] = $stmt->execute( [ $inputs['autoAck'], $inputs['merchant_id'] ] );
			break;

		case 'eraseApp':
			if($inputs['code'] == 'chicker') { 				// verify code word
				$response['auth'] = 'Authorized!';
				// TODO: verify they don't have a subscription

				// erase webhook if we still have a square token
				try {
					$sq = SquareService::fromStored( $inputs['appId'] );		// send without the 'sq'
					$sq->ezWebhook( Array('location_id' => '*', 'hook' => 'unset') );
				} catch (\Exception $e) {
					// this is okay... we probably didn't have a token for them anyway
				}


				// Delete the app record from CloudKit
				$ck = new CloudKitService($sandbox);				// input: {appId:'XXXXX',style:base64}
				$ckResults = Array();
				$ckResults[] = $ck->deleteRecords(['sq'.$inputs['appId']]);


				// Now get rid of our own detail storage (reporting)
				$db = Db::shared();
				$query = 'DELETE locations, orders, orderItems, orderModifications
								FROM locations
								LEFT JOIN orders on orders.vendorId = locations.locationId
								LEFT JOIN orderItems on orderItems.orderId = orders.orderId
								LEFT JOIN orderModifications on orderModifications.itemId = orderItems.id
								WHERE locations.appId=?';		// I verified that this works and doesn't delete everybody's stuff 10/30/19 -Jon
				$s = $db->prepare($query);
				$localResult = $s->execute([$inputs['appId']]);
				$response['msg'] = json_encode( Array( 'CloudKit' => $ckResults, 'MySQL' => $localResult ) , true );
			}
			else {
				throw new \Exception('Not authorized');
			}
			break;

		case 'squareImport':
			$merchant = $inputs['merchant_id'];
			$sq = SquareService::fromStored( $merchant );
			$locations = $sq->getLocations();	
			$n = count($locations['locations']);
			if($n < 1) { throw new \Exception('Error: no locations are set up in Square for this account'); }
			$ck = new CloudKitService($sandbox);
			$ck->createNewApp($merchant, $locations['locations'][0]['business_name']);		// we're just using the first one to get the business name
			$locationRecords = Array();
			$locationIds = Array();
			for($i=0;$i<$n;$i++){
				$location = $locations['locations'][$i];
				$sq->setWebhook($location['id']);
				$locationRecords[] = $ck->newVendorRecord($location, $merchant);
				$locationIds[] = 'sq' . $location['id'];
			}
			$results = $ck->saveRecords($locationRecords,true);
			break;

		case 'square_v2':
			$response['square_v2'] = 'deprecated';
			break; // this is all deprecated
			switch($inputs['type']) {
				case 'terminal.checkout.updated':
					$data = $inputs['data']['object'];
					$updates = Array();
					$orderId = $data['reference_id'];

					switch ($data['status']) {
						case 'PENDING': break;
						case 'IN_PROGRESS':	break;
						case 'REQUIRES_INPUT': break;
						case 'COMPLETED':

							$db = Db::shared();
							$stmt = $db->prepare('SELECT amount+tip as `total` FROM orders WHERE orderId=?');
							$stmt->execute([ $orderId ]);
							$total = $stmt->fetch(PDO::FETCH_ASSOC)['total'];
							$centsShouldBe = intval(floatVal($total*100)+.0001);

							$centsPaid = $data['amount_money']['amount'];
							if($centsShouldBe > $centsPaid) {
								$response['stillOwes'] = $centsShouldBe - $centsPaid;
								// TODO: initiate another request
							}
							else {
								$stmt = $db->prepare('UPDATE orders SET amount=?/100,charge=? WHERE orderId=?');
								$stmt->execute([ $centsPaid, $data['id'], $orderId ]);

								$updates['ready'] = 0; 
								$updates['paymentCents'] = $centsPaid;
								// TODO: save the customer
							}
							
							break;
						case 'CANCELED':
							$updates['ready'] = CloudKitService::STATUS_CANCELED;
							break;
						case 'FAILED':
							$updates['ready'] = CloudKitService::STATUS_FAILED;
							break;
					}
					if(count($updates)>0) {
						// Notify listeners of status change
						$queueId = Db::orderIdToInt($orderId);
						$queue = msg_get_queue($queueId);
						msg_send($queue,8,$updates['ready'],false,false,$err);
						msg_remove_queue($queue);

						// Save to cloud
						$ck = new CloudKitService($sandbox);
						$response['ckUpdate'] = $ck->setRecordValues('Order', $orderId, $updates);
					}
					break;

				case 'device.code.paired':
					$response['wsApi'] = file_get_contents($wsApi . '/squareTerminalPaired/' . $inputs['data']['object']['id']);
					break;

				default: 
					error_log('UNRECOGNIZED SQUARE WEBHOOK: ' . json_encode($inputs));
					break;
			}
			break;

		case 'squareRaw':
			switch($inputs['type']) {
				case 'payment':
					if(isset($inputs['orderId'])) {
						$stmt = Db::shared()->prepare('SELECT TRIM(LEADING "sq" FROM charge) AS "entity_id",
																	TRIM(LEADING "sq" FROM vendorId) AS "location_id",
																	TRIM(LEADING "sq" FROM merchant) AS "merchant_id" 
																FROM orders WHERE orderId=?');
						$stmt->execute([$inputs['orderId']]) or die('bad query');
						$inputs = $stmt->fetch(PDO::FETCH_ASSOC);
					}
					$sq = SquareService::fromStored( $inputs['merchant_id'] );		// send without the 'sq'
					$response = $sq->getPayment( $inputs['location_id'], $inputs['entity_id'] );
					break;

				default:
					throw new Exception('Missing/invalid \'type\' parameter');
			}
			break;

		case 'square':
			// TODO: Could validate signature with function from square's page
			//$notificationSignature = $headers['X-Square-Signature'];

			switch($inputs['event_type']) {						// SEE: https://docs.connect.squareup.com/basics/api101/webhooks
				case 'PAYMENT_UPDATED':										// a square payment went through

					// if($inputs['merchant_id'] == 'SWAPEQD7SM6FV' && $env != 'staging') {	// send Flash Order to staging server
					// 	header('Location: https://staging.flashordr.com/api/square');
					// 	exit;
					// }
					//
					
					// Forward newer versions to our newer server
					if(Db::shared()->isOnV7('sq' . $inputs['merchant_id']) == false) {
						Sleep(3);
						forwardToSwift($inputs);
						exit; 
					}


					// Otherwise check if they have ever had a subscription
					if(Db::shared()->hasHadASubscription('sq' . $inputs['merchant_id']) == false) {	// TODO: check if they are expired
						// TODO: unset webhook
						$response['ignored'] = 'no subscription';
						break;
					}

					//if($inputs['merchant_id'] == 'SWAPEQD7SM6FV' 		// flash order
					//	|| $inputs['merchant_id'] == '5CFQ34MHGWHGR'		// chop chop
					//	|| $inputs['merchant_id'] == '4RCPE1JTWBT24'		// hookt
					//	|| $inputs['merchant_id'] == 'ML7VB4KJ446J9'		// pizza bros
					//	|| $inputs['merchant_id'] == 'FK12JD2WMP5S2'		// grant montour
					//	|| $inputs['merchant_id'] == 'AN9MH2KW227BB'		// dragonfly tea bar (peterduong)
					//	|| $inputs['merchant_id'] == '9JFSVCQHWCCWN'		// the breakroom
					//	|| $inputs['merchant_id'] == 'GDSCME4VHRBA5'		// crepe neptune
					//	|| $inputs['merchant_id'] == 'MZM9Q1K92WTB9'		// world cafe
					//	|| $inputs['merchant_id'] == 'MLKNRF2430JYZ'		// lenny's casita
					//	|| $inputs['merchant_id'] == 'ML8B84WBTDEPR'		// all good food
					//	|| $inputs['merchant_id'] == '3759RAF36F4E7'		// pretty bird
					//	|| $inputs['merchant_id'] == 'AGRB9RNZ946AW'		// boo's philly
					//	|| $inputs['merchant_id'] == '5BPEGMG53JX41'		// liberty cheesesteak
					//	|| $inputs['merchant_id'] == 'MLPWNGBMA67KF'		// ramen matsu
					//	|| $inputs['merchant_id'] == '39JQF3T990CSD'		// dan and jon's wings
					//	|| $inputs['merchant_id'] == 'ML8FEAR5W5V4D'		// east coast restaurant group inc
					//	|| $inputs['merchant_id'] == '9MFBS4J0FBPDB'		// cupcake junkie
					//	|| $inputs['merchant_id'] == 'ML5RW32VB61RH'		// ober here
					//	|| $inputs['merchant_id'] == 'A1437YESD0400'		// dtown dumplings
					//	|| $inputs['merchant_id'] == '6A787EE55ZZV5'		// cold spoon
					//	|| $inputs['merchant_id'] == 'B0279514N2YFT'		// crazy corn maze
					//	|| $inputs['merchant_id'] == '5BPEGMG53JX41'		// liberty cheesesteaks
					//	|| $inputs['merchant_id'] == 'V88Z3GZ7EQT5N'		// ammon mediterranean market
					//	|| $inputs['merchant_id'] == 'CBE8K2KWTTRGF'		// Yoshi's
					//	|| $inputs['merchant_id'] == '0AKH4AB51VEXJ'		// Topless Tacos 
					//	|| $inputs['merchant_id'] == '2AFJ5VR79SN0M'		// The Halal Spot: 
					//	|| $inputs['merchant_id'] == 'AW537APSNP9B7'		// Burbs Burgers 
					//	|| $inputs['merchant_id'] == '8061C8AWA6NVZ'		// Tortas Lokas 
					//	|| $inputs['merchant_id'] == 'ML7VB4KJ446J9'		// Pizza Bros
					//	|| $inputs['merchant_id'] == 'CZCJH3A2M0RWN'		// Damoyo2 Inc (yelee deli)
					//	|| $inputs['merchant_id'] == 'CHF5372Z8X6N0') { // Roll Up Crepes
					//	Sleep(3);
					//	forwardToSwift($inputs);
					//	exit; 
					//}			// leave our account to the new stuff


					$sq = SquareService::fromStored( $inputs['merchant_id'] );		// send without the 'sq'
					$payment = $sq->getPayment( $inputs['location_id'], $inputs['entity_id'] );

					// // send to our new webhook:
					// $fwd = $inputs;
					// $fwd['payment'] = $payment;
					// forwardToSwift($fwd);


					$response = $sq->importPayment( $payment );						// get and parse transaction


					if(isset($response['ignore'])) {
						// ignore (don't need to do anything)
					}
					else if($response['justCollect'] ?? false == true) { 				// this was a collection for a Flash Order
						$ck = new CloudKitService($sandbox);
						$results = $ck->saveRecords([$response['record']], true);
					}

					else if(isset($response['record'])) { 								// a new transaction
						$db = Db::shared();
						if($db->webhookOn( $inputs['location_id']) == false){ 	// check location for webhook-disabled
							$response['ignore'] = ['webhook turned off'];
							break;
						}
						$ck = new CloudKitService($sandbox);
						try {
							$results = $ck->saveRecords([$response['record']]);
							$response['record'] = $results[0];
						} catch (\Exception $e) {
							$response['error'] = $e->getMessage();
						}
					}
					else {
						$response['ignore'] = 'ignoring kiosk order';
					}
					break;

				case 'TEST_NOTIFICATION':									// a square test
					$response['message'] = 'Hello!';
					break;

				default: 
					error_log('UNRECOGNIZED SQUARE WEBHOOK: ' . json_encode($inputs));
					break;
			}
			break;

		case 'waitForTender':
			ini_set('max_execution_time', 60);
			$queueId = Db::orderIdToInt($inputs['orderId']);
			$queue = msg_get_queue($queueId);
			msg_receive($queue,8,$msgtype,4,$readyValue,false,null,$err);
			$response['err'] = ($err != 0 ? $err : null);
			$response['ready'] = (int)$readyValue;
			break;

		case 'collected':
			$db = Db::shared();
			$db->validateSession();
			$ck = new CloudKitService($sandbox);
			$isComplete = isset($inputs['complete']) ? boolVal($inputs['complete']) : false;
			$response['points'] = $db->markTendered( $inputs['order'], CloudKitService::PAID_CASH, $isComplete );
			$response['status'] = $ck->markTendered( $inputs['order'], CloudKitService::PAID_CASH, $isComplete );
			break;

		case 'receipt':

			// Try to get it from our new server code that can look it up based on just email address (definitely a hack job!)
			$receipt_url = 'https://dev.flashordr.com/flashorder/receipt/' . $inputs['orderID'];
			// $ch = curl_init();
			// 
			// $receipt_api = 'https://dev.flashordr.com/flashorder/receipt/';

			// // find server that will generate the receipt, based on their session
			// $db = DB::shared();
			// $stmt = $db->prepare('select settings, appId from sessions join apps on apps.appId=sessions.appId WHERE sessionId=?');
			// $sessionId = $headers['Authorization'] ?? $inputs['sessionId'] ?? "noSession";
			// $result = $stmt->execute([ $sessionId ]); 
			// if($stmt->rowCount() == 0 || $result == false) {
			// 	error_log('no session found in email request');
			// }
			// else {
			// 	$arr = $stmt->fetch(PDO::FETCH_ASSOC);
			// 	$appSettings = json_decode( $arr['settings'], true );
			// 	if($arr['appId'] == 'sqKNZZZXRYX525V'			// bumblebees bbq
			// 		|| $arr['appId'] == 'sqML38W189683C3')		// accesso (fresh bistro) 
			// 	{
			// 		$receipt_api = 'https://flashordr.com/v2/receipt/';
			// 	}
			// 	else if(isset($appSettings['backend'])) {
			// 		$receipt_api = $appSettings['backend']['api'] . '/receipt/';
			// 	}
			// 	else if($appSettings['server2'] == 0) {
			// 		$receipt_api = 'https://flashordr.com/v2/receipt/';
			// 		error_log('receipt api: ' . $receipt_api);
			// 	}
			// 	//error_log( 'server2 = ' . ($appSettings['server2'] ?? 'unknown') );
			// }

			// curl_setopt($ch, CURLOPT_URL, $receipt_api . $inputs['orderID']);
			// $query = json_encode($inputs);
			// curl_setopt($ch, CURLOPT_HTTPHEADER, Array("Server: OldPHP"));
			// curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
			// curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
			// $result = curl_exec($ch);
			$result = false;	// disable curl receipt
			if($result && !curl_errno($ch) && json_decode($result,true)['error'] == null) {
				curl_close($ch);
				//return json_decode($result,true);
				$body = $result;
			}
			else if(true) {
				$body = "Your receipt is <a href='$receipt_url'>here</a>";
			}
			else {
				require_once __DIR__.'/resources/emailReceipt.php';
				$body = emailReceipt(
					$inputs['name'],
					$inputs['number'],
					$inputs['date'],
					$inputs['items'],
					$inputs['prices'],
					$inputs['total'],
					$inputs['vendor'] . ' ' . $inputs['address'],
					$inputs['contact']);
				error_log('CURL Receipt: ' . curl_error($ch));
				curl_close($ch);
			}
			$db = Db::shared();
			$db->validateSession();
			$to = $inputs['to'];
			$subject = 'Kiosk Receipt';
			$headers =  "From: {$inputs['vendor']}<mobileorderingsolutions@gmail.com>\r\n";
			$headers .= "Reply-To: Flash Order<kiosko@kioskomobile.com>\r\n";
			$headers .= "MIME-Version: 1.0\r\n";
			$headers .= "Content-type: text/html\r\n";
			$response['mailed'] = (mail($to,$subject,$body,$headers) != false);
			// TODO: save the information from $inputs[orderID] into database
			break;

		case 'sqNonce':
			$sq = SquareService::fromStored( $inputs['appId'], true );
			$response = $sq->saveNonce( $inputs['nonce'], $inputs['customerId'] ?? null, $inputs['deviceId']);
			break;

		case 'sqRmCard':
			$sq = SquareService::fromStored( $inputs['appId'], true );
			$response['success'] = $sq->rmCard($inputs['cardId'], $inputs['customerId']);
			break;


		case 'submit': 															// Process Payment
			$paymentId = null;
			$db = Db::shared();

			// sort out the customer
			if(isset($inputs['customer']['id'])) {								// sent id
				$customerId = $inputs['customer']['id'];
				$customer = $db->getCustomer($customerId);					// fetch
				if(isset($inputs['sqApp'])) {
					$customer['name'] = $customer['name'] ?? $inputs['deviceName'];
				}
				if($customer == null) { 											// if new, save the incoming information
					$customer = $inputs['customer'];
					$db->createCustomerFromUntrusted($customer);
				}
			}
			else {
				$customer = Array();
			}


			$price = doubleVal($inputs['amount']);
			$priceCents = intVal(round($price*100));
			$locationId = $inputs['vendorId'] ?? ('sq' . $inputs['location_id']);
			if(!isset($inputs['merchant_id'])) {
				$stmt = $db->prepare('SELECT appId from locations where locationId=?');
				if($stmt->execute([$locationId])) {
					$appId = $stmt->fetch(PDO::FETCH_ASSOC)['appId'];
				}
			} else {
				$appId = 'sq' . $inputs['merchant_id'];
			}


			// If they entered a name then use that
			$customer['name'] = $inputs['customer']['name'] ?? $customer['name'] ?? null;
			

			// Deduct loyalty used from their account
			$loyaltyUsed = $inputs['credit'] ?? 0;
			if($loyaltyUsed > 0.0001) {
				$amountCents = intVal($loyaltyUsed * 100 + 0.00001);
				if($customer['id'] == 'qwertyuiop' && $env == 'staging') {
					$customer['credit'] = 2300-$amountCents;
				} else {
					$customer['credit'] = Db::shared()->tenderLoyalty($amountCents, $customer['id']);
				}
				$response['redeemedLoyalty'] = $amountCents;
				$response['redeemedCustomer'] = $customer['id'];
			}

			/*if($price < 0.01) {										// promos... check them!
				if(isset($inputs['promo']) && strlen($inputs['promo'])>5) {			// Free stuff requires a promotion
					$paymentId = 'promotion_' . $inputs['promo'];
				}
				else if(isset($inputs['credit'])) { 										// paying in full for loyalty
					$inputs['points'] = 0;														// ensure no points will be rewarded
					$paymentId = CloudKitService::PAID_LOYALTY;
				}
				else {
					throw new Exception('Price must be greater than zero');
				}
			}
			else */
			if(isset($inputs['swipe'])) {					// Cash/deferred payments
				$swipe = $inputs['swipe'];

				if(strlen($swipe)<10) {
					throw new Exception('Swipe error. Please try again');
				} 
				else if ($swipe == CloudKitService::PMT_POSTPONED) {
					$paymentId = CloudKitService::PMT_POSTPONED;
				}
				else if ($swipe == CloudKitService::PMT_CASH) {
					$paymentId = CloudKitService::PMT_CASH;
				}
				else if ($swipe == CloudKitService::PAID_CASH) {
					$paymentId = CloudKitService::PAID_CASH;
				}
				else {
					throw new Exception('Invalid submission');
				}
			}
			else if(isset($inputs['sqTerminal'])) {			// Request payment from Square terminal
				// use Transactions API to charge them
				$sq = SquareService::fromStored( $inputs['merchant_id'] );
				$paymentResult = $sq->paymentRequest($priceCents, $inputs);
				$response['terminalResult'] = $paymentResult;
				//	$paymentId = CloudKitService::PMT_CASH;
				$paymentId = CloudKitService::PMT_POSTPONED;
				if(isset($paymentResult['errors'])) {
					error_log('Square Terminal Error: ' . json_encode($paymentResult['errors']));
					throw new Exception('Unable to queue payment on terminal');
				}
			}
			else if(isset($inputs['sqApp'])) {			// App payment through Apple Pay or saved card
				// determine if this actually came from the app based on the order format
				if($inputs['format'] & 1) {
					$sq = SquareService::fromStored( $inputs['merchant_id'], true );
				} else {
					$sq = SquareService::fromStored( $appId );
				}
				// use Transactions API to charge them
				if(!isset($inputs['squareCustomerId']) && isset($customer['squareId'])) {
					$inputs['squareCustomerId'] = $customer['squareId'];
				}
				$paymentResult = $sq->submitPayment($inputs['location_id'], $priceCents, $inputs['orderId'], $inputs['description'], $inputs);
				$paymentId = $paymentResult['transactionId'];
				if($paymentResult['squareCustomerId'] != null) {
					$squareCustomer = $db->getCustomerFromSqId($paymentResult['squareCustomerId']);
					if(isset($squareCustomer['id'])){
						$sid = $paymentResult['squareCustomerId'];
					}
					else {
						$sid = null;
					}
					$customerId = $customer['id'] ?? null;
					$response['lines'] = array();
				
					if($sid) {
						
						if($customerId) {
							if($customerId == $sid) {	// found account -> merge any info
								$justname = array('name'=>$inputs['deviceName']);
								$customer = array_merge( $justname, $customer, $squareCustomer );
							$response['lines'][] = __LINE__;
							}
							else {
								// different accounts... do nothing
							$response['lines'][] = __LINE__;
							}
						}
						else {
							$customer = $squareCustomer;
							$response['sqc'] = $squareCustomer;
							$response['lines'][] = __LINE__;
						}
					}
					else if($customerId) { 			// save square id
						$customer['squareId'] = $paymentResult['squareCustomerId'];
						$db->updateCustomer($customer['id'],Array(
							'squareId' => $customer['squareId']
						));
							$response['lines'][] = __LINE__;
					}
					else {												// create new
							$response['lines'][] = __LINE__;
						$customer = Array(
							'name' => $customer['name'] ?? $inputs['deviceName'] ?? PDO::PARAM_NULL,
							'squareId' => $paymentResult['squareCustomerId'],
							'appId' => $appId,
							'email' => $paymentResult['email'] ?? PDO::PARAM_NULL
						);
						$customer['id'] = $db->createCustomer($customer);
					}
				}
				else {
					$response['WARNING'] = 'no customer id from payment';
				}
				$customer['name'] = $customer['name'] ?? $inputs['deviceName'] ?? '';
			}
			else if(isset($inputs['sqTransaction'])) {			// coming from Reader SDK
				$paymentId = 'sq' . $inputs['sqTransaction'];	// just to flag that this should be the recordId (it gets removed later)


				// try to identify this customer
				if( !isset($customer['id'] )) {
					$sq = SquareService::fromStored( $appId ); //$inputs['merchant_id'] );
					try {
						$transaction = $sq->getTransaction($inputs['location_id'],$inputs['sqTransaction']);		// only find customer id on a transaction
						$squareId = $transaction['tenders'][0]['customer_id'] ?? null;
						if(is_null($squareId)) {
							throw new Exception('Warning: cannot get customer');
						}

						// First try to look up in our database
						$ourCustomer = $db->getCustomerFromSqId($squareId);

						if(!is_null($ourCustomer['id'] ?? null)) {										// use our record of this customer
							$customer['id'] = $ourCustomer['id'];
							$customer['name'] = $customer['name'] ?? $ourCustomer['name'];
							if(isset($ourCustomer['phone'])) {
								$customer['phone'] = $ourCustomer['phone'];
							}
							if($customer['name'] != $ourCustomer['name']) {					// save whatever name they entered
								$db->updateCustomer($customer['id'],Array(
									'name' => $customer['name'],
									'squareId' => ($squareId ?? PDO::PARAM_NULL)) );
							}
						}
						else {									// try to create a new customer from square
							// Create this customer
							$sqCustomer = $sq->getCustomer($squareId);
							$sqName = trim(($sqCustomer['given_name'] ?? '') . ' ' . ($sqCustomer['family_name'] ?? ''));
							if(strlen($sqName)==1) { $sqName = null; }
							$customer['name'] = $customer['name'] ?? $sqName;
							$customer['id'] = $db->createCustomer(Array(
								'name' => $customer['name'] ?? PDO::PARAM_NULL,
								'squareId' => $squareId,
								'appId' => $appId,
								'email' => $sqCustomer['email_address'] ?? PDO::PARAM_NULL
								// could add: ,'phone' => $sqCustomer['phone_number'] ?? PDO::PARAM_NULL
							));
						}
					} catch (\Exception $e) {
						// Unable to get a customer
					}
				}
			}
			else if(isset($inputs['card'])) {
				throw new Exception('card-not-present not supported yet');
			}
			else {
				throw new Exception('Invalid payment method');
			}


			// Compute estimated ready date			// calculated by the client else default is 5 min
			$response['estReady'] = $inputs['readyDate'] ?? time()+5*60;
			$inputs['tip'] = intVal(doubleval($inputs['tip'])*100);
			$response['name'] = $customer['name'] ?? null;				// set this before we scoop up device name
			//$customer['name'] = $customer['name'] ?? $inputs['deviceName'] ?? '';


			// Set autoacknowledge
			$stmt = $db->prepare('select autoAck from locations WHERE locationId=?');
			$acknowledged = time();
			if($stmt->execute([$locationId])) {
				$autoAck = $stmt->fetch(PDO::FETCH_COLUMN)[0];
				$response['autoAck'] = $autoAck;
				if( $autoAck == "0" ) {
					$acknowledged = null;
				}
			}


			// Save stuff to CloudKit
			$ck = new CloudKitService($sandbox);

			// Save the redemption
			if(isset($inputs['promo'])) {
				$response['redemption'] = $ck->saveRedemption($inputs['promo'], $inputs['userId'], $inputs['udid']);
			}

			// Generate an order number
			$inputs['orderNumber'] = $inputs['orderNumber'] ?? $db->getOrderNumber( $inputs['vendorId'] );


			$points = (int)($inputs['points'] ?? 0);
			$customerId = $customer['id'] ?? null;

			// if it's been paid and we have a customer id then save their points
			if($points>0 && $customerId != null && Db::isPaymentMade($paymentId)) {
				$result = $db->addPointsToCustomer($points,$customerId,null);
				$customer['balance'] = $result['balance'];
				$customer['credit'] = $result['credit'];
			}

			// make sure balance and credit are ints before we encode them
			$customer = $db->cleanedCustomer($customer);
			$customer['device'] = $inputs['deviceName'];
			$deviceName = $inputs['deviceName'];				// we need to overwrite this for the cloudkit record save cause it uses devicename
			$inputs['deviceName'] = json_encode($customer);


			// Now save the order
			$record = $ck->saveOrder2($paymentId, $priceCents, $inputs['orderInfo'], $response['estReady'],$inputs,true,$acknowledged); // TODO: orderId
			$response['orderId'] = $record->getRecordName();
			$response['ready'] = $record->getField('ready');
			$response['paymentId'] = $paymentId;
			$response['customerId'] = $customerId;
			$response['record'] = $record->toServerArray();//print_r($record,true);//json_encode($record,true);
			$response['name'] = $customer['name'];

			// Save this since we are turning off the log
			if(isset($inputs['tax'])) {
				$db->beginTransaction();
				$stmt = $db->prepare('INSERT INTO orders (orderId,charge,format,deviceName,udid,ts,description,vendorId,merchant,amount,tax,tip,customerId,orderHash,points)
													VALUES (?,?,?,?,?,UNIX_TIMESTAMP(),?,?,?,?,?,?/100,?,?,?)');
				$orderHash = $db->logItems($response['orderId'], $inputs['orderInfo']);
				if(isset($paymentResult['orderId'])) {							// if we have a POS orderId then save it here
					$orderHash = 'sq_' . $paymentResult['orderId'];
				}
				$binds = Array(
					$response['orderId'],		
					$paymentId,
					$inputs['format'],		// format 
					$deviceName,				//$customer['name'],		// deviceName
					$inputs['udid'],			// udid
					$inputs['description'],	// description
					$locationId,				// vendorId
					$appId,						// merchant
					$inputs['amount'],		// amount
					floatval($inputs['tax']),			// tax
					floatval($inputs['tip']),			// tip
					$customerId,				// PDO::PARAM_NULL
					$orderHash,
					$points
				);
				$response['logged'] = $stmt->execute($binds);
				if($response['logged'] == false) {
					error_log('WARNING: error logging transaction because ' . json_encode($stmt->errorInfo()));
				}
				$db->commit();
			}

			break;

		case 'saveLoyalty':	// this appears to be deprecated (never used, actually)
			$rewardStmt = Db::shared()->prepare('INSERT INTO loyalty (orderId,customerId,amount,description) VALUES (?,?,?,?)');
			$rewardStmt->execute([ $response['orderId'], $inputs['customer'], $inputs['points'], $inputs['reward'] ]);
			break;

		case 'bankPts':
			throw new Exception('Deprecated');
			// $response = Db::shared()->bankPoints( $inputs['customerId'], 
			// 	intval( $inputs['balance'] ), 
			// 	intval( $inputs['increment'] )
			// );
			break;

		case "collect":	// deprecated convenient payments
			if(isset($inputs['swipe'])) {
				$swipe = $inputs['swipe'];
				if(strlen($swipe)<10) {
					throw new Exception('Swipe error. Please try again');
				} 
				if($testMode) {
					$inputs['swipe'] = '%B4111111111111111^INTELLIPAY TEST CARD/^251250254321987123456789012345?;4111111111111111=25125025432198712345?';
				}
				// TODO: look up in orders table and verify the proper amount
				$merchant = new ConvenientService($inputs['account']);
				$response['swipe'] = $merchant->swipe($inputs['amount'] * 0.01,$inputs['description'],$inputs['swipe']);		// throws an exception if it fails
				$paymentId = (string)$response['swipe']['paymentid'];
				$isComplete = isset($inputs['complete']) ? boolVal($inputs['complete']) : false;
				$ck = new CloudKitService($sandbox);
				$db = Db::shared();
				$db->markTendered( $inputs['orderId'], $paymentId, $isComplete );
				$ck->markTendered( $inputs['orderId'], $paymentId, $isComplete );
				$response['paymentId'] = $paymentId;
			}
			break;

		case 'cashTendered':	// deprecated. Use markTendered instead									// returns 'status': { timestamp|false }
			$db = Db::shared();
			$db->validateSession();
			$ck = new CloudKitService($sandbox);
			$isComplete = isset($inputs['complete']) ? boolVal($inputs['complete']) : false;
			$response['points'] = $db->markTendered( $inputs['order'], CloudKitService::PAID_CASH, $isComplete );
			$response['status'] = $ck->markTendered( $inputs['order'], CloudKitService::PAID_CASH, $isComplete );
			break;

		case 'markTendered':										// returns 'status': { timestamp|false }
			$db = Db::shared();
			$db->validateSession();
			$ck = new CloudKitService($sandbox);
			$isComplete = isset($inputs['complete']) ? boolVal($inputs['complete']) : false;
			$response['points'] = $db->markTendered( $inputs['order'], $inputs['paymentId'], $isComplete );
			$response['status'] = $ck->markTendered( $inputs['order'], $inputs['paymentId'], $isComplete );
			break;

		case 'invalidateOrder':										// returns 'status': { timestamp|false }
			$db = Db::shared();
			$db->validateSession();
			$ck = new CloudKitService($sandbox);
			$response['status'] = $ck->deleteRecord( $inputs['order'] );
			break;

		case 'refund':
			$db = Db::shared();
			$db->validateSession();
			$merchant = new ConvenientService($inputs['account']);
			$response['refund'] = array();
			//$response['refund']['id'] = $merchant->refund( $inputs['chargeId'] );
			$response['refund']['id'] = "void_amount_" . $merchant->voidPayment( $inputs['chargeId'] );
			$ck = new CloudKitService($sandbox);
			$response['updated'] = $ck->saveRefund( $inputs['recordName'], $response['refund']['id']);
			break;

		case 'markComplete':											// about to be deprecated
			$ck = new CloudKitService($sandbox);
			if($response['np'] ?? false == true) {
				$result = $ck->markCompleteButUnpaid( $inputs['order'] );
				$response['record'] = ($result['records'] ?? Array())[0] ?? null;;
			}
			else {
				$result = $ck->markComplete( $inputs['order'] );
				$response['record'] = ($result['records'] ?? Array())[0] ?? null;;
			}
			Db::shared()->markComplete($inputs['order']);
			$response['complete'] = true;
			break;

		case 'unbump':
			$ck = new CloudKitService($testMode);
			$ck->unBump( $inputs['order'] );
			$response['unBump'] = true;
			break;

		case "acknowledge":
			//$db = Db::shared();
			//$db->validateSession();
			$ck = new CloudKitService($sandbox);
			$records = $ck->acknowledge( $inputs['order'] );
			$response['acknowledged'] = isset($records['records']);
			if($env=='staging') {
				$response['records'] = $records;
			}
			break;

		case "ephemeral":
			$s = new StripeService($testMode);
			if(!isset($inputs["stripeId"])) {
				$customer = $s->createCustomer();
				$inputs['stripeId'] = $customer['id'];
				$response['stripeId'] = $customer['id'];
			}
			$response['key'] = $s->ephemeral( $inputs['stripeId'], $inputs['api_version'] );
			break;

		case "connectSubmit":
			$ck = new CloudKitService($sandbox);

			// Save the redemption
			if(isset($inputs['promo'])) {
				$response['redemption'] = $ck->saveRedemption($inputs['promo'], $inputs['userId'], $inputs['udid']);
			}


			$price = $inputs['amount']; 										// verify price based on the order?
			if (is_null($inputs['token'])) {									// Handle $0.00 transactions	
				if ($price > 0) {
					throw new Exception('missing payment token');
				}
				// TODO: verify their promotion: get promo
				$response['charge'] = array("id" => "promo");
			}
			else {
				$s = new StripeService($testMode);
				if (isset($inputs['stripeId'])) { 
					$response['charge'] = $s->connectCharge( $inputs['token'], $price, $inputs['description'], $inputs['dest'], $inputs['stripeId'] );
				}
				else {
					$response['charge'] = $s->connectCharge( $inputs['token'], $price, $inputs['description'], $inputs['dest'], null );
				}
			}
			if(!isset($response['charge']['id'])) {			// just in case
			 	throw new Exception('transaction not created');
			}
			// TODO: compute estimated ready date
			$readyDate = time();
			$record = $ck->saveOrder2($response['charge']['id'], $price, $inputs['orderInfo'], $readyDate,$inputs);
			$response['orderId'] = $record->getRecordName();
			//TODO: put this in for Marley's: $response['readyDate'] = $readyDate;

			// Save split order if applicable
			if(isset($inputs['order2'])) {
				$readyDate2 = $readyDate;
				$record2 = $ck->saveOrder2($response['charge']['id'], $price, $inputs['order2'], $readyDate2,$inputs);
				$response['orderId2'] = $record2->getRecordName();
			}
			break;

		case 'connectRefund':
			$db = Db::shared();
			$db->validateSession();
			$s = new StripeService($testMode);
			$response['refund'] = $s->refund( $inputs['stripeAccountId'], $inputs['chargeId'] );
			$ck = new CloudKitService($sandbox);
			$response['updated'] = $ck->saveRefund( $inputs['recordName'], $response['refund']['id']);
			break;

		case "nameChange":
			// deprecated
			$ck = new CloudKitService($sandbox);
			$updatedRecord = $ck->setOrderName( $inputs['order'], $inputs['name'] );
			$response['nameChange'] = true;
			break;

		case 'saveToCustomer':		// inputs:  ['order':String,'fields':[String],'customerId':String?]
			$orderId = $inputs['order'];
			$merged = array_combine($inputs['fields'], $inputs['values']);
			$cleaned = array_filter($merged, function($k) {
				   return in_array($k,Array('name','email','phone','custom0','custom1'));
			}, ARRAY_FILTER_USE_KEY);


			// pull the order details
			$db = Db::shared();
			$stmt = $db->prepare('SELECT merchant,customerId,charge,points,deviceName FROM orders WHERE orderId=?');
			if($stmt->execute([$orderId]) == false) {
				throw new \Exception('unable to find order containing this customer');
			}
			$arr = $stmt->fetch(PDO::FETCH_ASSOC);
			$deviceName = $arr['deviceName'];							// so we can put on new cloudkit record
			$response['arr'] = $arr;



			if(isset($inputs['customerId'])) {						// save to known customer
				$customerId = $inputs['customerId'];
				$db->updateCustomer($customerId, $cleaned);
			}
																				// save to customer of order and apply loyalty
			else {
				$points = (int)$arr['points'];

				// if there are points but not paid make sure they aren't awarded
				if($points > 0 && Db::isPaymentMade($arr['charge']) == false) {
					$response['nopoints'] = 'notpaidyet';
					$points = null;
				}

				$customerId = $arr['customerId'] ?? "";
				if(strlen($customerId) == 0) { 			// CREATE A CUSTOMER and save back to order
					$cleaned['appId'] = $arr['merchant'];
					if(!is_null($points)) {
						$cleaned['balance'] = $points;
					}
					$response['cleaned'] = $cleaned;
					$response['prevCustomerId'] = $customerId;
					$customerId = $db->createCustomer($cleaned);
					$stmt2 = $db->prepare('UPDATE orders SET customerId=? WHERE orderId=?');
					if($stmt2->execute([$customerId, $orderId]) == false) {
						$s = $db->prepare('DELETE FROM customers WHERE id=?');
						$s->execute([$customerId]);
						error_log("unable to add customer id to order on line #" . __LINE__);
					}
					else {
						$response['balance'] = $points;
					}
				}
				else {
					$customerId = $arr['customerId'];
					$db->updateCustomer($customerId, $cleaned);

					// points should already be assigned
					// if($points>0) {	// already check for payment above
					// 	$response['balance'] = $db->addPointsToCustomer($pts,$customerId,$orderId);
					// }
				}
			}

			// Save info to CloudKit
			$stmt = $db->prepare('SELECT id,name,email,custom0,balance,credit FROM customers WHERE id=?');
			if( $stmt->execute([$customerId]) == false) { throw new \Exception('cannot get customer info'); }
			$arr = $stmt->fetch(PDO::FETCH_ASSOC);
			if( $arr == false ) { throw new \Exception('customer not found'); }
			$customer = array_filter($arr);
			if(isset($customer['balance'])) { $customer['balance'] = (int)$customer['balance']; }
			if(isset($customer['credit'])) { $customer['credit'] = (int)$customer['credit']; }
			$customer['device'] = $deviceName;								// tack this on for KDS display
			$ck = new CloudKitService($sandbox);
			$updatedRecord = $ck->setOrderName( $orderId, json_encode($customer,true) );
			$response['record'] = ($updatedRecord['records'] ?? Array())[0] ?? null;;
			$response['nameChange'] = true;
			$response['result'] = true;
			break;

		case "formatChange":
			$ck = new CloudKitService($sandbox);
			$response['formatChange'] = $ck->setOrderFormat( $inputs['order'], $inputs['format'] );
			break;

		// Administrative things
		case "errorLog":
			$db = Db::shared();
			if(isset($inputs['version'])) {
				$stmt = $db->prepare("INSERT INTO appErrors (timestamp,event,description,file,function,line,stack,version,build,ip_address,locationId) VALUES (UNIX_TIMESTAMP(),?,?,?,?,?,?,?,?,?,?)");
				$response['logged'] = $stmt->execute([ $inputs['event'], $inputs['description'], $inputs['file'], $inputs['function'], $inputs['line'], $inputs['stack'], $inputs['version'], $inputs['build'], $_SERVER['REMOTE_ADDR'], $inputs['vendor'] ?? null  ]);
			}
			else {		// deprecated
				$stmt = $db->prepare("INSERT INTO appErrors (timestamp,event,description,file,function,line,stack) VALUES (UNIX_TIMESTAMP(),?,?,?,?,?,?)");
				$response['logged'] = $stmt->execute([ $inputs['event'], $inputs['description'], $inputs['file'], $inputs['function'], $inputs['line'], $inputs['stack']]);
			}
			$logFile = __DIR__ . '/../logs/log.error';
			break;

		case "log":
			file_put_contents(__DIR__ . '/../logs/appLog', date(DATE_RFC2822) . $inputs['log'] . "\n", FILE_APPEND);
			$response['ok'] = 'logged';
			break;	

		case "createAccount":
			$s = new StripeService();
			$response['account'] = $s->createAccount();
			break;

		case "addBankAccount":
			$s = new StripeService();
			$response['addedBankAccount'] = $s->addBank( $inputs["stripeAccountId"], $inputs['token']);
			break;

		case "agreeToTerms":
			$s = new StripeService();
			$response['agreed'] = $s->agree( $inputs["stripeAccountId"] );
			break;

		// Tests
		case "testStripe":
			$s = new StripeService($testMode);
			$response['test'] = $s->test();
			break;

		case "testUpdate":
			$ck = new CloudKitService(true);
			$response['refund'] = $ck->saveRefund('0A15EEE8-5355-4FCB-BFD9-ADC729E73DA8','test');
			break;

		case "testCreate":
			$ck = new CloudKitService(true);
			$record = $ck->saveOrder($inputs['paymentId'], $inputs['amount'], $inputs['orderInfo'], $inputs['readyDate'],$inputs['deviceName']);
			$response['orderId'] = $record->getRecordName();
			break;

		case "testThrow":
			$cloud = new CloudKitService(true);
			$cloud->testException();
			break;

		case "cloudTest":
			$cloud = new CloudKitService(true);
			$ckResponse = $cloud->test();
			if($ckResponse->hasErrors()) {
				$response['errors'] = CloudKitService::errorReasons($ckResponse->getErrors());
			}
			else {
				$record = $ckResponse->getRecords()[0];
				$response['orderId'] = $record->getRecordName();
			}
			break;

		case "blah": 
			$db = Db::shared();
			$stmt = $db->prepare('SELECT COUNT(*) FROM apps');
			$stmt->execute([]);
			$response['error'] = $stmt->errorInfo();
			break;

		case "test":
			$response['error'] = print_r( $_REQUEST, true );
			break;


		default: 
			$response['error'] = "No method specified";
			if($testMode) { $response['request'] = $_REQUEST; }
	}

} catch (\Exception $e) {
	if($cli) {
		echo "\nStack Trace:\n";
		$tabs = "";
		foreach($e->getTrace() as $arr) {
			$tabs .= "   ";
			echo $tabs . basename($arr['file']) . "[{$arr['line']}]: {$arr['class']}->{$arr['function']}( " . implode($arr['args'],",") . " )\n";
		}
	}
	else if (preg_match('/(staging|jon)/',__FILE__)) {		// in non-production send errors straight to client
		$response['trace'] = $e->getTrace();
	}
	else {		// in production log errors and stack trace
		$errmsg = 'API error thrown: ' . $e->getMessage();
		$trace = $e->getTrace();
		if(isset($trace) && $trace != null) {
			$errmsg .= ("...Stack trace: " . json_encode($trace[0],true) );
		}
		error_log($errmsg);
	}
	if(isset($response['error'])) {
		$response['error'] .= "\n" . $e->getMessage();
	}
	else {
		$response['error'] = $e->getMessage();
	}
}

// Send response
if(!$cli) { header('Content-type: application/json'); }
echo json_encode($response);

if($cli) { echo "\n"; exit; }



// End session with client so they don't have to wait for our logging
	$output = ob_get_contents();
	ob_end_flush();
	ob_flush();
	flush();
	ob_end_clean();


logThis($action, $inputs, $output);//$response);

?>
