MJWIPNTrait.php 15.6 KB
Newer Older
mattwire's avatar
mattwire committed
1
<?php
2
3
4
5
6
7
8
9
/*
 +--------------------------------------------------------------------+
 | Copyright CiviCRM LLC. All rights reserved.                        |
 |                                                                    |
 | This work is published under the GNU AGPLv3 license with some      |
 | permitted exceptions and without any warranty. For full license    |
 | and copyright information, see https://civicrm.org/licensing       |
 +--------------------------------------------------------------------+
mattwire's avatar
mattwire committed
10
11
12
13
 */

/**
 * Shared payment IPN functions that should one day be migrated to CiviCRM core
mattwire's avatar
mattwire committed
14
15
 *
 * Trait CRM_Core_Payment_MJWIPNTrait
mattwire's avatar
mattwire committed
16
17
18
19
 */
trait CRM_Core_Payment_MJWIPNTrait {

  /**
20
   * @var \CRM_Core_Payment Payment processor
mattwire's avatar
mattwire committed
21
   */
22
  protected $_paymentProcessor;
mattwire's avatar
mattwire committed
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

  /**
   * Do we send an email receipt for each contribution?
   *
   * @var int
   */
  protected $is_email_receipt = NULL;

  /**
   * The recurring contribution ID associated with the transaction
   * @var int
   */
  protected $contribution_recur_id = NULL;

  /**
   *  The IPN event type
   * @var string
   */
41
  protected $eventType = NULL;
mattwire's avatar
mattwire committed
42

43
44
45
46
47
  /**
   * Exit on exceptions (TRUE), or just throw them (FALSE).
   */
  protected $exitOnException = TRUE;

mattwire's avatar
mattwire committed
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
  /**
   * Set the value of is_email_receipt to use when a new contribution is received for a recurring contribution
   * If not set, we respect the value set on the ContributionRecur entity.
   *
   * @param int $sendReceipt The value of is_email_receipt
   */
  public function setSendEmailReceipt($sendReceipt) {
    switch ($sendReceipt) {
      case 0:
        $this->is_email_receipt = 0;
        break;

      case 1:
        $this->is_email_receipt = 1;
        break;

      default:
        $this->is_email_receipt = 0;
    }
  }

  /**
   * Get the value of is_email_receipt to use when a new contribution is received for a recurring contribution
   * If not set, we respect the value set on the ContributionRecur entity.
   *
   * @return int
   * @throws \CiviCRM_API3_Exception
   */
  public function getSendEmailReceipt() {
    if (isset($this->is_email_receipt)) {
      return (int) $this->is_email_receipt;
    }
    if (!empty($this->contribution_recur_id)) {
81
82
83
84
85
86
87
88
89
      try {
        $this->is_email_receipt = civicrm_api3('ContributionRecur', 'getvalue', [
          'return' => "is_email_receipt",
          'id' => $this->contribution_recur_id,
        ]);
      }
      catch (Exception $e) {
        $this->is_email_receipt = 0;
      }
mattwire's avatar
mattwire committed
90
91
92
93
94
95
96
    }
    return (int) $this->is_email_receipt;
  }

  /**
   * Get the payment processor
   *   The $_GET['processor_id'] value is set by CRM_Core_Payment::handlePaymentMethod.
97
98
99
   *
   * @throws \CRM_Core_Exception
   * @throws \Civi\Payment\Exception\PaymentProcessorException
mattwire's avatar
mattwire committed
100
101
   */
  protected function getPaymentProcessor() {
102
103
104
105
   $paymentProcessorID = CRM_Utils_Request::retrieveValue('processor_id', 'Positive', NULL, FALSE, 'GET');
   if (!$paymentProcessorID) {
     $this->exception('Failed to get payment processor ID');
   }
mattwire's avatar
mattwire committed
106
    try {
107
      $this->_paymentProcessor = \Civi\Payment\System::singleton()->getById($paymentProcessorID);
mattwire's avatar
mattwire committed
108
109
110
111
112
113
114
    }
    catch(Exception $e) {
      $this->exception('Failed to get payment processor');
    }
  }

  /**
115
   * Check that required params are present
mattwire's avatar
mattwire committed
116
   *
117
118
119
120
   * @param string $description
   *   For error logs
   * @param array $requiredParams
   *   Array of params that are required
mattwire's avatar
mattwire committed
121
   * @param array $params
122
   *   Array of params to check
mattwire's avatar
mattwire committed
123
124
   *
   * @throws \Civi\Payment\Exception\PaymentProcessorException
mattwire's avatar
mattwire committed
125
   */
126
  protected function checkRequiredParams($description, $requiredParams, $params) {
mattwire's avatar
mattwire committed
127
128
    foreach ($requiredParams as $required) {
      if (!isset($params[$required])) {
129
        $this->exception("{$description}: Missing mandatory parameter: {$required}");
mattwire's avatar
mattwire committed
130
131
132
133
134
      }
    }
  }

  /**
135
   * Cancel a subscription (recurring contribution)
mattwire's avatar
mattwire committed
136
137
138
   * @param array $params
   *
   * @throws \CiviCRM_API3_Exception
mattwire's avatar
mattwire committed
139
   * @throws \Civi\Payment\Exception\PaymentProcessorException
mattwire's avatar
mattwire committed
140
   */
141
142
143
  protected function updateRecurCancelled($params) {
    $this->checkRequiredParams('updateRecurCancelled', ['id'], $params);
    civicrm_api3('ContributionRecur', 'cancel', $params);
mattwire's avatar
mattwire committed
144
145
146
  }

  /**
147
   * Update the subscription (recurring contribution) to a successful status
mattwire's avatar
mattwire committed
148
149
150
   * @param array $params
   *
   * @throws \CiviCRM_API3_Exception
mattwire's avatar
mattwire committed
151
   * @throws \Civi\Payment\Exception\PaymentProcessorException
mattwire's avatar
mattwire committed
152
   */
153
154
155
156
  private function updateRecurSuccess($params) {
    $this->checkRequiredParams('updateRecurSuccess', ['id'], $params);
    $params['failure_count'] = 0;
    $params['contribution_status_id'] = 'In Progress';
mattwire's avatar
mattwire committed
157

158
159
    // Successful charge & more to come.
    civicrm_api3('ContributionRecur', 'create', $params);
mattwire's avatar
mattwire committed
160
161
162
  }

  /**
163
   * Update the subscription (recurring contribution) to a completed status
mattwire's avatar
mattwire committed
164
165
166
   * @param array $params
   *
   * @throws \CiviCRM_API3_Exception
mattwire's avatar
mattwire committed
167
   * @throws \Civi\Payment\Exception\PaymentProcessorException
mattwire's avatar
mattwire committed
168
   */
169
170
171
  private function updateRecurCompleted($params) {
    $this->checkRequiredParams('updateRecurCompleted', ['id'], $params);
    $params['contribution_status_id'] = 'Completed';
mattwire's avatar
mattwire committed
172

173
    civicrm_api3('ContributionRecur', 'create', $params);
mattwire's avatar
mattwire committed
174
175
176
  }

  /**
177
   * Update the subscription (recurring contribution) to a failing status
mattwire's avatar
mattwire committed
178
179
180
   * @param array $params
   *
   * @throws \CiviCRM_API3_Exception
mattwire's avatar
mattwire committed
181
   * @throws \Civi\Payment\Exception\PaymentProcessorException
mattwire's avatar
mattwire committed
182
   */
183
184
  private function updateRecurFailed($params) {
    $this->checkRequiredParams('updateRecurFailed', ['id'], $params);
mattwire's avatar
mattwire committed
185

186
187
188
189
190
191
192
193
    $failureCount = civicrm_api3('ContributionRecur', 'getvalue', [
      'id' => $params['id'],
      'return' => 'failure_count',
    ]);
    $failureCount++;

    $params['failure_count'] = $failureCount;
    $params['contribution_status_id'] = 'Failed';
mattwire's avatar
mattwire committed
194

195
196
    // Change the status of the Recurring and update failed attempts.
    civicrm_api3('ContributionRecur', 'create', $params);
mattwire's avatar
mattwire committed
197
198
199
  }

  /**
200
   * Repeat a contribution (call the Contribution.repeattransaction API)
mattwire's avatar
mattwire committed
201
   *
mattwire's avatar
mattwire committed
202
   * @param array $params
203
   *
204
   * @return bool
205
   * @throws \CiviCRM_API3_Exception
206
   * @throws \Civi\Payment\Exception\PaymentProcessorException
mattwire's avatar
mattwire committed
207
   */
208
  private function repeatContribution($params) {
209
210
211
212
213
    $this->checkRequiredParams(
      'repeatContribution',
      [
        'contribution_status_id',
        'contribution_recur_id',
214
        // Optional: 'original_contribution_id',
215
216
217
218
219
220
221
222
223
        'receive_date',
        'order_reference',
        'trxn_id',
        'total_amount',
      ],
      $params
    );
    // Optional Params: fee_amount

224
225
226
    // Creat contributionParams for Contribution.repeattransaction and set some values
    $contributionParams = $params;
    // Status should be pending if we have a successful payment
227
228
229
230
231
232
233
234
235
236
237
238
239
    switch ($params['contribution_status_id']) {
      case CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'):
        // Create a contribution in Pending state using Contribution.repeattransaction and then complete using Payment.create
        $contributionParams['contribution_status_id'] = 'Pending';
        $createPayment = TRUE;
        break;

      default:
        // Failed etc.
        // For any other status create it directly using Contribution.repeattransaction and don't create a payment
        $contributionParams['contribution_status_id'] = $params['contribution_status_id'];
        $createPayment = FALSE;
    }
240

241
242
    // Create the new contribution
    $contributionParams['trxn_id'] = $params['order_reference'];
243
244
    // We send a receipt when adding a payment, not now
    $contributionParams['is_email_receipt'] = FALSE;
245
246
247
248
249
250
251
    try {
      $contribution = civicrm_api3('Contribution', 'repeattransaction', $contributionParams);
    }
    catch (Exception $e) {
      \Civi::log()->error('MJWIPNTrait call to repeattransaction failed: ' . $e->getMessage() . '; params: ' . print_r($contributionParams, TRUE));
      return FALSE;
    }
252

253
254
255
256
257
258
259
260
261
262
263
264
265
266
    if ($createPayment) {
      $paymentParamsKeys = [
        'receive_date' => 'trxn_date',
        'order_reference' => 'order_reference',
        'trxn_id' => 'trxn_id',
        'total_amount' => 'total_amount',
        'fee_amount' => 'fee_amount',
      ];
      foreach ($paymentParamsKeys as $contributionKey => $paymentKey) {
        if (isset($params[$contributionKey])) {
          $paymentParams[$paymentKey] = $params[$contributionKey];
        }
      }
      $paymentParams['contribution_id'] = $contribution['id'];
267
      $paymentParams['payment_processor_id'] = $this->_paymentProcessor->getID();
268
269
      $paymentParams['is_send_contribution_notification'] = $this->getSendEmailReceipt();
      $paymentParams['skipCleanMoney'] = TRUE;
270
      civicrm_api3('Mjwpayment', 'create_payment', $paymentParams);
271
    }
272
    return TRUE;
mattwire's avatar
mattwire committed
273
274
  }

275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
  /**
   * @param array $params
   *
   * @throws \CiviCRM_API3_Exception
   * @throws \Civi\Payment\Exception\PaymentProcessorException
   */
  private function updateContribution($params) {
    $this->checkRequiredParams('updateContribution', ['contribution_id'], $params);
    $params['id'] = $params['contribution_id'];
    unset($params['contribution_id']);
    $params['skipCleanMoney'] = TRUE;
    $params['skipRecentView'] = TRUE;
    $params['skipLineItem'] = TRUE;
    $params['is_post_payment_create'] = TRUE;
    civicrm_api3('Contribution', 'create', $params);
  }

mattwire's avatar
mattwire committed
292
  /**
293
   * Complete a pending contribution and update associated entities (recur/membership)
mattwire's avatar
mattwire committed
294
   *
mattwire's avatar
mattwire committed
295
296
   * @param array $params
   *
mattwire's avatar
mattwire committed
297
   * @throws \CiviCRM_API3_Exception
mattwire's avatar
mattwire committed
298
   * @throws \Civi\Payment\Exception\PaymentProcessorException
mattwire's avatar
mattwire committed
299
   */
300
  private function updateContributionCompleted($params) {
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
    $this->checkRequiredParams('updateContributionCompleted', ['contribution_id', 'trxn_date', 'order_reference', 'trxn_id'], $params);

    $paymentParamsKeys = [
      'contribution_id' => 'contribution_id',
      'trxn_date' => 'trxn_date',
      'order_reference' => 'order_reference',
      'trxn_id' => 'trxn_id',
      'total_amount' => 'total_amount',
      'fee_amount' => 'fee_amount',
    ];
    foreach ($paymentParamsKeys as $contributionKey => $paymentKey) {
      if (isset($params[$contributionKey])) {
        $paymentParams[$paymentKey] = $params[$contributionKey];
      }
    }
316
317
318
319

    // CiviCRM does not (currently) allow changing contribution_status=Failed to anything else.
    // But we need to go from Failed to Completed if payment succeeds following a failure.
    // So we check for Failed status and update to Pending so it will transition to Completed.
320
    // Fixed in 5.29 with https://github.com/civicrm/civicrm-core/pull/17943
321
    $failedContributionStatus = (int) CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Failed');
322
    if (isset($params['contribution_status_id']) && ((int) $params['contribution_status_id'] === $failedContributionStatus)) {
323
324
325
326
327
328
329
      $sql = "UPDATE civicrm_contribution SET contribution_status_id=%1 WHERE id=%2";
      $queryParams = [
        1 => [CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'), 'Positive'],
        2 => [$params['contribution_id'], 'Positive'],
      ];
      CRM_Core_DAO::executeQuery($sql, $queryParams);
    }
330
331
    $paymentParams['is_send_contribution_notification'] = $this->getSendEmailReceipt();
    $paymentParams['skipCleanMoney'] = TRUE;
332
333
    $paymentParams['payment_processor_id'] = $this->_paymentProcessor->getID();
    civicrm_api3('Mjwpayment', 'create_payment', $paymentParams);
mattwire's avatar
mattwire committed
334
335
336
  }

  /**
337
   * Update a contribution to failed
338
   *
339
   * @param array $params ['contribution_id', 'order_reference'{, cancel_date, cancel_reason}]
mattwire's avatar
mattwire committed
340
341
   *
   * @throws \CiviCRM_API3_Exception
mattwire's avatar
mattwire committed
342
   * @throws \Civi\Payment\Exception\PaymentProcessorException
mattwire's avatar
mattwire committed
343
   */
344
  private function updateContributionFailed($params) {
345
    $this->checkRequiredParams('updateContributionFailed', ['contribution_id', 'order_reference'], $params);
346
    civicrm_api3('Contribution', 'create', [
347
      'contribution_status_id' => 'Failed',
348
      'id' => $params['contribution_id'],
349
      'trxn_id' => $params['order_reference'],
350
351
      'cancel_date' => $params['cancel_date'] ?? NULL,
      'cancel_reason' => $params['cancel_reason'] ?? NULL,
352
    ]);
353
354
    // No financial_trxn is created so we don't need to update that.
  }
355

356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
  /**
   * Record a refund on a contribution
   * This wraps around the payment.create API to support earlier releases than features were available
   *
   * Examples:
   * $result = civicrm_api3('Payment', 'create', [
   *   'contribution_id' => 590,
   *   'total_amount' => -3,
   *   'trxn_date' => 20191105200300,
   *   'trxn_result_code' => "Test a refund with fees",
   *   'fee_amount' => -0.25,
   *   'trxn_id' => "abctx123",
   *   'order_reference' => "abcor123",
   * ]);
   *
   *  Returns:
   * "is_error": 0,
   * "version": 3,
   * "count": 1,
   * "id": 465,
   * "values": {
   *   "465": {
   *     "id": "465",
   *     "from_financial_account_id": "7",
   *     "to_financial_account_id": "6",
   *     "trxn_date": "20191105200300",
   *     "total_amount": "-3",
   *     "fee_amount": "-0.25",
   *     "net_amount": "",
   *     "currency": "USD",
   *     "is_payment": "1",
   *     "trxn_id": "abctx123",
   *     "trxn_result_code": "Test a refund with fees",
   *     "status_id": "7",
   *     "payment_processor_id": ""
   *   }
   * }
   *
   * @param array $params
   *
   * @throws \CRM_Core_Exception
   * @throws \CiviCRM_API3_Exception
   */
  protected function updateContributionRefund($params) {
    $this->checkRequiredParams('updateContributionRefund', ['contribution_id', 'total_amount'], $params);
401
402
    $params['payment_processor_id'] = $this->_paymentProcessor->getID();
    civicrm_api3('Mjwpayment', 'create_payment', $params);
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420

    // https://github.com/civicrm/civicrm-core/pull/18930
    // Set contribution status to refunded even if cancelled_payment_id is set
    if (version_compare(CRM_Utils_System::version(), '5.32', '<')) {
      if ($params['total_amount'] < 0 && !empty($params['cancelled_payment_id'])) {
        $contribution = civicrm_api3('Contribution', 'getsingle', ['id' => $params['contribution_id']]);
        $contributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $contribution['contribution_status_id']);

        if ($contributionStatus === 'Completed' && ((float) CRM_Core_BAO_FinancialTrxn::getTotalPayments($params['contribution_id'], TRUE) === 0.0)) {
          // If the contribution has previously been completed (fully paid) and now has total payments adding up to 0
          //  change status to refunded.
          $this->updateContribution([
            'contribution_id' => $params['contribution_id'],
            'contribution_status_id' => 'Refunded',
          ]);
        }
      }
    }
mattwire's avatar
mattwire committed
421
422
  }

423
424
425
426
427
428
429
430
431
432
433
  /**
   * Switch between "exit on exception" mode and "regular exception handling".
   *
   * @param bool $exitOnException Switch between:
   * - TRUE (default): Exit with HTTP response code 400 when an exception occurs
   * - FALSE: Just throw the exception regularly
   */
  public function setExceptionMode($exitOnException) {
    $this->exitOnException = $exitOnException;
  }

mattwire's avatar
mattwire committed
434
435
436
437
  /**
   * Log and throw an IPN exception
   *
   * @param string $message
mattwire's avatar
mattwire committed
438
439
   *
   * @throws \Civi\Payment\Exception\PaymentProcessorException
mattwire's avatar
mattwire committed
440
441
   */
  protected function exception($message) {
442
443
444
    $label = method_exists($this->_paymentProcessor, 'getPaymentProcessorLabel') ? $this->_paymentProcessor->getPaymentProcessorLabel() : __CLASS__;
    $errorMessage = $label . ' Exception: Event: ' . $this->eventType . ' Error: ' . $message;
    Civi::log()->error($errorMessage);
445
446
447
448
    if ($this->exitOnException) {
      http_response_code(400);
      exit(1);
    } else {
mattwire's avatar
mattwire committed
449
      Throw new \Civi\Payment\Exception\PaymentProcessorException($message);
450
    }
mattwire's avatar
mattwire committed
451
  }
mattwire's avatar
mattwire committed
452

mattwire's avatar
mattwire committed
453
}