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