Skip to content

Commit

Permalink
dev/membership#13 Add handling for missing MembershipPayment record.
Browse files Browse the repository at this point in the history
We have an intermittent regression on extending memberships off paypal payments. This adds debug messages
and handling / tests for the scenario I suspect may be in play.

The 'decision' the code makes about how to create recurring transactions is all dependent on the 'template transaction'
- if that is not correct the follow on payments will not be.

This PR allows the membershipID input from paypal to be passed through and used.  This will override any
intended changes - but that just restores previous behaviour and we do not have good handling for these
  • Loading branch information
eileenmcnaughton committed Aug 16, 2019
1 parent 86b0454 commit bbce0f0
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CRM/Contribute/BAO/Contribution.php
Original file line number Diff line number Diff line change
Expand Up @@ -2468,6 +2468,9 @@ protected static function repeatTransaction(&$contribution, &$input, $contributi
$createContribution = civicrm_api3('Contribution', 'create', $contributionParams);
$contribution->id = $createContribution['id'];
CRM_Contribute_BAO_ContributionRecur::copyCustomValues($contributionParams['contribution_recur_id'], $contribution->id);
if (!empty($input['membership_id'])) {
civicrm_api3('MembershipPayment', 'create', ['contribution_id' => $contribution->id, 'membership_id' => $input['membership_id']]);
}
return TRUE;
}
}
Expand Down Expand Up @@ -4410,6 +4413,7 @@ public static function isSingleLineItem($id) {
* @param CRM_Contribute_BAO_Contribution $contribution
*
* @return array
* @throws \CiviCRM_API3_Exception
*/
public static function completeOrder(&$input, &$ids, $objects, $transaction, $recur, $contribution) {
$primaryContributionID = isset($contribution->id) ? $contribution->id : $objects['first_contribution']->id;
Expand Down
38 changes: 34 additions & 4 deletions CRM/Core/Payment/PayPalIPN.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,9 @@ public function main() {
$input['component'] = $component;

$ids['contact'] = $this->retrieve('contactID', 'Integer', TRUE);
$ids['contribution'] = $this->retrieve('contributionID', 'Integer', TRUE);
$contributionID = $ids['contribution'] = $this->retrieve('contributionID', 'Integer', TRUE);
$membershipID = $this->retrieve('membershipID', 'Integer', FALSE);
$contributionRecurID = $this->retrieve('contributionRecurID', 'Integer', FALSE);

$this->getInput($input, $ids);

Expand All @@ -323,17 +325,45 @@ public function main() {
}
else {
// get the optional ids
$ids['membership'] = $this->retrieve('membershipID', 'Integer', FALSE);
$ids['contributionRecur'] = $this->retrieve('contributionRecurID', 'Integer', FALSE);
$ids['membership'] = $membershipID;
$ids['contributionRecur'] = $contributionRecurID;
$ids['contributionPage'] = $this->retrieve('contributionPageID', 'Integer', FALSE);
$ids['related_contact'] = $this->retrieve('relatedContactID', 'Integer', FALSE);
$ids['onbehalf_dupe_alert'] = $this->retrieve('onBehalfDupeAlert', 'Integer', FALSE);
}

$paymentProcessorID = self::getPayPalPaymentProcessorID($input, $ids);
$paymentProcessorID = $this->getPayPalPaymentProcessorID($input, $ids);

Civi::log()->debug('PayPalIPN: Received (ContactID: ' . $ids['contact'] . '; trxn_id: ' . $input['trxn_id'] . ').');

if ($this->retrieve('membershipID', 'Integer', FALSE)) {
$membershipPayment = civicrm_api3('MembershipPayment', 'get', [
'contribution_id' => $contributionID,
'membership_id' => $membershipID,
]);
$lineItems = civicrm_api3('LineItem', 'get', [
'contribution_id' => $contributionID,
'entity_id' => $membershipID,
]);
Civi::log()->debug('PayPalIPN: Received payment for membership ' . (int) $membershipID
. '. Original contribution ' . (int) $contributionID . ' is linked to ' . $membershipPayment['count']
. 'payments for this membership. It has ' . $lineItems['count'] . ' line items linked to this membership.'
. ' it is expected the original contribution will be linked by both entities to the membership.'
);
if (empty($membershipPayment['count'])) {
Civi::log()->debug('PayPalIPN: Will attempt to compensate');
$input['membership_id'] = $this->retrieve('membershipID', 'Integer', FALSE);
}
if ($contributionRecurID) {
$recurLinks = civicrm_api3('ContributionRecur', 'get', [
'membership_id' => $membershipID,
'contribution_recur_id' => $contributionRecurID,
]);
Civi::log()->debug('PayPalIPN: Membership should be linked to contribution recur record ' . $contributionRecurID
. ' ' . $recurLinks['count'] . 'links found'
);
}
}
if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) {
return;
}
Expand Down
1 change: 1 addition & 0 deletions api/v3/Contribution.php
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,7 @@ function civicrm_api3_contribution_repeattransaction($params) {
'fee_amount',
'financial_type_id',
'contribution_status_id',
'membership_id',
];
$input = array_intersect_key($params, array_fill_keys($passThroughParams, NULL));

Expand Down
45 changes: 44 additions & 1 deletion tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ public function testIPNPaymentRecurSuccess() {

/**
* Test IPN response updates contribution_recur & contribution for first & second contribution.
*
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
*/
public function testIPNPaymentMembershipRecurSuccess() {
$durationUnit = 'year';
Expand All @@ -169,7 +172,7 @@ public function testIPNPaymentMembershipRecurSuccess() {
$this->assertEquals(1, $contribution['contribution_status_id']);
$this->assertEquals('8XA571746W2698126', $contribution['trxn_id']);
// source gets set by processor
$this->assertTrue(substr($contribution['contribution_source'], 0, 20) == "Online Contribution:");
$this->assertTrue(substr($contribution['contribution_source'], 0, 20) === "Online Contribution:");
$contributionRecur = $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $this->_contributionRecurID]);
$this->assertEquals(5, $contributionRecur['contribution_status_id']);
$paypalIPN = new CRM_Core_Payment_PaypalIPN($this->getPaypalRecurSubsequentTransaction());
Expand All @@ -191,7 +194,47 @@ public function testIPNPaymentMembershipRecurSuccess() {
'entity_table' => 'civicrm_membership',
]);
$this->callAPISuccessGetSingle('membership_payment', ['contribution_id' => $contribution['values'][1]['id']]);
}

/**
* Test IPN that we can force membership when the membership payment has been deleted.
*
* https://lab.civicrm.org/dev/membership/issues/13
*
* In this scenario the membership payment record was deleted (or not created) for the first contribution but we
* 'recover' by using the input membership id.
*
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
*/
public function testIPNPaymentInputMembershipRecurSuccess() {
$durationUnit = 'year';
$this->setupMembershipRecurringPaymentProcessorTransaction(['duration_unit' => $durationUnit, 'frequency_unit' => $durationUnit]);
$membershipPayment = $this->callAPISuccessGetSingle('membership_payment', []);
$paypalIPN = new CRM_Core_Payment_PayPalIPN(array_merge($this->getPaypalRecurTransaction(), ['membershipID' => $membershipPayment['membership_id']]));
$paypalIPN->main();
$membershipEndDate = $this->callAPISuccessGetValue('membership', ['return' => 'end_date']);
CRM_Core_DAO::executeQuery('DELETE FROM civicrm_membership_payment WHERE id = ' . $membershipPayment['id']);

$paypalIPN = new CRM_Core_Payment_PaypalIPN(array_merge($this->getPaypalRecurSubsequentTransaction(), ['membershipID' => $membershipPayment['membership_id']]));
$paypalIPN->main();
$renewedMembershipEndDate = $this->membershipRenewalDate($durationUnit, $membershipEndDate);
$this->assertEquals($renewedMembershipEndDate, $this->callAPISuccessGetValue('membership', ['return' => 'end_date']));
$contribution = $this->callAPISuccess('contribution', 'get', [
'contribution_recur_id' => $this->_contributionRecurID,
'sequential' => 1,
]);
$this->assertEquals(2, $contribution['count']);
$this->assertEquals('secondone', $contribution['values'][1]['trxn_id']);
$this->callAPISuccessGetCount('line_item', [
'entity_id' => $this->ids['membership'],
'entity_table' => 'civicrm_membership',
], 2);
$this->callAPISuccessGetSingle('line_item', [
'contribution_id' => $contribution['values'][1]['id'],
'entity_table' => 'civicrm_membership',
]);
$this->callAPISuccessGetSingle('membership_payment', ['contribution_id' => $contribution['values'][1]['id']]);
}

/**
Expand Down

0 comments on commit bbce0f0

Please sign in to comment.