| 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 | /** |
| 13 | * |
| 14 | * @package CRM |
| 15 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
| 16 | */ |
| 17 | |
| 18 | /** |
| 19 | * This class contains payment related functions. |
| 20 | */ |
| 21 | class CRM_Financial_BAO_Payment { |
| 22 | |
| 23 | /** |
| 24 | * Function to process additional payment for partial and refund contributions. |
| 25 | * |
| 26 | * This function is called via API payment.create function. All forms that add payments |
| 27 | * should use this. |
| 28 | * |
| 29 | * @param array $params |
| 30 | * - contribution_id |
| 31 | * - total_amount |
| 32 | * - line_item |
| 33 | * |
| 34 | * @return \CRM_Financial_DAO_FinancialTrxn |
| 35 | * |
| 36 | * @throws \CRM_Core_Exception |
| 37 | * @throws \CiviCRM_API3_Exception |
| 38 | */ |
| 39 | public static function create(array $params): CRM_Financial_DAO_FinancialTrxn { |
| 40 | $contribution = civicrm_api3('Contribution', 'getsingle', ['id' => $params['contribution_id']]); |
| 41 | $contributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_BAO_Contribution', 'contribution_status_id', $contribution['contribution_status_id']); |
| 42 | $isPaymentCompletesContribution = self::isPaymentCompletesContribution($params['contribution_id'], $params['total_amount'], $contributionStatus); |
| 43 | $lineItems = self::getPayableLineItems($params); |
| 44 | |
| 45 | $whiteList = ['check_number', 'payment_processor_id', 'fee_amount', 'total_amount', 'contribution_id', 'net_amount', 'card_type_id', 'pan_truncation', 'trxn_result_code', 'payment_instrument_id', 'trxn_id', 'trxn_date', 'order_reference']; |
| 46 | $paymentTrxnParams = array_intersect_key($params, array_fill_keys($whiteList, 1)); |
| 47 | $paymentTrxnParams['is_payment'] = 1; |
| 48 | // Really we should have a DB default. |
| 49 | $paymentTrxnParams['fee_amount'] = $paymentTrxnParams['fee_amount'] ?? 0; |
| 50 | |
| 51 | if (isset($paymentTrxnParams['payment_processor_id']) && empty($paymentTrxnParams['payment_processor_id'])) { |
| 52 | // Don't pass 0 - ie the Pay Later processor as it is a pseudo-processor. |
| 53 | unset($paymentTrxnParams['payment_processor_id']); |
| 54 | } |
| 55 | if (empty($paymentTrxnParams['payment_instrument_id'])) { |
| 56 | if (!empty($params['payment_processor_id'])) { |
| 57 | $paymentTrxnParams['payment_instrument_id'] = civicrm_api3('PaymentProcessor', 'getvalue', ['return' => 'payment_instrument_id', 'id' => $paymentTrxnParams['payment_processor_id']]); |
| 58 | } |
| 59 | else { |
| 60 | // Fall back on the payment instrument already used - should we deprecate this? |
| 61 | $paymentTrxnParams['payment_instrument_id'] = $contribution['payment_instrument_id']; |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | $paymentTrxnParams['currency'] = $contribution['currency']; |
| 66 | |
| 67 | $accountsReceivableAccount = CRM_Financial_BAO_FinancialAccount::getFinancialAccountForFinancialTypeByRelationship($contribution['financial_type_id'], 'Accounts Receivable Account is'); |
| 68 | $paymentTrxnParams['to_financial_account_id'] = CRM_Contribute_BAO_Contribution::getToFinancialAccount($contribution, $params); |
| 69 | $paymentTrxnParams['from_financial_account_id'] = $accountsReceivableAccount; |
| 70 | |
| 71 | if ($params['total_amount'] > 0) { |
| 72 | $paymentTrxnParams['status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_FinancialTrxn', 'status_id', 'Completed'); |
| 73 | } |
| 74 | elseif ($params['total_amount'] < 0) { |
| 75 | $paymentTrxnParams['status_id'] = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Refunded'); |
| 76 | } |
| 77 | |
| 78 | //If Payment is recorded on Failed contribution, update it to Pending. |
| 79 | if ($contributionStatus === 'Failed' && $params['total_amount'] > 0) { |
| 80 | //Enter a financial trxn to record a payment in receivable account |
| 81 | //as failed transaction does not insert any trxn values. Hence, if Payment is |
| 82 | //recorded on a failed contribution, the transition happens from Failed -> Pending -> Completed. |
| 83 | $ftParams = array_merge($paymentTrxnParams, [ |
| 84 | 'from_financial_account_id' => NULL, |
| 85 | 'to_financial_account_id' => $accountsReceivableAccount, |
| 86 | 'is_payment' => 0, |
| 87 | 'status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'), |
| 88 | ]); |
| 89 | CRM_Core_BAO_FinancialTrxn::create($ftParams); |
| 90 | $contributionStatus = 'Pending'; |
| 91 | self::updateContributionStatus($contribution['id'], $contributionStatus); |
| 92 | } |
| 93 | $trxn = CRM_Core_BAO_FinancialTrxn::create($paymentTrxnParams); |
| 94 | |
| 95 | if ($params['total_amount'] < 0 && !empty($params['cancelled_payment_id'])) { |
| 96 | self::reverseAllocationsFromPreviousPayment($params, $trxn->id); |
| 97 | } |
| 98 | else { |
| 99 | [$ftIds, $taxItems] = CRM_Contribute_BAO_Contribution::getLastFinancialItemIds($params['contribution_id']); |
| 100 | |
| 101 | foreach ($lineItems as $key => $value) { |
| 102 | if ($value['allocation'] === (float) 0) { |
| 103 | continue; |
| 104 | } |
| 105 | |
| 106 | if (!empty($ftIds[$value['price_field_value_id']])) { |
| 107 | $financialItemID = $ftIds[$value['price_field_value_id']]; |
| 108 | } |
| 109 | else { |
| 110 | $financialItemID = self::getNewFinancialItemID($value, $params['trxn_date'], $contribution['contact_id'], $paymentTrxnParams['currency']); |
| 111 | } |
| 112 | |
| 113 | $eftParams = [ |
| 114 | 'entity_table' => 'civicrm_financial_item', |
| 115 | 'financial_trxn_id' => $trxn->id, |
| 116 | 'entity_id' => $financialItemID, |
| 117 | 'amount' => $value['allocation'], |
| 118 | ]; |
| 119 | |
| 120 | civicrm_api3('EntityFinancialTrxn', 'create', $eftParams); |
| 121 | |
| 122 | if (array_key_exists($value['price_field_value_id'], $taxItems)) { |
| 123 | // @todo - this is expected to be broken - it should be fixed to |
| 124 | // a) have the getPayableLineItems add the amount to allocate for tax |
| 125 | // b) call EntityFinancialTrxn directly - per above. |
| 126 | // - see https://github.com/civicrm/civicrm-core/pull/14763 |
| 127 | $entityParams = [ |
| 128 | 'contribution_total_amount' => $contribution['total_amount'], |
| 129 | 'trxn_total_amount' => $params['total_amount'], |
| 130 | 'trxn_id' => $trxn->id, |
| 131 | 'line_item_amount' => $taxItems[$value['price_field_value_id']]['amount'], |
| 132 | ]; |
| 133 | $eftParams['entity_id'] = $taxItems[$value['price_field_value_id']]['financial_item_id']; |
| 134 | CRM_Contribute_BAO_Contribution::createProportionalEntry($entityParams, $eftParams); |
| 135 | } |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | if ($isPaymentCompletesContribution) { |
| 140 | if ($contributionStatus === 'Pending refund') { |
| 141 | // Ideally we could still call completetransaction as non-payment related actions should |
| 142 | // be outside this class. However, for now we just update the contribution here. |
| 143 | // Unit test cover in CRM_Event_BAO_AdditionalPaymentTest::testTransactionInfo. |
| 144 | civicrm_api3('Contribution', 'create', |
| 145 | [ |
| 146 | 'id' => $contribution['id'], |
| 147 | 'contribution_status_id' => 'Completed', |
| 148 | ] |
| 149 | ); |
| 150 | } |
| 151 | else { |
| 152 | civicrm_api3('Contribution', 'completetransaction', [ |
| 153 | 'id' => $contribution['id'], |
| 154 | 'is_post_payment_create' => TRUE, |
| 155 | 'is_email_receipt' => $params['is_send_contribution_notification'], |
| 156 | 'trxn_date' => $params['trxn_date'], |
| 157 | 'payment_instrument_id' => $paymentTrxnParams['payment_instrument_id'], |
| 158 | 'payment_processor_id' => $paymentTrxnParams['payment_processor_id'] ?? NULL, |
| 159 | ]); |
| 160 | // Get the trxn |
| 161 | $trxnId = CRM_Core_BAO_FinancialTrxn::getFinancialTrxnId($contribution['id'], 'DESC'); |
| 162 | $ftParams = ['id' => $trxnId['financialTrxnId']]; |
| 163 | $trxn = CRM_Core_BAO_FinancialTrxn::retrieve($ftParams); |
| 164 | } |
| 165 | } |
| 166 | elseif ($contributionStatus === 'Pending' && $params['total_amount'] > 0) { |
| 167 | self::updateContributionStatus($contribution['id'], 'Partially Paid'); |
| 168 | $participantPayments = civicrm_api3('ParticipantPayment', 'get', [ |
| 169 | 'contribution_id' => $contribution['id'], |
| 170 | 'participant_id.status_id' => ['IN' => ['Pending from pay later', 'Pending from incomplete transaction']], |
| 171 | ])['values']; |
| 172 | foreach ($participantPayments as $participantPayment) { |
| 173 | civicrm_api3('Participant', 'create', ['id' => $participantPayment['participant_id'], 'status_id' => 'Partially paid']); |
| 174 | } |
| 175 | } |
| 176 | elseif ($contributionStatus === 'Completed' && ((float) CRM_Core_BAO_FinancialTrxn::getTotalPayments($contribution['id'], TRUE) === 0.0)) { |
| 177 | // If the contribution has previously been completed (fully paid) and now has total payments adding up to 0 |
| 178 | // change status to refunded. |
| 179 | self::updateContributionStatus($contribution['id'], 'Refunded'); |
| 180 | } |
| 181 | self::updateRelatedContribution($params, $params['contribution_id']); |
| 182 | CRM_Contribute_BAO_Contribution::recordPaymentActivity($params['contribution_id'], CRM_Utils_Array::value('participant_id', $params), $params['total_amount'], $trxn->currency, $trxn->trxn_date); |
| 183 | return $trxn; |
| 184 | } |
| 185 | |
| 186 | /** |
| 187 | * Function to update contribution's check_number and trxn_id by |
| 188 | * concatenating values from financial trxn's check_number and trxn_id |
| 189 | * respectively |
| 190 | * |
| 191 | * @param array $params |
| 192 | * @param int $contributionID |
| 193 | * |
| 194 | * @throws \CiviCRM_API3_Exception |
| 195 | */ |
| 196 | public static function updateRelatedContribution(array $params, int $contributionID): void { |
| 197 | $contributionDAO = new CRM_Contribute_DAO_Contribution(); |
| 198 | $contributionDAO->id = $contributionID; |
| 199 | $contributionDAO->find(TRUE); |
| 200 | if (isset($params['fee_amount'])) { |
| 201 | // Update contribution.fee_amount to be be the total of all fees |
| 202 | // since the payment is already saved the total here will be right. |
| 203 | $payments = civicrm_api3('Payment', 'get', [ |
| 204 | 'contribution_id' => $contributionID, |
| 205 | 'return' => 'fee_amount', |
| 206 | ])['values']; |
| 207 | $totalFees = 0; |
| 208 | foreach ($payments as $payment) { |
| 209 | $totalFees += $payment['fee_amount'] ?? 0; |
| 210 | } |
| 211 | $contributionDAO->fee_amount = $totalFees; |
| 212 | $contributionDAO->net_amount = $contributionDAO->total_amount - $contributionDAO->fee_amount; |
| 213 | } |
| 214 | |
| 215 | foreach (['trxn_id', 'check_number'] as $fieldName) { |
| 216 | if (!empty($params[$fieldName])) { |
| 217 | $values = []; |
| 218 | if (!empty($contributionDAO->$fieldName)) { |
| 219 | $values = explode(',', $contributionDAO->$fieldName); |
| 220 | } |
| 221 | // if submitted check_number or trxn_id value is |
| 222 | // already present then ignore else add to $values array |
| 223 | if (!in_array($params[$fieldName], $values)) { |
| 224 | $values[] = $params[$fieldName]; |
| 225 | } |
| 226 | $contributionDAO->$fieldName = implode(',', $values); |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | $contributionDAO->save(); |
| 231 | } |
| 232 | |
| 233 | /** |
| 234 | * Send an email confirming a payment that has been received. |
| 235 | * |
| 236 | * @param array $params |
| 237 | * |
| 238 | * @return array |
| 239 | * |
| 240 | * @throws \CiviCRM_API3_Exception |
| 241 | */ |
| 242 | public static function sendConfirmation($params) { |
| 243 | |
| 244 | $entities = self::loadRelatedEntities($params['id']); |
| 245 | $sendTemplateParams = [ |
| 246 | 'groupName' => 'msg_tpl_workflow_contribution', |
| 247 | 'valueName' => 'payment_or_refund_notification', |
| 248 | 'PDFFilename' => ts('notification') . '.pdf', |
| 249 | 'contactId' => $entities['contact']['id'], |
| 250 | 'toName' => $entities['contact']['display_name'], |
| 251 | 'toEmail' => $entities['contact']['email'], |
| 252 | 'tplParams' => self::getConfirmationTemplateParameters($entities), |
| 253 | ]; |
| 254 | if (!empty($params['from']) && !empty($params['check_permissions'])) { |
| 255 | // Filter from against permitted emails. |
| 256 | $validEmails = self::getValidFromEmailsForPayment($entities['event']['id'] ?? NULL); |
| 257 | if (!isset($validEmails[$params['from']])) { |
| 258 | // Ignore unpermitted parameter. |
| 259 | unset($params['from']); |
| 260 | } |
| 261 | } |
| 262 | $sendTemplateParams['from'] = $params['from'] ?? key(CRM_Core_BAO_Email::domainEmails()); |
| 263 | return CRM_Core_BAO_MessageTemplate::sendTemplate($sendTemplateParams); |
| 264 | } |
| 265 | |
| 266 | /** |
| 267 | * Get valid from emails for payment. |
| 268 | * |
| 269 | * @param int $eventID |
| 270 | * |
| 271 | * @return array |
| 272 | */ |
| 273 | public static function getValidFromEmailsForPayment($eventID = NULL) { |
| 274 | if ($eventID) { |
| 275 | $emails = CRM_Event_BAO_Event::getFromEmailIds($eventID); |
| 276 | } |
| 277 | else { |
| 278 | $emails['from_email_id'] = CRM_Core_BAO_Email::getFromEmail(); |
| 279 | } |
| 280 | return $emails['from_email_id']; |
| 281 | } |
| 282 | |
| 283 | /** |
| 284 | * Load entities related to the current payment id. |
| 285 | * |
| 286 | * This gives us all the data we need to send an email confirmation but avoiding |
| 287 | * getting anything not tested for the confirmations. We retrieve the 'full' event as |
| 288 | * it has been traditionally assigned in full. |
| 289 | * |
| 290 | * @param int $id |
| 291 | * |
| 292 | * @return array |
| 293 | * - contact = ['id' => x, 'display_name' => y, 'email' => z] |
| 294 | * - event = [.... full event details......] |
| 295 | * - contribution = ['id' => x], |
| 296 | * - payment = [payment info + payment summary info] |
| 297 | * @throws \CiviCRM_API3_Exception |
| 298 | */ |
| 299 | protected static function loadRelatedEntities($id) { |
| 300 | $entities = []; |
| 301 | $contributionID = (int) civicrm_api3('EntityFinancialTrxn', 'getvalue', [ |
| 302 | 'financial_trxn_id' => $id, |
| 303 | 'entity_table' => 'civicrm_contribution', |
| 304 | 'return' => 'entity_id', |
| 305 | ]); |
| 306 | $entities['contribution'] = ['id' => $contributionID]; |
| 307 | $entities['payment'] = array_merge(civicrm_api3('FinancialTrxn', 'getsingle', ['id' => $id]), |
| 308 | CRM_Contribute_BAO_Contribution::getPaymentInfo($contributionID) |
| 309 | ); |
| 310 | |
| 311 | $contactID = self::getPaymentContactID($contributionID); |
| 312 | [$displayName, $email] = CRM_Contact_BAO_Contact_Location::getEmailDetails($contactID); |
| 313 | $entities['contact'] = ['id' => $contactID, 'display_name' => $displayName, 'email' => $email]; |
| 314 | $contact = civicrm_api3('Contact', 'getsingle', ['id' => $contactID, 'return' => 'email_greeting']); |
| 315 | $entities['contact']['email_greeting'] = $contact['email_greeting_display']; |
| 316 | |
| 317 | $participantRecords = civicrm_api3('ParticipantPayment', 'get', [ |
| 318 | 'contribution_id' => $contributionID, |
| 319 | 'api.Participant.get' => ['return' => 'event_id'], |
| 320 | 'sequential' => 1, |
| 321 | ])['values']; |
| 322 | if (!empty($participantRecords)) { |
| 323 | $entities['event'] = civicrm_api3('Event', 'getsingle', ['id' => $participantRecords[0]['api.Participant.get']['values'][0]['event_id']]); |
| 324 | if (!empty($entities['event']['is_show_location'])) { |
| 325 | $locationParams = [ |
| 326 | 'entity_id' => $entities['event']['id'], |
| 327 | 'entity_table' => 'civicrm_event', |
| 328 | ]; |
| 329 | $entities['location'] = CRM_Core_BAO_Location::getValues($locationParams, TRUE); |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | return $entities; |
| 334 | } |
| 335 | |
| 336 | /** |
| 337 | * @param int $contributionID |
| 338 | * |
| 339 | * @return int |
| 340 | * @throws \CiviCRM_API3_Exception |
| 341 | * @throws \CiviCRM_API3_Exception |
| 342 | */ |
| 343 | public static function getPaymentContactID($contributionID) { |
| 344 | $contribution = civicrm_api3('Contribution', 'getsingle', [ |
| 345 | 'id' => $contributionID , |
| 346 | 'return' => ['contact_id'], |
| 347 | ]); |
| 348 | return (int) $contribution['contact_id']; |
| 349 | } |
| 350 | |
| 351 | /** |
| 352 | * @param array $entities |
| 353 | * Related entities as an array keyed by the various entities. |
| 354 | * |
| 355 | * @return array |
| 356 | * Values required for the notification |
| 357 | * - contact_id |
| 358 | * - template_variables |
| 359 | * - event (DAO of event if relevant) |
| 360 | */ |
| 361 | public static function getConfirmationTemplateParameters($entities) { |
| 362 | $templateVariables = [ |
| 363 | 'contactDisplayName' => $entities['contact']['display_name'], |
| 364 | 'emailGreeting' => $entities['contact']['email_greeting'], |
| 365 | 'totalAmount' => $entities['payment']['total'], |
| 366 | 'amountOwed' => $entities['payment']['balance'], |
| 367 | 'totalPaid' => $entities['payment']['paid'], |
| 368 | 'paymentAmount' => $entities['payment']['total_amount'], |
| 369 | 'checkNumber' => $entities['payment']['check_number'] ?? NULL, |
| 370 | 'receive_date' => $entities['payment']['trxn_date'], |
| 371 | 'paidBy' => CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_FinancialTrxn', 'payment_instrument_id', $entities['payment']['payment_instrument_id']), |
| 372 | 'isShowLocation' => (!empty($entities['event']) ? $entities['event']['is_show_location'] : FALSE), |
| 373 | 'location' => $entities['location'] ?? NULL, |
| 374 | 'event' => $entities['event'] ?? NULL, |
| 375 | 'component' => (!empty($entities['event']) ? 'event' : 'contribution'), |
| 376 | 'isRefund' => $entities['payment']['total_amount'] < 0, |
| 377 | 'isAmountzero' => $entities['payment']['total_amount'] === 0, |
| 378 | 'refundAmount' => ($entities['payment']['total_amount'] < 0 ? $entities['payment']['total_amount'] : NULL), |
| 379 | 'paymentsComplete' => ($entities['payment']['balance'] == 0), |
| 380 | ]; |
| 381 | |
| 382 | return self::filterUntestedTemplateVariables($templateVariables); |
| 383 | } |
| 384 | |
| 385 | /** |
| 386 | * Filter out any untested variables. |
| 387 | * |
| 388 | * This just serves to highlight if any variables are added without a unit test also being added. |
| 389 | * |
| 390 | * (if hit then add a unit test for the param & add to this array). |
| 391 | * |
| 392 | * @param array $params |
| 393 | * |
| 394 | * @return array |
| 395 | */ |
| 396 | public static function filterUntestedTemplateVariables($params) { |
| 397 | $testedTemplateVariables = [ |
| 398 | 'contactDisplayName', |
| 399 | 'totalAmount', |
| 400 | 'amountOwed', |
| 401 | 'paymentAmount', |
| 402 | 'event', |
| 403 | 'component', |
| 404 | 'checkNumber', |
| 405 | 'receive_date', |
| 406 | 'paidBy', |
| 407 | 'isShowLocation', |
| 408 | 'location', |
| 409 | 'isRefund', |
| 410 | 'isAmountzero', |
| 411 | 'refundAmount', |
| 412 | 'totalPaid', |
| 413 | 'paymentsComplete', |
| 414 | 'emailGreeting', |
| 415 | ]; |
| 416 | // These are assigned by the payment form - they still 'get through' from the |
| 417 | // form for now without being in here but we should ideally load |
| 418 | // and assign. Note we should update the tpl to use {if $billingName} |
| 419 | // and ditch contributeMode - although it might need to be deprecated rather than removed. |
| 420 | $todoParams = [ |
| 421 | 'contributeMode', |
| 422 | 'billingName', |
| 423 | 'address', |
| 424 | 'credit_card_type', |
| 425 | 'credit_card_number', |
| 426 | 'credit_card_exp_date', |
| 427 | ]; |
| 428 | $filteredParams = []; |
| 429 | foreach ($testedTemplateVariables as $templateVariable) { |
| 430 | // This will cause an a-notice if any are NOT set - by design. Ensuring |
| 431 | // they are set prevents leakage. |
| 432 | $filteredParams[$templateVariable] = $params[$templateVariable]; |
| 433 | } |
| 434 | return $filteredParams; |
| 435 | } |
| 436 | |
| 437 | /** |
| 438 | * Does this payment complete the contribution. |
| 439 | * |
| 440 | * @param int $contributionID |
| 441 | * @param float $paymentAmount |
| 442 | * @param string $previousStatus |
| 443 | * |
| 444 | * @return bool |
| 445 | */ |
| 446 | protected static function isPaymentCompletesContribution($contributionID, $paymentAmount, $previousStatus) { |
| 447 | if ($previousStatus === 'Completed') { |
| 448 | return FALSE; |
| 449 | } |
| 450 | $outstandingBalance = CRM_Contribute_BAO_Contribution::getContributionBalance($contributionID); |
| 451 | $cmp = bccomp($paymentAmount, $outstandingBalance, 5); |
| 452 | return ($cmp == 0 || $cmp == 1); |
| 453 | } |
| 454 | |
| 455 | /** |
| 456 | * Update the status of the contribution. |
| 457 | * |
| 458 | * We pass the is_post_payment_create as we have already created the line items |
| 459 | * |
| 460 | * @param int $contributionID |
| 461 | * @param string $status |
| 462 | * |
| 463 | * @throws \CiviCRM_API3_Exception |
| 464 | */ |
| 465 | private static function updateContributionStatus(int $contributionID, string $status) { |
| 466 | civicrm_api3('Contribution', 'create', |
| 467 | [ |
| 468 | 'id' => $contributionID, |
| 469 | 'is_post_payment_create' => TRUE, |
| 470 | 'contribution_status_id' => $status, |
| 471 | ] |
| 472 | ); |
| 473 | } |
| 474 | |
| 475 | /** |
| 476 | * Get the line items for the contribution. |
| 477 | * |
| 478 | * Retrieve the line items and wrangle the following |
| 479 | * |
| 480 | * - get the outstanding balance on a line item basis. |
| 481 | * - determine what amount is being paid on this line item - we get the total being paid |
| 482 | * for the whole contribution and determine the ratio of the balance that is being paid |
| 483 | * and then assign apply that ratio to each line item. |
| 484 | * - if overrides have been passed in we use those amounts instead. |
| 485 | * |
| 486 | * @param $params |
| 487 | * |
| 488 | * @return array |
| 489 | * @throws \CiviCRM_API3_Exception |
| 490 | */ |
| 491 | protected static function getPayableLineItems($params): array { |
| 492 | $lineItems = CRM_Price_BAO_LineItem::getLineItemsByContributionID($params['contribution_id']); |
| 493 | $lineItemOverrides = []; |
| 494 | if (!empty($params['line_item'])) { |
| 495 | // The format is a bit weird here - $params['line_item'] => [[1 => 10], [2 => 40]] |
| 496 | // Squash to [1 => 10, 2 => 40] |
| 497 | foreach ($params['line_item'] as $lineItem) { |
| 498 | $lineItemOverrides += $lineItem; |
| 499 | } |
| 500 | } |
| 501 | $outstandingBalance = CRM_Contribute_BAO_Contribution::getContributionBalance($params['contribution_id']); |
| 502 | if ($outstandingBalance !== 0.0) { |
| 503 | $ratio = $params['total_amount'] / $outstandingBalance; |
| 504 | } |
| 505 | elseif ($params['total_amount'] < 0) { |
| 506 | $ratio = $params['total_amount'] / (float) CRM_Core_BAO_FinancialTrxn::getTotalPayments($params['contribution_id'], TRUE); |
| 507 | } |
| 508 | else { |
| 509 | // Help we are making a payment but no money is owed. We won't allocate the overpayment to any line item. |
| 510 | $ratio = 0; |
| 511 | } |
| 512 | foreach ($lineItems as $lineItemID => $lineItem) { |
| 513 | // Ideally id would be set deeper but for now just add in here. |
| 514 | $lineItems[$lineItemID]['id'] = $lineItemID; |
| 515 | $lineItems[$lineItemID]['paid'] = self::getAmountOfLineItemPaid($lineItemID); |
| 516 | $lineItems[$lineItemID]['balance'] = $lineItem['subTotal'] - $lineItems[$lineItemID]['paid']; |
| 517 | if (!empty($lineItemOverrides)) { |
| 518 | $lineItems[$lineItemID]['allocation'] = $lineItemOverrides[$lineItemID] ?? NULL; |
| 519 | } |
| 520 | else { |
| 521 | if (empty($lineItems[$lineItemID]['balance']) && !empty($ratio) && $params['total_amount'] < 0) { |
| 522 | $lineItems[$lineItemID]['allocation'] = $lineItem['subTotal'] * $ratio; |
| 523 | } |
| 524 | else { |
| 525 | $lineItems[$lineItemID]['allocation'] = $lineItems[$lineItemID]['balance'] * $ratio; |
| 526 | } |
| 527 | } |
| 528 | } |
| 529 | return $lineItems; |
| 530 | } |
| 531 | |
| 532 | /** |
| 533 | * Get the amount paid so far against this line item. |
| 534 | * |
| 535 | * @param int $lineItemID |
| 536 | * |
| 537 | * @return float |
| 538 | * |
| 539 | * @throws \CiviCRM_API3_Exception |
| 540 | */ |
| 541 | protected static function getAmountOfLineItemPaid($lineItemID) { |
| 542 | $paid = 0; |
| 543 | $financialItems = civicrm_api3('FinancialItem', 'get', [ |
| 544 | 'entity_id' => $lineItemID, |
| 545 | 'entity_table' => 'civicrm_line_item', |
| 546 | 'options' => ['sort' => 'id DESC', 'limit' => 0], |
| 547 | ])['values']; |
| 548 | if (!empty($financialItems)) { |
| 549 | $entityFinancialTrxns = civicrm_api3('EntityFinancialTrxn', 'get', [ |
| 550 | 'entity_table' => 'civicrm_financial_item', |
| 551 | 'entity_id' => ['IN' => array_keys($financialItems)], |
| 552 | 'options' => ['limit' => 0], |
| 553 | 'financial_trxn_id.is_payment' => 1, |
| 554 | ])['values']; |
| 555 | foreach ($entityFinancialTrxns as $entityFinancialTrxn) { |
| 556 | $paid += $entityFinancialTrxn['amount']; |
| 557 | } |
| 558 | } |
| 559 | return (float) $paid; |
| 560 | } |
| 561 | |
| 562 | /** |
| 563 | * Reverse the entity financial transactions associated with the cancelled payment. |
| 564 | * |
| 565 | * The reversals are linked to the new payemnt. |
| 566 | * |
| 567 | * @param array $params |
| 568 | * @param int $trxnID |
| 569 | * |
| 570 | * @throws \CiviCRM_API3_Exception |
| 571 | */ |
| 572 | protected static function reverseAllocationsFromPreviousPayment($params, $trxnID) { |
| 573 | // Do a direct reversal of any entity_financial_trxn records being cancelled. |
| 574 | $entityFinancialTrxns = civicrm_api3('EntityFinancialTrxn', 'get', [ |
| 575 | 'entity_table' => 'civicrm_financial_item', |
| 576 | 'options' => ['limit' => 0], |
| 577 | 'financial_trxn_id.id' => $params['cancelled_payment_id'], |
| 578 | ])['values']; |
| 579 | foreach ($entityFinancialTrxns as $entityFinancialTrxn) { |
| 580 | civicrm_api3('EntityFinancialTrxn', 'create', [ |
| 581 | 'entity_table' => 'civicrm_financial_item', |
| 582 | 'entity_id' => $entityFinancialTrxn['entity_id'], |
| 583 | 'amount' => -$entityFinancialTrxn['amount'], |
| 584 | 'financial_trxn_id' => $trxnID, |
| 585 | ]); |
| 586 | } |
| 587 | } |
| 588 | |
| 589 | /** |
| 590 | * Create a financial items & return the ID. |
| 591 | * |
| 592 | * Ideally this will never be called. |
| 593 | * |
| 594 | * However, I hit a scenario in testing where 'something' had created a pending payment with |
| 595 | * no financial items and that would result in a fatal error without handling here. I failed |
| 596 | * to replicate & am not investigating via a new test methodology |
| 597 | * https://github.com/civicrm/civicrm-core/pull/15706 |
| 598 | * |
| 599 | * After this is in I will do more digging & once I feel confident new instances are not being |
| 600 | * created I will add deprecation notices into this function with a view to removing. |
| 601 | * |
| 602 | * However, I think we want to add it in 5.20 as there is a risk of users experiencing an error |
| 603 | * if there is incorrect data & we need time to ensure that what I hit was not a 'thing. |
| 604 | * (it might be the demo site data is a bit flawed & that was the issue). |
| 605 | * |
| 606 | * @param array $lineItem |
| 607 | * @param string $trxn_date |
| 608 | * @param int $contactID |
| 609 | * @param string $currency |
| 610 | * |
| 611 | * @return int |
| 612 | * |
| 613 | * @throws \CiviCRM_API3_Exception |
| 614 | */ |
| 615 | protected static function getNewFinancialItemID($lineItem, $trxn_date, $contactID, $currency): int { |
| 616 | $financialAccount = CRM_Financial_BAO_FinancialAccount::getFinancialAccountForFinancialTypeByRelationship( |
| 617 | $lineItem['financial_type_id'], |
| 618 | 'Income Account Is' |
| 619 | ); |
| 620 | $itemParams = [ |
| 621 | 'transaction_date' => $trxn_date, |
| 622 | 'contact_id' => $contactID, |
| 623 | 'currency' => $currency, |
| 624 | 'amount' => $lineItem['line_total'], |
| 625 | 'description' => $lineItem['label'], |
| 626 | 'status_id' => 'Unpaid', |
| 627 | 'financial_account_id' => $financialAccount, |
| 628 | 'entity_table' => 'civicrm_line_item', |
| 629 | 'entity_id' => $lineItem['id'], |
| 630 | ]; |
| 631 | return (int) civicrm_api3('FinancialItem', 'create', $itemParams)['id']; |
| 632 | } |
| 633 | |
| 634 | } |