diff --git a/CRM/Contribute/BAO/Contribution.php b/CRM/Contribute/BAO/Contribution.php index 75afd52626f1886e39dab59f034ce31dba7b4930..dabaa52f5ac9f3aa46c27d1234428fd76a7b8bdc 100644 --- a/CRM/Contribute/BAO/Contribution.php +++ b/CRM/Contribute/BAO/Contribution.php @@ -4481,10 +4481,6 @@ INNER JOIN civicrm_activity ON civicrm_activity_contact.activity_id = civicrm_ac } $changeDate = CRM_Utils_Array::value('trxn_date', $input, date('YmdHis')); - if (empty($contributionParams['receive_date']) && $changeDate) { - $contributionParams['receive_date'] = $changeDate; - } - self::repeatTransaction($contribution, $input, $contributionParams, $paymentProcessorId); $contributionParams['financial_type_id'] = $contribution->financial_type_id; diff --git a/CRM/Contribute/Form/Task/PDF.php b/CRM/Contribute/Form/Task/PDF.php index 76dac1989fb10104e73d07fed177da16684294bb..7552bf9222908043946b581a0532337cf0e741c7 100644 --- a/CRM/Contribute/Form/Task/PDF.php +++ b/CRM/Contribute/Form/Task/PDF.php @@ -253,7 +253,7 @@ AND {$this->_componentClause}"; $pdfElements['contribIDs'] = implode(',', $contribIds); - $pdfElements['details'] = CRM_Contribute_Form_Task_Status::getDetails($pdfElements['contribIDs']); + $pdfElements['details'] = self::getDetails($pdfElements['contribIDs']); $pdfElements['baseIPN'] = new CRM_Core_Payment_BaseIPN(); @@ -295,4 +295,47 @@ AND {$this->_componentClause}"; return $pdfElements; } + /** + * @param string $contributionIDs + * + * @return array + */ + private static function getDetails($contributionIDs) { + if (empty($contributionIDs)) { + return []; + } + $query = " +SELECT c.id as contribution_id, + c.contact_id as contact_id , + mp.membership_id as membership_id , + pp.participant_id as participant_id , + p.event_id as event_id +FROM civicrm_contribution c +LEFT JOIN civicrm_membership_payment mp ON mp.contribution_id = c.id +LEFT JOIN civicrm_participant_payment pp ON pp.contribution_id = c.id +LEFT JOIN civicrm_participant p ON pp.participant_id = p.id +WHERE c.id IN ( $contributionIDs )"; + + $rows = []; + $dao = CRM_Core_DAO::executeQuery($query); + + while ($dao->fetch()) { + $rows[$dao->contribution_id]['component'] = $dao->participant_id ? 'event' : 'contribute'; + $rows[$dao->contribution_id]['contact'] = $dao->contact_id; + if ($dao->membership_id) { + if (!array_key_exists('membership', $rows[$dao->contribution_id])) { + $rows[$dao->contribution_id]['membership'] = []; + } + $rows[$dao->contribution_id]['membership'][] = $dao->membership_id; + } + if ($dao->participant_id) { + $rows[$dao->contribution_id]['participant'] = $dao->participant_id; + } + if ($dao->event_id) { + $rows[$dao->contribution_id]['event'] = $dao->event_id; + } + } + return $rows; + } + } diff --git a/CRM/Contribute/Form/Task/Status.php b/CRM/Contribute/Form/Task/Status.php index e3c2a6e7daa1b769f2e689ddc4efc3b55755c9dc..b501327a88f5757e27a1e0f25b5f9eab757cd832 100644 --- a/CRM/Contribute/Form/Task/Status.php +++ b/CRM/Contribute/Form/Task/Status.php @@ -68,21 +68,6 @@ AND {$this->_componentClause}"; * Build the form object. */ public function buildQuickForm() { - $status = CRM_Contribute_BAO_Contribution_Utils::getContributionStatuses( - 'contribution', $this->_contributionIds[0] - ); - $byName = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name'); - // FIXME: if it's invalid to transition from Pending to - // In Progress or Overdue, we should move that logic to - // CRM_Contribute_BAO_Contribution_Utils::getContributionStatuses. - foreach (['Pending', 'In Progress', 'Overdue'] as $suppress) { - unset($status[CRM_Utils_Array::key($suppress, $byName)]); - } - $this->add('select', 'contribution_status_id', - ts('Contribution Status'), - $status, - TRUE - ); $this->add('checkbox', 'is_email_receipt', ts('Send e-mail receipt')); $this->setDefaults(['is_email_receipt' => 1]); @@ -198,7 +183,7 @@ AND co.id IN ( $contribIDs )"; // submit the form with values. self::processForm($this, $params); - CRM_Core_Session::setStatus(ts('Contribution status has been updated for selected record(s).'), ts('Status Updated'), 'success'); + CRM_Core_Session::setStatus(ts('Payments have been recorded for selected record(s).'), ts('Payments recorded'), 'success'); } /** @@ -212,124 +197,26 @@ AND co.id IN ( $contribIDs )"; * @throws \Exception */ public static function processForm($form, $params) { - $statusID = $params['contribution_status_id'] ?? NULL; - $baseIPN = new CRM_Core_Payment_BaseIPN(); - - // get the missing pieces for each contribution - $contribIDs = implode(',', $form->_contributionIds); - $details = self::getDetails($contribIDs); - $template = CRM_Core_Smarty::singleton(); - - // for each contribution id, we just call the baseIPN stuff foreach ($form->_rows as $row) { - $input = $ids = $objects = []; - $input['component'] = $details[$row['contribution_id']]['component']; - - $ids['contact'] = $row['contact_id']; - $ids['contribution'] = $row['contribution_id']; - $ids['contributionRecur'] = NULL; - $ids['contributionPage'] = NULL; - $ids['membership'] = $details[$row['contribution_id']]['membership'] ?? NULL; - $ids['participant'] = $details[$row['contribution_id']]['participant'] ?? NULL; - $ids['event'] = $details[$row['contribution_id']]['event'] ?? NULL; - - if (!$baseIPN->validateData($input, $ids, $objects, FALSE)) { - CRM_Core_Error::statusBounce('Supplied data was not able to be validated'); - } - - $contribution = &$objects['contribution']; - - $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, - 'name' - ); - - if ($statusID == array_search('Cancelled', $contributionStatuses)) { - $transaction = new CRM_Core_Transaction(); - $baseIPN->cancelled($objects, $transaction); - $transaction->commit(); - continue; - } - elseif ($statusID == array_search('Failed', $contributionStatuses)) { - $transaction = new CRM_Core_Transaction(); - $baseIPN->failed($objects, $transaction); - $transaction->commit(); - continue; - } - - // status is not pending - if ($contribution->contribution_status_id != array_search('Pending', - $contributionStatuses - ) - ) { - continue; - } - - // set some fake input values so we can reuse IPN code - $input['amount'] = $contribution->total_amount; - $input['is_test'] = $contribution->is_test; - $input['fee_amount'] = $params["fee_amount_{$row['contribution_id']}"]; - $input['check_number'] = $params["check_number_{$row['contribution_id']}"]; - $input['payment_instrument_id'] = $params["payment_instrument_id_{$row['contribution_id']}"]; - $input['net_amount'] = $contribution->total_amount - $input['fee_amount']; - - if (!empty($params["trxn_id_{$row['contribution_id']}"])) { - $input['trxn_id'] = trim($params["trxn_id_{$row['contribution_id']}"]); - } - else { - $input['trxn_id'] = $contribution->invoice_id; - } - $input['trxn_date'] = $params["trxn_date_{$row['contribution_id']}"] . ' ' . date('H:i:s'); - $input['is_email_receipt'] = !empty($params['is_email_receipt']); - - // @todo calling CRM_Contribute_BAO_Contribution::completeOrder like this is a pattern in it's last gasps. Call contribute.completetransaction api. - CRM_Contribute_BAO_Contribution::completeOrder($input, $ids, $objects); - - // reset template values before processing next transactions - $template->clearTemplateVars(); - } - } - - /** - * @param string $contributionIDs - * - * @return array - */ - public static function &getDetails($contributionIDs) { - if (empty($contributionIDs)) { - return []; - } - $query = " -SELECT c.id as contribution_id, - c.contact_id as contact_id , - mp.membership_id as membership_id , - pp.participant_id as participant_id , - p.event_id as event_id -FROM civicrm_contribution c -LEFT JOIN civicrm_membership_payment mp ON mp.contribution_id = c.id -LEFT JOIN civicrm_participant_payment pp ON pp.contribution_id = c.id -LEFT JOIN civicrm_participant p ON pp.participant_id = p.id -WHERE c.id IN ( $contributionIDs )"; - - $rows = []; - $dao = CRM_Core_DAO::executeQuery($query); - - while ($dao->fetch()) { - $rows[$dao->contribution_id]['component'] = $dao->participant_id ? 'event' : 'contribute'; - $rows[$dao->contribution_id]['contact'] = $dao->contact_id; - if ($dao->membership_id) { - if (!array_key_exists('membership', $rows[$dao->contribution_id])) { - $rows[$dao->contribution_id]['membership'] = []; - } - $rows[$dao->contribution_id]['membership'][] = $dao->membership_id; - } - if ($dao->participant_id) { - $rows[$dao->contribution_id]['participant'] = $dao->participant_id; - } - if ($dao->event_id) { - $rows[$dao->contribution_id]['event'] = $dao->event_id; - } + $contribData = civicrm_api3('Contribution', 'getSingle', ['id' => $row['contribution_id']]); + $trxnParams = [ + 'contribution_id' => $row['contribution_id'], + // We are safe assuming that payments will be for the total amount of + // the contribution because the contributions must be in "Pending" + // status. + 'total_amount' => $contribData['total_amount'], + 'fee_amount' => $params["fee_amount_{$row['contribution_id']}"], + 'check_number' => $params["check_number_{$row['contribution_id']}"], + 'payment_instrument_id' => $params["payment_instrument_id_{$row['contribution_id']}"], + 'net_amount' => $contribData['total_amount'] - $params["fee_amount_{$row['contribution_id']}"], + // Not sure why to default to invoice_id, but that's what the form has + // been doing historically + 'trxn_id' => $params["trxn_id_{$row['contribution_id']}"] ?? $contribData['invoice_id'], + 'trxn_date' => $params["trxn_date_{$row['contribution_id']}"] ?? 'now', + 'is_send_contribution_notification' => !empty($params['is_email_receipt']), + ]; + $result = civicrm_api3('Payment', 'create', $trxnParams); } - return $rows; } } diff --git a/CRM/Contribute/Task.php b/CRM/Contribute/Task.php index 9a88cfc6d51ad802e97268955d6a8e6e82d02a0b..24feae8a8797bf4ffe93389c0a468782a6e46544 100644 --- a/CRM/Contribute/Task.php +++ b/CRM/Contribute/Task.php @@ -81,7 +81,7 @@ class CRM_Contribute_Task extends CRM_Core_Task { 'result' => TRUE, ], self::UPDATE_STATUS => [ - 'title' => ts('Update pending contribution status'), + 'title' => ts('Record payments for contributions'), 'class' => 'CRM_Contribute_Form_Task_Status', 'result' => TRUE, ], diff --git a/api/v3/Contribution.php b/api/v3/Contribution.php index bbe016918144e5c0ad12852764d0970b6c4ff544..f80cd79d2944a2cb4d4117eef601dc50c7e831b2 100644 --- a/api/v3/Contribution.php +++ b/api/v3/Contribution.php @@ -488,7 +488,8 @@ function civicrm_api3_contribution_completetransaction($params) { elseif ($contribution->contribution_status_id == CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed')) { throw new API_Exception(ts('Contribution already completed'), 'contribution_completed'); } - $input['trxn_id'] = !empty($params['trxn_id']) ? $params['trxn_id'] : $contribution->trxn_id; + $input['trxn_id'] = $params['trxn_id'] ?? $contribution->trxn_id; + if (!empty($params['fee_amount'])) { $input['fee_amount'] = $params['fee_amount']; } diff --git a/templates/CRM/Contribute/Form/Task/Status.tpl b/templates/CRM/Contribute/Form/Task/Status.tpl index 635c4890d754714b33b4f0cd98b697a7a999e0e6..9d16b90aeb7f2741d9e6b1c8932c838042a6ef8d 100644 --- a/templates/CRM/Contribute/Form/Task/Status.tpl +++ b/templates/CRM/Contribute/Form/Task/Status.tpl @@ -8,50 +8,49 @@ +--------------------------------------------------------------------+ *} <div class="form-item crm-block crm-form-block crm-contribution-form-block"> -<div class="help"> - {ts}Use this form to record received payments for 'pay later' online contributions, membership signups and event registrations. You can use the Transaction ID field to record account+check number, bank transfer identifier, or other unique payment identifier.{/ts} -</div> -<fieldset> - <legend>{ts}Update Contribution Status{/ts}</legend> - <table class="form-layout-compressed"> - <tr class="crm-contribution-form-block-contribution_status_id"><td class="label">{$form.contribution_status_id.label}</td><td class="html-adjust">{$form.contribution_status_id.html}<br /> - <span class="description">{ts}Assign the selected status to all contributions listed below.{/ts}</td></tr> - <tr class="crm-contribution-form-block-is_email_receipt"><td class="label">{$form.is_email_receipt.label}</td> - <td class="html-adjust">{$form.is_email_receipt.html}<br /> - <span class="description">{ts}When checked CiviCRM will send an e-mail receipt to the donor. Leave unchecked when you don't want to send an e-mail.{/ts} - </td> - </tr> - </table> -<table> -<tr class="columnheader"> - <th>{ts}Name{/ts}</th> - <th class="right">{ts}Amount{/ts} </th> - <th>{ts}Source{/ts}</th> - <th>{ts}Fee Amount{/ts}</th> - <th>{ts}Payment Method{/ts}</th> - <th>{ts}Check{/ts} #</th> - <th>{ts}Transaction ID{/ts}</th> - <th>{ts}Transaction Date{/ts}</th> -</tr> + <h3>{ts}Record payments for contributions{/ts}</h3> + <div class="help"> + <p>{ts}Use this form to record received payments for "pay later" online contributions, membership signups and event registrations. You can use the Transaction ID field to record account+check number, bank transfer identifier, or other unique payment identifier.{/ts}</p> + <p>{ts}The contribution status will be updated as appropriate. To update contribution statuses directly, return to the search results and select "Update multiple contributions".{/ts}</p> + </div> + + <table class="form-layout-compressed"> + <tr class="crm-contribution-form-block-is_email_receipt"> + <td class="label">{$form.is_email_receipt.label}</td> + <td class="html-adjust">{$form.is_email_receipt.html}<br /> + <span class="description">{ts}When checked CiviCRM will send an e-mail receipt to the donor. Leave unchecked when you don't want to send an e-mail.{/ts}</span> + </td> + </tr> + </table> + <table> + <tr class="columnheader"> + <th>{ts}Name{/ts}</th> + <th class="right">{ts}Amount{/ts} </th> + <th>{ts}Source{/ts}</th> + <th>{ts}Fee Amount{/ts}</th> + <th>{ts}Payment Method{/ts}</th> + <th>{ts}Check{/ts} #</th> + <th>{ts}Transaction ID{/ts}</th> + <th>{ts}Transaction Date{/ts}</th> + </tr> -{foreach from=$rows item=row} -<tr class="{cycle values="odd-row,even-row"}"> - <td>{$row.display_name}</td> - <td class="right nowrap">{$row.amount|crmMoney} </td> - <td>{$row.source}</td> - {assign var="element_name" value="fee_amount_"|cat:$row.contribution_id} - <td>{$form.$element_name.html}</td> - {assign var="element_name" value="payment_instrument_id_"|cat:$row.contribution_id} - <td class="form-text four">{$form.$element_name.html}</td> - {assign var="element_name" value="check_number_"|cat:$row.contribution_id} - <td class="form-text four">{$form.$element_name.html|crmAddClass:four}</td> - {assign var="element_name" value="trxn_id_"|cat:$row.contribution_id} - <td>{$form.$element_name.html|crmAddClass:eight}</td> - {assign var="element_name" value="trxn_date_"|cat:$row.contribution_id} - <td>{$form.$element_name.html}</td> -</tr> -{/foreach} -</table> + {foreach from=$rows item=row} + <tr class="{cycle values="odd-row,even-row"}"> + <td>{$row.display_name}</td> + <td class="right nowrap">{$row.amount|crmMoney} </td> + <td>{$row.source}</td> + {assign var="element_name" value="fee_amount_"|cat:$row.contribution_id} + <td>{$form.$element_name.html}</td> + {assign var="element_name" value="payment_instrument_id_"|cat:$row.contribution_id} + <td class="form-text four">{$form.$element_name.html}</td> + {assign var="element_name" value="check_number_"|cat:$row.contribution_id} + <td class="form-text four">{$form.$element_name.html|crmAddClass:four}</td> + {assign var="element_name" value="trxn_id_"|cat:$row.contribution_id} + <td>{$form.$element_name.html|crmAddClass:eight}</td> + {assign var="element_name" value="trxn_date_"|cat:$row.contribution_id} + <td>{$form.$element_name.html}</td> + </tr> + {/foreach} + </table> <div class="crm-submit-buttons">{$form.buttons.html}</div> -</fieldset> </div> diff --git a/tests/phpunit/api/v3/ContributionTest.php b/tests/phpunit/api/v3/ContributionTest.php index b5779446337865592541cd580a48ffa8b3c883dc..5de4e1caad346b98eb0fd19cfc3b93938bb25b66 100644 --- a/tests/phpunit/api/v3/ContributionTest.php +++ b/tests/phpunit/api/v3/ContributionTest.php @@ -2905,7 +2905,10 @@ class api_v3_ContributionTest extends CiviUnitTestCase { $this->callAPISuccess('contribution', 'completetransaction', ['id' => $contribution['id'], 'trxn_date' => date('Y-m-d')]); $contribution = $this->callAPISuccess('contribution', 'get', ['id' => $contribution['id'], 'sequential' => 1]); $this->assertEquals('Completed', $contribution['values'][0]['contribution_status']); - $this->assertEquals(date('Y-m-d'), date('Y-m-d', strtotime($contribution['values'][0]['receive_date']))); + // Make sure receive_date is original date and make sure payment date is today + $this->assertEquals('2012-05-11', date('Y-m-d', strtotime($contribution['values'][0]['receive_date']))); + $payment = $this->callAPISuccess('payment', 'get', ['contribution_id' => $contribution['id'], 'sequential' => 1]); + $this->assertEquals(date('Y-m-d'), date('Y-m-d', strtotime($payment['values'][0]['trxn_date']))); $mut->checkMailLog([ 'Receipt - Contribution', 'receipt_date:::' . date('Ymd'), @@ -3184,7 +3187,11 @@ class api_v3_ContributionTest extends CiviUnitTestCase { $this->assertEquals(1, $status); $mut->checkMailLog([ 'amount:::500.00', - 'receive_date:::20130201000000', + // The `receive_date` should remain as it was created. + // TODO: the latest payment transaction date (and maybe other details, + // such as amount and payment instrument) would be a useful token to make + // available. + 'receive_date:::20120511000000', "receipt_date:::\n", ]); $mut->stop(); @@ -4738,4 +4745,30 @@ class api_v3_ContributionTest extends CiviUnitTestCase { return $result; } + /** + * Make sure that recording a payment doesn't alter the receive_date of a + * pending contribution. + */ + public function testPaymentDontChangeReceiveDate() { + $params = [ + 'contact_id' => $this->_individualId, + 'total_amount' => 100, + 'receive_date' => '2020-02-02', + 'contribution_status_id' => 'Pending', + ]; + $contributionID = $this->contributionCreate($params); + $paymentParams = [ + 'contribution_id' => $contributionID, + 'total_amount' => 100, + 'trxn_date' => '2020-03-04', + ]; + $this->callAPISuccess('payment', 'create', $paymentParams); + + //check if contribution status is set to "Completed". + $contribution = $this->callAPISuccess('Contribution', 'getSingle', [ + 'id' => $contributionID, + ]); + $this->assertEquals('2020-02-02 00:00:00', $contribution['receive_date']); + } + } diff --git a/tests/phpunit/api/v3/PaymentTest.php b/tests/phpunit/api/v3/PaymentTest.php index c01bf4b60829a788b9f8e740af85491642485d2a..19ff25b8f1a833020f6bdf19583e457fef6da60d 100644 --- a/tests/phpunit/api/v3/PaymentTest.php +++ b/tests/phpunit/api/v3/PaymentTest.php @@ -1124,9 +1124,7 @@ class api_v3_PaymentTest extends CiviUnitTestCase { $this->assertEquals($trxnID, $contribution['trxn_id'], "Contribution trxn_id should have been set to that of the payment."); - // change $trxnDate for $receiveDate if we agree that transactions should NOT - // update contributions. - $this->assertEquals($trxnDate, $contribution['receive_date'], + $this->assertEquals($originalReceiveDate, $contribution['receive_date'], "Contribution receive date was changed, but should not have been."); $this->validateAllPayments();