HEX
Server: Apache/2.4.41 (Ubuntu)
System: Linux vm8 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64
User: afleverb (1000)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //var/www/drakkar_site/wp-content/plugins/woocommerce-mercadopago/src/Order/OrderStatus.php
<?php

namespace MercadoPago\Woocommerce\Order;

use Exception;
use MercadoPago\Woocommerce\Configs\Seller;
use MercadoPago\Woocommerce\Helpers\Requester;
use MercadoPago\Woocommerce\Translations\StoreTranslations;
use MercadoPago\Woocommerce\Libraries\Logs\Logs;
use WC_Order;

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

class OrderStatus
{
    private array $translations;

    private array $commonMessages;

    private OrderMetadata $orderMetadata;

    private Seller $seller;

    private Requester $requester;

    private Logs $logs;

    private const PAYMENTS_ENDPOINT = '/v1/payments/';
    private const NOTIFICATION_ENDPOINT = '/v1/asgard/notification/';

    /**
     * Order constructor
     */
    public function __construct(
        StoreTranslations $storeTranslations,
        OrderMetadata $orderMetadata,
        Seller $seller,
        Requester $requester,
        Logs $logs
    ) {
        $this->translations   = $storeTranslations->orderStatus;
        $this->commonMessages = $storeTranslations->commonMessages;
        $this->orderMetadata  = $orderMetadata;
        $this->seller         = $seller;
        $this->requester      = $requester;
        $this->logs           = $logs;
    }

    /**
     * Set order status from/to
     *
     * @param WC_Order $order
     * @param string $fromStatus
     * @param string $toStatus
     *
     * @return void
     */
    public function setOrderStatus(WC_Order $order, string $fromStatus, string $toStatus): void
    {
        if ($order->get_status() === $fromStatus) {
            $order->set_status($toStatus);
            $order->save();
        }
    }

    /**
     * Get order status message
     *
     * @param string $statusDetail
     *
     * @return string
     */
    public function getOrderStatusMessage(string $statusDetail): string
    {
        if (isset($this->commonMessages['cho_' . $statusDetail])) {
            return $this->commonMessages['cho_' . $statusDetail];
        }
        return $this->commonMessages['cho_default'];
    }

    /**
     * Process order status
     *
     * @param string $processedStatus
     * @param array $data
     * @param WC_Order $order
     * @param string $usedGateway
     *
     * @return void
     * @throws Exception
     */
    public function processStatus(string $processedStatus, array $data, WC_Order $order, string $usedGateway): void
    {
        switch ($processedStatus) {
            case 'approved':
                $this->approvedFlow($data, $order);
                break;
            case 'pending':
                $this->pendingFlow($data, $order, $usedGateway);
                break;
            case 'in_process':
                $this->inProcessFlow($data, $order);
                break;
            case 'rejected':
                $this->rejectedFlow($data, $order);
                break;
            case 'refunded':
                $this->refundedFlow($data, $order);
                break;
            case 'cancelled':
                $this->cancelledFlow($data, $order);
                break;
            case 'in_mediation':
                $this->inMediationFlow($order);
                break;
            case 'charged_back':
                $this->chargedBackFlow($order);
                break;
            default:
                throw new Exception('Process Status - Invalid Status: ' . $processedStatus);
        }
    }

    /**
     * Rule of approved payment
     *
     * @param array $data
     * @param WC_Order $order
     *
     * @return void
     */
    private function approvedFlow(array $data, WC_Order $order): void
    {
        if (isset($data['status_detail']) && $data['status_detail'] === 'partially_refunded') {
            return;
        }

        $status = $order->get_status();

        if ($status === 'pending' || $status === 'on-hold' || $status === 'failed') {
            $order->add_order_note('Mercado Pago: ' . $this->translations['payment_approved']);
            $order->add_order_note('Mercado Pago: ' . $this->translations['payment_approved'], 1);

            $payment_completed_status = apply_filters(
                'woocommerce_payment_complete_order_status',
                $order->needs_processing() ? 'processing' : 'completed',
                $order->get_id(),
                $order
            );

            if (method_exists($order, 'get_status') && $order->get_status() !== 'completed') {
                $order->payment_complete();
                if ($payment_completed_status !== 'completed') {
                    $order->update_status(self::mapMpStatusToWoocommerceStatus('approved'));
                }
            }
        }
    }

    /**
     * Rule of pending
     *
     * @param array $data
     * @param WC_Order $order
     * @param string $usedGateway
     *
     * @return void
     */
    private function pendingFlow(array $data, WC_Order $order, string $usedGateway): void
    {
        if ($this->canUpdateOrderStatus($order)) {
            $order->update_status(self::mapMpStatusToWoocommerceStatus('pending'));

            switch ($usedGateway) {
                case 'MercadoPago\Woocommerce\Gateways\PixGateway':
                    $notes = $order->get_customer_order_notes();

                    if (count($notes) > 1) {
                        break;
                    }

                    $order->add_order_note('Mercado Pago: ' . $this->translations['pending_pix']);
                    $order->add_order_note('Mercado Pago: ' . $this->translations['pending_pix'], 1);
                    break;

                case 'MercadoPago\Woocommerce\Gateways\TicketGateway':
                    $notes = $order->get_customer_order_notes();

                    if (count($notes) > 1) {
                        break;
                    }

                    $order->add_order_note('Mercado Pago: ' . $this->translations['pending_ticket']);
                    $order->add_order_note('Mercado Pago: ' . $this->translations['pending_ticket'], 1);
                    break;

                default:
                    $order->add_order_note('Mercado Pago: ' . $this->translations['pending']);
                    break;
            }
        } else {
            $this->validateOrderNoteType($data, $order, 'pending');
        }
    }

    /**
     * Rule of In Process
     *
     * @param array $data
     * @param WC_Order $order
     *
     * @return void
     */
    private function inProcessFlow(array $data, WC_Order $order): void
    {
        if ($this->canUpdateOrderStatus($order)) {
            $order->update_status(
                self::mapMpStatusToWoocommerceStatus('inprocess'),
                'Mercado Pago: ' . $this->translations['in_process']
            );
        } else {
            $this->validateOrderNoteType($data, $order, 'in_process');
        }
    }

    /**
     * Rule of Rejected
     *
     * @param array $data
     * @param WC_Order $order
     *
     * @return void
     */
    private function rejectedFlow(array $data, WC_Order $order): void
    {
        if ($this->canUpdateOrderStatus($order)) {
            $order->update_status(
                self::mapMpStatusToWoocommerceStatus('rejected'),
                'Mercado Pago: ' . $this->translations['rejected']
            );
        } else {
            $this->validateOrderNoteType($data, $order, 'rejected');
        }
    }

    /**
     * Rule of Refunded
     *
     * @param array $data
     * @param WC_Order $order
     *
     * @return void
     * @throws Exception
     */
    private function refundedFlow(array $data, WC_Order $order): void
    {
        if (!$this->isNotification($data)) {
            return;
        }

        try {
            $paymentsData = $this->getAllPaymentsData($order);
            $refundStatus = $this->getRefundedStatusDetail($paymentsData);

            if ($refundStatus['description'] === 'partially_refunded') {
                if ($this->refundAlreadyProcessed($order, $paymentsData)) {
                    $this->logs->file->info('Mercado Pago: Refund already processed, skipping...', __CLASS__);
                    return;
                }

                $refundAmount = 0;

                if (isset($data['current_refund']['id'])) {
                    $refundId = $data['current_refund']['id'];

                    $refund = array_filter($data['refunds_notifying'], function ($item) use ($refundId) {
                        return isset($item['id']) && $item['id'] == $refundId;
                    });
                    $refund = reset($refund);
                    if ($refund === false) {
                        throw new Exception('Refund not found for the given refund ID: ' . $refundId);
                    }
                    $refundAmount = floatval($refund['amount']);
                } else {
                    $refundAmount = $this->getUnprocessedRefundAmount($order, $paymentsData);
                    if ($refundAmount <= 0) {
                        $this->logs->file->info('Mercado Pago: No unprocessed refund amount found, skipping...', __CLASS__);
                        return;
                    }
                }

                wc_create_refund(array(
                    'amount'   => $refundAmount,
                    'reason'   => $this->translations['refunded'],
                    'order_id' => $order->get_id(),
                ));
                $order->add_order_note('Mercado Pago: ' . $this->translations['partial_refunded'] . $refundAmount);

                return;
            }
        } catch (Exception $e) {
            $this->logs->file->error('Mercado Pago: Error processing refund validation: ' . $e->getMessage(), __CLASS__);
            return;
        }

        $order->update_status(
            self::mapMpStatusToWoocommerceStatus('refunded'),
            'Mercado Pago: ' . $this->translations['refunded']
        );
    }

    /**
     * Rule of Cancelled
     *
     * @param array $data
     * @param WC_Order $order
     *
     * @return void
     */
    private function cancelledFlow(array $data, WC_Order $order): void
    {
        if ($this->canUpdateOrderStatus($order)) {
            $order->update_status(
                self::mapMpStatusToWoocommerceStatus('cancelled'),
                'Mercado Pago: ' . $this->translations['cancelled']
            );
        } else {
            $this->validateOrderNoteType($data, $order, 'cancelled');
        }
    }

    /**
     * Rule of In mediation
     *
     * @param WC_Order $order
     *
     * @return void
     */
    private function inMediationFlow(WC_Order $order): void
    {
        $order->update_status(self::mapMpStatusToWoocommerceStatus('inmediation'));
        $order->add_order_note('Mercado Pago: ' . $this->translations['in_mediation']);
    }

    /**
     * Rule of Charged back
     *
     * @param WC_Order $order
     *
     * @return void
     */
    private function chargedBackFlow(WC_Order $order): void
    {
        $order->update_status(self::mapMpStatusToWoocommerceStatus('chargedback'));
        $order->add_order_note('Mercado Pago: ' . $this->translations['charged_back']);
    }

    /**
     * Mercado Pago status
     *
     * @param string $mpStatus
     *
     * @return string
     */
    public static function mapMpStatusToWoocommerceStatus(string $mpStatus): string
    {
        $statusMap = array(
            'pending'     => 'pending',
            'approved'    => 'processing',
            'inprocess'   => 'on_hold',
            'inmediation' => 'on_hold',
            'rejected'    => 'failed',
            'cancelled'   => 'cancelled',
            'refunded'    => 'refunded',
            'chargedback' => 'refunded',
        );

        $status = $statusMap[$mpStatus];

        return str_replace('_', '-', $status);
    }

    /**
     * Can update order status?
     *
     * @param WC_Order $order
     *
     * @return bool
     */
    protected function canUpdateOrderStatus(WC_Order $order): bool
    {
        return method_exists($order, 'get_status') &&
            $order->get_status() !== 'completed' &&
            $order->get_status() !== 'processing';
    }

    /**
     * Validate Order Note by Type
     *
     * @param array $data
     * @param WC_Order $order
     * @param string $status
     *
     * @return void
     */
    protected function validateOrderNoteType(array $data, WC_Order $order, string $status): void
    {
        $paymentId = $data['id'];

        if (isset($data['ipn_type']) && $data['ipn_type'] === 'merchant_order') {
            $payments = array();

            foreach ($data['payments'] as $payment) {
                $payments[] = $payment['id'];
            }

            $paymentId = implode(',', $payments);
        }

        $order->add_order_note("Mercado Pago: {$this->translations['validate_order_1']} $paymentId {$this->translations['validate_order_1']} $status");
    }

    /**
     * Check if the data is a notification
     *
     * @param array $data
     *
     * @return bool
     */
    private function isNotification(array $data): bool
    {
        return isset($data['notification_id']);
    }

    /**
     * Get the status detail of the refunded payments for consolidated status
     *
     * @param array $paymentsData
     *
     * @return array
     */
    public function getRefundedStatusDetail(array $paymentsData): array
    {
        foreach ($paymentsData as $payment) {
            if (in_array('approved', [$payment['status'], $payment['status_detail']])) {
                return [
                    'title' => 'approved',
                    'description' => 'partially_refunded',
                ];
            }
        }

        return [
            'title' => 'refunded',
            'description' => 'refunded',
        ];
    }

    /**
     * Check if refund was already processed in WooCommerce
     *
     * @param WC_Order $order
     * @param array $paymentsData - Array of payment data from getAllPaymentsData()
     *
     * @return bool
     */
    private function refundAlreadyProcessed(WC_Order $order, array $paymentsData): bool
    {
        $totalRefundedWC = (float) $order->get_total_refunded();
        $totalRefundedMP = $this->calculateTotalRefunded($paymentsData);

        return $totalRefundedMP <= $totalRefundedWC;
    }

    /**
     * Get amount of unprocessed refunds
     *
     * @param WC_Order $order
     * @param array $paymentsData - Array of payment data from getAllPaymentsData()
     *
     * @return float
     */
    private function getUnprocessedRefundAmount(WC_Order $order, array $paymentsData): float
    {
        $totalRefundedMP = $this->calculateTotalRefunded($paymentsData);
        $totalRefundedWC = (float) $order->get_total_refunded();

        $unprocessedAmount = $totalRefundedMP - $totalRefundedWC;

        $this->logs->file->info(
            sprintf(
                'Mercado Pago: Refund comparison - MP: %s, WC: %s, Unprocessed: %s',
                $totalRefundedMP,
                $totalRefundedWC,
                $unprocessedAmount
            ),
            __CLASS__
        );

        return max(0, $unprocessedAmount);
    }

    /**
     * Get all payments data from order
     *
     * @param WC_Order $order
     *
     * @return array
     */
    public function getAllPaymentsData(WC_Order $order): array
    {
        $result = [];
        $paymentsIds = array_filter(array_map(
            'trim',
            explode(',', $this->orderMetadata->getPaymentsIdMeta($order))
        ));

        $headers = ['Authorization: Bearer ' . $this->seller->getCredentialsAccessToken()];

        foreach ($paymentsIds as $paymentId) {
            $response = $this->requester->get(self::PAYMENTS_ENDPOINT . $paymentId, $headers);
            if ($response->getStatus() != 200 && $response->getStatus() != 201) {
                throw new Exception(json_encode($response->getData()));
            }

            $result[] = $response->getData();
        }

        return $result;
    }

    /**
     * Calculate total refunded from payments data array
     *
     * @param array $paymentsData - Array of payment data from getAllPaymentsData()
     *
     * @return float
     */
    private function calculateTotalRefunded(array $paymentsData): float
    {
        $totalRefunded = 0;

        foreach ($paymentsData as $payment) {
            $totalRefunded += (float) ($payment['transaction_amount_refunded'] ?? 0);
        }

        return $totalRefunded;
    }

    /**
     * Get the last merchant's payment notification data or last payment's notification data
     *
     * @param WC_Order $order
     * @return array
     * @throws Exception
     */
    public function getLastNotification(WC_Order $order): array
    {
        $result = [];

        $paymentsIds = array_filter(array_map(
            'trim',
            explode(',', $this->orderMetadata->getPaymentsIdMeta($order))
        ));

        $headers = ['Authorization: Bearer ' . $this->seller->getCredentialsAccessToken()];

        try {
            $lastPaymentId = end($paymentsIds);
            $notificationId = $this->getNotificationId($lastPaymentId, $headers);
            $response = $this->requester->get(self::NOTIFICATION_ENDPOINT . $notificationId["id"], $headers);

            if ($response->getStatus() != 200 && $response->getStatus() != 201) {
                throw new Exception(json_encode($response->getData()));
            }

            $result[] = $response->getData();

            return $result;
        } catch (Exception $e) {
            $this->logs->file->error('Mercado Pago: Error getting last notification: ' . $e->getMessage(), __CLASS__);
            return [];
        }
    }

    /**
     * Get notification id from payment or merchant order ID
     *
     * @param string $paymentId
     * @param array $headers
     *
     * @return array
     */
    public function getNotificationId(string $paymentId, array $headers): array
    {
        $response = $this->requester->get(self::PAYMENTS_ENDPOINT . $paymentId, $headers);

        if ($response->getStatus() != 200 && $response->getStatus() != 201) {
            throw new Exception(json_encode($response->getData()));
        }

        $paymentData = $response->getData();

        if (!isset($paymentData["notification_url"]) || empty($paymentData["notification_url"])) {
            throw new Exception("Notification URL not found for payment ID: $paymentId");
        }

        if (strpos($paymentData["notification_url"], "merchant-order-notification") !== false) {
            $orderId = $paymentData["order"]["id"];
            return ["id" => "M-$orderId", "type" => "merchant_order"];
        }

        return ["id" => "P-$paymentId", "type" => "payment"];
    }
}