<?php

/* 
 * File name  : paynetworx_class.php
 * Created on : Apr 6, 2023, 4:43:50 PM
 * Author     : Thomas Kirby <tkirby at dynengsys.com>
 * Company    : Dynamic Engineering Systems
 * Rights     : Under license to assignee. Not for resale or redistribution.
 */
 
require_once($sysfiles."/paynetworx/ksuid.php");					// load paynetworx helper class


class PayNetworx
{
	// storage
	var $id = "???";		// transaction source group
	var $mid = "???";		// transaction source store
	var $base_url = "";		// end-point base url
	var $access_user;		// Access Token User
	var $access_password;	// Access Token Password
	var $debug = false;
	// local storage
	var $method = "";
	var $request_url = "";
	var $data = null;
	var $httpCode = 0;

	// id, mid just for logging data which we are not doing.
	
	// dual constructors, in case we need to keep both old and new methods
	// https://www.php.net/manual/en/language.oop5.decon.php#:~:text=PHP%20only%20supports%20a%20single,static%20methods%20as%20constructor%20wrappers.
	
	public function __construct( $id, $mid, $base_url, $access_user, $access_password, $debugMode=false ) {
		if ($debugMode) { print "PayNetworx(id=$id, mid=$mid, base_url=$base_url, access_user=$access_user, access_password=$access_password, debug=$debugMode)<br>"; }

		$this->id = $id;
		$this->mid = $mid;

		// base url is required
		if (empty($base_url)) {
			throw new Exception(__METHOD__.": Missing base_url");
		}
		$this->base_url = $base_url;
		
		// user and password are required
		if (empty($access_user)) {
			throw new Exception(__METHOD__.": Missing access_user");
		}
		$this->access_user = $access_user;
		
		if (empty($access_password)) {
			throw new Exception(__METHOD__.": Missing access_password");
		}
		$this->access_password = $access_password;
		
		// set debug mode on/off
		$this->debug = $debugMode;
	}

	
	//==========================================================================
	//== AUTHCAPTURE                                                          ==
	//==========================================================================

	/*
	 * AUTHCAPTURE is commonly known as a 'sale' transaction. 
	 * It combines the functions of authorization and capture into a single transaction.
	 * An AUTHCAPTURE is used for 'finalized' sale transactions in eCommerce, retail, and restaurant environments that have these features:
	 * With eCommerce transactions, the AUTHCAPTURE is used for completed sales for for virtual products only; this DOES NOT include physical products that are to be delivered at a later 
	 * time. In the restaurant environment, the AUTHCAPTURE is used for completed sales with a known tip amount, or 'tip at the table'; NOTE: There is NO tip adjustment after the sale, 
	 * as this is no longer supported by various card associations with the use of chip cards. Successful requests will be indicated with a 2xx HTTP response code and the Approved 
	 * attribute set to 'true'. NOTE: It is possible to receive a 2xx HTTP response code and have the Approved attribute set to 'false'.
	 */
	
	public function AuthCapture( 
		$Total, $Tip, $Tax, $Currency,
		$TokenID, $CardNumber, $expMonth, $expYear, $CVN,
		$Name, $Line1, $Line2, $City, $State, $Zip, $Country, $Phone, $Email,
		$OrderNumber, $CustomerID, $TxnType=""
		) {
		
		debug_log("paynetworx.AuthCapture(
			Total=$Total, Tip=$Tip, Tax=$Tax, Currency=$Currency, 
			TokenID=$TokenID, CardNumber=$CardNumber, expMonth=$expMonth, expYear=$expYear, CVN=$CVN,
			Name=$Name, Line1=$Line1, Line2=$Line2, City=$City, State=$State, Zip=$Zip, Country=$Country, Phone=$Phone, Email=$Email,
			OrderNumber=$OrderNumber, CustomerID=$CustomerID, TxnType=$TxnType )");

//qqq AuthCapture
		$Total = $this->MoneyFmt($Total);
		if ($this->debug) print "Total = $Total<br>";
		
		$Tip = $this->MoneyFmt($Tip);
		if ($this->debug) print "Tip = $Tip<br>";
		
		$Fee = $this->MoneyFmt(0);
		if ($this->debug) print "Fee = $Fee<br>";
		
		$Tax = $this->MoneyFmt($Tax);
		if ($this->debug) print "Tax = $Tax<br>";
		
		if ($this->debug) { print "Currency = $Currency<br>"; }
		if (strlen($Currency) != 3) { die("Invalid currency = $Currency"); }
		
		$Shipping = $this->MoneyFmt(0);
		if ($this->debug) print "Shipping = $Shipping<br>";
		
		$Duty = $this->MoneyFmt(0);
		if ($this->debug) print "Duty = $Duty<br>";

		// pre-process customer info fields
		// $Name
		// $Line1
		// $Line2
		// $City
		// $State
		// $Zip
		if ($Country == 'USA') $Country = "US";	// translate non-standard coding for ISO standard abbreviation
		// $Phone - unknown format
		// $Email
		
		// strip non-numeric characters
		if ($this->debug) print "CardNumber = $CardNumber<br>";
		$cleanedCardNumber = preg_replace("/[^0-9]/", "", $CardNumber);
		if ($this->debug) print "cleanedCardNumber = $cleanedCardNumber<br>";
		
		if ($this->debug) print "expMonth = $expMonth<br>";
		if ($this->debug) print "expYear = $expYear<br>";
		if ($this->debug) print "CVN = $CVN<br>";
		
		// use CardNumber, not TokenID
		if (!empty($CardNumber)) {
			// TokenID should not be present
			if (!empty($TokenID)) {
				throw new Exception(__METHOD__.": TokenId present with Card fields");
			}
			// all Card fields should be present
			if (empty($CardNumber) || empty($expMonth) || empty($expYear) || empty($CVN)) {
				throw new Exception(__METHOD__.": Card field(s) missing");
			}
		} 
		// use TokenID, not CardNumber
		elseif (!empty($TokenID)) {

			// check for unwanted fields present
			if (!empty($CardNumber) || !empty($expMonth) || !empty($expYear) || !empty($CVN)) {
				throw new Exception(__METHOD__.": Card fields present with TokenId");
			}
			
			// do not create new token
			$DataAction = "";
		}
		// just not right
		else {
			throw new Exception(__METHOD__.": TokenId and/or Card fields wonky");
		}
		
		// build PaymentMethod section of request
		$PaymentMethod = array();	// object	Transaction payment method
		if (!empty($CardNumber)) {
			$PaymentMethod['Card'] = array(		// object	Card info
				"CardPresent" => false,				// bool	Indicates if this is card-present
				"PAN" => array(						// object	Personal account number info
					"PAN" => $cleanedCardNumber,	// string	Personal account number
					"ExpMonth" => $expMonth,		// string	Account expiration month (mm)
					"ExpYear" => $expYear			// string	Account expiration year (yy)
				),
				"CVC" => array(			// object	Card Verification Value (AKA CVV2) (optional)
					"CVC" => $CVN			// string	Card Verification Value (AKA CVV2)
				),
				//"Track2" => array(			// object	Track 2 data
				//	"Track2" => $track2		// string	Track 2 string, start and end sentinels required.
				//),
				//"PIN" => array(				// object	PIN (optional)
				//	"Data" => $pinData,		// string	PIN data
				//	"KSN" => $pinKSN		// string	Key serial number
				//),
				"BillingAddress" => array(	// object	Customer billing address (optional)
					"Name" => $Name,		// string	Customer name
					"Line1" => $Line1,		// string	Customer address line 1
					"Line2" => $Line2,		// string	Customer address line 2
					"City" => $City,		// string	Customer city
					"State" => $State,		// string	Customer state
					"PostalCode" => $Zip,	// string	Customer postal (zip) code
					"Country" => $Country,	// string	Customer country
					"Phone" => $Phone,		// string	Customer phone number
					"Email" => $Email		// string	Customer email
				),
				//"ICC" => array(				// object	See attributes in Integrated circuit card data section
				//),
			);	// end card object
		}
		if (!empty($TokenID)) {
			$PaymentMethod['Token'] = array(	// object	Indicates the Token ID as the payment method
				"TokenID" => $TokenID			// ksuid	Token ID
			);
		}
		// end PaymentMethod
		
		// REQUEST Example: eCommerce
		$request = array();				// #REQUEST Object Attributes

		// build Amount section of request data
		// commented Taxable and Discount due to error "Key: 'FrontEndRequest.Amount.Taxable' Error:Field validation for 'Taxable' failed on the 'currency_error' tag"
		$request['Amount'] = array(
			"Total" => $Total,			// decimal	Total transaction amount
			"Tip" => $Tip,				// decimal	Tip associated with the transaction (optional)
			"Fee" => $Fee,				// decimal	Fee associated with the transaction (optional)
			"Tax" => $Tax,				// decimal	Tax associated with the transaction (optional) ** difference not clear
			//"Taxable" => 0,				// decimal	Tax associated with the transaction (optional) ** difference not clear
			"Shipping" => $Shipping,	// decimal	Shipping associated with the transaction (optional)
			"Duty" => $Duty,			// decimal	Duty associated with the transaction (optional)
			//"Discount" => 0,			// decimal	Discount associated with the transaction (optional)
			"Currency" => $Currency		// string	ISO 3-letter currency code
		);
		
		$request['PaymentMethod'] = $PaymentMethod;	
	
	/*	$request['Attributes'] = array(					// object	Transaction attributes
			"TransactionDescriptor" => array(		// object	Description of the transaction used for dynamic descriptors (optional). Dynamic Descriptor
				"Prefix" => $Prefix,				// string	The Prefix for the transaction description. Dynamic Descriptor
				"Detail" => $Detail					// string	The Detail for the transaction description. Dynamic Descriptor
			)
		); */
			
/*		
Token IDs are generated only by Paynetworx.
A new Token ID can be requested by a Token Add request through the Data Action field.
A generated Token ID from Paynetworx is associated with a specific merchant for a specific customer and specific card.
This allows the Token ID to be used as a payment method between the merchant and customer.
The Token ID references the customer's card or account as an on-file record which is kept securely at Paynetworx for that merchant.
To get the value of the Token ID and the Token Name, look to the response message in the Token object.
Token IDs are never provided in any type of request that adds a Token ID.		
*/	
		if (!empty($CardNumber)) {
			// create new TokenID
			$request['DataAction'] = "token/add";		// string	Data Action
		}

		// EntryMode = Point Of Sale Entry Mode (Card-On-File, Manual, Manual-Fallback, Magstripe, Magstripe-Fallback, Chip, Contactless-Chip)
		$posEntryMode = "";
		
		if (!empty($CardNumber)) {
			$posEntryMode = "manual";			// paynetworx documentation says "Manual" - this is not correct
		}
		
		if (!empty($TokenID)) {
			$posEntryMode = "card-on-file";
		}
		
debug_log(__METHOD__.": EntryMode = $posEntryMode");

		// Type	= POS Type (Pos, Recurring (Used With EntryMode Card-On-File))		
		//$posType = "ecommerce";	// selected initiation type or terminal type is incompatible with entry mode
		$posType = "pos";			// FrontEndRequest.POS.Type' Error:Field validation for 'Type' failed on the 'tran_type' tag
		//$posType = "Pos";			// Key: 'FrontEndRequest.POS.Type' Error:Field validation for 'Type' failed on the 'tran_type' tag
		//$posType = "";			// selected initiation type or terminal type is incompatible with entry mode
		//$posType = "POS";			// Key: 'FrontEndRequest.POS.Type' Error:Field validation for 'Type' failed on the 'tran_type' tag
		
debug_log(__METHOD__.": Type = $posType");
		
		$request['POS'] = array(					// object	Point of sale transaction information
			"EntryMode" => $posEntryMode,			// string	Entry mode
			"Type" => $posType,						// string	Transaction Type
			//"DebitType" => $DebitType,			// string	Debit Type = Indicates A Debit Card Transaction (Direct, Pinless)
			"Device" => "NA",						// string	Device submitting the transaction (no values specified)
			"DeviceVersion" => "NA",				// string	Device version (no values specified)
			"Application" => "EmberPay",			// string	POS application name
			"ApplicationVersion" => "1.0",			// string	POS application version
			"Timestamp" => date("c")				// string	Local Timestamp
		);
		
		$request['Detail'] = array(					// object	Transaction details (optional)
			"MerchantData" => array(				// object	Merchant-defined transaction data
				"Transaction Type" => $TxnType,		// string	Merchant-defined transaction data field 1
				"CustomerID" => $CustomerID			// string	Merchant-defined transaction data field 2
			),
		);
		
debug_log("AuthCapture request = ".var_export($request,true));
		
		//URL = $base_url . "transaction/authcapture"
		// curl -sS --user $HTTPAUTHTOKEN -H 'Content-Type: application/json' -H "Request-ID: $(ksuid)" $URL -d '
		$response = $this->SendRequest( "POST", $this->base_url . "transaction/authcapture", $request, "AuthCapture" );
		//print "<h2>SendRequest response</h2><pre>"; var_dump($response); print "</pre>";
		
debug_log("AuthCapture response = ".var_export($response,true));
		
		/*
		ERROR RESPONSE
		"TransactionID": "2OlWu8Ify3Og1EjAOrQQ3n1p0Em",
		"EventID": "2OlWuCcEmUul473kk89rfa2Jd44",
		"RequestID": "000000000000000000000000000",
		"Error": "Access Denied",
		"Approved": false
		*/
		
		$logdata['GroupId'] = $this->id;								// transaction source group
		$logdata['MerchantId'] = $this->mid;							// transaction source store
		$logdata['MethodName'] = "AuthCapture";							// transaction type
		$logdata['TransactionID'] = $response->TransactionID;			// '2OlWu8Ify3Og1EjAOrQQ3n1p0Em' (length=27)
		$logdata['EventID'] = $response->EventID;						// '2OlWuCcEmUul473kk89rfa2Jd44' (length=27)
		$logdata['RequestID'] = $response->RequestID;					// '000000000000000000000000000' (length=27)
		$logdata['Approved'] = $response->Approved ? "Y" : "N";			// boolean false
		$logdata['RequestData'] = serialize($request);
		$logdata['ResponseData'] = $response;
		$logdata['Error'] = (!empty($response->Error)) ? $response->Error : "";

		// log the transaction
		$this->LogTransaction($logdata);

/*		
SUCCESS RESPONSE
object(stdClass)[12]
	public 'TransactionID'	// TransactionID	ksuid	Unique transaction lifetime system ID => string '2P1vKgr3T5etdsgBYn5npG31Aeu' (length=27)
	public 'EventID'			// EventID			ksuid	Unique transaction event system ID => string '2P1vKe5oKgRgFXSJOf0JJFPheED' (length=27)
	public 'RequestID'		// RequestID		ksuid	Unique transaction request system ID => string '000000000000000000000000000' (length=27)
	public 'AuthCode' => string 'EIQ1EM' (length=6)
	public 'ResponseCode' => string '00' (length=2)
	public 'ResponseText' => string 'Approval and completed successfully (V00)' (length=41)
	public 'Approved'			// Approved			bool	Request approval indicator (e.g., true, false) => boolean true
	public 'AddressLine1Check'	// AddressLine1Check	string	Address verification result (e.g., pass, fail, unavailable, unchecked) => string 'fail' (length=4)
	public 'AddressZipCheck'		// AddressZipCheck		string	Postal code verification result (e.g., pass, fail, unavailable, unchecked) => string 'fail' (length=4)
	public 'CVCCheck'				// CVCCheck				string	CVC result (e.g., pass, fail, unavailable, unchecked) => string 'pass' (length=4)
	Token			object		// Token-related response fields
		TokenID		ksuid		// Token ID
		TokenName	string		// Unique Name of the Token ID used
*/
		
		return $response;
		
	} // end AuthCapture

	
	//==========================================================================
	//== AUTH                                                                 ==
	//==========================================================================
	/*
	AUTH
	AUTH simply authorizes a transaction WITHOUT CAPTURE. It is generally used for eCommercce transactions with delayed product shipment.
	Successful requests will be indicated with a 2xx HTTP response code and the Approved attribute set to 'true'. NOTE: It is possible to receive a 2xx HTTP response code and have the Approved attribute set to 'false'.
	*/
	public function Auth( 
		$Total, $Tip, $Tax, $Currency,
		$FullName, $BankRouting, $BankAccount, $BankAccountType, $BankAccountHolderType ) {
		if ($this->debug) print "paynetworx.Auth( $Total, $Tip, $Tax, $Currency, $FullName, $BankRouting, $BankAccount, $BankAccountType, $BankAccountHolderType )<br>";
		
		return false;
	}


	//==========================================================================
	// BALANCE
	//==========================================================================
	public function Balance( $arg1, $arg2, $arg3, $arg4, $arg5 ) {
		if ($this->debug) print "paynetworx.Balance( $arg1, $arg2, $arg3, $arg4, $arg5 )<br>";
		return false;
	}

	//==========================================================================
	// CAPTURE
	//==========================================================================
	public function Capture( $arg1, $arg2, $arg3, $arg4, $arg5 ) {
		if ($this->debug) print "paynetworx.Capture( $arg1, $arg2, $arg3, $arg4, $arg5 )<br>";
		return false;
	}

	//==========================================================================
	// REFUND
	//==========================================================================
/*		
REFUND
REFUND processes a refund for a transaction that has been previously captured. The capture may be either full or partial.
Successful requests will be indicated with a 2xx HTTP response code and the Approved attribute set to 'true'. NOTE: It is possible to receive a 2xx HTTP response code and have the Approved attribute set to 'false'.
#Refund Window
Captured transactions may be refunded when inside the refund window. Uncaptured transactions cannot be refunded.
The refund window begins after the 10 minute void window. Before the refund window begins, a refund request will receive a response indicating the refund window is not open, yet.
*/
	public function Refund( $Total, $Currency, $TokenID, $TransactionID="" ) {
		if ($this->debug) { print "paynetworx.Refund( $Total, $Currency, $TransactionID, $TokenID )<br>"; }
		
		$Total = $this->MoneyFmt($Total);
		if ($Total === false) {
			throw new Exception(__METHOD__.": Total Invalid");
		}
		if ($this->debug) { print "Total = $Total<br>"; }

//qqq Refund validate		
		
		if (strlen($Currency) != 3) {
			throw new Exception(__METHOD__.": Currency Invalid '$Currency'");
		}
		if ($this->debug) { print "Currency = $Currency<br>"; }
		
		$request = array();				// #REQUEST Object Attributes

		// build Amount section of request data
		$request['Amount'] = array(
			"Total" => $Total,			// decimal	Total transaction amount
			"Currency" => $Currency		// string	ISO 3-letter currency code
		);

		if (!empty($TransactionID)) {
			// PaymentMethod must not be present if submitting TransactionID
			$request['TransactionID'] = $TransactionID;	// ksuid	TransactionID from the CAPTURE response (optional)
		}
		else if (!empty($TokenID)) {
			// build PaymentMethod section of request
			// object	Transaction payment method
			$request['PaymentMethod']['Token'] = array(	// object	Indicates the Token ID as the payment method
				"TokenID" => $TokenID			// ksuid	Token ID
			);
		}

		// Refund
		$request['POS'] = array(					// object	Point of sale transaction information
			"EntryMode" => "card-on-file",			// string	Entry mode
			"Type" => "ecommerce",					// string	Transaction Type
			//"DebitType" => $DebitType,			// string	Debit Type
			"Device" => "NA",						// string	Device submitting the transaction
			"DeviceVersion" => "NA",				// string	Device version
			"Application" => "Skoop! PayNetworx",	// string	POS application name
			"ApplicationVersion" => "1.0",			// string	POS application version
			"Timestamp" => date("c")				// string	Local Timestamp
		);
		
		/*$request['Detail'] = array(					// object	Transaction details (optional)
			"MerchantData" => array(				// object	Merchant-defined transaction data
				"OrderNumber" => $OrderNumber,		// string	Merchant-defined transaction data field 1
				"CustomerID" => $CustomerID			// string	Merchant-defined transaction data field 2
			),
		);*/
		
		//print "<h2>Refund request</h2><pre>"; var_dump($request); print "</pre>";

		// curl -sS --user $HTTPAUTHTOKEN -H 'Content-Type: application/json' -H "Request-ID: $(ksuid)" $URL -d '
		$response = $this->SendRequest( "POST", $this->base_url . "transaction/refund", $request, "Refund" );
		
		//print "<h2>SendRequest response</h2><pre>"; var_dump($response); print "</pre>";
		
/*
#REQUEST Example: eCommerce
URL=xxx/transaction/refund
HTTPAUTHTOKEN=1fHfjpw86udrDQHRMKabypEmhY4:1fHfkJrOs7iVBnC07HDxCPZuPsK
curl -sS --user $HTTPAUTHTOKEN -H 'Content-Type: application/json' -H "Request-ID: $(ksuid)" $URL -d '
{
    "Amount": {
        "Total": 5.50,
        "Currency": "USD"
    },
    "TransactionID": "1XSnl0X7GC44qPT2CVV8BCz0bQi",
    
}'

Refund response
(object) array(
   'TransactionID' => '2PCpN9PnOg8zBmSsLVzLjnTYuNR',
   'EventID' => '2PI9DRD4NPRkdrvlRPWITywZO0T',
   'RequestID' => '000000000000000000000000000',
   'AuthCode' => 'S4T7OQ',
   'ResponseCode' => '00',
   'ResponseText' => 'Approval and completed successfully (V00)',
   'Approved' => true,
   'Token' => 
  (object) array(
     'TokenID' => '2PI94yq5n2kcne9gUDVKLGnChZm',
     'TokenName' => '************0123',
  ),
)
*/
		
		return $response;
	}

	//==========================================================================
	// REFUNDAUTH
	//==========================================================================
	public function RefundAuth( $arg1, $arg2, $arg3, $arg4, $arg5 ) {
		if ($this->debug) print "paynetworx.RefundAuth( $arg1, $arg2, $arg3, $arg4, $arg5 )<br>";
		return false;
	}

	//==========================================================================
	// REFUNDCAPTURE
	//==========================================================================
	public function RefundCapture( $arg1, $arg2, $arg3, $arg4, $arg5 ) {
		if ($this->debug) print "paynetworx.RefundCapture( $arg1, $arg2, $arg3, $arg4, $arg5 )<br>";
		return false;
	}

	// VERIFY
	public function Verify( $arg1, $arg2, $arg3, $arg4, $arg5 ) {
		if ($this->debug) print "paynetworx.Verify( $arg1, $arg2, $arg3, $arg4, $arg5 )<br>";
		return false;
	}

	// VOID
	public function Void( $arg1, $arg2, $arg3, $arg4, $arg5 ) {
		if ($this->debug) print "paynetworx.Void( $arg1, $arg2, $arg3, $arg4, $arg5 )<br>";
		return false;
	}

	//zzz
	//==========================================================================
	// ACHDEBIT
	// Takes money out of the customer's account, and puts it into the merchant account.
	// The customer account receives a debit. The merchant account will be credited.
	// Successful requests will be indicated with a 2xx HTTP response code and the Approved attribute set to 'true'.
	// CheckNumber must be string type, else returns access denied error
	//==========================================================================
	public function AchDebit( $Total, $Tip, $Tax, $Currency,
			$TokenID, $Routing, $Account, $Type, 
			$CustomerName, $CustomerID, $CustomerInfo="", $CheckNumber="", $EffectiveDate="", $TxnType="" ) {
		if ($this->debug) { print "paynetworx.AchDebit( $Total, $Tip, $Tax, $Currency, $TokenID, $Routing, $Account, $Type, $CustomerName, $CustomerID, $CustomerInfo, $CheckNumber, $EffectiveDate, $TxnType )<br>"; }
		debug_log("paynetworx.AchDebit( $Total, $Tip, $Tax, $Currency, $TokenID, $Routing, $Account, $Type, $CustomerName, $CustomerID, $CustomerInfo, $CheckNumber, $EffectiveDate, $TxnType )");
		
		$Total = $this->MoneyFmt($Total);
		if ($Total === false) {
			throw new Exception(__METHOD__.": Total Invalid");
		}
		if ($this->debug) { print "Total = $Total<br>"; }
		
		if ($this->debug) { print "Currency = $Currency<br>"; }
		if (strlen($Currency) != 3) { die("Invalid currency = $Currency"); }
     
		// The stored type (PC,BC,PS,BS) is common to all payment processors,
		// and is translated to processor-specific terms here.
		switch($Type) {
			case 'PC': $translated_account_type = "PersonalChecking"; break;
			case 'BC': $translated_account_type = "BusinessChecking"; break;
			case 'PS': $translated_account_type = "PersonalSavings"; break;
			case 'BS': $translated_account_type = "BusinessSavings"; break;
			default:   $translated_account_type = ""; break;
		}
		
		$URL = $this->base_url . "transaction/achdebit";
		
		if (empty($EffectiveDate)) {
			$EffectiveDate = date("Y-m-d");
		} else {
			$EffectiveDate = date("Y-m-d",strtotime($EffectiveDate));
		}
		
		$request = array();
		
		$request['Amount'] = array(				// Amount Object
			"Total" => $Total,					// decimal	Total transaction amount
			"Currency" => $Currency				// string	ISO 3-letter currency code
		);
		
		// Merchant-defined transaction data (optional)
		$request['Detail']['MerchantData'] = array(
			"Transaction Type" => $TxnType,		// string	Merchant-defined transaction data field 1
			"CustomerID" => $CustomerID			// string	Merchant-defined transaction data field 2
		);
		
		// build PaymentMethod section of request
		// including this for both methods, api logic seems 
		// to have changed since initial tests were run.
		$request['PaymentMethod']['ACH'] = array(			// object	ACH info
			"BankRoutingNumber" => $Routing,				// string	Bank routing number of the customer
			"AccountNumber" => $Account,					// string	Account number of the customer
			//"AchAccountType" => $translated_account_type,	// string	ACH Account Types: PersonalChecking, BusinessChecking, PersonalSavings, BusinessSavings
			"ACHAccountType" => $translated_account_type,	// error message implies that documentation is wrong in letter case
			"CustomerName" => $CustomerName,				// string	Name of the customer to be debited (e.g. "Polly Paynetworx")
			"CustomerIdentifier" => $CustomerID,			// string	Identification value for the customer (optional) "A12345",
			"PaymentRelatedInformation" => $CustomerInfo,	// string	Additional, discretionary customer payment information such as address, etc. (optional) 
			"CheckNumber" => $CheckNumber,					// string	Customer check number (optional)  e.g. "5678"
			"EffectiveDate" => $EffectiveDate				// string	Date that the customer should be debited as YYYY-MM-DD "2012-04-21"
		);
		
		// if using account info
		if (!empty($Routing) && !empty($Account)) {
			//print "AchDebit BY ROUTING/ACCOUNT<br>";
//!!!! NOTE !!!!
//Assumption here that routing/account means save the info and create a token.
//Do we really want to do that for micro deposits?			
			// create new TokenID
			$request['DataAction'] = "token/add";				// string	Data Action
		}
		else if (!empty($TokenID)) {
			//print "AchDebit BY TOKEN<br>";
			
			$request['PaymentMethod']['Token'] = array(			// object	Indicates the Token ID as the payment method
				"TokenID" => $TokenID							// ksuid	Token ID
			);
			
			// 'POS object with EntryMode data is required for transactions with Token ID for the Payment Method' (length=96)
		
		// AchDebit
		/*	$request['POS'] = array(					// object	Point of sale transaction information
				"EntryMode" => "card-on-file",			// string	Entry mode
				"Type" => "ecommerce",					// string	Transaction Type
				"DebitType" => "pinless",				// string	Debit Type
				"Device" => "NA",						// string	Device submitting the transaction
				"DeviceVersion" => "NA",				// string	Device version
				"Application" => "Ember PayNetworx",	// string	POS application name
				"ApplicationVersion" => "1.0",			// string	POS application version
				"Timestamp" => date("c")				// string	Local Timestamp
			); */
			
		}

		//----------------------------------------------------------------------
		
		debug_log("AchDebit request = ".var_export($request,true));
		
		$response = $this->SendRequest( "POST", $this->base_url . "transaction/achdebit", $request, "AchDebit" );
		
		debug_log("AchDebit response = ".var_export($response,true));
		
/*
AchDebit response

object(stdClass)[2]
  public 'TransactionID' => string '2PhY0AmMqAZYNOmji22y4wCk5CR' (length=27)
  public 'EventID' => string '2PhY04Rp9Moe0zlilYOux285iDF' (length=27)
  public 'RequestID' => string '000000000000000000000000000' (length=27)
  public 'ResponseText' => string 'ACH request submitted successfully' (length=34)
  public 'Approved' => boolean true


AchDebit response

object(stdClass)[2]
  public 'TransactionID' => string '2PhZTSpEfJxY7xgGOuYYq3awuGZ' (length=27)
  public 'EventID' => string '2PhZTOfsouilHTfukN0HxpvAVFY' (length=27)
  public 'RequestID' => string '000000000000000000000000000' (length=27)
  public 'ResponseText' => string 'ACH request submitted successfully' (length=34)
  public 'Approved' => boolean true
  public 'Token' => 
    object(stdClass)[3]
      public 'TokenID' => string '2PhZTXhHyK7rbI36qz26qrGWCZd' (length=27)
      public 'TokenName' => string '1234' (length=4)
*/		
		
		return $response;
	}

	//zzz
	//==========================================================================
	// ACHCREDIT
	// Takes money out of the merchant account, and puts it into the customer account.
	// The customer account receives a credit. The merchant account will be debited.
	// Successful requests will be indicated with a 2xx HTTP response code and the Approved attribute set to 'true'.
	// CheckNumber must be string type, else returns access denied error
	//==========================================================================
	public function AchCredit( $Total, $Currency, $TokenID, $Routing, $Account, $Type, 
			$CustomerName, $CustomerID, $CustomerInfo="", $CheckNumber="", $EffectiveDate="" ) {
		if ($this->debug) { print "paynetworx.AchCredit( $Total, $Currency, $TokenID, $Routing, $Account, $Type, $CustomerName, $CustomerID, $CustomerInfo, $CheckNumber, $EffectiveDate )<br>"; }
		
		$Total = $this->MoneyFmt($Total);
		if ($Total === false) {
			throw new Exception(__METHOD__.": Total Invalid");
		}
		if ($this->debug) { print "Total = $Total<br>"; }
		
		if ($this->debug) { print "Currency = $Currency<br>"; }
		if (strlen($Currency) != 3) { die("Invalid currency = $Currency"); }
     
		// translate $ach_accountType to payment processor specific string
		// PC=Personal Checking, PS=Personal Savings, BC=Business Checking, BS=Business Savings
		switch($Type) {
			case 'PC': $translated_account_type = "PersonalChecking"; break;
			case 'BC': $translated_account_type = "BusinessChecking"; break;
			case 'PS': $translated_account_type = "PersonalSavings"; break;
			case 'BS': $translated_account_type = "BusinessSavings"; break;
			default:   $translated_account_type = ""; break;
		}
		
		if (empty($EffectiveDate)) {
			$EffectiveDate = date("Y-m-d");
		} else {
			$EffectiveDate = date("Y-m-d",strtotime($EffectiveDate));
		}
		
		$request = array();
		
		$request['Amount'] = array(				// Amount Object
			"Total" => $Total,					// decimal	Total transaction amount
			"Currency" => $Currency				// string	ISO 3-letter currency code
		);
		
		// if using account info
		if (!empty($Routing) && !empty($Account)) {
			//print "AchCredit BY ACCOUNT<br>";
			//die("AchCredit BY ACCOUNT not implemented");
			
			// build PaymentMethod section of request
			$request['PaymentMethod']['ACH'] = array(			// object	ACH info
				"BankRoutingNumber" => $Routing,				// string	Bank routing number of the customer
				"AccountNumber" => $Account,					// string	Account number of the customer
				"AchAccountType" => $translated_account_type,	// string	ACH Account Types: PersonalChecking, BusinessChecking, PersonalSavings, BusinessSavings
				"CustomerName" => $CustomerName,				// string	Name of the customer to be credited
				"CustomerIdentifier" => $CustomerID,			// string	Identification value for the customer (optional) "A12345",
				"PaymentRelatedInformation" => $CustomerInfo,	// string	Additional, discretionary customer payment information such as address, etc. (optional) 
				"CheckNumber" => $CheckNumber,					// string	Customer check number (optional)  e.g. "5678"
				"EffectiveDate" => $EffectiveDate				// string	Date that the customer should be credited as YYYY-MM-DD "2012-04-21"
			);
		 
			// create new TokenID
			/* DataAction	string	Data Action */		
			$request['DataAction'] = "token/add";				// string	Data Action

		}
		else if (!empty($TokenID)) {
			//print "AchCredit BY TOKEN<br>";
			
			$request['PaymentMethod']['Token'] = array(			// object	Indicates the Token ID as the payment method
				"TokenID" => $TokenID							// ksuid	Token ID
			);
			
			// build PaymentMethod section of request
			$request['PaymentMethod']['ACH'] = array(			// object	ACH info
				//"BankRoutingNumber" => $Routing,				// string	Bank routing number of the customer
				//"AccountNumber" => $Account,					// string	Account number of the customer
				"AchAccountType" => $translated_account_type,	// string	ACH Account Types: PersonalChecking, BusinessChecking, PersonalSavings, BusinessSavings
				"CustomerName" => $CustomerName,				// string	Name of the customer to be credited
				"CustomerIdentifier" => $CustomerID,			// string	Identification value for the customer (optional) "A12345",
				"PaymentRelatedInformation" => $CustomerInfo,	// string	Additional, discretionary customer payment information such as address, etc. (optional) 
				"CheckNumber" => $CheckNumber,					// string	Customer check number (optional)  e.g. "5678"
				"EffectiveDate" => $EffectiveDate				// string	Date that the customer should be credited as YYYY-MM-DD "2012-04-21"
			);
			
		}

		// Merchant-defined transaction data (optional)
		$request['Detail']['MerchantData'] = array(
			//"OrderNumber" => $OrderNumber,	// string	Merchant-defined transaction data field 1
			"CustomerID" => $CustomerID			// string	Merchant-defined transaction data field 2
		);
		
		//----------------------------------------------------------------------
		
		//print "<h2>AchCredit request</h2><pre>"; var_dump($request); print "</pre>";
		//print "<h2>AchCredit response</h2><pre>"; var_dump($response); print "</pre>";
		
		debug_log("AchCredit request = ".var_export($request,true));
		
		$response = $this->SendRequest( "POST", $this->base_url . "transaction/achcredit", $request, "AchCredit" );
		
		debug_log("AchCredit response = ".var_export($response,true));
		
		return $response;		
	}

	// ACHVOID
	public function AchVoid( $arg1, $arg2, $arg3, $arg4, $arg5 ) {
		if ($this->debug) { print "paynetworx.AchVoid( $arg1, $arg2, $arg3, $arg4, $arg5 )<br>"; }
		return false;
	}

	// STATUS
	public function Status( $TransactionId ) {
		if ($this->debug) { print "paynetworx.Status( $TransactionId )<br>"; }
		
		if (empty($TransactionId)) {
			throw new Exception(__METHOD__.": TransactionId Invalid");
		}
		if ($this->debug) { print "TransactionId = $TransactionId<br>"; }
				
		$request = array();
		
		$request['TransactionId'] = $TransactionId;
		
		//print "<h2>Status request</h2><pre>"; var_dump($request); print "</pre>";
		
		$response = $this->SendRequest( "POST", $this->base_url . "query/transaction/status", $request, "Status" );
		
		//print "<h2>Status response</h2><pre>"; var_dump($response); print "</pre>";
		
		return $response;		
		
		return false;
	}

	// LIST
	public function List( $StartTime, $EndTime, $Approved ) {
		if ($this->debug) { print "List( $StartTime, $EndTime, $Approved )<br>"; }
		
		$fmt = "c";
		//$fmt = "Y-m-d\TH:i:sp";
		
		$StartTime = date($fmt,strtotime($StartTime));
		$StartTime = str_replace('+00:00', 'Z', $StartTime);
		
		$EndTime = date($fmt,strtotime($EndTime));
		$EndTime = str_replace('+00:00', 'Z', $EndTime);
		
		if ($this->debug) { print "List( $StartTime, $EndTime, $Approved )<br>"; }

		$request['Query'] = array(
			"StartTime" => $StartTime,
			"EndTime"   => $EndTime,
			//"Approved"  => $Approved
		);

		//print "<h2>List request</h2><pre>"; var_dump($request); print "</pre>";
		
		$response = $this->SendRequest( "POST", $this->base_url . "query/transaction/list", $request, "List" );
		
		//print "<h2>List response</h2><pre>"; var_dump($response); print "</pre>";
		
		return false;
	}

	
	//==========================================================================
	// send json payload via curl to REST api (POST ONLY)
	// auth is user/pass
	//==========================================================================
	
	public function SendRequest( $httpVerb, $url, $request, $MethodName='' ) {
		if ($this->debug) { print __METHOD__.       "( httpVerb=$httpVerb, url=$url, request=request, MethodName=$MethodName )<br>"; }
				
		// httpVerb is required
		if (empty($httpVerb)) {
			throw new Exception(__METHOD__.": Missing httpVerb");
		}
		$this->httpVerb = $httpVerb;
		
		// url is required
		if (empty($url)) {
			throw new Exception(__METHOD__.": Missing url");
		}
		$this->request_url = $url;
		//debug_out("SendRequest: url = $url");
		
		// request is required
		if (empty($request)) {
			throw new Exception(__METHOD__.": Missing request");
		}
		$this->request = $request;
		
		// build curl request
		
		$ch = curl_init();
		
		//URL=$base_url . "xxx/xxx
		curl_setopt($ch, CURLOPT_URL, $url);

		// what is this?
		//HTTPAUTHTOKEN=1fHfjpw86udrDQHRMKabypEmhY4:1fHfkJrOs7iVBnC07HDxCPZuPsK
		if ($this->debug) { print "user={$this->access_user}, pass={$this->access_password}<br>"; }
				
		curl_setopt($ch, CURLOPT_USERPWD, $this->access_user.":".$this->access_password);
		
		curl_setopt($ch, CURLOPT_POST, 1);
		curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $httpVerb);	// same if POST? why diff?
		
		$json_data = json_encode($request);
//print "<h1>json_data=</h1>";
//var_dump($json_data);
		
		curl_setopt($ch, CURLOPT_POSTFIELDS, $json_data);
		
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
		
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
		curl_setopt($ch, CURLOPT_TIMEOUT, 10);
		curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);

		//curl_setopt($ch, CURLOPT_HEADER, 1);	// implied?

		// generate new ksuid for RequestID
		$ksuid = (new Ksuid) -> string();
		
		$RequestID = $ksuid;
		
		curl_setopt($ch, CURLOPT_HTTPHEADER, array(
			'Content-Type: application/json',
			'Content-Length: ' . strlen($json_data),
			'Request-ID: ' . $RequestID
		));
		//curl -sS --user $HTTPAUTHTOKEN -H 'Content-Type: application/json' -H "Request-ID: $(ksuid)" $URL -d '

		$response = curl_exec($ch);
		
		$this->response = $response;
//print "<h1>response=</h1>";
//var_dump($response);
		
//$info = curl_getinfo($ch);
//print "<h1>curl_getinfo=</h1>";
//var_dump($info);
		
		if ($response === false) {
			throw new Exception("Curl Error 1: ".curl_error($ch), curl_errno($ch));
		}
		
		$decoded_response = json_decode( $response );
		$this->response = $decoded_response;
		//if ($this->debug) { print "<h2>SendRequest: decoded response</h2><pre>"; print_r($decoded_response); print "</pre><br>"; }

		if (curl_errno($ch) != 0) {
			throw new Exception("Curl Error 2: ".curl_error($ch), curl_errno($ch));
		}

		$this->httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		curl_close($ch);
		
		// 200	Request received without error
		// 201	Object created
		// 202	Request accepted/executed
		if ( $this->httpCode != 200 && $this->httpCode != 201 && $this->httpCode != 202 ) {
			//throw new Exception("Unexpected response code = $this->httpCode", $this->httpCode);
			if ($this->debug) { print "Unexpected response code = $this->httpCode<br>"; }
		}

		// Error
		if ($this->debug && isset($decoded_response->Error)) {
			debug_log("<b>SendRequest: Error </b> = ".$decoded_response->Error."<br>");
		}
		
		return $decoded_response;
	}

	//==========================================================================
	// LogTransaction
	//==========================================================================
	
	public function LogTransaction( $logdata ) {
return;	// disable function

		if ($this->debug) { print __METHOD__.": logdata = ".var_export($logdata,true)."<br>"; }
				
		// this is all about getting main_db to exist here,
		// inside this static function called by a class method
		require($_SERVER['HOME']."/httpdocs/sysfiles/inc.php");	// get $sysfiles
		require($sysfiles."/connect_to_main.php");	// get main_db

		if (empty($main_db)) die("main_db NOT FOUND in LogTransaction");
/*
CREATE TABLE `paynetworx_transfer_log` (
	ptl_id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT,
	ptl_GroupId VARCHAR(32) DEFAULT NULL,
	ptl_MerchantId VARCHAR(20) NOT NULL,
	ptl_MethodName VARACHAR(40) NOT NULL,
	TransactionID VARACHAR(40) NOT NULL,
	EventID VARACHAR(40) NOT NULL,
	RequestID VARACHAR(40) NOT NULL,
	Error VARACHAR(40) NOT NULL,
	Approved VARACHAR(40) NOT NULL,
	PRIMARY KEY (ptl_id)
);
*/
		$GroupId = $logdata['GroupId'];
		$MerchantId = $logdata['MerchantId'];
		$MethodName = $logdata['MethodName'];
		$TransactionID = $logdata['TransactionID'];	// '2OlWu8Ify3Og1EjAOrQQ3n1p0Em' (length=27)
		$EventID = $logdata['EventID'];				// '2OlWuCcEmUul473kk89rfa2Jd44' (length=27)
		$RequestID = $logdata['RequestID'];			// '000000000000000000000000000' (length=27)
		$Error = $logdata['Error'];					// 'Access Denied' (length=13)
		$Approved = $logdata['Approved'];			// boolean false
		
		$res = mysqli_query($dbc,"
			INSERT INTO `paynetworx_transfer_log` 
			(id, transaction_token, gateway_token, payment_token, transaction_type, method, MerchantId, updated_at)
			VALUES
			('$GroupId', '$MerchantId', '$MethodName', '$TransactionID', '$EventID', '$RequestID', '$Error', '$Approved')
		") /*or die("select paynetworx_transfer_log table ".mysqli_error($dbc))*/;
		
		// update the records for the MerchantId
		if ($MethodName == "AuthCapture") {
			
			// set the MID for all other records with this gateway token
			$res = mysqli_query($dbc,"
				UPDATE `paynetworx_transfer_log` 
				SET `MerchantId` = '$MerchantId',
					`transaction_type` = 'AddGateway'
				WHERE `gateway_token` = '$EventID'
				AND `transaction_type` = 'Other Methods'
			") /*or die("update paynetworx_transfer_log table 1".mysqli_error($dbc))*/;
			
			// set the MID for all other records with this payment token
			$res = mysqli_query($dbc,"
				UPDATE `paynetworx_transfer_log` 
				SET `MerchantId` = '$MerchantId'
				WHERE `payment_token` = '$RequestID'
				AND `transaction_type` = 'RetainPaymentMethod'
			") /*or die("update paynetworx_transfer_log table 2".mysqli_error($dbc))*/;
			
		}
	}
	
	
	public function MoneyFmt($amount) {
		if ($amount === "") return false;
		if (!is_numeric($amount)) return false;	// $amount is not numeric
		return number_format($amount,2);
	}
		
}

?>
