src/Service/App/Merchant/Invoice/Listener/RefundPaymentListener.php line 69

Open in your IDE?
  1. <?php
  2. namespace App\Service\App\Merchant\Invoice\Listener;
  3. use App\Entity\Merchant\InvoiceEntity;
  4. use App\Repository\Merchant\Contract\IOrderRepository;
  5. use App\Service\App\Merchant\CustomFeature\Implementation\InvoiceCustomFeature;
  6. use App\Service\App\Merchant\Invoice\Contract\ICreateCreditNoteByInvoiceIdService;
  7. use App\Service\App\Merchant\Invoice\Contract\IPayInvoiceService;
  8. use App\Service\App\Merchant\Payment\Event\RefundPaymentEvent;
  9. use App\Types\InvoiceStatus;
  10. use App\Types\InvoiceType;
  11. class RefundPaymentListener
  12. {
  13.     /**
  14.      * Create credit invoice service
  15.      *
  16.      * @var ICreateCreditNoteByInvoiceIdService
  17.      */
  18.     private ICreateCreditNoteByInvoiceIdService $createCreditNoteByInvoiceIdService;
  19.     /**
  20.      * Pay invoice service
  21.      *
  22.      * @var IPayInvoiceService
  23.      */
  24.     private IPayInvoiceService $payInvoiceService;
  25.     /**
  26.      * Invoice custom feature instance
  27.      *
  28.      * @var InvoiceCustomFeature
  29.      */
  30.     private InvoiceCustomFeature $invoiceCustomFeature;
  31.     /**
  32.      * Order repository
  33.      *
  34.      * @var IOrderRepository
  35.      */
  36.     private IOrderRepository $orderRepository;
  37.     /**
  38.      * Constructor
  39.      *
  40.      * @param ICreateCreditNoteByInvoiceIdService $createCreditNoteByInvoiceIdService
  41.      * @param IPayInvoiceService $payInvoiceService
  42.      * @param InvoiceCustomFeature $invoiceCustomFeature
  43.      * @param IOrderRepository $orderRepository
  44.      */
  45.     public function __construct(
  46.         ICreateCreditNoteByInvoiceIdService $createCreditNoteByInvoiceIdService,
  47.         IPayInvoiceService $payInvoiceService,
  48.         InvoiceCustomFeature $invoiceCustomFeature,
  49.         IOrderRepository $orderRepository
  50.     )
  51.     {
  52.         $this->createCreditNoteByInvoiceIdService = $createCreditNoteByInvoiceIdService;
  53.         $this->payInvoiceService = $payInvoiceService;
  54.         $this->invoiceCustomFeature = $invoiceCustomFeature;
  55.         $this->orderRepository = $orderRepository;
  56.     }
  57.     /**
  58.      * Listen on refund payment event.
  59.      *
  60.      * A refund ALWAYS issues/settles a credit note whenever invoicing is enabled โ€” there is
  61.      * no opt-in flag anymore (a refund without a credit note is not offered). The reversal
  62.      * must be reflected by an avoir (and, under fiscal mode, net into the signed turnover +
  63.      * the FEC). Guarded so it only credits a finalized commercial invoice (no exception on an
  64.      * ineligible document).
  65.      *
  66.      * A credit note issued BEFORE its invoice was paid could not refund anything at issue
  67.      * time, so it was finalized but left unpaid (issued only). When the money is finally
  68.      * collected and this refund runs, that existing avoir is settled now: it is marked paid +
  69.      * seals its ยง7.2 INVOICE_PAID refund record. A credit note created together with its
  70.      * refund is already paid, so there is nothing left to do.
  71.      *
  72.      * @param RefundPaymentEvent $event
  73.      * @return void
  74.      */
  75.     public function onAction(RefundPaymentEvent $event): void
  76.     {
  77.         if (!$this->invoiceCustomFeature->isEnabled()) {
  78.             return;
  79.         }
  80.         $order = $this->orderRepository->getOrder($event->getRequest()->getId());
  81.         $invoice = $order?->getInvoice();
  82.         if ($invoice === null
  83.             || $invoice->getType() !== InvoiceType::INVOICE_TYPE_INVOICE
  84.             || !in_array($invoice->getStatus(), [InvoiceStatus::STATUS_OPEN, InvoiceStatus::STATUS_PAID], true)
  85.         ) {
  86.             return;
  87.         }
  88.         // One avoir per invoice: reuse the invoice's existing avoir, otherwise create it.
  89.         // findUnpaidCreditNote() returns null when that avoir is already paid โ€” the reversal
  90.         // is settled, so there is nothing to do (and we must NOT create a second avoir:
  91.         // checkMakeCreditAbility rejects that). An OPEN avoir issued before payment is settled
  92.         // by the single pay call below now that the refund has actually happened.
  93.         $creditNote = $invoice->hasCreditNotes()
  94.             ? $this->findUnpaidCreditNote($invoice)
  95.             : $this->createCreditNoteByInvoiceIdService->perform($invoice->getId());
  96.         if ($creditNote === null) {
  97.             return;
  98.         }
  99.         $this->payInvoiceService->perform($creditNote->getId(), $event->getRequest()->getReason());
  100.     }
  101.     /**
  102.      * The invoice's issued-but-unpaid avoir (status OPEN), if any โ€” the credit note that
  103.      * was finalized before its invoice was paid and is now awaiting refund settlement.
  104.      *
  105.      * @param InvoiceEntity $invoice
  106.      * @return InvoiceEntity|null
  107.      */
  108.     private function findUnpaidCreditNote(InvoiceEntity $invoice): ?InvoiceEntity
  109.     {
  110.         /** @var InvoiceEntity $creditNote */
  111.         foreach ($invoice->getCreditNotes() as $creditNote) {
  112.             if ($creditNote->getStatus() === InvoiceStatus::STATUS_OPEN) {
  113.                 return $creditNote;
  114.             }
  115.         }
  116.         return null;
  117.     }
  118. }