PageRenderTime 64ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/includes/modules/payment/paypal/paypal_functions.php

https://github.com/happyxlq/zencart_svn
PHP | 994 lines | 820 code | 61 blank | 113 comment | 440 complexity | 4e17b6a2f462d6c709cb5ed55bcbedcc MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * functions used by payment module class for Paypal IPN payment method
  4. *
  5. * @package paymentMethod
  6. * @copyright Copyright 2003-2010 Zen Cart Development Team
  7. * @copyright Portions Copyright 2003 osCommerce
  8. * @copyright Portions Copyright 2004 DevosC.com
  9. * @license http://www.zen-cart.com/license/2_0.txt GNU Public License V2.0
  10. * @version $Id: paypal_functions.php 16940 2010-07-21 19:54:44Z drbyte $
  11. */
  12. // Functions for paypal processing
  13. function datetime_to_sql_format($paypalDateTime) {
  14. //Copyright (c) 2004 DevosC.com
  15. $months = array('Jan' => '01', 'Feb' => '02', 'Mar' => '03', 'Apr' => '04', 'May' => '05', 'Jun' => '06', 'Jul' => '07', 'Aug' => '08', 'Sep' => '09', 'Oct' => '10', 'Nov' => '11', 'Dec' => '12');
  16. $hour = substr($paypalDateTime, 0, 2);$minute = substr($paypalDateTime, 3, 2);$second = substr($paypalDateTime, 6, 2);
  17. $month = $months[substr($paypalDateTime, 9, 3)];
  18. $day = (strlen($day = preg_replace("/,/" , '' , substr($paypalDateTime, 13, 2))) < 2) ? '0'.$day: $day;
  19. $year = substr($paypalDateTime, -8, 4);
  20. if (strlen($day)<2) $day = '0'.$day;
  21. return ($year . "-" . $month . "-" . $day . " " . $hour . ":" . $minute . ":" . $second);
  22. }
  23. function ipn_debug_email($message, $email_address = '', $always_send = false, $subjecttext = 'IPN DEBUG message') {
  24. static $paypal_error_counter;
  25. static $paypal_instance_id;
  26. $logfile = '';
  27. if ($email_address == '') $email_address = (defined('MODULE_PAYMENT_PAYPAL_DEBUG_EMAIL_ADDRESS') ? MODULE_PAYMENT_PAYPAL_DEBUG_EMAIL_ADDRESS : STORE_OWNER_EMAIL_ADDRESS);
  28. if(!isset($paypal_error_counter)) $paypal_error_counter = 0;
  29. if(!isset($paypal_instance_id)) $paypal_instance_id = time() . '_' . zen_create_random_value(4);
  30. if ((defined('MODULE_PAYMENT_PAYPALWPP_DEBUGGING') && MODULE_PAYMENT_PAYPALWPP_DEBUGGING == 'Log and Email') || (defined('MODULE_PAYMENT_PAYPAL_IPN_DEBUG') && MODULE_PAYMENT_PAYPAL_IPN_DEBUG == 'Log and Email') || $always_send) {
  31. $paypal_error_counter ++;
  32. zen_mail(STORE_OWNER, $email_address, $subjecttext . ' (' . $paypal_instance_id . ') #' . $paypal_error_counter, $message, STORE_OWNER, STORE_OWNER_EMAIL_ADDRESS, array('EMAIL_MESSAGE_HTML'=>$message), 'debug');
  33. }
  34. if ((defined('MODULE_PAYMENT_PAYPAL_IPN_DEBUG') && (MODULE_PAYMENT_PAYPAL_IPN_DEBUG == 'Log and Email' || MODULE_PAYMENT_PAYPAL_IPN_DEBUG == 'Log File' || MODULE_PAYMENT_PAYPAL_IPN_DEBUG == 'Yes')) || (defined('MODULE_PAYMENT_PAYPALWPP_DEBUGGING') && (MODULE_PAYMENT_PAYPALWPP_DEBUGGING == 'Log File' || MODULE_PAYMENT_PAYPALWPP_DEBUGGING == 'Log and Email'))) $logfile = ipn_add_error_log($message, $paypal_instance_id);
  35. return $logfile;
  36. }
  37. function ipn_get_stored_session($session_stuff) {
  38. global $db;
  39. if (!is_array($session_stuff)) {
  40. ipn_debug_email('IPN FATAL ERROR :: Could not find Zen Cart custom variable in POST, cannot validate or re-create session as a transaction initiated from this store. Might be from another source such as eBay or another PayPal store using this PayPal account.');
  41. return false;
  42. }
  43. $sql = "SELECT *
  44. FROM " . TABLE_PAYPAL_SESSION . "
  45. WHERE session_id = :sessionID";
  46. $sql = $db->bindVars($sql, ':sessionID', $session_stuff[1], 'string');
  47. $stored_session = $db->Execute($sql);
  48. if ($stored_session->recordCount() < 1) {
  49. global $isECtransaction, $isDPtransaction;
  50. if ($_POST['payment_type'] == 'instant' && $isDPtransaction && ((isset($_POST['auth_status']) && $_POST['auth_status'] == 'Completed') || $_POST['payment_status'] == 'Completed')) {
  51. $session_stuff[1] = '(EC/DP transaction)';
  52. }
  53. ipn_debug_email('IPN ERROR :: Could not find stored session {' . $session_stuff[1] . '} in DB; thus cannot validate or re-create session as a transaction awaiting PayPal Website Payments Standard confirmation initiated by this store. Might be an Express Checkout or eBay transaction or some other action that triggers PayPal IPN notifications.');
  54. return false;
  55. }
  56. $_SESSION = unserialize(base64_decode($stored_session->fields['saved_session']));
  57. return true;
  58. }
  59. /**
  60. * look up parent/original transaction record data and return matching order info if found, along with txn_type
  61. */
  62. function ipn_lookup_transaction($postArray) {
  63. global $db;
  64. // find Zen Cart order number from the transactionID in the IPN
  65. $ordersID = 0;
  66. $paypalipnID = 0;
  67. $transType = 'unknown';
  68. $sql = "SELECT order_id, paypal_ipn_id, payment_status, txn_type, pending_reason
  69. FROM " . TABLE_PAYPAL . "
  70. WHERE txn_id = :transactionID
  71. ORDER BY order_id DESC LIMIT 1 ";
  72. $sqlParent = $db->bindVars($sql, ':transactionID', $postArray['parent_txn_id'], 'string');
  73. $sqlTxn = $db->bindVars($sql, ':transactionID', $postArray['txn_id'], 'string');
  74. if (isset($postArray['parent_txn_id']) && trim($postArray['parent_txn_id']) != '') {
  75. $ipn_id = $db->Execute($sqlParent);
  76. if($ipn_id->RecordCount() > 0) {
  77. ipn_debug_email('IPN NOTICE :: This transaction HAS a parent record. Thus this is an update of some sort.');
  78. $transType = ($ipn_id->fields['pending_reason'] == 'paymentreview') ? 'reviewed' : 'parent';
  79. $ordersID = $ipn_id->fields['order_id'];
  80. $paypalipnID = $ipn_id->fields['paypal_ipn_id'];
  81. }
  82. } else {
  83. $ipn_id = $db->Execute($sqlTxn);
  84. if ($ipn_id->RecordCount() <= 0) {
  85. ipn_debug_email('IPN NOTICE :: Could not find matched txn_id record in DB. Therefore is new to us. ');
  86. $transType = 'unique';
  87. } else {
  88. while(!$ipn_id->EOF) {
  89. switch ($ipn_id->fields['pending_reason']) {
  90. case 'address':
  91. ipn_debug_email('IPN NOTICE :: Found pending-address record in database');
  92. if ($postArray['payment_status'] == 'Completed') $transType = 'cleared-address';
  93. if ($postArray['payment_status'] == 'Denied') $transType = 'denied-address';
  94. if ($postArray['payment_status'] == 'Pending') $transType = 'pending-address';
  95. break;
  96. case 'multi_currency':
  97. ipn_debug_email('IPN NOTICE :: Found pending-multicurrency record in database');
  98. if ($postArray['payment_status'] == 'Completed') $transType = 'cleared-multicurrency';
  99. if ($postArray['payment_status'] == 'Denied') $transType = 'denied-multicurrency';
  100. if ($postArray['payment_status'] == 'Pending') $transType = 'pending-multicurrency';
  101. break;
  102. case 'echeck':
  103. ipn_debug_email('IPN NOTICE :: Found pending-echeck record in database');
  104. if ($postArray['payment_status'] == 'Completed') $transType = 'cleared-echeck';
  105. if ($postArray['payment_status'] == 'Completed' && $postArray['txn_type'] == 'web_accept') $transType = 'cleared-echeck';
  106. if ($postArray['payment_status'] == 'Denied') $transType = 'denied-echeck';
  107. if ($postArray['payment_status'] == 'Failed') $transType = 'failed-echeck';
  108. if ($postArray['payment_status'] == 'Pending') $transType = 'pending-echeck';
  109. break;
  110. case 'authorization':
  111. ipn_debug_email('IPN NOTICE :: Found pending-authorization record in database');
  112. $transType = 'cleared-authorization';
  113. if ($postArray['payment_status'] == 'Voided') $transType = 'voided';
  114. if ($postArray['payment_status'] == 'Pending') $transType = 'pending-authorization';
  115. if ($postArray['payment_status'] == 'Captured') $transType = 'captured';
  116. if ($postArray['payment_status'] == 'Completed') $transType = 'cleared-authorization';
  117. if ($postArray['auth_status'] == 'In_Progress') $transType = 'partial-authorization';
  118. break;
  119. case 'verify':
  120. ipn_debug_email('IPN NOTICE :: Found pending-verify record in database');
  121. $transType = 'cleared-verify';
  122. break;
  123. case 'paymentreview':
  124. ipn_debug_email('IPN NOTICE :: Found pending-review record in database');
  125. $transType = 'pending-paymentreview';
  126. if ($postArray['payment_status'] == 'Completed') $transType = 'cleared-review';
  127. break;
  128. case 'intl':
  129. ipn_debug_email('IPN NOTICE :: Found pending-intl record in database');
  130. if ($postArray['payment_status'] == 'Completed') $transType = 'cleared-intl';
  131. if ($postArray['payment_status'] == 'Denied') $transType = 'denied-intl';
  132. if ($postArray['payment_status'] == 'Pending') $transType = 'pending-intl';
  133. break;
  134. case 'unilateral':
  135. ipn_debug_email('IPN NOTICE :: Found record in database.' . "\n" . '*** NOTE: TRANSACTION IS IN *unilateral* STATUS pending creation of a PayPal account for this receiver_email address.' . "\n" . 'Please create the account, or make sure the account is *Verified*.');
  136. $transType = 'pending-unilateral';
  137. break;
  138. }
  139. if ($transType != 'unknown') {
  140. $ordersID = $ipn_id->fields['order_id'];
  141. $paypalipnID = $ipn_id->fields['paypal_ipn_id'];
  142. }
  143. $ipn_id->MoveNext();
  144. }
  145. }
  146. }
  147. return array('order_id' => $ordersID, 'paypal_ipn_id' => $paypalipnID, 'txn_type' => $transType);
  148. }
  149. /**
  150. * IPN Validation
  151. * - match email addresses
  152. * - ensure that "VERIFIED" has been returned (otherwise somebody is trying to spoof)
  153. */
  154. function ipn_validate_transaction($info, $postArray, $mode='IPN') {
  155. if ($mode == 'IPN' && !preg_match("/VERIFIED/i", $info) && !preg_match("/SUCCESS/i", $info)) {
  156. ipn_debug_email('IPN WARNING :: Transaction was NOT marked as VERIFIED. Keep this report for potential use in fraud investigations.' . "\n" . 'IPN Info: ' . "\n" . $info);
  157. return false;
  158. } elseif ($mode == 'PDT' && (!preg_match("/SUCCESS/i", $info) || preg_match("/FAIL/i", $info))) {
  159. ipn_debug_email('IPN WARNING :: PDT Transaction was NOT marked as SUCCESS. Keep this report for potential use in fraud investigations.' . "\n" . 'IPN Info: ' . "\n" . $info);
  160. return false;
  161. }
  162. $ppBusEmail = false;
  163. $ppRecEmail = false;
  164. if (defined('MODULE_PAYMENT_PAYPAL_BUSINESS_ID')) {
  165. if (strtolower(trim($postArray['business'])) == strtolower(trim(MODULE_PAYMENT_PAYPAL_BUSINESS_ID))) $ppBusEmail = true;
  166. if (strtolower(trim($postArray['receiver_email'])) == strtolower(trim(MODULE_PAYMENT_PAYPAL_BUSINESS_ID))) $ppRecEmail = true;
  167. if (!$ppBusEmail && !$ppRecEmail) {
  168. ipn_debug_email('IPN WARNING :: Transaction email address NOT matched.' . "\n" . 'From IPN = ' . $postArray['business'] . ' | ' . $postArray['receiver_email'] . "\n" . 'From CONFIG = ' . MODULE_PAYMENT_PAYPAL_BUSINESS_ID);
  169. return false;
  170. }
  171. ipn_debug_email('IPN INFO :: Transaction email details.' . "\n" . 'From IPN = ' . $postArray['business'] . ' | ' . $postArray['receiver_email'] . "\n" . 'From CONFIG = ' . MODULE_PAYMENT_PAYPAL_BUSINESS_ID);
  172. }
  173. return true;
  174. }
  175. // determine acceptable currencies
  176. function select_pp_currency() {
  177. if (MODULE_PAYMENT_PAYPAL_CURRENCY == 'Selected Currency') {
  178. $my_currency = $_SESSION['currency'];
  179. } else {
  180. $my_currency = substr(MODULE_PAYMENT_PAYPAL_CURRENCY, 5);
  181. }
  182. $pp_currencies = array('CAD', 'EUR', 'GBP', 'JPY', 'USD', 'AUD', 'CHF', 'CZK', 'DKK', 'HKD', 'HUF', 'NOK', 'NZD', 'PLN', 'SEK', 'SGD', 'THB', 'MXN', 'ILS', 'PHP', 'TWD', 'BRL', 'MYR');
  183. if (!in_array($my_currency, $pp_currencies)) {
  184. $my_currency = 'USD';
  185. }
  186. return $my_currency;
  187. }
  188. function valid_payment($amount, $currency, $mode = 'IPN') {
  189. global $currencies;
  190. $my_currency = select_pp_currency();
  191. $exchanged_amount = ($mode == 'IPN' ? ($amount * $currencies->get_value($my_currency)) : $amount);
  192. $transaction_amount = preg_replace('/[^0-9.]/', '', number_format($exchanged_amount, $currencies->get_decimal_places($my_currency), '.', ''));
  193. if ( ($_POST['mc_currency'] != $my_currency) || ($_POST['mc_gross'] != $transaction_amount && $_POST['mc_gross'] != -0.01) && MODULE_PAYMENT_PAYPAL_TESTING != 'Test' ) {
  194. ipn_debug_email('IPN WARNING :: Currency/Amount Mismatch. Details: ' . "\n" . 'PayPal email address = ' . $_POST['business'] . "\n" . ' | mc_currency = ' . $_POST['mc_currency'] . "\n" . ' | submitted_currency = ' . $my_currency . "\n" . ' | order_currency = ' . $currency . "\n" . ' | mc_gross = ' . $_POST['mc_gross'] . "\n" . ' | converted_amount = ' . $transaction_amount . "\n" . ' | order_amount = ' . $amount );
  195. return false;
  196. }
  197. ipn_debug_email('IPN INFO :: Currency/Amount Details: ' . "\n" . 'PayPal email address = ' . $_POST['business'] . "\n" . ' | mc_currency = ' . $_POST['mc_currency'] . "\n" . ' | submitted_currency = ' . $my_currency . "\n" . ' | order_currency = ' . $currency . "\n" . ' | mc_gross = ' . $_POST['mc_gross'] . "\n" . ' | converted_amount = ' . $transaction_amount . "\n" . ' | order_amount = ' . $amount );
  198. return true;
  199. }
  200. /**
  201. * is this an existing transaction?
  202. * (1) we find a matching record in the "paypal" table
  203. * (2) we check for valid txn_types or payment_status such as Denied, Refunded, Partially-Refunded, Reversed, Voided, Expired
  204. */
  205. function ipn_determine_txn_type($postArray, $txn_type = 'unknown') {
  206. global $db, $parentLookup;
  207. if (substr($txn_type,0,8) == 'cleared-') return $txn_type;
  208. if ($postArray['txn_type'] == 'send_money') return $postArray['txn_type'];
  209. if ($postArray['txn_type'] == 'express_checkout' || $postArray['txn_type'] == 'cart') $txn_type = $postArray['txn_type'];
  210. // if it's not unique or linked to a parent, then:
  211. // 1. could be an e-check denied / cleared
  212. // 2. could be an express-checkout "pending" transaction which has been Accepted in the merchant's PayPal console and needs activation in Zen Cart
  213. if ($postArray['payment_status']=='Completed' && txn_type=='express_checkout' && $postArray['payment_type']=='echeck') {
  214. $txn_type = 'express-checkout-cleared';
  215. return $txn_type;
  216. }
  217. if ($postArray['payment_status']=='Completed' && $postArray['payment_type']=='echeck') {
  218. $txn_type = 'echeck-cleared';
  219. return $txn_type;
  220. }
  221. if (($postArray['payment_status']=='Denied' || $postArray['payment_status']=='Failed') && $postArray['payment_type']=='echeck') {
  222. $txn_type = 'echeck-denied';
  223. return $txn_type;
  224. }
  225. if ($postArray['payment_status']=='Denied') {
  226. $txn_type = 'denied';
  227. return $txn_type;
  228. }
  229. if (($postArray['payment_status']=='Pending') && $postArray['pending_reason']=='echeck') {
  230. $txn_type = 'pending-echeck';
  231. return $txn_type;
  232. }
  233. if (($postArray['payment_status']=='Pending') && $postArray['pending_reason']=='address') {
  234. $txn_type = 'pending-address';
  235. return $txn_type;
  236. }
  237. if (($postArray['payment_status']=='Pending') && $postArray['pending_reason']=='intl') {
  238. $txn_type = 'pending-intl';
  239. return $txn_type;
  240. }
  241. if (($postArray['payment_status']=='Pending') && $postArray['pending_reason']=='multi_currency') {
  242. $txn_type = 'pending-multicurrency';
  243. return $txn_type;
  244. }
  245. if (($postArray['payment_status']=='Pending') && $postArray['pending_reason']=='paymentreview') {
  246. $txn_type = 'pending-paymentreview';
  247. return $txn_type;
  248. }
  249. if (($postArray['payment_status']=='Pending') && $postArray['pending_reason']=='verify') {
  250. $txn_type = 'pending-verify';
  251. return $txn_type;
  252. }
  253. if ($parentLookup == 'parent' && $postArray['payment_status']=='Completed' && $postArray['payment_type']=='instant') {
  254. $txn_type = 'cleared-authorization';
  255. return $txn_type;
  256. }
  257. if (($postArray['payment_status']=='Voided') && $postArray['payment_type']=='instant') {
  258. $txn_type = 'voided';
  259. return $txn_type;
  260. }
  261. return $txn_type;
  262. }
  263. /**
  264. * Create order record from IPN data
  265. */
  266. function ipn_create_order_array($new_order_id, $txn_type) {
  267. $sql_data_array = array('order_id' => $new_order_id,
  268. 'txn_type' => $txn_type,
  269. 'module_name' => 'paypal (ipn-handler)',
  270. 'module_mode' => 'IPN',
  271. 'reason_code' => $_POST['reason_code'],
  272. 'payment_type' => $_POST['payment_type'],
  273. 'payment_status' => $_POST['payment_status'],
  274. 'pending_reason' => $_POST['pending_reason'],
  275. 'invoice' => $_POST['invoice'],
  276. 'mc_currency' => $_POST['mc_currency'],
  277. 'first_name' => $_POST['first_name'],
  278. 'last_name' => $_POST['last_name'],
  279. 'payer_business_name' => $_POST['payer_business_name'],
  280. 'address_name' => $_POST['address_name'],
  281. 'address_street' => $_POST['address_street'],
  282. 'address_city' => $_POST['address_city'],
  283. 'address_state' => $_POST['address_state'],
  284. 'address_zip' => $_POST['address_zip'],
  285. 'address_country' => $_POST['address_country'],
  286. 'address_status' => $_POST['address_status'],
  287. 'payer_email' => $_POST['payer_email'],
  288. 'payer_id' => $_POST['payer_id'],
  289. 'payer_status' => $_POST['payer_status'],
  290. 'payment_date' => datetime_to_sql_format($_POST['payment_date']),
  291. 'business' => $_POST['business'],
  292. 'receiver_email' => $_POST['receiver_email'],
  293. 'receiver_id' => $_POST['receiver_id'],
  294. 'txn_id' => $_POST['txn_id'],
  295. 'parent_txn_id' => $_POST['parent_txn_id'],
  296. 'num_cart_items' => (int)$_POST['num_cart_items'],
  297. 'mc_gross' => $_POST['mc_gross'],
  298. 'mc_fee' => $_POST['mc_fee'],
  299. 'settle_amount' => (isset($_POST['settle_amount']) && $_POST['settle_amount'] != '' ? $_POST['settle_amount'] : 0),
  300. 'settle_currency' => $_POST['settle_currency'],
  301. 'exchange_rate' => (isset($_POST['exchange_rate']) && $_POST['exchange_rate'] != '' ? $_POST['exchange_rate'] : 1),
  302. 'notify_version' => $_POST['notify_version'],
  303. 'verify_sign' => $_POST['verify_sign'],
  304. 'date_added' => 'now()',
  305. 'memo' => '{Record generated by IPN}'
  306. );
  307. if (isset($_POST['protection_eligibility']) && $_POST['protection_eligibility'] != '') $sql_data_array['memo'] .= ' [ProtectionEligibility:' . $_POST['protection_eligibility'] .']';
  308. if (isset($_POST['memo']) && $_POST['memo'] != '') $sql_data_array['memo'] .= ' [Customer Comments:' . $_POST['memo'] .']';
  309. return $sql_data_array;
  310. }
  311. /**
  312. * Create order-history record from IPN data
  313. */
  314. function ipn_create_order_history_array($insert_id) {
  315. $sql_data_array = array ('paypal_ipn_id' => $insert_id,
  316. 'txn_id' => $_POST['txn_id'],
  317. 'parent_txn_id' => $_POST['parent_txn_id'],
  318. 'payment_status' => $_POST['payment_status'],
  319. 'pending_reason' => $_POST['pending_reason'],
  320. 'date_added' => 'now()'
  321. );
  322. return $sql_data_array;
  323. }
  324. /**
  325. * Create order-update from IPN data
  326. */
  327. function ipn_create_order_update_array($txn_type) {
  328. $sql_data_array = array('payment_type' => $_POST['payment_type'],
  329. 'txn_type' => $txn_type,
  330. 'parent_txn_id' => $_POST['parent_txn_id'],
  331. 'payment_status' => $_POST['payment_status'],
  332. 'pending_reason' => $_POST['pending_reason'],
  333. 'payer_email' => $_POST['payer_email'],
  334. 'payer_id' => $_POST['payer_id'],
  335. 'business' => $_POST['business'],
  336. 'receiver_email' => $_POST['receiver_email'],
  337. 'receiver_id' => $_POST['receiver_id'],
  338. 'notify_version' => $_POST['notify_version'],
  339. 'verify_sign' => $_POST['verify_sign'],
  340. 'last_modified' => 'now()'
  341. );
  342. if (isset($_POST['address_street']) && $_POST['address_street'] != '')
  343. $sql_data_array = array_merge($sql_data_array,
  344. array('address_name' => $_POST['address_name'],
  345. 'address_street' => $_POST['address_street'],
  346. 'address_city' => $_POST['address_city'],
  347. 'address_state' => $_POST['address_state'],
  348. 'address_zip' => $_POST['address_zip'],
  349. 'address_country' => $_POST['address_country']));
  350. if (isset($_POST['payer_business_name']) && $_POST['payer_business_name'] != '') $sql_data_array['payer_business_name'] = $_POST['payer_business_name'];
  351. if (isset($_POST['reason_code']) && $_POST['reason_code'] != '') $sql_data_array['reason_code'] = $_POST['reason_code'];
  352. if (isset($_POST['invoice']) && $_POST['invoice'] != '') $sql_data_array['invoice'] = $_POST['invoice'];
  353. if (isset($_POST['mc_gross']) && $_POST['mc_gross'] > 0) $sql_data_array['mc_gross'] = $_POST['mc_gross'];
  354. if (isset($_POST['mc_fee']) && $_POST['mc_fee'] > 0) $sql_data_array['mc_fee'] = $_POST['mc_fee'];
  355. if (isset($_POST['settle_amount']) && $_POST['settle_amount'] > 0) $sql_data_array['settle_amount'] = $_POST['settle_amount'];
  356. if (isset($_POST['first_name']) && $_POST['first_name'] != '') $sql_data_array['first_name'] = $_POST['first_name'];
  357. if (isset($_POST['last_name']) && $_POST['last_name'] != '') $sql_data_array['last_name'] = $_POST['last_name'];
  358. if (isset($_POST['mc_currency']) && $_POST['mc_currency'] != '') $sql_data_array['mc_currency'] = $_POST['mc_currency'];
  359. if (isset($_POST['settle_currency']) && $_POST['settle_currency'] != '') $sql_data_array['settle_currency'] = $_POST['settle_currency'];
  360. if (isset($_POST['num_cart_items']) && $_POST['num_cart_items'] > 0) $sql_data_array['num_cart_items'] = $_POST['num_cart_items'];
  361. if (isset($_POST['exchange_rate']) && $_POST['exchange_rate'] > 0) $sql_data_array['exchange_rate'] = $_POST['exchange_rate'];
  362. $sql_data_array['memo'] = '{Record generated by IPN}';
  363. if (isset($_POST['protection_eligibility']) && $_POST['protection_eligibility'] != '') $sql_data_array['memo'] .= ' [ProtectionEligibility:' . $_POST['protection_eligibility'] .']';
  364. if (isset($_POST['memo']) && $_POST['memo'] != '') $sql_data_array['memo'] .= ' [Customer Comments:' . $_POST['memo'] .']';
  365. return $sql_data_array;
  366. }
  367. /**
  368. * Debug to file
  369. */
  370. function ipn_fopen($filename) {
  371. $response = '';
  372. $fp = @fopen($filename,'rb');
  373. if ($fp) {
  374. $response = getRequestBodyContents($fp);
  375. fclose($fp);
  376. }
  377. return $response;
  378. }
  379. function getRequestBodyContents(&$handle) {
  380. if ($handle) {
  381. while(!feof($handle)) {
  382. $line .= @fgets($handle, 1024);
  383. }
  384. return $line;
  385. }
  386. return false;
  387. }
  388. /**
  389. * Verify IPN by sending it back to PayPal for confirmation
  390. */
  391. function ipn_postback($mode = 'IPN', $pdtTX = '') {
  392. $postdata = '';
  393. $postback = '';
  394. $postback_array = array();
  395. //build postback string
  396. if ($mode == 'PDT') {
  397. if ($pdtTX == '') return FALSE; // TX value not supplied, therefore PDT is disabled on merchant's PayPal profile.
  398. ipn_debug_email('PDT PROCESSING INITIATED.' . "\n" . 'Preparing to verify transaction via PDT.' . "\n\n" . 'The TX token for verification is: ' . print_r($_GET, TRUE));
  399. $postback .= "cmd=_notify-synch";
  400. $postback .= "&tx=" . $_GET['tx'];
  401. $postback .= "&at=" . trim(MODULE_PAYMENT_PAYPAL_PDTTOKEN);
  402. $postback .= "&";
  403. $postback_array['cmd'] = "_notify-sync";
  404. $postback_array['tx'] = $_GET['tx'];
  405. $postback_array['at'] = substr(MODULE_PAYMENT_PAYPAL_PDTTOKEN, 0, 5) . '**********' . substr(MODULE_PAYMENT_PAYPAL_PDTTOKEN,-5);
  406. } elseif ($mode == 'IPN') {
  407. $postback .= "cmd=_notify-validate";
  408. $postback .= "&";
  409. $postback_array['cmd'] = "_notify-validate";
  410. }
  411. foreach($_POST as $key=>$value) {
  412. $postdata .= $key . "=" . urlencode(stripslashes($value)) . "&";
  413. $postback .= $key . "=" . urlencode(stripslashes($value)) . "&";
  414. $postback_array[$key] = $value;
  415. }
  416. if (substr($postdata, -2) == '=&') {
  417. ipn_debug_email('IPN NOTICE :: No POST data to process -- Bad IPN data');
  418. return $postdata;
  419. }
  420. $postback = rtrim($postback, '&');
  421. $postdata = rtrim($postdata, '&');
  422. $postdata_array = $_POST;
  423. ksort($postdata_array);
  424. if ($mode == 'IPN') {
  425. ipn_debug_email('IPN INFO - POST VARS received (sorted):' . "\n" . stripslashes(urldecode(print_r($postdata_array, true))));
  426. if (sizeof($postdata_array) == 0) die('Nothing to process. Please return to home page.');
  427. }
  428. // send received data back to PayPal for validation
  429. $scheme = 'http://';
  430. //Parse url
  431. $web = parse_url($scheme . (defined('MODULE_PAYMENT_PAYPAL_HANDLER') ? MODULE_PAYMENT_PAYPAL_HANDLER : 'www.paypal.com/cgi-bin/webscr'));
  432. if (isset($_POST['test_ipn']) && $_POST['test_ipn'] == 1) {
  433. $web = parse_url($scheme . 'www.sandbox.paypal.com/cgi-bin/webscr');
  434. }
  435. //Set the port number
  436. if($web['scheme'] == "https") {
  437. $web['port']="443"; $ssl = "ssl://";
  438. } else {
  439. $web['port']="80"; $ssl = "";
  440. }
  441. $result = '';
  442. if (function_exists('curl_init')) {
  443. $result = doPayPalIPNCurlPostback($web, $postback, $postback_array, $mode);
  444. }
  445. if ($mode == 'PDT') {
  446. $info = $result['info'];
  447. $result = $result['status'];
  448. }
  449. //DEBUG ONLY: ipn_debug_email('After CURL: $result='.$result);
  450. if (!in_array(trim($result), array('VERIFIED', 'SUCCESS', 'INVALID', 'FAIL'))) {
  451. ipn_debug_email('IPN NOTICE: Could not get usable response via CURL. Trying fsockopen() as fallback.' . ($result != '' ? ' ['.$result.']' : ''));
  452. $result = doPayPalIPNFsockopenPostback($web, $postback, $postback_array, $ssl, $mode);
  453. if ($mode == 'PDT') {
  454. $info = $result['info'];
  455. $result = $result['status'];
  456. }
  457. }
  458. return ($mode == 'PDT') ? array('status' => $result, 'info' => $info) : trim($result);
  459. }
  460. function doPayPalIPNFsockopenPostback($web, $postback, $postback_array, $ssl, $mode = 'IPN') {
  461. $header = "POST " . $web['path'] . " HTTP/1.1\r\n";
  462. $header .= "Host: " . $web['host'] . "\r\n";
  463. $header .= "Content-type: application/x-www-form-urlencoded\r\n";
  464. $header .= "Content-length: " . strlen($postback) . "\r\n";
  465. $header .= "Connection: close\r\n\r\n";
  466. $errnum = 0;
  467. $errstr = '';
  468. ipn_debug_email('IPN INFO - POST VARS to be sent back (unsorted) for validation (using fsockopen): ' . "\n" . 'To: ' . $ssl . $web['host'] . ':' . $web['port'] . "\n" . $header . stripslashes(print_r($postback_array, true)));
  469. //Create paypal connection
  470. if (MODULE_PAYMENT_PAYPAL_IPN_DEBUG == 'Yes') {
  471. $fp=fsockopen($ssl . $web['host'], $web['port'], $errnum, $errstr, 30);
  472. } else {
  473. $fp=@fsockopen($ssl . $web['host'], $web['port'], $errnum, $errstr, 30);
  474. }
  475. if(!$fp && $ssl == 'ssl://') {
  476. ipn_debug_email('IPN ERROR :: Could not establish fsockopen: ' . "\n" . 'Host Details = ' . $ssl . $web['host'] . ':' . $web['port'] . ' (' . $errnum . ') ' . $errstr . "\n Trying again with HTTPS over 443 ...");
  477. $ssl = 'https://';
  478. $web['port'] = '443';
  479. $fp=@fsockopen($ssl . $web['host'], $web['port'], $errnum, $errstr, 30);
  480. }
  481. if(!$fp && $ssl == 'https://') {
  482. ipn_debug_email('IPN ERROR :: Could not establish fsockopen: ' . "\n" . 'Host Details = ' . $ssl . $web['host'] . ':' . $web['port'] . ' (' . $errnum . ') ' . $errstr . "\n Trying again directly over 443 ...");
  483. $ssl = '';
  484. $web['port'] = '443';
  485. $fp=@fsockopen($ssl . $web['host'], $web['port'], $errnum, $errstr, 30);
  486. }
  487. if(!$fp) {
  488. ipn_debug_email('IPN ERROR :: Could not establish fsockopen: ' . "\n" . 'Host Details = ' . $ssl . $web['host'] . ':' . $web['port'] . ' (' . $errnum . ') ' . $errstr . "\n Trying again with HTTP over port 80 ...");
  489. $ssl = 'http://';
  490. $web['port'] = '80';
  491. $fp=@fsockopen($ssl . $web['host'], $web['port'], $errnum, $errstr, 30);
  492. }
  493. if(!$fp) {
  494. ipn_debug_email('IPN ERROR :: Could not establish fsockopen: ' . "\n" . 'Host Details = ' . $ssl . $web['host'] . ':' . $web['port'] . ' (' . $errnum . ') ' . $errstr . "\n Trying again without any specified protocol, using port 80 ...");
  495. $ssl = '';
  496. $web['port'] = '80';
  497. $fp=@fsockopen($ssl . $web['host'], $web['port'], $errnum, $errstr, 30);
  498. }
  499. if(!$fp) {
  500. ipn_debug_email('IPN FATAL ERROR :: Could not establish fsockopen. ' . "\n" . 'Host Details = ' . $ssl . $web['host'] . ':' . $web['port'] . ' (' . $errnum . ') ' . $errstr . "\nABORTED.");
  501. die();
  502. }
  503. $info = array();
  504. fputs($fp, $header . $postback . "\r\n\r\n");
  505. $header_data = '';
  506. $headerdone = false;
  507. //loop through the response from the server
  508. while(!feof($fp)) {
  509. $line = @fgets($fp, 1024);
  510. if (strcmp($line, "\r\n") == 0) {
  511. // this is a header row
  512. $headerdone = true;
  513. $header_data .= $line;
  514. } else if ($headerdone) {
  515. // header has been read. now read the contents
  516. $info[] = $line;
  517. }
  518. }
  519. //close $fp - we are done with it
  520. fclose($fp);
  521. //break up results into a string
  522. $info = implode("", $info);
  523. $firstline = trim(substr($info, 0, 20));
  524. $status = '';
  525. if ($status == '' && substr($firstline, 0, 8) == 'VERIFIED') $status = 'VERIFIED';
  526. if ($status == '' && substr($firstline, 0, 7) == 'SUCCESS') $status = 'SUCCESS';
  527. if ($status == '' && substr($firstline, 0, 4) == 'FAIL') $status = 'FAIL';
  528. if ($status == '' && substr($firstline, 0, 7) == 'INVALID') $status = 'INVALID';
  529. if ($status == '' && substr($firstline, 0, 12) == 'UNDETERMINED') $status = 'UNDETERMINED';
  530. ipn_debug_email('IPN INFO (fs) - Confirmation/Validation response ' . ($status != '' ? $status : $header_data . $info));
  531. return ($mode == 'PDT') ? array('status' => $status, 'info' => $info) : $status;
  532. }
  533. function doPayPalIPNCurlPostback($url, $vars, $varsArray, $mode = 'IPN') {
  534. ipn_debug_email('IPN INFO - POST VARS to be sent back (unsorted) for validation (using CURL): ' . "\n" . 'To: ' . $url['host'] . ':' . $url['port'] . "\n" . stripslashes(print_r($varsArray, true)));
  535. $curlOpts = array(CURLOPT_URL => 'https://' . $url['host'] . $url['path'],
  536. CURLOPT_POST => TRUE,
  537. CURLOPT_POSTFIELDS => $vars,
  538. CURLOPT_TIMEOUT => 45,
  539. CURLOPT_CONNECTTIMEOUT => 30,
  540. CURLOPT_VERBOSE => FALSE,
  541. CURLOPT_HEADER => FALSE,
  542. CURLOPT_FOLLOWLOCATION => FALSE,
  543. CURLOPT_RETURNTRANSFER => TRUE,
  544. CURLOPT_SSL_VERIFYPEER => FALSE,
  545. CURLOPT_SSL_VERIFYHOST => 2,
  546. CURLOPT_FORBID_REUSE => TRUE,
  547. CURLOPT_FRESH_CONNECT => TRUE,
  548. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  549. CURLOPT_USERAGENT => 'Zen Cart(tm) - IPN Postback',
  550. );
  551. if (CURL_PROXY_REQUIRED == 'True') {
  552. $proxy_tunnel_flag = (defined('CURL_PROXY_TUNNEL_FLAG') && strtoupper(CURL_PROXY_TUNNEL_FLAG) == 'FALSE') ? false : true;
  553. $curlOpts[CURLOPT_HTTPPROXYTUNNEL] = $proxy_tunnel_flag;
  554. $curlOpts[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP;
  555. $curlOpts[CURLOPT_PROXY] = CURL_PROXY_SERVER_DETAILS;
  556. }
  557. $ch = curl_init();
  558. curl_setopt_array($ch, $curlOpts);
  559. $response = curl_exec($ch);
  560. $commError = curl_error($ch);
  561. $commErrNo = curl_errno($ch);
  562. $commInfo = @curl_getinfo($ch);
  563. curl_close($ch);
  564. $errors = ($commErrNo != 0 ? "CURL communication ERROR: (" . $commErrNo . ') ' . $commError : '');
  565. $response .= ($commErrNo != 0 ? '&CURL_ERRORS=' . urlencode('(' . $commErrNo . ') ' . $commError) : '') ;
  566. // $response .= ($commErrNo != 0 ? '&CURL_INFO=' . urlencode($commInfo) : '');
  567. ipn_debug_email('CURL OPTS: ' . print_r($curlOpts, true));
  568. ipn_debug_email('CURL response: ' . $response);
  569. if ($errors != '') {
  570. ipn_debug_email('CURL errors: ' . $errors, print_r($commInfo, true));
  571. }
  572. //echo 'INFO: <pre>'; print_r($commInfo); echo '</pre><br />';
  573. //echo 'ERROR: ' . $errors . '<br />';
  574. //print_r($response) ;
  575. if (($response == '' || $errors != '') && ($url['scheme'] != 'http')) {
  576. $url['scheme'] = 'http';
  577. $url['port'] = '80';
  578. ipn_debug_email('CURL ERROR: ' . $errors . "\n" . 'Trying direct HTTP on port 80 instead ... ' . $url['scheme'] . '://' . $url['host'] . $url['path'] . "\n");
  579. $ch = curl_init();
  580. $curlOpts[CURLOPT_URL] = $url['scheme'] . '://' . $url['host'] . $url['path'];
  581. $curlOpts[CURLOPT_FOLLOWLOCATION] = TRUE; // allow to follow redirects since PP usually redirects all non-SSL to SSL etc, do a redirect is almost certain to occur
  582. curl_setopt_array($ch, $curlOpts);
  583. curl_setopt($ch, CURLOPT_PORT, $url['port']);
  584. $response = curl_exec($ch);
  585. $commError = curl_error($ch);
  586. $commErrNo = curl_errno($ch);
  587. $commInfo = @curl_getinfo($ch);
  588. curl_close($ch);
  589. ipn_debug_email('CURL OPTS: ' . print_r($curlOpts, true));
  590. ipn_debug_email('CURL response: ' . $response);
  591. $errors = ($commErrNo != 0 ? "\n(" . $commErrNo . ') ' . $commError : '');
  592. if ($errors != '') {
  593. ipn_debug_email (nl2br('CURL ERROR: ' . $errors . "\n" . 'ABORTING CURL METHOD ...' . "\n\n"));
  594. }
  595. }
  596. $firstline = trim(substr($response, 0, 20));
  597. $status = '';
  598. if ($status == '' && substr($firstline, 0, 8) == 'VERIFIED') $status = 'VERIFIED';
  599. if ($status == '' && substr($firstline, 0, 7) == 'SUCCESS') $status = 'SUCCESS';
  600. if ($status == '' && substr($firstline, 0, 4) == 'FAIL') $status = 'FAIL';
  601. if ($status == '' && substr($firstline, 0, 7) == 'INVALID') $status = 'INVALID';
  602. if ($status == '' && substr($firstline, 0, 12) == 'UNDETERMINED') $status = 'UNDETERMINED';
  603. ipn_debug_email('IPN INFO (cl) - Confirmation/Validation response ' . ($status != '' ? $status : $response));
  604. if ($response != '') {
  605. return ($mode == 'PDT') ? array('status' => $status, 'info' => $response) : $response;
  606. } else {
  607. return $errors;
  608. }
  609. }
  610. /**
  611. * Write order-history update to ZC tables denoting the update supplied by the IPN
  612. */
  613. function ipn_update_orders_status_and_history($ordersID, $new_status = 1, $txn_type) {
  614. global $db;
  615. ipn_debug_email('IPN NOTICE :: Updating order #' . (int)$ordersID . ' to status: ' . (int)$new_status . ' (txn_type: ' . $txn_type . ')');
  616. $db->Execute("update " . TABLE_ORDERS . "
  617. set orders_status = '" . (int)$new_status . "'
  618. where orders_id = '" . (int)$ordersID . "'");
  619. $sql_data_array = array('orders_id' => (int)$ordersID,
  620. 'orders_status_id' => (int)$new_status,
  621. 'date_added' => 'now()',
  622. 'comments' => 'PayPal status: ' . $_POST['payment_status'] . ' ' . ' @ ' . $_POST['payment_date'] . (($_POST['parent_txn_id'] !='') ? "\n" . ' Parent Trans ID:' . $_POST['parent_txn_id'] : '') . "\n" . ' Trans ID:' . $_POST['txn_id'] . "\n" . ' Amount: ' . $_POST['mc_gross'] . ' ' . $_POST['mc_currency'],
  623. 'customer_notified' => false
  624. );
  625. zen_db_perform(TABLE_ORDERS_STATUS_HISTORY, $sql_data_array);
  626. ipn_debug_email('IPN NOTICE :: Update complete.');
  627. /**
  628. * Activate any downloads associated with an order which has now been cleared
  629. */
  630. if ($txn_type=='echeck-cleared' || $txn_type == 'express-checkout-cleared' || substr($txn_type,0,8) == 'cleared-') {
  631. $check_status = $db->Execute("select date_purchased from " . TABLE_ORDERS . " where orders_id = '" . (int)$ordersID . "'");
  632. $zc_max_days = zen_date_diff($check_status->fields['date_purchased'], date('Y-m-d H:i:s', time())) + (int)DOWNLOAD_MAX_DAYS;
  633. ipn_debug_email('IPN NOTICE :: Updating order #' . (int)$ordersID . ' downloads (if any). New max days: ' . (int)$zc_max_days . ', New count: ' . (int)DOWNLOAD_MAX_COUNT);
  634. $update_downloads_query = "update " . TABLE_ORDERS_PRODUCTS_DOWNLOAD . " set download_maxdays='" . (int)$zc_max_days . "', download_count='" . (int)DOWNLOAD_MAX_COUNT . "' where orders_id='" . (int)$ordersID . "'";
  635. $db->Execute($update_downloads_query);
  636. }
  637. }
  638. /**
  639. * Prepare subtotal and line-item detail content to send to PayPal
  640. */
  641. function ipn_getLineItemDetails() {
  642. global $order, $currencies, $order_totals, $order_total_modules;
  643. // if not default currency, do not send subtotals or line-item details
  644. if (DEFAULT_CURRENCY != $order->info['currency']) {
  645. ipn_logging('getLineItemDetails 1', 'Not using default currency. Thus, no line-item details can be submitted.');
  646. return array();
  647. }
  648. if ($currencies->currencies[$_SESSION['currency']]['value'] != 1 || $currencies->currencies[$order->info['currency']]['value'] != 1) {
  649. ipn_logging('getLineItemDetails 2', 'currency val not equal to 1.0000 - cannot proceed without coping with currency conversions. Aborting line-item details.');
  650. return array();
  651. }
  652. $optionsST = array();
  653. $optionsLI = array();
  654. $optionsNB = array();
  655. $numberOfLineItemsProcessed = 0;
  656. $creditsApplied = 0;
  657. $surcharges = 0;
  658. $sumOfLineItems = 0;
  659. $sumOfLineTax = 0;
  660. $optionsST['amount'] = 0;
  661. $optionsST['subtotal'] = 0;
  662. $optionsST['tax_cart'] = 0;
  663. $optionsST['shipping'] = 0;
  664. $flagSubtotalsUnknownYet = true;
  665. $subTotalLI = 0;
  666. $subTotalTax = 0;
  667. $subTotalShipping = 0;
  668. $subtotalPRE = array('no data');
  669. if (sizeof($order_totals)) {
  670. // prepare subtotals
  671. for ($i=0, $n=sizeof($order_totals); $i<$n; $i++) {
  672. if ($order_totals[$i]['code'] == '') continue;
  673. if (in_array($order_totals[$i]['code'], array('ot_total','ot_subtotal','ot_tax','ot_shipping')) || strstr($order_totals[$i]['code'], 'insurance')) {
  674. if ($order_totals[$i]['code'] == 'ot_shipping') $optionsST['shipping'] = round($order_totals[$i]['value'],2);
  675. if ($order_totals[$i]['code'] == 'ot_total') $optionsST['amount'] = round($order_totals[$i]['value'],2);
  676. if ($order_totals[$i]['code'] == 'ot_tax') $optionsST['tax_cart'] = round($order_totals[$i]['value'],2);
  677. if ($order_totals[$i]['code'] == 'ot_subtotal') $optionsST['subtotal'] = round($order_totals[$i]['value'],2);
  678. } else {
  679. // handle other order totals:
  680. global $$order_totals[$i]['code'];
  681. if ((substr($order_totals[$i]['text'], 0, 1) == '-') || (isset($$order_totals[$i]['code']->credit_class) && $$order_totals[$i]['code']->credit_class == true)) {
  682. // handle credits
  683. $creditsApplied += round($order_totals[$i]['value'], 2);
  684. } else {
  685. // treat all other OT's as if they're related to handling fees or other extra charges to be added/included
  686. $surcharges += $order_totals[$i]['value'];
  687. }
  688. }
  689. }
  690. if ($creditsApplied > 0) $optionsST['subtotal'] -= $creditsApplied;
  691. if ($surcharges > 0) $optionsST['subtotal'] += $surcharges;
  692. $optionsNB['creditsExist'] = ($creditsApplied > 0) ? TRUE : FALSE;
  693. // Handle tax-included scenario
  694. if (DISPLAY_PRICE_WITH_TAX == 'true') $optionsST['tax_cart'] = 0;
  695. $subtotalPRE = $optionsST;
  696. // Move shipping tax amount from Tax subtotal into Shipping subtotal for submission to PayPal, since PayPal applies tax to each line-item individually
  697. $module = strpos($_SESSION['shipping']['id'], '_') > 0 ? substr($_SESSION['shipping']['id'], 0, strpos($_SESSION['shipping']['id'], '_')) : $_SESSION['shipping'];
  698. if (isset($GLOBALS[$module]) && zen_not_null($order->info['shipping_method']) && DISPLAY_PRICE_WITH_TAX != 'true') {
  699. if ($GLOBALS[$module]->tax_class > 0) {
  700. $shipping_tax_basis = (!isset($GLOBALS[$module]->tax_basis)) ? STORE_SHIPPING_TAX_BASIS : $GLOBALS[$module]->tax_basis;
  701. $shippingOnBilling = zen_get_tax_rate($GLOBALS[$module]->tax_class, $order->billing['country']['id'], $order->billing['zone_id']);
  702. $shippingOnDelivery = zen_get_tax_rate($GLOBALS[$module]->tax_class, $order->delivery['country']['id'], $order->delivery['zone_id']);
  703. if ($shipping_tax_basis == 'Billing') {
  704. $shipping_tax = $shippingOnBilling;
  705. } elseif ($shipping_tax_basis == 'Shipping') {
  706. $shipping_tax = $shippingOnDelivery;
  707. } else {
  708. if (STORE_ZONE == $order->billing['zone_id']) {
  709. $shipping_tax = $shippingOnBilling;
  710. } elseif (STORE_ZONE == $order->delivery['zone_id']) {
  711. $shipping_tax = $shippingOnDelivery;
  712. } else {
  713. $shipping_tax = 0;
  714. }
  715. }
  716. $taxAdjustmentForShipping = zen_calculate_tax($order->info['shipping_cost'], $shipping_tax);
  717. $optionsST['shipping'] += $taxAdjustmentForShipping;
  718. $optionsST['tax_cart'] -= $taxAdjustmentForShipping;
  719. }
  720. }
  721. $flagSubtotalsUnknownYet = (($optionsST['shipping'] + $optionsST['amount'] + $optionsST['tax_cart'] + $optionsST['subtotal']) == 0);
  722. } else {
  723. // if we get here, we don't have any order-total information yet because the customer has clicked Express before starting normal checkout flow
  724. // thus, we must make a note to manually calculate subtotals, rather than relying on the more robust order-total infrastructure
  725. $flagSubtotalsUnknownYet = TRUE;
  726. }
  727. // loop thru all products to prepare details of quantity and price.
  728. for ($i=0, $n=sizeof($order->products), $k=1; $i<$n; $i++, $k++) {
  729. // PayPal won't accept zero-value line-items, so skip this entry if price is zero
  730. if ($order->products[$i]['final_price'] == 0) continue;
  731. $optionsLI["item_number_$k"] = $order->products[$i]['model'];
  732. $optionsLI["item_name_$k"] = $order->products[$i]['name'];
  733. // Append *** if out-of-stock.
  734. $optionsLI["item_name_$k"] .= ((zen_get_products_stock($order->products[$i]['id']) - $order->products[$i]['qty']) < 0 ? STOCK_MARK_PRODUCT_OUT_OF_STOCK : '');
  735. // if there are attributes, loop thru them and add to description
  736. if (isset($order->products[$i]['attributes']) && sizeof($order->products[$i]['attributes']) > 0 ) {
  737. $optionsLI["item_name_$k"] = '';
  738. for ($j=0, $n2=sizeof($order->products[$i]['attributes']); $j<$n2; $j++) {
  739. $optionsLI["item_name_$k"] .= "\n " . $order->products[$i]['attributes'][$j]['option'] .
  740. ': ' . $order->products[$i]['attributes'][$j]['value'];
  741. } // end loop
  742. } // endif attribute-info
  743. // PayPal can't handle partial-quantity values, so fudge it here
  744. if ($order->products[$i]['qty'] != (int)$order->products[$i]['qty']) {
  745. $optionsLI["item_name_$k"] = '('.$order->products[$i]['qty'].' x ) ' . $optionsLI["item_name_$k"];
  746. $optionsLI["amount_$k"] = ($order->products[$i]['qty'] * $order->products[$i]['final_price']);
  747. $optionsLI["tax_$k"] = zen_calculate_tax($order->products[$i]['qty'] * $order->products[$i]['final_price'], $order->products[$i]['tax']);
  748. $optionsLI["quantity_$k"] = 1;
  749. } else {
  750. $optionsLI["amount_$k"] = $order->products[$i]['final_price'];
  751. $optionsLI["quantity_$k"] = $order->products[$i]['qty'];
  752. $optionsLI["tax_$k"] = zen_calculate_tax(1 * $order->products[$i]['final_price'], $order->products[$i]['tax']);
  753. }
  754. // For tax-included pricing, combine tax with price instead of treating separately:
  755. if (DISPLAY_PRICE_WITH_TAX == 'true') {
  756. $optionsLI["amount_$k"] += $optionsLI["tax_$k"];
  757. $optionsLI["tax_$k"] = 0;
  758. }
  759. $subTotalLI += ($optionsLI["quantity_$k"] * $optionsLI["amount_$k"]);
  760. $subTotalTax += ($optionsLI["quantity_$k"] * $optionsLI["tax_$k"]);
  761. // add line-item for one-time charges on this product
  762. if ($order->products[$i]['onetime_charges'] != 0 ) {
  763. $k++;
  764. $optionsLI["item_name_$k"] = MODULES_PAYMENT_PAYPALSTD_LINEITEM_TEXT_ONETIME_CHARGES_PREFIX . substr(htmlentities($order->products[$i]['name'], ENT_QUOTES, 'UTF-8'), 0, 120);
  765. $optionsLI["amount_$k"] = $order->products[$i]['onetime_charges'];
  766. $optionsLI["quantity_$k"] = 1;
  767. $optionsLI["tax_$k"] = zen_calculate_tax($order->products[$i]['onetime_charges'], $order->products[$i]['tax']);
  768. $subTotalLI += $order->products[$i]['onetime_charges'];
  769. $subTotalTax += $optionsLI["tax_$k"];
  770. }
  771. $numberOfLineItemsProcessed = $k;
  772. } // end for loopthru all products
  773. // add line items for any surcharges added by order-total modules
  774. if ($surcharges > 0) {
  775. $numberOfLineItemsProcessed++;
  776. $k = $numberOfLineItemsProcessed;
  777. $optionsLI["item_name_$k"] = MODULES_PAYMENT_PAYPALSTD_LINEITEM_TEXT_SURCHARGES_SHORT;
  778. $optionsLI["amount_$k"] = $surcharges;
  779. $optionsLI["quantity_$k"] = 1;
  780. $subTotalLI += $surcharges;
  781. }
  782. // add line items for discounts such as gift certificates and coupons
  783. if ($creditsApplied > 0) {
  784. $numberOfLineItemsProcessed++;
  785. $k = $numberOfLineItemsProcessed;
  786. $optionsLI["item_name_$k"] = MODULES_PAYMENT_PAYPALSTD_LINEITEM_TEXT_DISCOUNTS_SHORT;
  787. $optionsLI["amount_$k"] = (-1 * $creditsApplied);
  788. $optionsLI["quantity_$k"] = 1;
  789. $subTotalLI -= $creditsApplied;
  790. }
  791. // Reformat properly
  792. for ($k=1, $n=$numberOfLineItemsProcessed+1; $k<$n; $k++) {
  793. // Replace & and = with * if found.
  794. $optionsLI["item_name_$k"] = str_replace(array('&','='), '*', $optionsLI["item_name_$k"]);
  795. // Remove HTML markup if found
  796. $optionsLI["item_name_$k"] = zen_clean_html($optionsLI["item_name_$k"], 'strong');
  797. // reformat properly according to API specs
  798. $optionsLI["item_name_$k"] = substr($optionsLI["item_name_$k"], 0, 127);
  799. if (isset($optionsLI["item_number_$k"])) $optionsLI["item_number_$k"] = substr($optionsLI["item_number_$k"], 0, 127);
  800. ;
  801. if (isset($optionsLI["tax_$k"]) && ($optionsLI["tax_$k"] != '' || $optionsLI["tax_$k"] > 0)) {
  802. $optionsLI["tax_$k"] = round($optionsLI["tax_$k"], 2);
  803. }
  804. }
  805. /**
  806. * PayPal says their math works like this:
  807. * a) ITEMAMT = L_AMTn * L_QTYn
  808. * b) TAXAMT = L_QTYn * L_TAXAMTn
  809. * c) AMT = ITEMAMT + SHIPPINGAMT + HANDLINGAMT + TAXAMT
  810. */
  811. // Sanity Check of line-item subtotals
  812. $optionsLI['num_cart_items'] = 0;
  813. for ($j=1; $j<$k; $j++) {
  814. $itemAMT = $optionsLI["amount_$j"];
  815. $itemQTY = $optionsLI["quantity_$j"];
  816. $itemTAX = (isset($optionsLI["tax_$j"]) ? $optionsLI["tax_$j"] : 0);
  817. $sumOfLineItems += ($itemQTY * $itemAMT);
  818. $sumOfLineTax += ($itemQTY * $itemTAX);
  819. $optionsLI['num_cart_items']++;
  820. }
  821. $sumOfLineItems = round($sumOfLineItems, 2);
  822. $sumOfLineTax = round($sumOfLineTax, 2);
  823. if ($sumOfLineItems == 0) {
  824. $sumofLineTax = 0;
  825. $optionsLI = array();
  826. $discountProblemsFlag = TRUE;
  827. if ($optionsST['shipping'] == $optionsST['amount']) {
  828. $optionsST['shipping'] = 0;
  829. }
  830. }
  831. // Sanity check -- if tax-included pricing is causing problems, remove the numbers and put them in a comment instead:
  832. $stDiffTaxOnly = (strval($sumOfLineItems - $sumOfLineTax - round($optionsST['amount'], 2)) + 0);
  833. if (DISPLAY_PRICE_WITH_TAX == 'true' && $stDiffTaxOnly == 0) {
  834. //$optionsNB['DESC'] = 'Tax included in prices: ' . $sumOfLineTax . ' (' . $optionsST['tax_cart'] . ') ';
  835. $optionsST['tax_cart'] = 0;
  836. for ($k=1, $n=$numberOfLineItemsProcessed+1; $k<$n; $k++) {
  837. if (isset($optionsLI["tax_$k"])) unset($optionsLI["tax_$k"]);
  838. }
  839. }
  840. // Do sanity check -- if any of the line-item subtotal math doesn't add up properly, skip line-item details,
  841. // so that the order can go through even though PayPal isn't being flexible to handle Zen Cart's diversity
  842. if ((strval($subTotalTax) - strval($sumOfLineTax)) > 0.02) {
  843. ipn_logging('getLineItemDetails 3', 'Tax Subtotal does not match sum of taxes for line-items. Tax details are being removed from line-item submission data.' . "\n" . $sumOfLineTax . ' ' . $subTotalTax . print_r(array_merge($optionsST, $optionsLI), true));
  844. for ($k=1, $n=$numberOfLineItemsProcessed+1; $k<$n; $k++) {
  845. if (isset($optionsLI["tax_$k"])) unset($optionsLI["tax_$k"]);
  846. }
  847. $subTotalTax = 0;
  848. $sumOfLineTax = 0;
  849. }
  850. // If coupons exist and there's a calculation problem, then it's likely that taxes are incorrect, so reset L_TAXAMTn values
  851. if ($creditsApplied > 0 && (strval($options

Large files files are truncated, but you can click here to view the full file