Skip to content

Commit 41a95fa

Browse files
committed
Fix AllocateOverpaymentActionTest to avoid Payment API interference
Create overpaid contributions in setUpBeforeClass() before parent::setUp() modifies financial accounts, which was causing 'Array to string conversion' errors in Payment.create calls. The test now pre-creates all needed overpaid contributions before the test class setup runs, avoiding the conflict between financial account modifications and the Payment API.
1 parent f73e44a commit 41a95fa

File tree

1 file changed

+124
-102
lines changed

1 file changed

+124
-102
lines changed

tests/phpunit/Civi/Api4/Action/CreditNote/AllocateOverpaymentActionTest.php

Lines changed: 124 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Civi\Api4\FinancialTrxn;
99
use Civi\Financeextras\Test\Helper\CreditNoteTrait;
1010
use Civi\Financeextras\Test\Fabricator\ContactFabricator;
11+
use Civi\Financeextras\Test\Fabricator\ContributionFabricator;
1112

1213
/**
1314
* CreditNote.AllocateOverpayment API Test Case.
@@ -18,13 +19,90 @@ class Civi_Api4_CreditNote_AllocateOverpaymentActionTest extends BaseHeadlessTes
1819

1920
use CreditNoteTrait;
2021

22+
/**
23+
* Pre-created overpaid contributions for tests.
24+
*
25+
* Created in setUpBeforeClass() before parent::setUp() modifies
26+
* financial accounts, which would cause Payment.create to fail.
27+
*
28+
* @var array
29+
*/
30+
private static array $overpaidContributions = [];
31+
32+
/**
33+
* Pre-created contacts for tests.
34+
*
35+
* @var array
36+
*/
37+
private static array $contacts = [];
38+
39+
/**
40+
* Counter for which contribution to use next.
41+
*
42+
* @var int
43+
*/
44+
private static int $contributionIndex = 0;
45+
2146
/**
2247
* The overpayment financial type ID.
2348
*
2449
* @var int
2550
*/
2651
private $overpaymentFinancialTypeId;
2752

53+
/**
54+
* {@inheritDoc}
55+
*/
56+
public static function setUpBeforeClass(): void {
57+
// Create contributions BEFORE parent::setUp() runs to avoid interference
58+
// with the Payment API. The parent::setUp() modifies financial accounts
59+
// which can cause "Array to string conversion" errors in Payment.create.
60+
61+
// Create 10 overpaid contributions (more than enough for all tests).
62+
for ($i = 0; $i < 10; $i++) {
63+
$contact = ContactFabricator::fabricate();
64+
self::$contacts[$i] = $contact;
65+
66+
// Determine amounts - most use 100/150, one uses 100/175.
67+
$invoiceAmount = 100;
68+
$paidAmount = ($i === 9) ? 175 : 150;
69+
70+
$trxnId = md5(uniqid() . $i);
71+
$contributionParams = [
72+
'financial_type_id' => 'Donation',
73+
'receive_date' => date('Y-m-d'),
74+
'total_amount' => $invoiceAmount,
75+
'contact_id' => $contact['id'],
76+
'payment_instrument_id' => 'Credit Card',
77+
'trxn_id' => $trxnId,
78+
'currency' => 'GBP',
79+
'contribution_status_id' => 2,
80+
];
81+
82+
$contribution = ContributionFabricator::fabricate($contributionParams);
83+
84+
// Create overpayment by paying more than invoice amount.
85+
civicrm_api3('Payment', 'create', [
86+
'contribution_id' => $contribution['id'],
87+
'total_amount' => $paidAmount,
88+
'trxn_date' => date('Y-m-d H:i:s'),
89+
'trxn_id' => $trxnId,
90+
'is_send_contribution_notification' => FALSE,
91+
]);
92+
93+
// Store with amounts for reference.
94+
self::$overpaidContributions[$i] = [
95+
'id' => $contribution['id'],
96+
'contact_id' => $contact['id'],
97+
'invoice_amount' => $invoiceAmount,
98+
'paid_amount' => $paidAmount,
99+
'overpayment_amount' => $paidAmount - $invoiceAmount,
100+
];
101+
}
102+
103+
self::$contributionIndex = 0;
104+
}
105+
28106
/**
29107
* {@inheritDoc}
30108
*/
@@ -50,20 +128,43 @@ public function setUp() {
50128
->execute();
51129
}
52130

131+
/**
132+
* Get the next available overpaid contribution.
133+
*
134+
* @return array
135+
* The contribution data with id, contact_id, invoice_amount,
136+
* paid_amount, overpayment_amount.
137+
*/
138+
private function getNextOverpaidContribution(): array {
139+
$contribution = self::$overpaidContributions[self::$contributionIndex];
140+
self::$contributionIndex++;
141+
return $contribution;
142+
}
143+
144+
/**
145+
* Get the contribution with larger overpayment (75 instead of 50).
146+
*
147+
* @return array
148+
* The contribution data.
149+
*/
150+
private function getLargerOverpaymentContribution(): array {
151+
// Index 9 has 100/175 = 75 overpayment.
152+
return self::$overpaidContributions[9];
153+
}
154+
53155
/**
54156
* Test allocate overpayment creates credit note with correct data.
55157
*/
56158
public function testAllocateOverpaymentCreatesCreditNote() {
57-
$contact = ContactFabricator::fabricate();
58-
$contribution = $this->createOverpaidContribution($contact['id'], 100.00, 150.00);
159+
$contribution = $this->getNextOverpaidContribution();
59160

60161
$result = CreditNote::allocateOverpayment(FALSE)
61162
->setContributionId($contribution['id'])
62163
->execute()
63164
->first();
64165

65166
$this->assertNotEmpty($result['id']);
66-
$this->assertEquals($contact['id'], $result['contact_id']);
167+
$this->assertEquals($contribution['contact_id'], $result['contact_id']);
67168
$this->assertEquals('GBP', $result['currency']);
68169

69170
$creditNote = CreditNote::get(FALSE)
@@ -79,12 +180,8 @@ public function testAllocateOverpaymentCreatesCreditNote() {
79180
* Test allocate overpayment creates line item with correct data.
80181
*/
81182
public function testAllocateOverpaymentCreatesCorrectLineItem() {
82-
$contact = ContactFabricator::fabricate();
83-
$invoiceAmount = 100.00;
84-
$paidAmount = 150.00;
85-
$overpaymentAmount = $paidAmount - $invoiceAmount;
86-
87-
$contribution = $this->createOverpaidContribution($contact['id'], $invoiceAmount, $paidAmount);
183+
$contribution = $this->getNextOverpaidContribution();
184+
$overpaymentAmount = $contribution['overpayment_amount'];
88185

89186
$result = CreditNote::allocateOverpayment(FALSE)
90187
->setContributionId($contribution['id'])
@@ -110,12 +207,8 @@ public function testAllocateOverpaymentCreatesCorrectLineItem() {
110207
* Test allocate overpayment records negative payment on contribution.
111208
*/
112209
public function testAllocateOverpaymentRecordsNegativePayment() {
113-
$contact = ContactFabricator::fabricate();
114-
$invoiceAmount = 100.00;
115-
$paidAmount = 150.00;
116-
$overpaymentAmount = $paidAmount - $invoiceAmount;
117-
118-
$contribution = $this->createOverpaidContribution($contact['id'], $invoiceAmount, $paidAmount);
210+
$contribution = $this->getNextOverpaidContribution();
211+
$overpaymentAmount = $contribution['overpayment_amount'];
119212

120213
$result = CreditNote::allocateOverpayment(FALSE)
121214
->setContributionId($contribution['id'])
@@ -136,8 +229,7 @@ public function testAllocateOverpaymentRecordsNegativePayment() {
136229
* Test allocate overpayment changes contribution status to completed.
137230
*/
138231
public function testAllocateOverpaymentChangesContributionStatusToCompleted() {
139-
$contact = ContactFabricator::fabricate();
140-
$contribution = $this->createOverpaidContribution($contact['id'], 100.00, 150.00);
232+
$contribution = $this->getNextOverpaidContribution();
141233

142234
$initialContribution = Contribution::get(FALSE)
143235
->addWhere('id', '=', $contribution['id'])
@@ -163,8 +255,7 @@ public function testAllocateOverpaymentChangesContributionStatusToCompleted() {
163255
* Test allocate overpayment payment status is completed not refunded.
164256
*/
165257
public function testAllocateOverpaymentPaymentStatusIsCompleted() {
166-
$contact = ContactFabricator::fabricate();
167-
$contribution = $this->createOverpaidContribution($contact['id'], 100.00, 150.00);
258+
$contribution = $this->getNextOverpaidContribution();
168259

169260
$result = CreditNote::allocateOverpayment(FALSE)
170261
->setContributionId($contribution['id'])
@@ -186,8 +277,7 @@ public function testAllocateOverpaymentPaymentStatusIsCompleted() {
186277
public function testAllocateOverpaymentFailsWhenSettingDisabled() {
187278
\Civi::settings()->set('financeextras_enable_overpayments', FALSE);
188279

189-
$contact = ContactFabricator::fabricate();
190-
$contribution = $this->createOverpaidContribution($contact['id'], 100.00, 150.00);
280+
$contribution = $this->getNextOverpaidContribution();
191281

192282
$this->expectException(\CRM_Core_Exception::class);
193283
$this->expectExceptionMessage('not eligible');
@@ -203,7 +293,16 @@ public function testAllocateOverpaymentFailsWhenSettingDisabled() {
203293
public function testAllocateOverpaymentFailsForNonOverpaidContribution() {
204294
$contact = ContactFabricator::fabricate();
205295

206-
$contribution = $this->createContribution($contact['id'], 100.00, 'Completed');
296+
// Create a non-overpaid contribution using API4 (no Payment.create needed).
297+
$contribution = Contribution::create(FALSE)
298+
->addValue('contact_id', $contact['id'])
299+
->addValue('financial_type_id', 1)
300+
->addValue('total_amount', 100)
301+
->addValue('contribution_status_id:name', 'Completed')
302+
->addValue('currency', 'GBP')
303+
->addValue('receive_date', date('Y-m-d'))
304+
->execute()
305+
->first();
207306

208307
$this->expectException(\CRM_Core_Exception::class);
209308
$this->expectExceptionMessage('not eligible');
@@ -238,8 +337,7 @@ public function testAllocateOverpaymentFailsWhenFinancialTypeNotConfigured() {
238337
->addValue('overpayment_financial_type_id', NULL)
239338
->execute();
240339

241-
$contact = ContactFabricator::fabricate();
242-
$contribution = $this->createOverpaidContribution($contact['id'], 100.00, 150.00);
340+
$contribution = $this->getNextOverpaidContribution();
243341

244342
$this->expectException(\CRM_Core_Exception::class);
245343
$this->expectExceptionMessage('financial type is not configured');
@@ -253,12 +351,8 @@ public function testAllocateOverpaymentFailsWhenFinancialTypeNotConfigured() {
253351
* Test credit note remains open with full credit available.
254352
*/
255353
public function testCreditNoteRemainsOpenWithFullCredit() {
256-
$contact = ContactFabricator::fabricate();
257-
$invoiceAmount = 100.00;
258-
$paidAmount = 175.00;
259-
$overpaymentAmount = $paidAmount - $invoiceAmount;
260-
261-
$contribution = $this->createOverpaidContribution($contact['id'], $invoiceAmount, $paidAmount);
354+
$contribution = $this->getLargerOverpaymentContribution();
355+
$overpaymentAmount = $contribution['overpayment_amount'];
262356

263357
$result = CreditNote::allocateOverpayment(FALSE)
264358
->setContributionId($contribution['id'])
@@ -281,76 +375,4 @@ public function testCreditNoteRemainsOpenWithFullCredit() {
281375
$this->assertCount(0, $allocations);
282376
}
283377

284-
/**
285-
* Create an overpaid contribution with 'Pending refund' status.
286-
*
287-
* @param int $contactId
288-
* The contact ID.
289-
* @param float $invoiceAmount
290-
* The invoice amount.
291-
* @param float $paidAmount
292-
* The amount paid (greater than invoice).
293-
*
294-
* @return array
295-
* The created contribution.
296-
*/
297-
private function createOverpaidContribution(int $contactId, float $invoiceAmount, float $paidAmount): array {
298-
$trxnId = md5(uniqid());
299-
300-
$result = civicrm_api3('Contribution', 'create', [
301-
'financial_type_id' => 'Donation',
302-
'receive_date' => date('Y-m-d'),
303-
'total_amount' => $invoiceAmount,
304-
'contact_id' => $contactId,
305-
'payment_instrument_id' => 'Credit Card',
306-
'trxn_id' => $trxnId,
307-
'currency' => 'GBP',
308-
'contribution_status_id' => 2,
309-
]);
310-
$contributionId = $result['id'];
311-
312-
civicrm_api3('Payment', 'create', [
313-
'contribution_id' => $contributionId,
314-
'total_amount' => $paidAmount,
315-
'trxn_date' => date('Y-m-d H:i:s'),
316-
'trxn_id' => $trxnId,
317-
'is_send_contribution_notification' => FALSE,
318-
]);
319-
320-
return Contribution::get(FALSE)
321-
->addWhere('id', '=', $contributionId)
322-
->addSelect('*', 'contribution_status_id:name')
323-
->execute()
324-
->first();
325-
}
326-
327-
/**
328-
* Create a contribution with specified status.
329-
*
330-
* @param int $contactId
331-
* The contact ID.
332-
* @param float $amount
333-
* The contribution amount.
334-
* @param string $status
335-
* The contribution status.
336-
*
337-
* @return array
338-
* The created contribution.
339-
*/
340-
private function createContribution(int $contactId, float $amount, string $status): array {
341-
$result = civicrm_api3('Contribution', 'create', [
342-
'contact_id' => $contactId,
343-
'financial_type_id' => 'Donation',
344-
'total_amount' => $amount,
345-
'contribution_status_id' => $status,
346-
'currency' => 'GBP',
347-
'receive_date' => date('Y-m-d'),
348-
]);
349-
350-
return Contribution::get(FALSE)
351-
->addWhere('id', '=', $result['id'])
352-
->execute()
353-
->first();
354-
}
355-
356378
}

0 commit comments

Comments
 (0)