@@ -330,9 +330,16 @@ public function refundGroupTransactions(array &$buildSubject)
330330
331331 $ requestParams = $ this ->request ->getParams ();
332332 if (!empty ($ requestParams ['creditmemo ' ]['buckaroo_already_paid ' ])) {
333+ // Admin backend flow: giftcard amounts are explicitly specified in the creditmemo form
333334 foreach ($ requestParams ['creditmemo ' ]['buckaroo_already_paid ' ] as $ transaction => $ giftCardValue ) {
334335 $ this ->createRefundGroupRequest ($ buildSubject , $ transaction , $ giftCardValue );
335336 }
337+ } else {
338+ // API / headless flow (e.g. Returnless): no form params are present.
339+ // Automatically refund giftcard group transactions proportionally so that
340+ // the remaining amount passed to the primary payment method never exceeds
341+ // what was actually charged on that method.
342+ $ this ->refundGiftcardTransactionsAutomatically ($ buildSubject , $ order );
336343 }
337344
338345 if ($ this ->amountLeftToRefund >= 0.01 && $ originalRefundAmount > $ this ->amountLeftToRefund ) {
@@ -506,6 +513,69 @@ public function setAmountLeftToRefund(float $amountLeftToRefund): void
506513 $ this ->amountLeftToRefund = $ amountLeftToRefund ;
507514 }
508515
516+ /**
517+ * Automatically refund giftcard group transactions for API/headless flows.
518+ *
519+ * When a refund is triggered via the REST API (e.g. Returnless), the
520+ * creditmemo form POST params are not available. This method reads the
521+ * group_transaction table directly, identifies giftcard transactions and
522+ * refunds each one proportionally so that only the remainder is sent to
523+ * the primary payment method (e.g. iDEAL).
524+ *
525+ * Example: order paid €10 VVV giftcard + €22 iDEAL = €32 total.
526+ * Full refund of €32 via API → refund €10 on VVV first → €22 left → refund €22 on iDEAL.
527+ *
528+ * @param array $buildSubject
529+ * @param Order $order
530+ * @return void
531+ * @throws ClientException
532+ * @throws ConverterException
533+ */
534+ private function refundGiftcardTransactionsAutomatically (array $ buildSubject , Order $ order ): void
535+ {
536+ $ groupTransactions = $ this ->paymentGroupTransaction ->getAnyGroupTransactionItems ($ order ->getIncrementId ());
537+
538+ foreach ($ groupTransactions as $ transaction ) {
539+ $ servicecode = $ transaction ->getData ('servicecode ' );
540+
541+ if (!$ this ->isGiftcardService ($ servicecode )) {
542+ // Skip non-giftcard transactions; they are handled by the primary refund flow
543+ continue ;
544+ }
545+
546+ if ($ this ->amountLeftToRefund < 0.01 ) {
547+ break ;
548+ }
549+
550+ $ availableOnTransaction = (float )$ transaction ->getData ('amount ' )
551+ - (float )$ transaction ->getData ('refunded_amount ' );
552+
553+ if ($ availableOnTransaction < 0.01 ) {
554+ continue ;
555+ }
556+
557+ // Refund as much as needed from this giftcard, capped at what was charged on it
558+ $ refundAmount = min ($ this ->amountLeftToRefund , $ availableOnTransaction );
559+
560+ $ this ->buckarooLog ->addDebug (sprintf (
561+ '[REFUND_AUTO_GIFTCARD] | Automatically refunding giftcard | Service: %s | Amount: €%.2f | Transaction: %s ' ,
562+ $ servicecode ,
563+ $ refundAmount ,
564+ $ transaction ->getData ('transaction_id ' )
565+ ));
566+
567+ // Build the transaction key string in the same format used by createRefundGroupRequest:
568+ // transactionId|servicecode|amount
569+ $ transactionKey = implode ('| ' , [
570+ $ transaction ->getData ('transaction_id ' ),
571+ $ servicecode ,
572+ $ transaction ->getData ('amount ' ),
573+ ]);
574+
575+ $ this ->createRefundGroupRequest ($ buildSubject , $ transactionKey , $ refundAmount );
576+ }
577+ }
578+
509579 /**
510580 * Get the non-giftcard payment transaction from group_transaction table
511581 *
0 commit comments