diff --git a/Civi/Paymentprocessingcore/DTO/RecurringContributionDTO.php b/Civi/Paymentprocessingcore/DTO/RecurringContributionDTO.php index a02f716..909fc7d 100644 --- a/Civi/Paymentprocessingcore/DTO/RecurringContributionDTO.php +++ b/Civi/Paymentprocessingcore/DTO/RecurringContributionDTO.php @@ -55,6 +55,11 @@ class RecurringContributionDTO { */ private int $frequencyInterval; + /** + * @var int|null + */ + private ?int $paymentInstrumentId; + /** * Private constructor — use fromApiResult() factory method. * @@ -76,6 +81,8 @@ class RecurringContributionDTO { * Frequency unit (day, week, month, year). * @param int $frequencyInterval * Frequency interval. + * @param int|null $paymentInstrumentId + * Payment instrument ID or NULL. */ private function __construct( int $id, @@ -86,7 +93,8 @@ private function __construct( ?int $campaignId, string $nextSchedContributionDate, string $frequencyUnit, - int $frequencyInterval + int $frequencyInterval, + ?int $paymentInstrumentId ) { $this->id = $id; $this->contactId = $contactId; @@ -97,6 +105,7 @@ private function __construct( $this->nextSchedContributionDate = $nextSchedContributionDate; $this->frequencyUnit = $frequencyUnit; $this->frequencyInterval = $frequencyInterval; + $this->paymentInstrumentId = $paymentInstrumentId; } /** @@ -148,6 +157,10 @@ public static function fromApiResult(array $record): self { ? (int) $record['campaign_id'] : NULL; + $paymentInstrumentId = isset($record['payment_instrument_id']) && is_numeric($record['payment_instrument_id']) + ? (int) $record['payment_instrument_id'] + : NULL; + return new self( (int) $id, (int) $contactId, @@ -157,7 +170,8 @@ public static function fromApiResult(array $record): self { $campaignId, (string) $nextSchedDate, (string) $frequencyUnit, - (int) $frequencyInterval + (int) $frequencyInterval, + $paymentInstrumentId ); } @@ -224,4 +238,11 @@ public function getFrequencyInterval(): int { return $this->frequencyInterval; } + /** + * Get the payment instrument ID, or NULL if not set. + */ + public function getPaymentInstrumentId(): ?int { + return $this->paymentInstrumentId; + } + } diff --git a/Civi/Paymentprocessingcore/Service/InstalmentGenerationService.php b/Civi/Paymentprocessingcore/Service/InstalmentGenerationService.php index 5c49bed..60793fa 100644 --- a/Civi/Paymentprocessingcore/Service/InstalmentGenerationService.php +++ b/Civi/Paymentprocessingcore/Service/InstalmentGenerationService.php @@ -149,7 +149,8 @@ public function getDueRecurringContributions(string $processorType, int $batchSi 'amount', 'currency', 'financial_type_id', - 'campaign_id' + 'campaign_id', + 'payment_instrument_id' ) ->addJoin( 'PaymentProcessor AS pp', @@ -258,6 +259,10 @@ public function createInstalment(RecurringContributionDTO $recur, string $receiv $createAction->addValue('campaign_id', $recur->getCampaignId()); } + if ($recur->getPaymentInstrumentId() !== NULL) { + $createAction->addValue('payment_instrument_id', $recur->getPaymentInstrumentId()); + } + $result = $createAction->execute()->first(); if (!is_array($result)) { diff --git a/tests/phpunit/Civi/Paymentprocessingcore/DTO/RecurringContributionDTOTest.php b/tests/phpunit/Civi/Paymentprocessingcore/DTO/RecurringContributionDTOTest.php index f9e2273..f0675dc 100644 --- a/tests/phpunit/Civi/Paymentprocessingcore/DTO/RecurringContributionDTOTest.php +++ b/tests/phpunit/Civi/Paymentprocessingcore/DTO/RecurringContributionDTOTest.php @@ -23,6 +23,7 @@ public function testFromApiResultWithValidData(): void { 'next_sched_contribution_date' => '2025-07-15', 'frequency_unit:name' => 'week', 'frequency_interval' => 2, + 'payment_instrument_id' => 4, ]; $dto = RecurringContributionDTO::fromApiResult($record); @@ -36,6 +37,7 @@ public function testFromApiResultWithValidData(): void { $this->assertSame('2025-07-15', $dto->getNextSchedContributionDate()); $this->assertSame('week', $dto->getFrequencyUnit()); $this->assertSame(2, $dto->getFrequencyInterval()); + $this->assertSame(4, $dto->getPaymentInstrumentId()); } /** @@ -104,6 +106,45 @@ public function testFromApiResultThrowsOnMissingDate(): void { ]); } + /** + * Tests that null payment_instrument_id returns null. + */ + public function testFromApiResultWithNullPaymentInstrumentId(): void { + $record = [ + 'id' => 1, + 'contact_id' => 2, + 'amount' => 10.00, + 'currency' => 'USD', + 'financial_type_id' => 1, + 'payment_instrument_id' => NULL, + 'next_sched_contribution_date' => '2025-07-15', + 'frequency_unit:name' => 'month', + 'frequency_interval' => 1, + ]; + + $dto = RecurringContributionDTO::fromApiResult($record); + + $this->assertNull($dto->getPaymentInstrumentId()); + } + + /** + * Tests that missing payment_instrument_id returns null. + */ + public function testFromApiResultWithMissingPaymentInstrumentId(): void { + $record = [ + 'id' => 1, + 'contact_id' => 2, + 'amount' => 10.00, + 'currency' => 'USD', + 'financial_type_id' => 1, + 'next_sched_contribution_date' => '2025-07-15', + ]; + + $dto = RecurringContributionDTO::fromApiResult($record); + + $this->assertNull($dto->getPaymentInstrumentId()); + } + /** * Tests that missing contact_id throws InvalidArgumentException. */ diff --git a/tests/phpunit/Civi/Paymentprocessingcore/Service/InstalmentGenerationServiceTest.php b/tests/phpunit/Civi/Paymentprocessingcore/Service/InstalmentGenerationServiceTest.php index 9a88430..8065d17 100644 --- a/tests/phpunit/Civi/Paymentprocessingcore/Service/InstalmentGenerationServiceTest.php +++ b/tests/phpunit/Civi/Paymentprocessingcore/Service/InstalmentGenerationServiceTest.php @@ -584,6 +584,45 @@ public function testCreateInstalmentCopiesCampaignId(): void { $this->assertEquals($campaignId, (int) $result); } + /** + * Tests createInstalment copies payment_instrument_id when present. + */ + public function testCreateInstalmentCopiesPaymentInstrumentId(): void { + $processorId = $this->createPaymentProcessor(); + $contactId = $this->createContact(); + + // Look up EFT payment instrument (non-default, avoids false positives). + $eftInstrument = \Civi\Api4\OptionValue::get(FALSE) + ->addSelect('value') + ->addWhere('option_group_id:name', '=', 'payment_instrument') + ->addWhere('name', '=', 'EFT') + ->execute() + ->first(); + + $this->assertNotNull($eftInstrument); + $paymentInstrumentId = (int) $eftInstrument['value']; + + $recurId = $this->createRecurringContribution($contactId, $processorId); + $recur = RecurringContributionDTO::fromApiResult([ + 'id' => $recurId, + 'contact_id' => $contactId, + 'amount' => 50.00, + 'currency' => 'GBP', + 'financial_type_id' => 1, + 'payment_instrument_id' => $paymentInstrumentId, + 'next_sched_contribution_date' => date('Y-m-d'), + ]); + + $contributionId = $this->service->createInstalment($recur, date('Y-m-d')); + + $result = \CRM_Core_DAO::singleValueQuery( + 'SELECT payment_instrument_id FROM civicrm_contribution WHERE id = %1', + [1 => [$contributionId, 'Integer']] + ); + + $this->assertEquals($paymentInstrumentId, (int) $result); + } + /** * Tests createInstalment sets is_pay_later to 0. */ @@ -721,7 +760,7 @@ public function testInjectableReferenceDateControlsDueContributions(): void { */ private function getRecurRecord(int $recurId): RecurringContributionDTO { $recur = \Civi\Api4\ContributionRecur::get(FALSE) - ->addSelect('id', 'contact_id', 'amount', 'currency', 'financial_type_id', 'campaign_id', 'next_sched_contribution_date') + ->addSelect('id', 'contact_id', 'amount', 'currency', 'financial_type_id', 'campaign_id', 'payment_instrument_id', 'next_sched_contribution_date') ->addWhere('id', '=', $recurId) ->execute() ->first();