Skip to content

Commit

Permalink
Merge pull request #21106 from eileenmcnaughton/order
Browse files Browse the repository at this point in the history
dev/core#2634 Add v4 Membership api, access it via order
  • Loading branch information
seamuslee001 authored Aug 25, 2021
2 parents c4f72df + 8806004 commit 375952f
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 95 deletions.
122 changes: 66 additions & 56 deletions CRM/Member/BAO/Membership.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,70 +339,80 @@ public static function create(&$params, $ids = []) {
}

$params['membership_id'] = $membership->id;
// @todo further cleanup required to remove use of $ids['contribution'] from here
if (isset($ids['membership'])) {
$contributionID = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipPayment',
$membership->id,
'contribution_id',
'membership_id'
);
// @todo this is a temporary step to removing $ids['contribution'] completely
if (empty($params['contribution_id']) && !empty($contributionID)) {
$params['contribution_id'] = $contributionID;
// For api v4 we skip all of this stuff. There is an expectation that v4 users either use
// the order api, or handle any financial / related processing themselves.
// Note that the processing below is fairly intertwined with core usage and in some places
// problematic or to be removed.
// Note the choice of 'version' as a parameter is to make it
// unavailable through apiv3.
// once we are rid of direct calls to the BAO::create from core
// we will deprecate this stuff into the v3 api.
if (($params['version'] ?? 0) !== 4) {
// @todo further cleanup required to remove use of $ids['contribution'] from here
if (isset($ids['membership'])) {
$contributionID = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipPayment',
$membership->id,
'contribution_id',
'membership_id'
);
// @todo this is a temporary step to removing $ids['contribution'] completely
if (empty($params['contribution_id']) && !empty($contributionID)) {
$params['contribution_id'] = $contributionID;
}
}
}

// This code ensures a line item is created but it is recommended you pass in 'skipLineItem' or 'line_item'
if (empty($params['line_item']) && !empty($params['membership_type_id']) && empty($params['skipLineItem'])) {
CRM_Price_BAO_LineItem::getLineItemArray($params, NULL, 'membership', $params['membership_type_id']);
}
$params['skipLineItem'] = TRUE;

// Record contribution for this membership and create a MembershipPayment
// @todo deprecate this.
if (!empty($params['contribution_status_id'])) {
$memInfo = array_merge($params, ['membership_id' => $membership->id]);
$params['contribution'] = self::recordMembershipContribution($memInfo);
}

// If the membership has no associated contribution then we ensure
// the line items are 'correct' here. This is a lazy legacy
// hack whereby they are deleted and recreated
if (empty($contributionID)) {
if (!empty($params['lineItems'])) {
$params['line_item'] = $params['lineItems'];
// This code ensures a line item is created but it is recommended you pass in 'skipLineItem' or 'line_item'
if (empty($params['line_item']) && !empty($params['membership_type_id']) && empty($params['skipLineItem'])) {
CRM_Price_BAO_LineItem::getLineItemArray($params, NULL, 'membership', $params['membership_type_id']);
}
// do cleanup line items if membership edit the Membership type.
if (!empty($ids['membership'])) {
CRM_Price_BAO_LineItem::deleteLineItems($ids['membership'], 'civicrm_membership');
$params['skipLineItem'] = TRUE;

// Record contribution for this membership and create a MembershipPayment
// @todo deprecate this.
if (!empty($params['contribution_status_id'])) {
$memInfo = array_merge($params, ['membership_id' => $membership->id]);
$params['contribution'] = self::recordMembershipContribution($memInfo);
}
// @todo - we should ONLY do the below if a contribution is created. Let's
// get some deprecation notices in here & see where it's hit & work to eliminate.
// This could happen if there is no contribution or we are in one of many
// weird and wonderful flows. This is scary code. Keep adding tests.
if (!empty($params['line_item']) && empty($params['contribution_id'])) {

foreach ($params['line_item'] as $priceSetId => $lineItems) {
foreach ($lineItems as $lineIndex => $lineItem) {
$lineMembershipType = $lineItem['membership_type_id'] ?? NULL;
if (!empty($params['contribution'])) {
$params['line_item'][$priceSetId][$lineIndex]['contribution_id'] = $params['contribution']->id;
}
if ($lineMembershipType && $lineMembershipType == ($params['membership_type_id'] ?? NULL)) {
$params['line_item'][$priceSetId][$lineIndex]['entity_id'] = $membership->id;
$params['line_item'][$priceSetId][$lineIndex]['entity_table'] = 'civicrm_membership';
}
elseif (!$lineMembershipType && !empty($params['contribution'])) {
$params['line_item'][$priceSetId][$lineIndex]['entity_id'] = $params['contribution']->id;
$params['line_item'][$priceSetId][$lineIndex]['entity_table'] = 'civicrm_contribution';
// If the membership has no associated contribution then we ensure
// the line items are 'correct' here. This is a lazy legacy
// hack whereby they are deleted and recreated
if (empty($contributionID)) {
if (!empty($params['lineItems'])) {
$params['line_item'] = $params['lineItems'];
}
// do cleanup line items if membership edit the Membership type.
if (!empty($ids['membership'])) {
CRM_Price_BAO_LineItem::deleteLineItems($ids['membership'], 'civicrm_membership');
}
// @todo - we should ONLY do the below if a contribution is created. Let's
// get some deprecation notices in here & see where it's hit & work to eliminate.
// This could happen if there is no contribution or we are in one of many
// weird and wonderful flows. This is scary code. Keep adding tests.
if (!empty($params['line_item']) && empty($params['contribution_id'])) {

foreach ($params['line_item'] as $priceSetId => $lineItems) {
foreach ($lineItems as $lineIndex => $lineItem) {
$lineMembershipType = $lineItem['membership_type_id'] ?? NULL;
if (!empty($params['contribution'])) {
$params['line_item'][$priceSetId][$lineIndex]['contribution_id'] = $params['contribution']->id;
}
if ($lineMembershipType && $lineMembershipType == ($params['membership_type_id'] ?? NULL)) {
$params['line_item'][$priceSetId][$lineIndex]['entity_id'] = $membership->id;
$params['line_item'][$priceSetId][$lineIndex]['entity_table'] = 'civicrm_membership';
}
elseif (!$lineMembershipType && !empty($params['contribution'])) {
$params['line_item'][$priceSetId][$lineIndex]['entity_id'] = $params['contribution']->id;
$params['line_item'][$priceSetId][$lineIndex]['entity_table'] = 'civicrm_contribution';
}
}
}
CRM_Price_BAO_LineItem::processPriceSet(
$membership->id,
$params['line_item'],
$params['contribution'] ?? NULL
);
}
CRM_Price_BAO_LineItem::processPriceSet(
$membership->id,
$params['line_item'],
$params['contribution'] ?? NULL
);
}
}

Expand Down
23 changes: 23 additions & 0 deletions Civi/Api4/Membership.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/*
+--------------------------------------------------------------------+
| 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 |
+--------------------------------------------------------------------+
*/
namespace Civi\Api4;

/**
* Membership entity.
*
* @searchable primary
* @since 5.42
* @package Civi\Api4
*/
class Membership extends Generic\DAOEntity {
use Generic\Traits\OptionList;

}
23 changes: 23 additions & 0 deletions Civi/Api4/MembershipBlock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/*
+--------------------------------------------------------------------+
| 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 |
+--------------------------------------------------------------------+
*/
namespace Civi\Api4;

/**
* MembershipBlock entity.
*
* @searchable secondary
* @since 5.42
* @package Civi\Api4
*/
class MembershipBlock extends Generic\DAOEntity {
use Generic\Traits\OptionList;

}
23 changes: 23 additions & 0 deletions Civi/Api4/MembershipStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/*
+--------------------------------------------------------------------+
| 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 |
+--------------------------------------------------------------------+
*/
namespace Civi\Api4;

/**
* MembershipStatus entity.
*
* @searchable secondary
* @since 5.42
* @package Civi\Api4
*/
class MembershipStatus extends Generic\DAOEntity {
use Generic\Traits\OptionList;

}
51 changes: 51 additions & 0 deletions Civi/Api4/Service/Spec/Provider/MembershipCreationSpecProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/*
+--------------------------------------------------------------------+
| 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 |
+--------------------------------------------------------------------+
*/

namespace Civi\Api4\Service\Spec\Provider;

use Civi\Api4\Service\Spec\FieldSpec;
use Civi\Api4\Service\Spec\RequestSpec;

class MembershipCreationSpecProvider implements Generic\SpecProviderInterface {

/**
* @param \Civi\Api4\Service\Spec\RequestSpec $spec
*/
public function modifySpec(RequestSpec $spec): void {
$spec->getFieldByName('status_id')->setRequired(FALSE);
// This is a bit of dark-magic - the membership BAO code is particularly
// nasty. It has a lot of logic in it that does not belong there in
// terms of our current expectations. Removing it is difficult
// so the plan is that new api v4 membership.create users will either
// use the Order api flow when financial code should kick in. Otherwise
// the crud flow will bypass all the financial processing in membership.create.
// The use of the 'version' parameter to drive this is to be sure that
// we know the bypass will not affect the v3 api to the extent
// it cannot be reached by the v3 api at all (in time we can move some
// of the code we are deprecating into the v3 api, to die of natural deprecation).
$spec->addFieldSpec(new FieldSpec('version', 'Membership', 'Integer'));
$spec->getFieldByName('version')->setDefaultValue(4)->setRequired(TRUE);
}

/**
* When does this apply.
*
* @param string $entity
* @param string $action
*
* @return bool
*/
public function applies($entity, $action): bool {
return $entity === 'Membership' && $action === 'create';
}

}
39 changes: 35 additions & 4 deletions api/v3/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
* @package CiviCRM_APIv3
*/

use Civi\Api4\Membership;

/**
* Retrieve a set of Order.
*
Expand Down Expand Up @@ -140,15 +142,18 @@ function civicrm_api3_order_create(array $params): array {

if ($entityParams['entity'] === 'membership') {
if (empty($entityParams['id'])) {
$entityParams['status_id'] = 'Pending';
$entityParams['status_id:name'] = 'Pending';
}
if (!empty($params['contribution_recur_id'])) {
$entityParams['contribution_recur_id'] = $params['contribution_recur_id'];
}
$entityParams['skipLineItem'] = TRUE;
$entityResult = civicrm_api3('Membership', 'create', $entityParams);
// At this stage we need to get this passed through.
$entityParams['version'] = 4;
_order_create_wrangle_membership_params($entityParams);

$membershipID = Membership::save($params['check_permissions'] ?? FALSE)->setRecords([$entityParams])->execute()->first()['id'];
foreach ($entityParams['line_references'] as $lineIndex) {
$order->setLineItemValue('entity_id', $entityResult['id'], $lineIndex);
$order->setLineItemValue('entity_id', $membershipID, $lineIndex);
}
}
}
Expand Down Expand Up @@ -286,3 +291,29 @@ function _civicrm_api3_order_delete_spec(array &$params) {
];
$params['id']['api.aliases'] = ['contribution_id'];
}

/**
* Handle possibility of v3 style params.
*
* We used to call v3 Membership.create. Now we call v4.
* This converts membership input parameters.
*
* @param array $membershipParams
*
* @throws \API_Exception
*/
function _order_create_wrangle_membership_params(array &$membershipParams) {
$fields = Membership::getFields(FALSE)->execute()->indexBy('name');
foreach ($fields as $fieldName => $field) {
$customFieldName = 'custom_' . ($field['custom_field_id'] ?? NULL);
if ($field['type'] === ['Custom'] && isset($membershipParams[$customFieldName])) {
$membershipParams[$field['custom_group'] . '.' . $field['custom_field']] = $membershipParams[$customFieldName];
unset($membershipParams[$customFieldName]);
}

if (!empty($membershipParams[$fieldName]) && $field['data_type'] === 'Integer' && !is_numeric($membershipParams[$fieldName])) {
$membershipParams[$field['name'] . ':name'] = $membershipParams[$fieldName];
unset($membershipParams[$field['name']]);
}
}
}
6 changes: 4 additions & 2 deletions tests/phpunit/CRM/Core/Payment/PayPalIPNTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,14 @@ public function testIPNPaymentRecurSuccess(): void {
}

/**
* Test IPN response updates contribution_recur & contribution for first & second contribution.
* Test IPN response updates contribution_recur & contribution for first &
* second contribution.
*
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
* @throws \API_Exception
*/
public function testIPNPaymentMembershipRecurSuccess() {
public function testIPNPaymentMembershipRecurSuccess(): void {
$durationUnit = 'year';
$this->setupMembershipRecurringPaymentProcessorTransaction(['duration_unit' => $durationUnit, 'frequency_unit' => $durationUnit]);
$this->callAPISuccessGetSingle('membership_payment', []);
Expand Down
3 changes: 1 addition & 2 deletions tests/phpunit/CiviTest/CiviUnitTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -670,9 +670,8 @@ public function membershipTypeCreate($params = []) {
* @param array $params
*
* @return int
* @throws \CRM_Core_Exception
*/
public function contactMembershipCreate($params) {
public function contactMembershipCreate(array $params): int {
$params = array_merge([
'join_date' => '2007-01-21',
'start_date' => '2007-01-21',
Expand Down
6 changes: 4 additions & 2 deletions tests/phpunit/api/v3/MembershipTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -965,9 +965,11 @@ public function testSearchWithCustomDataCRM16036(): void {
/**
* Test civicrm_contact_memberships_create with membership id (edit
* membership).
* success expected.
*
* @dataProvider versionThreeAndFour
*/
public function testMembershipCreateWithId() {
public function testMembershipCreateWithId($version): void {
$this->_apiversion = $version;
$membershipID = $this->contactMembershipCreate($this->_params);
$params = [
'id' => $membershipID,
Expand Down
Loading

0 comments on commit 375952f

Please sign in to comment.