Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion CRM/Financial/BAO/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,30 @@ private function calculateContributionValues() {
}
}

/**
* Reconcile the params shared between Contribution & ContributionRecur
* ('currency', 'is_test') so both end up with the same value: if only one
* side provides a value, copy it to the other.
*
* Note: this only reconciles values passed in for this create; it does not
* look up values from an existing Contribution/ContributionRecur, since
* nothing currently calls validate() with an existing id set (see the
* "existing contributions" TODO on CreateContribution::getSubscribedEvents).
*/
private function calculateSharedValues(): void {
foreach (['currency', 'is_test'] as $field) {
$recurValue = $this->contributionRecurValues[$field] ?? NULL;
$contributionValue = $this->contributionValues[$field] ?? NULL;

if ($recurValue !== NULL && $contributionValue === NULL) {
$this->contributionValues[$field] = $recurValue;
}
elseif ($contributionValue !== NULL && $recurValue === NULL) {
$this->contributionRecurValues[$field] = $contributionValue;
}
}
}

/**
* @return $this
*
Expand All @@ -1644,7 +1668,14 @@ private function calculateContributionValues() {
* @throws \Civi\API\Exception\UnauthorizedException
*/
public function validate(): CRM_Financial_BAO_Order {
// First we calculate remaining parameters for Contribution/ContributionRecur
// Reconcile currency/is_test between Contribution & ContributionRecur before
// calculateContributionRecurValues() below creates & saves the ContributionRecur
// (when there's no existing one) - otherwise it's too late to share a value
// provided only on the Contribution side.
if (!empty($this->contributionRecurValues)) {
$this->calculateSharedValues();
}
// Now we calculate remaining parameters for Contribution/ContributionRecur
$this->calculateContributionRecurValues();
$this->calculateContributionValues();
// Then we get/calculate the lineitems - they won't have related entity IDs Membership/Participant etc. for new records.
Expand Down
47 changes: 10 additions & 37 deletions ext/civi_contribute/Civi/Contribute/Service/CreateContribution.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,18 +183,16 @@ public function saveNewContribution(AfformSubmitEvent $event) {
}

// use order to create the contribution record
$savedContribution = \Civi\Api4\Order::create(FALSE)
$orderAPI = \Civi\Api4\Order::create(FALSE)
->setContributionValues($contribution)
->setLineItems($lineItems)
->execute()
->first();

$event->setEntityId(0, $savedContribution['id']);

->setLineItems($lineItems);
if ($contribution['recur_period'] ?? NULL) {
$this->createContributionRecur($savedContribution['id'], $contribution['recur_period']);
$orderAPI->setContributionRecurValues($this->getContributionRecurValues($contribution['recur_period']));
}
$savedContribution = $orderAPI->execute()
->first();

$event->setEntityId(0, $savedContribution['id']);
}

/**
Expand All @@ -220,14 +218,7 @@ private function getLineItemsForRecord(string $entityType, array $values, array
/**
* For a recurring contribution, create a ContributionRecur record as well
*/
public function createContributionRecur(int $contributionId, string $recurPeriod) {
// get values we need to reuse from the contribution record
$contribution = \Civi\Api4\Contribution::get(FALSE)
->addSelect('contact_id', 'total_amount', 'currency', 'is_test')
->addWhere('id', '=', $contributionId)
->execute()
->single();

private function getContributionRecurValues(string $recurPeriod): array {
// unpack recurPeriod parameter
// TODO: provide extendable options (option group) for this
$recurParams = match($recurPeriod) {
Expand All @@ -243,27 +234,9 @@ public function createContributionRecur(int $contributionId, string $recurPeriod
};

// calculate the next scheduled date
$nextSched = (new DateTime("+ {$recurParams['frequency_interval']} {$recurParams['frequency_unit']}"))->format('Y-m-d');

$recurRecordId = \Civi\Api4\ContributionRecur::create(FALSE)
->addValue('contact_id', $contribution['contact_id'])
->addValue('amount', $contribution['total_amount'])
->addValue('currency', $contribution['currency'])
->addValue('is_test', $contribution['is_test'])
->addValue('frequency_unit', $recurParams['frequency_unit'])
->addValue('frequency_interval', $recurParams['frequency_interval'])
->addValue('next_sched_contribution_date', $nextSched)
->execute()
->single()['id'];

// attach the existing contribution to the recurring record
\Civi\Api4\Contribution::update(FALSE)
->addWhere('id', '=', $contributionId)
->addValue('contribution_recur_id', $recurRecordId)
->execute();

// TODO: do we need to copy the first contribution as a template?
// or will it be used anyway if no template contribution exists
// @todo: Don't think we need this as it should be calculated automatically by BAO/ContributionRecur
$recurParams['next_sched_contribution_date'] = (new DateTime("+ {$recurParams['frequency_interval']} {$recurParams['frequency_unit']}"))->format('Y-m-d');
return $recurParams;
}

public function onAfformEntitySort(AfformEntitySortEvent $e): void {
Expand Down
90 changes: 90 additions & 0 deletions tests/phpunit/CRM/Financial/BAO/OrderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,96 @@ public function testCreateRecurringOrderForMembership(): void {
$this->assertEquals(0, $contributionRecur['is_email_receipt']);
}

/**
* The 'currency' and 'is_test' params should be copied across between
* Contribution & ContributionRecur when only one side provides them.
*
* @throws \CRM_Core_Exception
*/
public function testCreateRecurringOrderSharesCurrencyAndIsTestValues(): void {
$this->setUpMembershipPriceSet();
$contactID = $this->individualCreate();

// Provided on the Contribution only -> should be copied to the ContributionRecur.
$contribution = Order::create()
->setContributionValues([
'contact_id' => $contactID,
'financial_type_id:name' => 'Member Dues',
'currency' => 'NZD',
'is_test' => 1,
])
->setContributionRecurValues(['frequency_unit' => 'year'])
->addLineItem([
'price_field_value_id' => $this->ids['PriceFieldValue']['membership_first'],
'entity_id.join_date' => '2006-01-21',
'entity_id.start_date' => '2006-01-21',
'entity_id.end_date' => '2006-12-21',
'entity_id.source' => 'Payment',
])
->execute()->first();
$contributionRecur = \Civi\Api4\ContributionRecur::get(FALSE)
->addWhere('id', '=', $contribution['contribution_recur_id'])
->execute()->single();
$this->assertEquals('NZD', $contributionRecur['currency']);
$this->assertEquals(1, $contributionRecur['is_test']);

// Provided on the ContributionRecur only -> should be copied to the Contribution.
$contribution = Order::create()
->setContributionValues([
'contact_id' => $contactID,
'financial_type_id:name' => 'Member Dues',
])
->setContributionRecurValues([
'frequency_unit' => 'year',
'currency' => 'EUR',
'is_test' => 1,
])
->addLineItem([
'price_field_value_id' => $this->ids['PriceFieldValue']['membership_first'],
'entity_id.join_date' => '2007-01-21',
'entity_id.start_date' => '2007-01-21',
'entity_id.end_date' => '2007-12-21',
'entity_id.source' => 'Payment',
])
->execute()->first();
$this->assertEquals('EUR', $contribution['currency']);
$this->assertEquals(1, $contribution['is_test']);
}

/**
* If both sides explicitly provide a different currency, neither should
* be overridden by the other (there's no reason to prioritise one entity
* over the other).
*
* @throws \CRM_Core_Exception
*/
public function testCreateRecurringOrderDoesNotOverrideExplicitSharedValues(): void {
$this->setUpMembershipPriceSet();
$contribution = Order::create()
->setContributionValues([
'contact_id' => $this->individualCreate(),
'financial_type_id:name' => 'Member Dues',
'currency' => 'NZD',
])
->setContributionRecurValues([
'frequency_unit' => 'year',
'currency' => 'EUR',
])
->addLineItem([
'price_field_value_id' => $this->ids['PriceFieldValue']['membership_first'],
'entity_id.join_date' => '2006-01-21',
'entity_id.start_date' => '2006-01-21',
'entity_id.end_date' => '2006-12-21',
'entity_id.source' => 'Payment',
])
->execute()->first();
$this->assertEquals('NZD', $contribution['currency']);
$contributionRecur = \Civi\Api4\ContributionRecur::get(FALSE)
->addWhere('id', '=', $contribution['contribution_recur_id'])
->execute()->single();
$this->assertEquals('EUR', $contributionRecur['currency']);
}

/**
* Test creating an order containing items from 2 price sets plus an ad hoc amount.
*
Expand Down