PageRenderTime 45ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/sites/all/modules/contrib/civicrm/CRM/Core/Payment/AuthorizeNetIPN.php

https://gitlab.com/virtualrealms/d7civicrm
PHP | 361 lines | 209 code | 35 blank | 117 comment | 37 complexity | fdbd0763324ee0e5a04da3b2a00cc2a0 MD5 | raw file
  1. <?php
  2. /*
  3. +--------------------------------------------------------------------+
  4. | CiviCRM version 5 |
  5. +--------------------------------------------------------------------+
  6. | Copyright CiviCRM LLC (c) 2004-2019 |
  7. +--------------------------------------------------------------------+
  8. | This file is a part of CiviCRM. |
  9. | |
  10. | CiviCRM is free software; you can copy, modify, and distribute it |
  11. | under the terms of the GNU Affero General Public License |
  12. | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
  13. | |
  14. | CiviCRM is distributed in the hope that it will be useful, but |
  15. | WITHOUT ANY WARRANTY; without even the implied warranty of |
  16. | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
  17. | See the GNU Affero General Public License for more details. |
  18. | |
  19. | You should have received a copy of the GNU Affero General Public |
  20. | License and the CiviCRM Licensing Exception along |
  21. | with this program; if not, contact CiviCRM LLC |
  22. | at info[AT]civicrm[DOT]org. If you have questions about the |
  23. | GNU Affero General Public License or the licensing of CiviCRM, |
  24. | see the CiviCRM license FAQ at http://civicrm.org/licensing |
  25. +--------------------------------------------------------------------+
  26. */
  27. /**
  28. *
  29. * @package CRM
  30. * @copyright CiviCRM LLC (c) 2004-2019
  31. */
  32. class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN {
  33. /**
  34. * Constructor function.
  35. *
  36. * @param array $inputData
  37. * contents of HTTP REQUEST.
  38. *
  39. * @throws CRM_Core_Exception
  40. */
  41. public function __construct($inputData) {
  42. $this->setInputParameters($inputData);
  43. parent::__construct();
  44. }
  45. /**
  46. * @param string $component
  47. *
  48. * @return bool|void
  49. */
  50. public function main($component = 'contribute') {
  51. //we only get invoice num as a key player from payment gateway response.
  52. //for ARB we get x_subscription_id and x_subscription_paynum
  53. $x_subscription_id = $this->retrieve('x_subscription_id', 'String');
  54. $ids = $objects = $input = [];
  55. if ($x_subscription_id) {
  56. // Presence of the id means it is approved.
  57. $input['component'] = $component;
  58. // load post vars in $input
  59. $this->getInput($input, $ids);
  60. // load post ids in $ids
  61. $this->getIDs($ids, $input);
  62. // Attempt to get payment processor ID from URL
  63. if (!empty($this->_inputParameters['processor_id'])) {
  64. $paymentProcessorID = $this->_inputParameters['processor_id'];
  65. }
  66. else {
  67. // This is an unreliable method as there could be more than one instance.
  68. // Recommended approach is to use the civicrm/payment/ipn/xx url where xx is the payment
  69. // processor id & the handleNotification function (which should call the completetransaction api & by-pass this
  70. // entirely). The only thing the IPN class should really do is extract data from the request, validate it
  71. // & call completetransaction or call fail? (which may not exist yet).
  72. Civi::log()->warning('Unreliable method used to get payment_processor_id for AuthNet IPN - this will cause problems if you have more than one instance');
  73. $paymentProcessorTypeID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType',
  74. 'AuthNet', 'id', 'name'
  75. );
  76. $paymentProcessorID = (int) civicrm_api3('PaymentProcessor', 'getvalue', [
  77. 'is_test' => 0,
  78. 'options' => ['limit' => 1],
  79. 'payment_processor_type_id' => $paymentProcessorTypeID,
  80. 'return' => 'id',
  81. ]);
  82. }
  83. if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) {
  84. return FALSE;
  85. }
  86. if (!empty($ids['paymentProcessor']) && $objects['contributionRecur']->payment_processor_id != $ids['paymentProcessor']) {
  87. Civi::log()->warning('Payment Processor does not match the recurring processor id.', ['civi.tag' => 'deprecated']);
  88. }
  89. if ($component == 'contribute' && $ids['contributionRecur']) {
  90. // check if first contribution is completed, else complete first contribution
  91. $first = TRUE;
  92. if ($objects['contribution']->contribution_status_id == 1) {
  93. $first = FALSE;
  94. }
  95. return $this->recur($input, $ids, $objects, $first);
  96. }
  97. }
  98. return TRUE;
  99. }
  100. /**
  101. * @param array $input
  102. * @param array $ids
  103. * @param array $objects
  104. * @param $first
  105. *
  106. * @return bool
  107. */
  108. public function recur(&$input, &$ids, &$objects, $first) {
  109. $this->_isRecurring = TRUE;
  110. $recur = &$objects['contributionRecur'];
  111. $paymentProcessorObject = $objects['contribution']->_relatedObjects['paymentProcessor']['object'];
  112. // do a subscription check
  113. if ($recur->processor_id != $input['subscription_id']) {
  114. CRM_Core_Error::debug_log_message("Unrecognized subscription.");
  115. echo "Failure: Unrecognized subscription<p>";
  116. return FALSE;
  117. }
  118. $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
  119. $transaction = new CRM_Core_Transaction();
  120. $now = date('YmdHis');
  121. // fix dates that already exist
  122. $dates = ['create_date', 'start_date', 'end_date', 'cancel_date', 'modified_date'];
  123. foreach ($dates as $name) {
  124. if ($recur->$name) {
  125. $recur->$name = CRM_Utils_Date::isoToMysql($recur->$name);
  126. }
  127. }
  128. //load new contribution object if required.
  129. if (!$first) {
  130. // create a contribution and then get it processed
  131. $contribution = new CRM_Contribute_BAO_Contribution();
  132. $contribution->contact_id = $ids['contact'];
  133. $contribution->financial_type_id = $objects['contributionType']->id;
  134. $contribution->contribution_page_id = $ids['contributionPage'];
  135. $contribution->contribution_recur_id = $ids['contributionRecur'];
  136. $contribution->receive_date = $input['receive_date'];
  137. $contribution->currency = $objects['contribution']->currency;
  138. $contribution->payment_instrument_id = $objects['contribution']->payment_instrument_id;
  139. $contribution->amount_level = $objects['contribution']->amount_level;
  140. $contribution->address_id = $objects['contribution']->address_id;
  141. $contribution->campaign_id = $objects['contribution']->campaign_id;
  142. $contribution->_relatedObjects = $objects['contribution']->_relatedObjects;
  143. $objects['contribution'] = &$contribution;
  144. }
  145. $objects['contribution']->invoice_id = md5(uniqid(rand(), TRUE));
  146. $objects['contribution']->total_amount = $input['amount'];
  147. $objects['contribution']->trxn_id = $input['trxn_id'];
  148. $isFirstOrLastRecurringPayment = FALSE;
  149. if ($input['response_code'] == 1) {
  150. // Approved
  151. if ($first) {
  152. $recur->start_date = $now;
  153. $recur->trxn_id = $recur->processor_id;
  154. $isFirstOrLastRecurringPayment = CRM_Core_Payment::RECURRING_PAYMENT_START;
  155. }
  156. $statusName = 'In Progress';
  157. if (($recur->installments > 0) &&
  158. ($input['subscription_paynum'] >= $recur->installments)
  159. ) {
  160. // this is the last payment
  161. $statusName = 'Completed';
  162. $recur->end_date = $now;
  163. $isFirstOrLastRecurringPayment = CRM_Core_Payment::RECURRING_PAYMENT_END;
  164. }
  165. $recur->modified_date = $now;
  166. $recur->contribution_status_id = array_search($statusName, $contributionStatus);
  167. $recur->save();
  168. }
  169. else {
  170. // Declined
  171. // failed status
  172. $recur->contribution_status_id = array_search('Failed', $contributionStatus);
  173. $recur->cancel_date = $now;
  174. $recur->save();
  175. $message = ts("Subscription payment failed - %1", [1 => htmlspecialchars($input['response_reason_text'])]);
  176. CRM_Core_Error::debug_log_message($message);
  177. // the recurring contribution has declined a payment or has failed
  178. // so we just fix the recurring contribution and not change any of
  179. // the existing contributions
  180. // CRM-9036
  181. return TRUE;
  182. }
  183. // check if contribution is already completed, if so we ignore this ipn
  184. if ($objects['contribution']->contribution_status_id == 1) {
  185. $transaction->commit();
  186. CRM_Core_Error::debug_log_message("Returning since contribution has already been handled.");
  187. echo "Success: Contribution has already been handled<p>";
  188. return TRUE;
  189. }
  190. $this->completeTransaction($input, $ids, $objects, $transaction, $recur);
  191. // Only Authorize.net does this so it is on the a.net class. If there is a need for other processors
  192. // to do this we should make it available via the api, e.g as a parameter, changing the nuance
  193. // from isSentReceipt to an array of which receipts to send.
  194. // Note that there is site-by-site opinions on which notifications are good to send.
  195. if ($isFirstOrLastRecurringPayment) {
  196. CRM_Contribute_BAO_ContributionRecur::sendRecurringStartOrEndNotification($ids, $recur,
  197. $isFirstOrLastRecurringPayment);
  198. }
  199. }
  200. /**
  201. * Get the input from passed in fields.
  202. *
  203. * @param array $input
  204. * @param array $ids
  205. *
  206. * @return bool
  207. * @throws \CRM_Core_Exception
  208. */
  209. public function getInput(&$input, &$ids) {
  210. $input['amount'] = $this->retrieve('x_amount', 'String');
  211. $input['subscription_id'] = $this->retrieve('x_subscription_id', 'Integer');
  212. $input['response_code'] = $this->retrieve('x_response_code', 'Integer');
  213. $input['MD5_Hash'] = $this->retrieve('x_MD5_Hash', 'String', FALSE, '');
  214. $input['response_reason_code'] = $this->retrieve('x_response_reason_code', 'String', FALSE);
  215. $input['response_reason_text'] = $this->retrieve('x_response_reason_text', 'String', FALSE);
  216. $input['subscription_paynum'] = $this->retrieve('x_subscription_paynum', 'Integer', FALSE, 0);
  217. $input['trxn_id'] = $this->retrieve('x_trans_id', 'String', FALSE);
  218. $input['trxn_id'] = $this->retrieve('x_trans_id', 'String', FALSE);
  219. $input['receive_date'] = $this->retrieve('receive_date', 'String', FALSE, date('YmdHis', strtotime('now')));
  220. if ($input['trxn_id']) {
  221. $input['is_test'] = 0;
  222. }
  223. // Only assume trxn_id 'should' have been returned for success.
  224. // Per CRM-17611 it would also not be passed back for a decline.
  225. elseif ($input['response_code'] == 1) {
  226. $input['is_test'] = 1;
  227. $input['trxn_id'] = md5(uniqid(rand(), TRUE));
  228. }
  229. if (!$this->getBillingID($ids)) {
  230. return FALSE;
  231. }
  232. $billingID = $ids['billing'];
  233. $params = [
  234. 'first_name' => 'x_first_name',
  235. 'last_name' => 'x_last_name',
  236. "street_address-{$billingID}" => 'x_address',
  237. "city-{$billingID}" => 'x_city',
  238. "state-{$billingID}" => 'x_state',
  239. "postal_code-{$billingID}" => 'x_zip',
  240. "country-{$billingID}" => 'x_country',
  241. "email-{$billingID}" => 'x_email',
  242. ];
  243. foreach ($params as $civiName => $resName) {
  244. $input[$civiName] = $this->retrieve($resName, 'String', FALSE);
  245. }
  246. }
  247. /**
  248. * Get ids from input.
  249. *
  250. * @param array $ids
  251. * @param array $input
  252. *
  253. * @throws \CRM_Core_Exception
  254. */
  255. public function getIDs(&$ids, &$input) {
  256. $ids['contact'] = $this->retrieve('x_cust_id', 'Integer', FALSE, 0);
  257. $ids['contribution'] = $this->retrieve('x_invoice_num', 'Integer');
  258. // joining with contribution table for extra checks
  259. $sql = "
  260. SELECT cr.id, cr.contact_id
  261. FROM civicrm_contribution_recur cr
  262. INNER JOIN civicrm_contribution co ON co.contribution_recur_id = cr.id
  263. WHERE cr.processor_id = '{$input['subscription_id']}' AND
  264. (cr.contact_id = {$ids['contact']} OR co.id = {$ids['contribution']})
  265. LIMIT 1";
  266. $contRecur = CRM_Core_DAO::executeQuery($sql);
  267. $contRecur->fetch();
  268. $ids['contributionRecur'] = $contRecur->id;
  269. if ($ids['contact'] != $contRecur->contact_id) {
  270. $message = ts("Recurring contribution appears to have been re-assigned from id %1 to %2, continuing with %2.", [1 => $ids['contact'], 2 => $contRecur->contact_id]);
  271. CRM_Core_Error::debug_log_message($message);
  272. $ids['contact'] = $contRecur->contact_id;
  273. }
  274. if (!$ids['contributionRecur']) {
  275. $message = ts("Could not find contributionRecur id");
  276. $log = new CRM_Utils_SystemLogger();
  277. $log->error('payment_notification', ['message' => $message, 'ids' => $ids, 'input' => $input]);
  278. throw new CRM_Core_Exception($message);
  279. }
  280. // get page id based on contribution id
  281. $ids['contributionPage'] = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution',
  282. $ids['contribution'],
  283. 'contribution_page_id'
  284. );
  285. if ($input['component'] == 'event') {
  286. // FIXME: figure out fields for event
  287. }
  288. else {
  289. // Get membershipId. Join with membership payment table for additional checks
  290. $sql = "
  291. SELECT m.id
  292. FROM civicrm_membership m
  293. INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contribution_id = {$ids['contribution']}
  294. WHERE m.contribution_recur_id = {$ids['contributionRecur']}
  295. LIMIT 1";
  296. if ($membershipId = CRM_Core_DAO::singleValueQuery($sql)) {
  297. $ids['membership'] = $membershipId;
  298. }
  299. // FIXME: todo related_contact and onBehalfDupeAlert. Check paypalIPN.
  300. }
  301. }
  302. /**
  303. * @param string $name
  304. * Parameter name.
  305. * @param string $type
  306. * Parameter type.
  307. * @param bool $abort
  308. * Abort if not present.
  309. * @param null $default
  310. * Default value.
  311. *
  312. * @throws CRM_Core_Exception
  313. * @return mixed
  314. */
  315. public function retrieve($name, $type, $abort = TRUE, $default = NULL) {
  316. $value = CRM_Utils_Type::validate(
  317. empty($this->_inputParameters[$name]) ? $default : $this->_inputParameters[$name],
  318. $type,
  319. FALSE
  320. );
  321. if ($abort && $value === NULL) {
  322. throw new CRM_Core_Exception("Could not find an entry for $name");
  323. }
  324. return $value;
  325. }
  326. }