Your IP : 216.73.216.189


Current Path : /var/www/magento.test.indacotrentino.com/www/vendor/stripe/module-payments/Helper/
Upload File :
Current File : /var/www/magento.test.indacotrentino.com/www/vendor/stripe/module-payments/Helper/Creditmemo.php

<?php

namespace StripeIntegration\Payments\Helper;

class Creditmemo
{
    private $creditmemoRepository;
    private $creditmemoManagement;
    private $creditmemoFactory;
    private $creditmemoService;
    private $helper;

    public function __construct(
        \Magento\Sales\Model\Order\CreditmemoFactory $creditmemoFactory,
        \Magento\Sales\Model\Service\CreditmemoService $creditmemoService,
        \Magento\Sales\Api\CreditmemoRepositoryInterface $creditmemoRepository,
        \Magento\Sales\Api\CreditmemoManagementInterface $creditmemoManagement,
        \StripeIntegration\Payments\Helper\Generic $helper
    ) {
        $this->creditmemoFactory = $creditmemoFactory;
        $this->creditmemoService = $creditmemoService;
        $this->creditmemoRepository = $creditmemoRepository;
        $this->creditmemoManagement = $creditmemoManagement;
        $this->helper = $helper;
    }

    public function save($creditmemo)
    {
        if (empty($creditmemo))
            return null;

        try
        {
            return $this->creditmemoRepository->save($creditmemo);
        }
        catch (\Exception $e)
        {
            $this->helper->logError($e->getMessage(), $e->getTraceAsString());
            return null;
        }
    }

    public function sendEmail($creditmemoId)
    {
        $this->creditmemoManagement->notify($creditmemoId);
    }

    public function validateBaseRefundAmount($order, $baseAmount)
    {
        if (!$order->canCreditmemo())
        {
            throw new \Exception("The order cannot be refunded");
        }

        if ($baseAmount <= 0)
        {
            throw new \Exception("Cannot refund an amount of $baseAmount.");
        }
    }

    public function refundOfflineOrderBaseAmount($order, $baseAmount, $sendCustomerEmail = true)
    {
        try
        {
            $this->validateBaseRefundAmount($order, $baseAmount);

            // Do not refund any order items
            $qtys = $this->getOrderItemQtys($order);

            $params = [
                "qtys" => $qtys,
                "shipping_amount" => '0',
                "adjustment_positive" => $baseAmount,
                "adjustment_negative" => '0',
                "send_email" => ($sendCustomerEmail ? '1' : '0'),
                "do_offline" => '1',
                "items" => [ $qtys ],
                // "comment_text" => __(""),
                // "comment_customer_notify" => 1
            ];

            $creditmemo = $this->creditmemoFactory->createByOrder($order, $params);

            // Create the credit memo
            $creditmemo = $this->creditmemoService->refund($creditmemo, $offline = true);

            if ($params["send_email"])
            {
                $this->sendEmail($creditmemo->getId());
            }

            return $creditmemo;
        }
        catch (\Exception $e)
        {
            $this->helper->logError($e->getMessage(), $e->getTraceAsString());
            return null;
        }
    }

    // Loops and tries to calculate the base amount to refund so that
    // Order.total_paid - Order.total_refunded  == StripeInvoice.total_paid
    // This fixes rounding errors when converting back to a base amount from a 2 decimal-places float
    public function getBaseRefundAmount(int $stripeInvoiceTotalPaid, string $stripeInvoiceCurrency, $order)
    {
        $otherAmounts = 0;
        $otherAmounts += round((float)$order->getTotalRefunded(), 2); // Previously refunded payments
        $otherAmounts += round((float)$order->getTotalCanceled(), 2); // Previously canceled payments

        $baseOtherAmounts = 0;
        $baseOtherAmounts += round((float)$order->getBaseTotalRefunded(), 2); // Previously refunded payments
        $baseOtherAmounts += round((float)$order->getBaseTotalCanceled(), 2); // Previously canceled payments

        $total = round((float)$order->getGrandTotal(), 2);
        $baseTotal = round((float)$order->getBaseGrandTotal(), 2);

        $paidAmount = $this->helper->convertStripeAmountToOrderAmount($stripeInvoiceTotalPaid, $stripeInvoiceCurrency, $order);
        $basePaidAmount = $this->helper->convertStripeAmountToBaseOrderAmount($stripeInvoiceTotalPaid, $stripeInvoiceCurrency, $order); // This is expected to cause a rounding error

        $baseRefundAmount = $baseTotal - $baseOtherAmounts - $basePaidAmount;

        // Fix the rounding error
        do
        {
            // First convert back to order amount
            $refundAmount = $this->helper->convertBaseAmountToOrderAmount($baseRefundAmount, $order, $stripeInvoiceCurrency);

            // We now expect that $amount + $otherAmounts == $total. If not, we adjust the base amount and retry
            $combinedAmount = round((float)($refundAmount + $otherAmounts + $paidAmount), 2);
            $difference = round(abs($total - $combinedAmount), 2);

            if ($difference > 0.01)
            {
                // It is possible that the order includes multiple charges, which means this is not a rounding error
                break;
            }
            else if ($combinedAmount > $total)
            {
                $baseRefundAmount -= 0.01;
            }
            else if ($combinedAmount < $total)
            {
                $baseRefundAmount += 0.01;
            }
            else
            {
                break;
            }
        }
        while (true);

        return $baseRefundAmount;
    }

    public function isUnderCharged($order, $invoiceAmountPaid, $invoiceCurrency)
    {
        if (strtolower($order->getOrderCurrencyCode()) != strtolower($invoiceCurrency))
            throw new \Exception("The order and the invoice are not in the same currency");

        $roundingErrorsAllowance = 0.01;
        $invoiceTotal = $this->helper->convertStripeAmountToOrderAmount($invoiceAmountPaid, $invoiceCurrency, $order);
        $round = round((float)$order->getGrandTotal(), 2);
        $isUnderCharged = (($invoiceTotal + $roundingErrorsAllowance) < round((float)$order->getGrandTotal(), 2));

        if ($isUnderCharged && $order->canCreditmemo())
        {
            // a) Includes a trial subscription (0 < invoiceAmountPaid < orderTotal)
            // b) The customer had a credit balance (0 <= invoiceAmountPaid < order total)

            return true;
        }

        return false;
    }

    public function refundUnderchargedOrder($order, $invoiceAmountPaid, $invoiceCurrency, $sendCustomerEmail = true)
    {
        try
        {
            if ($this->isUnderCharged($order, $invoiceAmountPaid, $invoiceCurrency))
            {
                // a) Includes a trial subscription (0 < invoiceAmountPaid < orderTotal)
                // b) The customer had a credit balance (0 <= invoiceAmountPaid < order total)

                // Make sure that the refund amount + paid amount match the order grand total, i.e. avoid rounding errors
                $baseRefundAmount = $this->getBaseRefundAmount($invoiceAmountPaid, $invoiceCurrency, $order);

                if ($baseRefundAmount > 0)
                {
                    $creditmemo = $this->refundOfflineOrderBaseAmount($order, $baseRefundAmount, $sendCustomerEmail);
                    $this->save($creditmemo);
                }
            }
        }
        catch (\Exception $e)
        {
            $this->helper->logError("Could not refund undercharged order: " . $e->getMessage(), $e->getTraceAsString());
        }
    }

    public function getOrderShippingAmount($order)
    {
        if ($order->getBaseShippingAmount())
        {
            return $order->getBaseShippingAmount();
        }
        else if ($order->getShippingAmount())
        {
            return $this->helper->convertOrderAmountToBaseAmount($order->getShippingAmount(), $order->getOrderCurrencyCode(), $order);
        }
        else
        {
            return '0';
        }
    }

    // Returns an array of [ $orderItemId => $qtyOrdered ], suitable for passing to Credit Memos
    public function getOrderItemQtys($order)
    {
        $qtys = [];

        foreach ($order->getAllVisibleItems() as $orderItem)
        {
            $orderItemId = $orderItem->getId();
            $qty = $orderItem->getQtyOrdered();

            if (in_array($orderItem->getProductType(), ['bundle']))
            {
                // If this is set to 0, Magento will add Bundle or Configurable items to the credit memo,
                // which is not the intended behavior. We instead want to create a credit memo without any items,
                // which will cause the order to remain in Processing/Complete status, instead of Closed/Canceled.
                // 0 will trigger the (count(array_unique($qtys)) === 1 && (int)end($qtys) === 0) condition at
                // https://github.com/magento/magento2/blob/2.4.5-p1/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php#L162
                $qtys[$orderItemId] = '-1';
            }
            else
            {
                $qtys[$orderItemId] = '0';
            }
        }

        return $qtys;
    }

    public function refundFromStripeDashboard($order, array $object)
    {
        if ($order->getState() == "holded" && $order->canUnhold())
            $order->unhold();

        // Check if the order has an invoice with the charge ID we are refunding
        $chargeId = $object['id'];
        $chargeAmount = $object['amount'];
        $currentRefund = $this->getCurrentRefundFrom($object);
        $currency = $currentRefund['currency'];
        $baseToOrderRate = $order->getBaseToOrderRate();

        if (isset($object["payment_intent"]))
            $pi = $object["payment_intent"];
        else
            $pi = "not_exists";

        // Calculate the real refund amount
        $isMultiCurrencyRefund = (strtolower($currentRefund['currency']) != strtolower($order->getOrderCurrencyCode()));
        $refundAmount = $this->helper->convertStripeAmountToOrderAmount($currentRefund['amount'], $currentRefund['currency'], $order);
        $baseRefundAmount = $this->helper->convertStripeAmountToBaseOrderAmount($currentRefund['amount'], $currentRefund['currency'], $order);

        $baseTotalNotRefunded = $order->getBaseGrandTotal() - $order->getBaseTotalRefunded();
        $totalNotRefunded = $order->getGrandTotal() - $order->getTotalRefunded();

        if ($isMultiCurrencyRefund)
            $isPartialRefund = ($totalNotRefunded > $refundAmount);
        else
            $isPartialRefund = ($baseTotalNotRefunded > $baseRefundAmount);

        if (!$order->canCreditmemo())
        {
            if ($order->canCancel())
            {
                if (!$isPartialRefund)
                {
                    $order->cancel();
                    $this->helper->saveOrder($order);
                    return true;
                }
                else if ($isPartialRefund)
                {
                    // Don't do anything on a partial refund, we expect a paynemt_intent.succeeded to arrive for the partial capture.
                    return false;
                }
            }
            else if (!$isPartialRefund)
            {
                $invoices = $order->getInvoiceCollection();
                $canceled = 0;
                foreach ($invoices as $invoice)
                {
                    if ($invoice->canCancel())
                    {
                        $invoice->cancel();
                        $this->helper->saveInvoice($invoice);
                        $canceled++;
                    }
                }
                if ($canceled > 0)
                {
                    if ($order->canCancel())
                    {
                        $order->getPayment()->setCancelOfflineWithComment(__("The authorization was canceled via Stripe."));
                        $order->cancel();
                    }

                    $this->helper->saveOrder($order);
                    return true;
                }
            }

            $msg = __('A refund was issued via Stripe, but a Credit Memo could not be created.');
            $this->helper->addOrderComment($msg, $order);
            $this->helper->saveOrder($order);
            return false;
        }

        if ($baseTotalNotRefunded < $baseRefundAmount)
        {
            $humanReadable1 = $this->helper->addCurrencySymbol($refundAmount, $currency);
            $humanReadable2 = $this->helper->addCurrencySymbol($totalNotRefunded, $currency);
            $msg = __('A refund of %1 was issued via Stripe, but the amount is bigger than the available of %2.', $humanReadable1, $humanReadable2);
            $this->helper->addOrderComment($msg, $order);
            $this->helper->saveOrder($order);
            return false;
        }

        $creditmemo = $this->refundOfflineOrderBaseAmount($order, $baseRefundAmount);
        $comment = __("We refunded %1 through Stripe.", $this->helper->addCurrencySymbol($refundAmount, $currency));

        if ($order->getBaseTotalRefunded() == $order->getBaseGrandTotal() ||
            $order->getTotalRefunded() == $order->getGrandTotal())
        {
            $state = \Magento\Sales\Model\Order::STATE_CLOSED;
            $status = $order->getConfig()->getStateDefaultStatus($state);
            $order->setState($state)->addStatusToHistory($status, $comment);
        }
        else
        {
            $order->addStatusToHistory($status = false, $comment);
        }

        $this->save($creditmemo);
        $this->helper->saveOrder($order);

        return true;
    }

    private function getCurrentRefundFrom($webhookData)
    {
        $lastRefundDate = 0;
        $currentRefund = null;

        foreach ($webhookData['refunds']['data'] as $refund)
        {
            // There might be multiple refunds, and we are looking for the most recent one
            if ($refund['created'] > $lastRefundDate)
            {
                $lastRefundDate = $refund['created'];
                $currentRefund = $refund;
            }
        }

        return $currentRefund;
    }

    private function getInvoiceWithTransactionId($transactionId, $order)
    {
        foreach($order->getInvoiceCollection() as $item)
        {
            $invoiceTransactionId = $this->helper->cleanToken($item->getTransactionId());
            if ($transactionId == $invoiceTransactionId)
                return $item;
        }

        return null;
    }
}