88use Civi \Api4 \FinancialTrxn ;
99use Civi \Financeextras \Test \Helper \CreditNoteTrait ;
1010use 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