Commit | Line | Data |
---|---|---|
6a488035 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
bc77d7c0 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
6a488035 | 5 | | | |
bc77d7c0 TO |
6 | | This work is published under the GNU AGPLv3 license with some | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
6a488035 | 9 | +--------------------------------------------------------------------+ |
d25dd0ee | 10 | */ |
6a488035 TO |
11 | |
12 | /** | |
13 | * | |
14 | * @package CRM | |
ca5cec67 | 15 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
6a488035 TO |
16 | */ |
17 | class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN { | |
0dbefed3 | 18 | |
b5c2afd0 | 19 | /** |
fe482240 | 20 | * Constructor function. |
0dbefed3 | 21 | * |
5a4f6742 CW |
22 | * @param array $inputData |
23 | * contents of HTTP REQUEST. | |
0dbefed3 EM |
24 | * |
25 | * @throws CRM_Core_Exception | |
b5c2afd0 | 26 | */ |
00be9182 | 27 | public function __construct($inputData) { |
0dbefed3 | 28 | $this->setInputParameters($inputData); |
6a488035 TO |
29 | parent::__construct(); |
30 | } | |
31 | ||
6c786a9b EM |
32 | /** |
33 | * @param string $component | |
34 | * | |
35 | * @return bool|void | |
36 | */ | |
00be9182 | 37 | public function main($component = 'contribute') { |
cd0f2a34 | 38 | try { |
39 | //we only get invoice num as a key player from payment gateway response. | |
40 | //for ARB we get x_subscription_id and x_subscription_paynum | |
41 | $x_subscription_id = $this->retrieve('x_subscription_id', 'String'); | |
aafec05e | 42 | if (!$x_subscription_id) { |
43 | // Presence of the id means it is approved. | |
44 | return TRUE; | |
45 | } | |
cd0f2a34 | 46 | $ids = $objects = $input = []; |
6a488035 | 47 | |
aafec05e | 48 | $input['component'] = $component; |
6a488035 | 49 | |
aafec05e | 50 | // load post vars in $input |
51 | $this->getInput($input, $ids); | |
6a488035 | 52 | |
aafec05e | 53 | // load post ids in $ids |
54 | $this->getIDs($ids, $input); | |
d0522e76 | 55 | $paymentProcessorID = $this->getPaymentProcessorID(); |
6a488035 | 56 | |
aafec05e | 57 | // Check if the contribution exists |
58 | // make sure contribution exists and is valid | |
59 | $contribution = new CRM_Contribute_BAO_Contribution(); | |
60 | $contribution->id = $ids['contribution']; | |
61 | if (!$contribution->find(TRUE)) { | |
62 | throw new CRM_Core_Exception('Failure: Could not find contribution record for ' . (int) $contribution->id, NULL, ['context' => "Could not find contribution record: {$contribution->id} in IPN request: " . print_r($input, TRUE)]); | |
63 | } | |
64 | $ids['contributionPage'] = $contribution->contribution_page_id; | |
65 | ||
66 | // make sure contact exists and is valid | |
67 | // use the contact id from the contribution record as the id in the IPN may not be valid anymore. | |
68 | $contact = new CRM_Contact_BAO_Contact(); | |
69 | $contact->id = $contribution->contact_id; | |
70 | $contact->find(TRUE); | |
71 | if ($contact->id != $ids['contact']) { | |
72 | // If the ids do not match then it is possible the contact id in the IPN has been merged into another contact which is why we use the contact_id from the contribution | |
73 | CRM_Core_Error::debug_log_message("Contact ID in IPN {$ids['contact']} not found but contact_id found in contribution {$contribution->contact_id} used instead"); | |
74 | echo "WARNING: Could not find contact record: {$ids['contact']}<p>"; | |
75 | $ids['contact'] = $contribution->contact_id; | |
76 | } | |
b577a1cd | 77 | |
f32e55f6 | 78 | $contributionRecur = new CRM_Contribute_BAO_ContributionRecur(); |
79 | $contributionRecur->id = $ids['contributionRecur']; | |
80 | if (!$contributionRecur->find(TRUE)) { | |
81 | throw new CRM_Core_Exception("Could not find contribution recur record: {$ids['ContributionRecur']} in IPN request: " . print_r($input, TRUE)); | |
aafec05e | 82 | } |
b577a1cd | 83 | |
aafec05e | 84 | $objects['contact'] = &$contact; |
85 | $objects['contribution'] = &$contribution; | |
b577a1cd | 86 | |
aafec05e | 87 | $this->loadObjects($input, $ids, $objects, TRUE, $paymentProcessorID); |
b577a1cd | 88 | |
f32e55f6 | 89 | if (!empty($ids['paymentProcessor']) && $contributionRecur->payment_processor_id != $ids['paymentProcessor']) { |
aafec05e | 90 | Civi::log()->warning('Payment Processor does not match the recurring processor id.', ['civi.tag' => 'deprecated']); |
91 | } | |
6a488035 | 92 | |
f32e55f6 | 93 | if ($component == 'contribute') { |
aafec05e | 94 | // check if first contribution is completed, else complete first contribution |
95 | $first = TRUE; | |
96 | if ($objects['contribution']->contribution_status_id == 1) { | |
97 | $first = FALSE; | |
98 | //load new contribution object if required. | |
99 | // create a contribution and then get it processed | |
100 | $contribution = new CRM_Contribute_BAO_Contribution(); | |
101 | $contribution->contact_id = $ids['contact']; | |
102 | $contribution->financial_type_id = $objects['contributionType']->id; | |
103 | $contribution->contribution_page_id = $ids['contributionPage']; | |
104 | $contribution->contribution_recur_id = $ids['contributionRecur']; | |
105 | $contribution->receive_date = $input['receive_date']; | |
106 | $contribution->currency = $objects['contribution']->currency; | |
107 | $contribution->amount_level = $objects['contribution']->amount_level; | |
108 | $contribution->address_id = $objects['contribution']->address_id; | |
109 | $contribution->campaign_id = $objects['contribution']->campaign_id; | |
110 | $contribution->_relatedObjects = $objects['contribution']->_relatedObjects; | |
111 | ||
112 | $objects['contribution'] = &$contribution; | |
6a488035 | 113 | } |
aafec05e | 114 | $input['payment_processor_id'] = $paymentProcessorID; |
115 | return $this->recur($input, [ | |
116 | 'related_contact' => $ids['related_contact'] ?? NULL, | |
117 | 'participant' => !empty($objects['participant']) ? $objects['participant']->id : NULL, | |
f32e55f6 | 118 | 'contributionRecur' => $contributionRecur->id, |
aafec05e | 119 | 'contact' => $ids['contact'] ?? NULL, |
120 | 'contributionPage' => $ids['contributionPage'] ?? NULL, | |
121 | ], $objects['contributionRecur'], $objects['contribution'], $first); | |
6a488035 | 122 | } |
aafec05e | 123 | |
cd0f2a34 | 124 | return TRUE; |
125 | } | |
126 | catch (CRM_Core_Exception $e) { | |
127 | Civi::log()->debug($e->getMessage()); | |
128 | echo 'Invalid or missing data'; | |
6a488035 TO |
129 | } |
130 | } | |
131 | ||
6c786a9b | 132 | /** |
7a6073fd EM |
133 | * @param array $input |
134 | * @param array $ids | |
7eecf6eb | 135 | * @param \CRM_Contribute_BAO_ContributionRecur $recur |
136 | * @param \CRM_Contribute_BAO_Contribution $contribution | |
137 | * @param bool $first | |
7a6073fd EM |
138 | * |
139 | * @return bool | |
55bc843d | 140 | * @throws \CRM_Core_Exception |
141 | * @throws \CiviCRM_API3_Exception | |
6c786a9b | 142 | */ |
7eecf6eb | 143 | public function recur($input, $ids, $recur, $contribution, $first) { |
6a488035 TO |
144 | |
145 | // do a subscription check | |
146 | if ($recur->processor_id != $input['subscription_id']) { | |
cd0f2a34 | 147 | throw new CRM_Core_Exception('Unrecognized subscription.'); |
6a488035 TO |
148 | } |
149 | ||
6a488035 TO |
150 | $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name'); |
151 | ||
6a488035 TO |
152 | $now = date('YmdHis'); |
153 | ||
7eecf6eb | 154 | $contribution->invoice_id = md5(uniqid(rand(), TRUE)); |
155 | $contribution->total_amount = $input['amount']; | |
156 | $contribution->trxn_id = $input['trxn_id']; | |
6a488035 | 157 | |
4086637a | 158 | $isFirstOrLastRecurringPayment = FALSE; |
6a488035 TO |
159 | if ($input['response_code'] == 1) { |
160 | // Approved | |
161 | if ($first) { | |
162 | $recur->start_date = $now; | |
163 | $recur->trxn_id = $recur->processor_id; | |
4086637a | 164 | $isFirstOrLastRecurringPayment = CRM_Core_Payment::RECURRING_PAYMENT_START; |
6a488035 | 165 | } |
ae659115 | 166 | |
6a488035 TO |
167 | if (($recur->installments > 0) && |
168 | ($input['subscription_paynum'] >= $recur->installments) | |
169 | ) { | |
170 | // this is the last payment | |
6a488035 | 171 | $recur->end_date = $now; |
4086637a | 172 | $isFirstOrLastRecurringPayment = CRM_Core_Payment::RECURRING_PAYMENT_END; |
ae659115 | 173 | // This end date update should occur in ContributionRecur::updateOnNewPayment |
174 | // testIPNPaymentRecurNoReceipt has test cover. | |
175 | $recur->save(); | |
6a488035 | 176 | } |
6a488035 TO |
177 | } |
178 | else { | |
179 | // Declined | |
180 | // failed status | |
181 | $recur->contribution_status_id = array_search('Failed', $contributionStatus); | |
182 | $recur->cancel_date = $now; | |
183 | $recur->save(); | |
184 | ||
ae659115 | 185 | $message = ts('Subscription payment failed - %1', [1 => htmlspecialchars($input['response_reason_text'])]); |
267353f4 | 186 | CRM_Core_Error::debug_log_message($message); |
6a488035 TO |
187 | |
188 | // the recurring contribution has declined a payment or has failed | |
189 | // so we just fix the recurring contribution and not change any of | |
7a6073fd | 190 | // the existing contributions |
6a488035 TO |
191 | // CRM-9036 |
192 | return TRUE; | |
193 | } | |
194 | ||
195 | // check if contribution is already completed, if so we ignore this ipn | |
7eecf6eb | 196 | if ($contribution->contribution_status_id == 1) { |
267353f4 | 197 | CRM_Core_Error::debug_log_message("Returning since contribution has already been handled."); |
f7935ad9 | 198 | echo 'Success: Contribution has already been handled<p>'; |
6a488035 TO |
199 | return TRUE; |
200 | } | |
201 | ||
7eecf6eb | 202 | CRM_Contribute_BAO_Contribution::completeOrder($input, $ids, $contribution); |
4086637a | 203 | |
4086637a | 204 | if ($isFirstOrLastRecurringPayment) { |
c3407e4e | 205 | //send recurring Notification email for user |
206 | CRM_Contribute_BAO_ContributionPage::recurringNotify(TRUE, | |
207 | $ids['contact'], | |
208 | $ids['contributionPage'], | |
209 | $recur, | |
210 | (bool) $this->getMembershipID($contribution->id, $recur->id) | |
211 | ); | |
4086637a | 212 | } |
6a488035 TO |
213 | } |
214 | ||
6c786a9b | 215 | /** |
dbb0d30b | 216 | * Get the input from passed in fields. |
217 | * | |
218 | * @param array $input | |
7a9ab499 | 219 | * |
23cfc7a6 | 220 | * @throws \CRM_Core_Exception |
6c786a9b | 221 | */ |
41ce57e7 | 222 | public function getInput(&$input) { |
0dbefed3 EM |
223 | $input['amount'] = $this->retrieve('x_amount', 'String'); |
224 | $input['subscription_id'] = $this->retrieve('x_subscription_id', 'Integer'); | |
225 | $input['response_code'] = $this->retrieve('x_response_code', 'Integer'); | |
226 | $input['MD5_Hash'] = $this->retrieve('x_MD5_Hash', 'String', FALSE, ''); | |
227 | $input['response_reason_code'] = $this->retrieve('x_response_reason_code', 'String', FALSE); | |
228 | $input['response_reason_text'] = $this->retrieve('x_response_reason_text', 'String', FALSE); | |
229 | $input['subscription_paynum'] = $this->retrieve('x_subscription_paynum', 'Integer', FALSE, 0); | |
230 | $input['trxn_id'] = $this->retrieve('x_trans_id', 'String', FALSE); | |
23cfc7a6 | 231 | $input['receive_date'] = $this->retrieve('receive_date', 'String', FALSE, date('YmdHis', strtotime('now'))); |
0dbefed3 | 232 | |
6a488035 TO |
233 | if ($input['trxn_id']) { |
234 | $input['is_test'] = 0; | |
235 | } | |
5c6c7e76 | 236 | // Only assume trxn_id 'should' have been returned for success. |
237 | // Per CRM-17611 it would also not be passed back for a decline. | |
238 | elseif ($input['response_code'] == 1) { | |
6a488035 TO |
239 | $input['is_test'] = 1; |
240 | $input['trxn_id'] = md5(uniqid(rand(), TRUE)); | |
241 | } | |
242 | ||
41ce57e7 | 243 | $billingID = CRM_Core_BAO_LocationType::getBilling(); |
be2fb01f | 244 | $params = [ |
6a488035 TO |
245 | 'first_name' => 'x_first_name', |
246 | 'last_name' => 'x_last_name', | |
247 | "street_address-{$billingID}" => 'x_address', | |
248 | "city-{$billingID}" => 'x_city', | |
249 | "state-{$billingID}" => 'x_state', | |
250 | "postal_code-{$billingID}" => 'x_zip', | |
251 | "country-{$billingID}" => 'x_country', | |
252 | "email-{$billingID}" => 'x_email', | |
be2fb01f | 253 | ]; |
6a488035 | 254 | foreach ($params as $civiName => $resName) { |
0dbefed3 | 255 | $input[$civiName] = $this->retrieve($resName, 'String', FALSE); |
6a488035 TO |
256 | } |
257 | } | |
258 | ||
6c786a9b | 259 | /** |
dbb0d30b | 260 | * Get ids from input. |
261 | * | |
262 | * @param array $ids | |
263 | * @param array $input | |
264 | * | |
265 | * @throws \CRM_Core_Exception | |
6c786a9b | 266 | */ |
85ab2d23 | 267 | public function getIDs(&$ids, $input) { |
268 | $ids['contact'] = (int) $this->retrieve('x_cust_id', 'Integer', FALSE, 0); | |
b5ab9764 | 269 | $ids['contribution'] = (int) $this->retrieve('x_invoice_num', 'Integer'); |
85ab2d23 | 270 | $contributionRecur = $this->getContributionRecurObject($input['subscription_id'], $ids['contact'], $ids['contribution']); |
271 | $ids['contributionRecur'] = (int) $contributionRecur->id; | |
272 | $ids['contact'] = $contributionRecur->contact_id; | |
6a488035 TO |
273 | } |
274 | ||
6c786a9b | 275 | /** |
6a0b768e TO |
276 | * @param string $name |
277 | * Parameter name. | |
278 | * @param string $type | |
279 | * Parameter type. | |
280 | * @param bool $abort | |
281 | * Abort if not present. | |
282 | * @param null $default | |
283 | * Default value. | |
6c786a9b | 284 | * |
0dbefed3 | 285 | * @throws CRM_Core_Exception |
6c786a9b EM |
286 | * @return mixed |
287 | */ | |
00be9182 | 288 | public function retrieve($name, $type, $abort = TRUE, $default = NULL) { |
0dbefed3 EM |
289 | $value = CRM_Utils_Type::validate( |
290 | empty($this->_inputParameters[$name]) ? $default : $this->_inputParameters[$name], | |
291 | $type, | |
292 | FALSE | |
6a488035 TO |
293 | ); |
294 | if ($abort && $value === NULL) { | |
0dbefed3 | 295 | throw new CRM_Core_Exception("Could not find an entry for $name"); |
6a488035 TO |
296 | } |
297 | return $value; | |
2aa397bc | 298 | } |
6a488035 | 299 | |
b5ab9764 | 300 | /** |
301 | * Get membership id, if any. | |
302 | * | |
303 | * @param int $contributionID | |
304 | * @param int $contributionRecurID | |
305 | * | |
306 | * @return int|null | |
307 | */ | |
308 | protected function getMembershipID(int $contributionID, int $contributionRecurID): ?int { | |
309 | // Get membershipId. Join with membership payment table for additional checks | |
310 | $sql = " | |
311 | SELECT m.id | |
312 | FROM civicrm_membership m | |
313 | INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contribution_id = {$contributionID} | |
314 | WHERE m.contribution_recur_id = {$contributionRecurID} | |
315 | LIMIT 1"; | |
316 | return CRM_Core_DAO::singleValueQuery($sql); | |
317 | } | |
318 | ||
85ab2d23 | 319 | /** |
320 | * Get the recurring contribution object. | |
321 | * | |
322 | * @param string $processorID | |
323 | * @param int $contactID | |
324 | * @param int $contributionID | |
325 | * | |
326 | * @return \CRM_Core_DAO|\DB_Error|object | |
327 | * @throws \CRM_Core_Exception | |
328 | */ | |
329 | protected function getContributionRecurObject(string $processorID, int $contactID, int $contributionID) { | |
330 | // joining with contribution table for extra checks | |
331 | $sql = " | |
332 | SELECT cr.id, cr.contact_id | |
333 | FROM civicrm_contribution_recur cr | |
334 | INNER JOIN civicrm_contribution co ON co.contribution_recur_id = cr.id | |
335 | WHERE cr.processor_id = '{$processorID}' AND | |
336 | (cr.contact_id = $contactID OR co.id = $contributionID) | |
337 | LIMIT 1"; | |
338 | $contRecur = CRM_Core_DAO::executeQuery($sql); | |
339 | if (!$contRecur->fetch()) { | |
340 | throw new CRM_Core_Exception('Could not find contributionRecur id'); | |
341 | } | |
342 | if ($contactID != $contRecur->contact_id) { | |
343 | $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]); | |
344 | CRM_Core_Error::debug_log_message($message); | |
345 | } | |
346 | return $contRecur; | |
347 | } | |
348 | ||
d0522e76 | 349 | /** |
350 | * Get the payment processor id. | |
351 | * | |
352 | * @return int | |
353 | * | |
354 | * @throws \CRM_Core_Exception | |
355 | * @throws \CiviCRM_API3_Exception | |
356 | */ | |
357 | protected function getPaymentProcessorID(): int { | |
358 | // Attempt to get payment processor ID from URL | |
359 | if (!empty($this->_inputParameters['processor_id'])) { | |
360 | return (int) $this->_inputParameters['processor_id']; | |
361 | } | |
362 | // This is an unreliable method as there could be more than one instance. | |
363 | // Recommended approach is to use the civicrm/payment/ipn/xx url where xx is the payment | |
364 | // processor id & the handleNotification function (which should call the completetransaction api & by-pass this | |
365 | // entirely). The only thing the IPN class should really do is extract data from the request, validate it | |
366 | // & call completetransaction or call fail? (which may not exist yet). | |
367 | 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'); | |
368 | $paymentProcessorTypeID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType', | |
369 | 'AuthNet', 'id', 'name' | |
370 | ); | |
371 | return (int) civicrm_api3('PaymentProcessor', 'getvalue', [ | |
372 | 'is_test' => 0, | |
373 | 'options' => ['limit' => 1], | |
374 | 'payment_processor_type_id' => $paymentProcessorTypeID, | |
375 | 'return' => 'id', | |
376 | ]); | |
377 | } | |
378 | ||
6a488035 | 379 | } |