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