HEX
Server: Apache
System: Linux vps-3158868-x.dattaweb.com 3.10.0-1160.119.1.el7.x86_64 #1 SMP Tue Jun 4 14:43:51 UTC 2024 x86_64
User: emerlux (1185)
PHP: 8.3.1
Disabled: system, shell, exec, system_exec, shell_exec, mysql_pconnect, passthru, popen, proc_open, proc_close, proc_nice, proc_terminate, proc_get_status, escapeshellarg, escapeshellcmd, eval
Upload Files
File: /home/emerlux/public_html/wp-content/plugins/woocommerce-mercadopago/src/Refund/RefundHandler.php
<?php

namespace MercadoPago\Woocommerce\Refund;

use Exception;
use MercadoPago\Woocommerce\Helpers\Device;
use MercadoPago\Woocommerce\Helpers\Numbers;
use MercadoPago\Woocommerce\Helpers\PaymentMetadata;
use MercadoPago\Woocommerce\Helpers\Requester;
use MercadoPago\Woocommerce\Helpers\RefundStatusCodes;
use MercadoPago\Woocommerce\Exceptions\RefundException;
use MercadoPago\PP\Sdk\HttpClient\Response;
use MercadoPago\Woocommerce\WoocommerceMercadoPago;
use MercadoPago\Woocommerce\Libraries\Metrics\Datadog;

if (!defined('ABSPATH')) {
    exit;
}

class RefundHandler
{
    private const REFUND_ENDPOINT = '/ppcore/prod/transaction/v1/payments/%s/refund';
    private const LOG_SOURCE = 'MercadoPago_RefundHandler';

    private const REFUND_ORIGIN = 'painel_woocommerce';
    private const PAYMENT_ID_META_KEY = '_Mercado_Pago_Payment_IDs';
    private const REFUND_METRIC_SUCCESS_WOO = 'woo_refund_success';
    private const REFUND_METRIC_ERROR_WOO = 'woo_refund_error';
    private const REFUND_ORIGIN_WOO = 'origin_woocommerce';
    private const CHECKOUT_TYPE = 'checkout_type';
    private const SUPER_TOKEN = 'super_token';

    private Requester $requester;
    private $order;
    private WoocommerceMercadoPago $mercadopago;
    private Datadog $datadog;
    private RefundStatusCodes $refundStatusCodes;

    public function __construct(Requester $requester, $order, WoocommerceMercadoPago $mercadopago)
    {
        $this->requester = $requester;
        $this->order = $order;
        $this->mercadopago = $mercadopago;
        $this->datadog = Datadog::getInstance();
        $this->refundStatusCodes = new RefundStatusCodes($mercadopago->adminTranslations);
    }

    /**
     * Process refund request
     *
     * @param float $amount
     * @param string $reason
     * @return array
     * @throws RefundException
     */
    public function processRefund(float $amount, string $reason = ''): array
    {
        if (!\current_user_can('manage_woocommerce')) {
            throw new Exception(RefundException::TYPE_NO_PERMISSION);
        }

        $checkoutType = $this->order->get_meta(self::CHECKOUT_TYPE);
        if (!empty($checkoutType) && $checkoutType === self::SUPER_TOKEN) {
            throw new Exception(RefundException::TYPE_SUPERTOKEN_NOT_SUPPORTED);
        }

        try {
            $paymentId = $this->getPaymentId();
            $paymentIds = explode(', ', $paymentId);

            if (count($paymentIds) > 1) {
                $amountToRefund = $amount;
                $amountRemainingInPayment = 0;
                $response = [];
                foreach ($paymentIds as $refundingPaymentId) {
                    $field = $this->order->get_meta(PaymentMetadata::getPaymentMetaKey($refundingPaymentId));

                    $paymentData = PaymentMetadata::extractPaymentDataFromMeta($field);

                    $paidAmount = $paymentData->paid ?? 0;
                    $refundedAmount = $paymentData->refund ?? 0;
                    $amountRemainingInPayment = max(0, $paidAmount - $refundedAmount);

                    if ($amountRemainingInPayment <= 0) {
                        continue;
                    }

                    if ($amountToRefund > $amountRemainingInPayment) {
                        $amountToRefundInPayment = $amountRemainingInPayment;
                        $amountToRefund = $amountToRefund - $amountRemainingInPayment;
                    } else {
                        $amountToRefundInPayment = $amountToRefund;
                        $amountToRefund = 0;
                    }

                    $result = $this->executeRefund($refundingPaymentId, $amountToRefundInPayment, $reason);
                    $response[] = $result;

                    if ($amountToRefund === 0) {
                        break;
                    }
                }
                return $response;
            } else {
                return $this->executeRefund($paymentId, $amount, $reason);
            }
        } catch (RefundException $e) {
            $this->sendRefundErrorMetric($e->getCode(), $e->getMessage());
            $this->mercadopago->logs->file->error('Refund processing failed - ' . $e->getMessage(), self::LOG_SOURCE, $e->getLoggingContext());

            throw $e;
        } catch (Exception $e) {
            $this->sendRefundErrorMetric($e->getCode(), $e->getMessage());
            $this->mercadopago->logs->file->error('Unexpected refund error: ' . $e->getMessage(), self::LOG_SOURCE);

            throw $e;
        }
    }

    /**
     * Execute refund process for a single payment
     *
     * @param string $paymentId
     * @param float $amount
     * @param string $reason
     *
     * @return array
     * @throws Exception
     */
    private function executeRefund(string $paymentId, float $amount, string $reason): array
    {
        $payload = $this->buildRefundPayload($amount, $reason);
        $headers = $this->buildRequestHeaders();

        $refundResponse = $this->executeRefundRequest($paymentId, $headers, $payload);
        $result = $this->processRefundResponse($refundResponse, $paymentId);

        $this->mercadopago->logs->file->info('Refund processed successfully', self::LOG_SOURCE, [
            'order_id' => $this->order->get_id(),
            'result' => $result
        ]);

        $this->sendRefundSuccessMetric();
        return $result;
    }

    /**
     * Build refund payload
     *
     * @param float $amount
     * @param string $reason
     *
     * @return array
     */
    private function buildRefundPayload(float $amount, string $reason): array
    {
        $payload = [
            'amount' => Numbers::format($amount),
            'metadata' => [
                'origin' => self::REFUND_ORIGIN
            ]
        ];

        if (!empty($reason)) {
            $payload['metadata']['reason'] = \sanitize_text_field($reason);
        }

        return $payload;
    }

    /**
     * Build request headers
     *
     * @return array
     * @throws Exception
     */
    private function buildRequestHeaders(): array
    {
        $accessToken = $this->mercadopago->sellerConfig->getCredentialsAccessToken();

        return [
            'Authorization' => 'Bearer ' . $accessToken,
            'x-platform-id' => MP_PLATFORM_ID,
            'x-product-id' => Device::getDeviceProductId()
        ];
    }

    /**
     * Execute refund request
     *
     * @param string $paymentId
     * @param array $headers
     * @param array $payload
     *
     * @return Response
     * @throws Exception
     */
    private function executeRefundRequest(string $paymentId, array $headers, array $payload): Response
    {
        $endpoint = sprintf(self::REFUND_ENDPOINT, $paymentId);

        return $this->requester->post($endpoint, $headers, $payload);
    }

    /**
     * Process refund response
     *
     * @param Response $response
     * @param string $paymentId
     *
     * @return array
     * @throws RefundException
     */
    private function processRefundResponse(Response $response, string $paymentId): array
    {
        $statusCode = $response->getStatus();
        $rawData = $response->getData();

        $data = [];
        if ($rawData !== null) {
            $data = is_array($rawData) ? $rawData : (array) $rawData;
        }

        if ($this->refundStatusCodes->isSuccessful($statusCode)) {
            return ['status' => 'approved', 'data' => $data];
        }

        throw $this->refundStatusCodes->createException($statusCode, $data, $paymentId, $this->order->get_id());
    }

    /**
     * Get payment ID from order
     *
     * @return string
     * @throws RefundException
     */
    private function getPaymentId(): string
    {
        $paymentId = $this->order->get_meta(self::PAYMENT_ID_META_KEY);

        if (empty($paymentId)) {
            throw $this->refundStatusCodes->createException(
                RefundStatusCodes::NOT_FOUND,
                ['message' => 'Payment ID not found in order metadata'],
                null,
                $this->order->get_id(),
                ['meta_key_searched' => self::PAYMENT_ID_META_KEY]
            );
        }

        return $paymentId;
    }

    /**
     * Send refund success metric to Datadog
     */
    private function sendRefundSuccessMetric(): void
    {
        $this->datadog->sendEvent(self::REFUND_METRIC_SUCCESS_WOO, 'refund_success', self::REFUND_ORIGIN_WOO);
    }

    /**
     * Send refund error metric to Datadog
     *
     * @param string $errorCode
     * @param string $errorMessage
     */
    private function sendRefundErrorMetric(string $errorCode, string $errorMessage): void
    {
        $this->datadog->sendEvent(self::REFUND_METRIC_ERROR_WOO, $errorCode, $errorMessage);
    }
}