Merge pull request #24148 from civicrm/5.52
[civicrm-core.git] / CRM / Core / Payment / BaseIPN.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 |
9 +--------------------------------------------------------------------+
10 */
11
12 use Civi\Api4\Contribution;
13
14 /**
15 * Class CRM_Core_Payment_BaseIPN.
16 */
17 class CRM_Core_Payment_BaseIPN {
18
19 public static $_now = NULL;
20
21 /**
22 * Input parameters from payment processor. Store these so that
23 * the code does not need to keep retrieving from the http request
24 * @var array
25 */
26 protected $_inputParameters = [];
27
28 /**
29 * Only used by AuthorizeNetIPN.
30 * @var bool
31 *
32 * @deprecated
33 *
34 */
35 protected $_isRecurring = FALSE;
36
37 /**
38 * Only used by AuthorizeNetIPN.
39 * @var bool
40 *
41 * @deprecated
42 *
43 */
44 protected $_isFirstOrLastRecurringPayment = FALSE;
45
46 /**
47 * Constructor.
48 */
49 public function __construct() {
50 self::$_now = date('YmdHis');
51 }
52
53 /**
54 * Store input array on the class.
55 *
56 * @param array $parameters
57 *
58 * @throws CRM_Core_Exception
59 */
60 public function setInputParameters($parameters) {
61 if (!is_array($parameters)) {
62 throw new CRM_Core_Exception('Invalid input parameters');
63 }
64 $this->_inputParameters = $parameters;
65 }
66
67 /**
68 * Set contribution to failed.
69 *
70 * @param array $objects
71 *
72 * @deprecated use the api.
73 *
74 * @return bool
75 * @throws \CiviCRM_API3_Exception|\CRM_Core_Exception
76 */
77 public function failed($objects) {
78 CRM_Core_Error::deprecatedFunctionWarning('use the api');
79 $contribution = &$objects['contribution'];
80 $memberships = [];
81 if (!empty($objects['membership'])) {
82 $memberships = &$objects['membership'];
83 if (is_numeric($memberships)) {
84 $memberships = [$objects['membership']];
85 }
86 }
87
88 $addLineItems = empty($contribution->id);
89 $participant = &$objects['participant'];
90 $contribution->contribution_status_id = CRM_Core_PseudoConstant::getKey('CRM_Contribute_DAO_Contribution', 'contribution_status_id', 'Failed');
91 $contribution->save();
92
93 // Add line items for recurring payments.
94 if (!empty($objects['contributionRecur']) && $objects['contributionRecur']->id && $addLineItems) {
95 CRM_Contribute_BAO_ContributionRecur::addRecurLineItems($objects['contributionRecur']->id, $contribution);
96 }
97
98 if (!empty($memberships)) {
99 foreach ($memberships as $membership) {
100 // @fixme Should we cancel only Pending memberships? per cancelled()
101 $this->cancelMembership($membership, $membership->status_id, FALSE);
102 }
103 }
104
105 if ($participant) {
106 $this->cancelParticipant($participant->id);
107 }
108
109 Civi::log()->debug("Setting contribution status to Failed");
110 return TRUE;
111 }
112
113 /**
114 * Handled pending contribution status.
115 *
116 * @deprecated
117 *
118 * @param array $objects
119 * @param object $transaction
120 *
121 * @return bool
122 */
123 public function pending(&$objects, &$transaction) {
124 CRM_Core_Error::deprecatedFunctionWarning('This function will be removed at some point');
125 $transaction->commit();
126 Civi::log()->debug('Returning since contribution status is Pending');
127 echo 'Success: Returning since contribution status is pending<p>';
128 return TRUE;
129 }
130
131 /**
132 * Process cancelled payment outcome.
133 *
134 * @deprecated The intended replacement code is
135 *
136 * Contribution::update(FALSE)->setValues([
137 * 'cancel_date' => 'now',
138 * 'contribution_status_id:name' => 'Cancelled',
139 * ])->addWhere('id', '=', $contribution->id)->execute();
140 *
141 * @param array $objects
142 *
143 * @return bool
144 * @throws \CiviCRM_API3_Exception|\CRM_Core_Exception
145 */
146 public function cancelled($objects) {
147 CRM_Core_Error::deprecatedFunctionWarning('Use Contribution create api to cancel the contribution');
148 $contribution = &$objects['contribution'];
149
150 if (empty($contribution->id)) {
151 // This code is believed to be unreachable.
152 // this entire function is due to be deprecated in the near future so
153 // this code will live in a deprecated function until it gets removed.
154 $addLineItems = TRUE;
155 // CRM-15546
156 $contributionStatuses = CRM_Core_PseudoConstant::get('CRM_Contribute_DAO_Contribution', 'contribution_status_id', [
157 'labelColumn' => 'name',
158 'flip' => 1,
159 ]);
160 $contribution->contribution_status_id = $contributionStatuses['Cancelled'];
161 $contribution->cancel_date = self::$_now;
162 $contribution->save();
163 // Add line items for recurring payments.
164 if (!empty($objects['contributionRecur']) && $objects['contributionRecur']->id && $addLineItems) {
165 CRM_Contribute_BAO_ContributionRecur::addRecurLineItems($objects['contributionRecur']->id, $contribution);
166 }
167 $memberships = [];
168 if (!empty($objects['membership'])) {
169 $memberships = &$objects['membership'];
170 if (is_numeric($memberships)) {
171 $memberships = [$objects['membership']];
172 }
173 }
174 if (!empty($memberships)) {
175 foreach ($memberships as $membership) {
176 if ($membership) {
177 $this->cancelMembership($membership, $membership->status_id);
178 }
179 }
180 }
181 $participant = &$objects['participant'];
182
183 if ($participant) {
184 $this->cancelParticipant($participant->id);
185 }
186 }
187 else {
188 Contribution::update(FALSE)->setValues([
189 'cancel_date' => 'now',
190 'contribution_status_id:name' => 'Cancelled',
191 ])->addWhere('id', '=', $contribution->id)->execute();
192 }
193
194 Civi::log()->debug("Setting contribution status to Cancelled");
195 return TRUE;
196 }
197
198 /**
199 * Rollback unhandled outcomes.
200 *
201 * @deprecated
202 *
203 * @param array $objects
204 * @param CRM_Core_Transaction $transaction
205 *
206 * @return bool
207 */
208 public function unhandled(&$objects, &$transaction) {
209 CRM_Core_Error::deprecatedFunctionWarning('This function will be removed at some point');
210 $transaction->rollback();
211 Civi::log()->debug('Returning since contribution status is not handled');
212 echo 'Failure: contribution status is not handled<p>';
213 return FALSE;
214 }
215
216 /**
217 * Logic to cancel a participant record when the related contribution changes to failed/cancelled.
218 * @todo This is part of a bigger refactor for dev/core/issues/927 - "duplicate" functionality exists in CRM_Contribute_BAO_Contribution::cancel()
219 *
220 * @deprecated
221 *
222 * @param $participantID
223 *
224 * @throws \CiviCRM_API3_Exception
225 */
226 private function cancelParticipant($participantID) {
227 // @fixme https://lab.civicrm.org/dev/core/issues/927 Cancelling membership etc is not desirable for all use-cases and we should be able to disable it
228 $participantParams['id'] = $participantID;
229 $participantParams['status_id'] = 'Cancelled';
230 civicrm_api3('Participant', 'create', $participantParams);
231 }
232
233 /**
234 * Logic to cancel a membership record when the related contribution changes to failed/cancelled.
235 * @todo This is part of a bigger refactor for dev/core/issues/927 - "duplicate" functionality exists in CRM_Contribute_BAO_Contribution::cancel()
236 * @param \CRM_Member_BAO_Membership $membership
237 * @param int $membershipStatusID
238 * @param bool $onlyCancelPendingMembership
239 * Do we only cancel pending memberships? OR memberships in any status? (see CRM-18688)
240 * @fixme Historically failed() cancelled membership in any status, cancelled() cancelled only pending memberships so we retain that behaviour for now.
241 * @deprecated
242 */
243 private function cancelMembership($membership, $membershipStatusID, $onlyCancelPendingMembership = TRUE) {
244 CRM_Core_Error::deprecatedFunctionWarning('use the api');
245 // @fixme https://lab.civicrm.org/dev/core/issues/927 Cancelling membership etc is not desirable for all use-cases and we should be able to disable it
246 // Cancel only Pending memberships
247 $pendingMembershipStatusId = CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'Pending');
248 if (($membershipStatusID == $pendingMembershipStatusId) || ($onlyCancelPendingMembership == FALSE)) {
249 $cancelledMembershipStatusId = CRM_Core_PseudoConstant::getKey('CRM_Member_BAO_Membership', 'status_id', 'Cancelled');
250
251 $membership->status_id = $cancelledMembershipStatusId;
252 $membership->save();
253
254 $params = ['status_id' => $cancelledMembershipStatusId];
255 CRM_Member_BAO_Membership::updateRelatedMemberships($membership->id, $params);
256
257 // @todo Convert the above to API
258 // $membershipParams = [
259 // 'id' => $membership->id,
260 // 'status_id' => $cancelledMembershipStatusId,
261 // ];
262 // civicrm_api3('Membership', 'create', $membershipParams);
263 // CRM_Member_BAO_Membership::updateRelatedMemberships($membershipParams['id'], ['status_id' => $cancelledMembershipStatusId]);
264 }
265
266 }
267
268 /**
269 * @deprecated
270 *
271 * Jumbled up function.
272 *
273 * The purpose of this function is to transition a pending transaction to Completed including updating any
274 * related entities.
275 *
276 * It has been overloaded to also add recurring transactions to the database, cloning the original transaction and
277 * updating related entities.
278 *
279 * It is recommended to avoid calling this function directly and call the api functions:
280 * - contribution.completetransaction
281 * - contribution.repeattransaction
282 *
283 * These functions are the focus of testing efforts and more accurately reflect the division of roles
284 * (the job of the IPN class is to determine the outcome, transaction id, invoice id & to validate the source
285 * and from there it should be possible to pass off transaction management.)
286 *
287 * This function has been problematic for some time but there are now several tests via the api_v3_Contribution test
288 * and the Paypal & Authorize.net IPN tests so any refactoring should be done in conjunction with those.
289 *
290 * This function needs to have the 'body' moved to the CRM_Contribute_BAO_Contribute class and to undergo
291 * refactoring to separate the complete transaction and repeat transaction functionality into separate functions with
292 * a shared function that updates related components.
293 *
294 * Note that it is not necessary payment processor extension to implement an IPN class now. In general the code on the
295 * IPN class is better accessed through the api which de-jumbles it a bit.
296 *
297 * e.g the payment class can have a function like (based on Omnipay extension):
298 *
299 * public function handlePaymentNotification() {
300 * $response = $this->getValidatedOutcome();
301 * if ($response->isSuccessful()) {
302 * try {
303 * // @todo check if it is a repeat transaction & call repeattransaction instead.
304 * civicrm_api3('contribution', 'completetransaction', array('id' => $this->transaction_id));
305 * }
306 * catch (CiviCRM_API3_Exception $e) {
307 * if (!stristr($e->getMessage(), 'Contribution already completed')) {
308 * $this->handleError('error', $this->transaction_id . $e->getMessage(), 'ipn_completion', 9000, 'An error may
309 * have occurred. Please check your receipt is correct');
310 * $this->redirectOrExit('success');
311 * }
312 * elseif ($this->transaction_id) {
313 * civicrm_api3('contribution', 'create', array('id' => $this->transaction_id, 'contribution_status_id' =>
314 * 'Failed'));
315 * }
316 *
317 * @param array $input
318 * @param array $ids
319 * @param array $objects
320 *
321 * @throws \CRM_Core_Exception
322 * @throws \CiviCRM_API3_Exception
323 */
324 public function completeTransaction($input, $ids, $objects) {
325 CRM_Core_Error::deprecatedFunctionWarning('Use Payment.create api');
326 CRM_Contribute_BAO_Contribution::completeOrder($input, !empty($objects['contributionRecur']) ? $objects['contributionRecur']->id : NULL, $objects['contribution']->id ?? NULL);
327 }
328
329 /**
330 * @deprecated
331 * Get site billing ID.
332 *
333 * @param array $ids
334 *
335 * @return bool
336 */
337 public function getBillingID(&$ids) {
338 CRM_Core_Error::deprecatedFunctionWarning('CRM_Core_BAO_LocationType::getBilling()');
339 $ids['billing'] = CRM_Core_BAO_LocationType::getBilling();
340 if (!$ids['billing']) {
341 CRM_Core_Error::debug_log_message(ts('Please set a location type of %1', [1 => 'Billing']));
342 echo "Failure: Could not find billing location type<p>";
343 return FALSE;
344 }
345 return TRUE;
346 }
347
348 /**
349 * @deprecated
350 *
351 * @todo confirm this function is not being used by any payment processor outside core & remove.
352 *
353 * Note that the compose message part has been moved to contribution
354 * In general LoadObjects is called first to get the objects but the composeMessageArray function now calls it
355 *
356 * @param array $input
357 * Incoming data from Payment processor.
358 * @param array $ids
359 * Related object IDs.
360 * @param array $objects
361 *
362 * @throws \CiviCRM_API3_Exception
363 */
364 public function sendMail($input, $ids, $objects) {
365 CRM_Core_Error::deprecatedFunctionWarning('this should be done via completetransaction api');
366 civicrm_api3('Contribution', 'sendconfirmation', [
367 'id' => $objects['contribution']->id,
368 ]);
369 }
370
371 }