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 |
55 | $this->getIDs($ids, $input); | |
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; |
6a488035 TO |
121 | if ($input['response_code'] == 1) { |
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 | 171 | $input['response_code'] = $this->retrieve('x_response_code', 'Integer'); |
0dbefed3 EM |
172 | $input['response_reason_code'] = $this->retrieve('x_response_reason_code', 'String', FALSE); |
173 | $input['response_reason_text'] = $this->retrieve('x_response_reason_text', 'String', FALSE); | |
174 | $input['subscription_paynum'] = $this->retrieve('x_subscription_paynum', 'Integer', FALSE, 0); | |
175 | $input['trxn_id'] = $this->retrieve('x_trans_id', 'String', FALSE); | |
23cfc7a6 | 176 | $input['receive_date'] = $this->retrieve('receive_date', 'String', FALSE, date('YmdHis', strtotime('now'))); |
0dbefed3 | 177 | |
6a488035 TO |
178 | if ($input['trxn_id']) { |
179 | $input['is_test'] = 0; | |
180 | } | |
5c6c7e76 | 181 | // Only assume trxn_id 'should' have been returned for success. |
182 | // Per CRM-17611 it would also not be passed back for a decline. | |
183 | elseif ($input['response_code'] == 1) { | |
6a488035 TO |
184 | $input['is_test'] = 1; |
185 | $input['trxn_id'] = md5(uniqid(rand(), TRUE)); | |
186 | } | |
187 | ||
41ce57e7 | 188 | $billingID = CRM_Core_BAO_LocationType::getBilling(); |
be2fb01f | 189 | $params = [ |
6a488035 TO |
190 | 'first_name' => 'x_first_name', |
191 | 'last_name' => 'x_last_name', | |
192 | "street_address-{$billingID}" => 'x_address', | |
193 | "city-{$billingID}" => 'x_city', | |
194 | "state-{$billingID}" => 'x_state', | |
195 | "postal_code-{$billingID}" => 'x_zip', | |
196 | "country-{$billingID}" => 'x_country', | |
197 | "email-{$billingID}" => 'x_email', | |
be2fb01f | 198 | ]; |
6a488035 | 199 | foreach ($params as $civiName => $resName) { |
0dbefed3 | 200 | $input[$civiName] = $this->retrieve($resName, 'String', FALSE); |
6a488035 TO |
201 | } |
202 | } | |
203 | ||
6c786a9b | 204 | /** |
dbb0d30b | 205 | * Get ids from input. |
206 | * | |
207 | * @param array $ids | |
208 | * @param array $input | |
209 | * | |
210 | * @throws \CRM_Core_Exception | |
6c786a9b | 211 | */ |
85ab2d23 | 212 | public function getIDs(&$ids, $input) { |
338ce4c6 | 213 | $ids['contribution'] = $this->getContributionID(); |
4848ac9e | 214 | $contributionRecur = $this->getContributionRecurObject($this->getRecurProcessorID(), (int) $this->retrieve('x_cust_id', 'Integer', FALSE, 0), $this->getContributionID()); |
85ab2d23 | 215 | $ids['contributionRecur'] = (int) $contributionRecur->id; |
6a488035 TO |
216 | } |
217 | ||
6c786a9b | 218 | /** |
6a0b768e TO |
219 | * @param string $name |
220 | * Parameter name. | |
221 | * @param string $type | |
222 | * Parameter type. | |
223 | * @param bool $abort | |
224 | * Abort if not present. | |
225 | * @param null $default | |
226 | * Default value. | |
6c786a9b | 227 | * |
0dbefed3 | 228 | * @throws CRM_Core_Exception |
6c786a9b EM |
229 | * @return mixed |
230 | */ | |
00be9182 | 231 | public function retrieve($name, $type, $abort = TRUE, $default = NULL) { |
0dbefed3 EM |
232 | $value = CRM_Utils_Type::validate( |
233 | empty($this->_inputParameters[$name]) ? $default : $this->_inputParameters[$name], | |
234 | $type, | |
235 | FALSE | |
6a488035 TO |
236 | ); |
237 | if ($abort && $value === NULL) { | |
0dbefed3 | 238 | throw new CRM_Core_Exception("Could not find an entry for $name"); |
6a488035 TO |
239 | } |
240 | return $value; | |
2aa397bc | 241 | } |
6a488035 | 242 | |
b5ab9764 | 243 | /** |
244 | * Get membership id, if any. | |
245 | * | |
246 | * @param int $contributionID | |
247 | * @param int $contributionRecurID | |
248 | * | |
249 | * @return int|null | |
250 | */ | |
251 | protected function getMembershipID(int $contributionID, int $contributionRecurID): ?int { | |
252 | // Get membershipId. Join with membership payment table for additional checks | |
253 | $sql = " | |
254 | SELECT m.id | |
255 | FROM civicrm_membership m | |
256 | INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contribution_id = {$contributionID} | |
257 | WHERE m.contribution_recur_id = {$contributionRecurID} | |
258 | LIMIT 1"; | |
259 | return CRM_Core_DAO::singleValueQuery($sql); | |
260 | } | |
261 | ||
85ab2d23 | 262 | /** |
263 | * Get the recurring contribution object. | |
264 | * | |
265 | * @param string $processorID | |
266 | * @param int $contactID | |
267 | * @param int $contributionID | |
268 | * | |
269 | * @return \CRM_Core_DAO|\DB_Error|object | |
270 | * @throws \CRM_Core_Exception | |
271 | */ | |
272 | protected function getContributionRecurObject(string $processorID, int $contactID, int $contributionID) { | |
273 | // joining with contribution table for extra checks | |
274 | $sql = " | |
275 | SELECT cr.id, cr.contact_id | |
276 | FROM civicrm_contribution_recur cr | |
277 | INNER JOIN civicrm_contribution co ON co.contribution_recur_id = cr.id | |
278 | WHERE cr.processor_id = '{$processorID}' AND | |
279 | (cr.contact_id = $contactID OR co.id = $contributionID) | |
280 | LIMIT 1"; | |
281 | $contRecur = CRM_Core_DAO::executeQuery($sql); | |
282 | if (!$contRecur->fetch()) { | |
283 | throw new CRM_Core_Exception('Could not find contributionRecur id'); | |
284 | } | |
285 | if ($contactID != $contRecur->contact_id) { | |
66092b51 | 286 | $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 | 287 | CRM_Core_Error::debug_log_message($message); |
288 | } | |
289 | return $contRecur; | |
290 | } | |
291 | ||
d0522e76 | 292 | /** |
293 | * Get the payment processor id. | |
294 | * | |
295 | * @return int | |
296 | * | |
c2d8f267 | 297 | * @throws \API_Exception |
d0522e76 | 298 | * @throws \CRM_Core_Exception |
299 | * @throws \CiviCRM_API3_Exception | |
300 | */ | |
301 | protected function getPaymentProcessorID(): int { | |
302 | // Attempt to get payment processor ID from URL | |
c2d8f267 | 303 | if (!empty($this->_inputParameters['processor_id']) && |
304 | 'AuthNet' === PaymentProcessor::get(FALSE) | |
305 | ->addSelect('payment_processor_type_id:name') | |
306 | ->addWhere('id', '=', $this->_inputParameters['processor_id']) | |
307 | ->execute()->first()['payment_processor_type_id:name'] | |
308 | ) { | |
d0522e76 | 309 | return (int) $this->_inputParameters['processor_id']; |
310 | } | |
311 | // This is an unreliable method as there could be more than one instance. | |
312 | // Recommended approach is to use the civicrm/payment/ipn/xx url where xx is the payment | |
313 | // processor id & the handleNotification function (which should call the completetransaction api & by-pass this | |
314 | // entirely). The only thing the IPN class should really do is extract data from the request, validate it | |
315 | // & call completetransaction or call fail? (which may not exist yet). | |
316 | 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'); | |
317 | $paymentProcessorTypeID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType', | |
318 | 'AuthNet', 'id', 'name' | |
319 | ); | |
320 | return (int) civicrm_api3('PaymentProcessor', 'getvalue', [ | |
321 | 'is_test' => 0, | |
322 | 'options' => ['limit' => 1], | |
323 | 'payment_processor_type_id' => $paymentProcessorTypeID, | |
324 | 'return' => 'id', | |
325 | ]); | |
326 | } | |
327 | ||
60c374e3 EM |
328 | /** |
329 | * Get the processor_id for the recurring. | |
330 | * | |
331 | * This is the value stored in civicrm_contribution_recur.processor_id, | |
332 | * sometimes called subscription_id. | |
333 | * | |
334 | * @return string | |
335 | * | |
336 | * @throws \CRM_Core_Exception | |
337 | */ | |
338 | protected function getRecurProcessorID(): string { | |
339 | return $this->retrieve('x_subscription_id', 'String'); | |
340 | } | |
341 | ||
338ce4c6 EM |
342 | /** |
343 | * Get the contribution ID to be updated. | |
344 | * | |
345 | * @return int | |
346 | * | |
347 | * @throws \CRM_Core_Exception | |
348 | */ | |
349 | protected function getContributionID(): int { | |
350 | return (int) $this->retrieve('x_invoice_num', 'Integer'); | |
351 | } | |
352 | ||
6a488035 | 353 | } |