<?php
namespace App\Service\App\Merchant\Invoice\Listener;
use App\Entity\Merchant\InvoiceEntity;
use App\Repository\Merchant\Contract\IOrderRepository;
use App\Service\App\Merchant\CustomFeature\Implementation\InvoiceCustomFeature;
use App\Service\App\Merchant\Invoice\Contract\ICreateCreditNoteByInvoiceIdService;
use App\Service\App\Merchant\Invoice\Contract\IPayInvoiceService;
use App\Service\App\Merchant\Payment\Event\RefundPaymentEvent;
use App\Types\InvoiceStatus;
use App\Types\InvoiceType;
class RefundPaymentListener
{
/**
* Create credit invoice service
*
* @var ICreateCreditNoteByInvoiceIdService
*/
private ICreateCreditNoteByInvoiceIdService $createCreditNoteByInvoiceIdService;
/**
* Pay invoice service
*
* @var IPayInvoiceService
*/
private IPayInvoiceService $payInvoiceService;
/**
* Invoice custom feature instance
*
* @var InvoiceCustomFeature
*/
private InvoiceCustomFeature $invoiceCustomFeature;
/**
* Order repository
*
* @var IOrderRepository
*/
private IOrderRepository $orderRepository;
/**
* Constructor
*
* @param ICreateCreditNoteByInvoiceIdService $createCreditNoteByInvoiceIdService
* @param IPayInvoiceService $payInvoiceService
* @param InvoiceCustomFeature $invoiceCustomFeature
* @param IOrderRepository $orderRepository
*/
public function __construct(
ICreateCreditNoteByInvoiceIdService $createCreditNoteByInvoiceIdService,
IPayInvoiceService $payInvoiceService,
InvoiceCustomFeature $invoiceCustomFeature,
IOrderRepository $orderRepository
)
{
$this->createCreditNoteByInvoiceIdService = $createCreditNoteByInvoiceIdService;
$this->payInvoiceService = $payInvoiceService;
$this->invoiceCustomFeature = $invoiceCustomFeature;
$this->orderRepository = $orderRepository;
}
/**
* Listen on refund payment event.
*
* A refund ALWAYS issues/settles a credit note whenever invoicing is enabled โ there is
* no opt-in flag anymore (a refund without a credit note is not offered). The reversal
* must be reflected by an avoir (and, under fiscal mode, net into the signed turnover +
* the FEC). Guarded so it only credits a finalized commercial invoice (no exception on an
* ineligible document).
*
* A credit note issued BEFORE its invoice was paid could not refund anything at issue
* time, so it was finalized but left unpaid (issued only). When the money is finally
* collected and this refund runs, that existing avoir is settled now: it is marked paid +
* seals its ยง7.2 INVOICE_PAID refund record. A credit note created together with its
* refund is already paid, so there is nothing left to do.
*
* @param RefundPaymentEvent $event
* @return void
*/
public function onAction(RefundPaymentEvent $event): void
{
if (!$this->invoiceCustomFeature->isEnabled()) {
return;
}
$order = $this->orderRepository->getOrder($event->getRequest()->getId());
$invoice = $order?->getInvoice();
if ($invoice === null
|| $invoice->getType() !== InvoiceType::INVOICE_TYPE_INVOICE
|| !in_array($invoice->getStatus(), [InvoiceStatus::STATUS_OPEN, InvoiceStatus::STATUS_PAID], true)
) {
return;
}
// One avoir per invoice: reuse the invoice's existing avoir, otherwise create it.
// findUnpaidCreditNote() returns null when that avoir is already paid โ the reversal
// is settled, so there is nothing to do (and we must NOT create a second avoir:
// checkMakeCreditAbility rejects that). An OPEN avoir issued before payment is settled
// by the single pay call below now that the refund has actually happened.
$creditNote = $invoice->hasCreditNotes()
? $this->findUnpaidCreditNote($invoice)
: $this->createCreditNoteByInvoiceIdService->perform($invoice->getId());
if ($creditNote === null) {
return;
}
$this->payInvoiceService->perform($creditNote->getId(), $event->getRequest()->getReason());
}
/**
* The invoice's issued-but-unpaid avoir (status OPEN), if any โ the credit note that
* was finalized before its invoice was paid and is now awaiting refund settlement.
*
* @param InvoiceEntity $invoice
* @return InvoiceEntity|null
*/
private function findUnpaidCreditNote(InvoiceEntity $invoice): ?InvoiceEntity
{
/** @var InvoiceEntity $creditNote */
foreach ($invoice->getCreditNotes() as $creditNote) {
if ($creditNote->getStatus() === InvoiceStatus::STATUS_OPEN) {
return $creditNote;
}
}
return null;
}
}