Lucas Fiege

Integrando una app con SDK PHP de MercadoPago v0

Aviso 04/07/2019: Ya está disponible la actualización de este post: Integrando una app en Laravel con SDK PHP de MercadoPago (dx-php)

Aviso 13/11/2018: A partir del 10/12/2018 Mercadopago migrará su SDK de PHP a la versión v1, para lo cual deberán cambiarse algunas lineas de código dentro de la implementación del web checkout. Puede consultarse la guía de migración acá.

image

Muchos de los que recién comenzamos a dar nuestros primeros pasos dentro del mundo del ecommerce nos encontramos con una infinidad de posibilidades al momento de elegir herramientas para crear aplicaciones que puedan transaccionar mediante alguna API externa y con ella recibir pagos, por ejemplo.

En mi caso particular me encuentro desarrollando varios sistemas para vender productos digitales y necesitaba emplear alguna API para gestionar la cobranza de las órdenes que se generen el sistema (crearlas y notificar al usuario el monto, actualizar los estados según haya pagado o no, etc.); entonces me decidí por mercadopago, por poseer bastante documentación frente a otras alternativas, entre otros factores.

No es la idea mostrar el desarrollo completo de la app, solamente responder a una cuestión esencial que llega a la mente de cualquier desarrolador en el mundo del ecommerce. Asumiendo de que ya tenemos desarrollado lo mínimo en un sistema para crear órdenes de compra, en el cual por cada compra que realice un cliente se genera un registro (orden) interno con los datos del mismo (más los ítems adquiridos, impuestos, ... ) podemos ir al punto en cuestión: ¿Como le digo a mercadopago “ésta es mi orden, cobrala y después avisame qué pasa con los pagos”?

Supongo que como en todo el mundo del desarrollo de software, hay varias maneras de solucionar esto, les aporto una de ellas:

Creando una preferencia de pago

Asumiendo que hemos instalado con composer el sdk de php de mercadopago en su versión 0.5.2, y que además disponemos de un controlador para crear/actualizar una orden y todos los elementos asociados a ella, el paso final es redirigir al cliente al gateway de pago. Para ello deberemos crear una preferencia de pago, que es la manera de indicarle a mercadopago cómo debe procesar dicha orden, puede verse más en detalle en la documentación de mercadopago.

# In controller class
// Redirect to order payment gateway
return redirect()->to($this->generatePaymentGateway());

...

Class CheckoutController
use MP;

...

public function generatePaymentGateway()
{
    $mp = new MP (env('MP_CLIENT_ID'), env('MP_CLIENT_SECRET'));

    $current_user = auth()->user();

    $preferenceData = [
        'external_reference' => $this->id,
        // also you can do this
        //'external_reference' => $this->prefix . $this->id,
...        
        'payer'              => [
            ...
        ],
        'back_urls'          => [
            ...
        ],
        'notification_url'   => env('MP_NOTIFICATION_URL')
    ];

    // add items
    foreach ($this->items as $item):
        $preferenceData['items'][] = [
            'id'          => ...,
            'category_id' => ...,
            'title'       => ...,            
            'description' => ...,
            'picture_url' => ...,
            'quantity'    => ...,
            'currency_id' => ...,
            'unit_price'  => ...,
        ];
    endforeach;

    $preference = $mp->create_preference($preferenceData);
    // also you can use try-catch for create the preference, DB transaction for the whole generatePaymentGateway method, etc...

    // finally return init point to be redirected - or
    // sandbox_init_point
    return $preference['response']['init_point'];
}

Aquí tenemos un aspecto clave: el atributo external_reference, el cual será útil posteriormente para poder identificar nuestra orden del sistema al momento de cotejar la información que nos de la notificación IPN de mercadopago.

A efectos prácticos utilicé solamente la ID de la orden en limpio para este ejemplo; pero el código de referencia externo podría ser algo más “vistoso” para el usuario final, por ejemplo OR0007896 (tener en cuenta que después deberá limpiarse lo que no sirva para obtener el ID de la orden, por ejemplo 7896 para este ejemplo).

Con esto ya tenemos lo suficiente para que el cliente sea redirigido a mercadopago, elija un medio de pago y posteriormente realice el pago del producto a adquirir.

Escuchando notificaciones IPN

Además, debemos indicar que URL será la encargada de “escuchar” las notificaciones IPN de mercadopago, esto con el fin de poder actualizar el estado de nuestra orden ni bien se procesen los pagos y realizar las acciones necesarias para bloquear stock, habilitar links de descargas, etc.

Para ello debemos crear alguna ruta que sea accesible para el servidor de mercadopago y excluirla de la verificación del csrf_token, dado que el request vendrá desde afuera de nuestro servidor (para ello debemos agregarla en el array $except del middleware VerifyCsrfToken).

Además, podemos aumentar el nivel de seguridad indicándole a laravel que rangos de IPs son confiables, mediante el nuevo feature trusted proxies, o bien, realizar una serie de controles adicionales sobre el método atado a la ruta de notificaciones, para evitar exponer algún tipo de vulnerabilidad.

Cuando el servidor de mercadopago realice un request a nuestro servidor, el método que se ejecutará dada la ruta para notificaciones sería más o menos similar a este:

Route::get('/notifications/mp', 'SomeController@ipnNotification')

...

public function ipnNotification(Request $request)
{
    $mp = new MP (env('MP_CLIENT_ID'), env('MP_CLIENT_SECRET'));

    if ( ! isset($_GET["id"], $_GET["topic"]) || ! ctype_digit($_GET["id"])) {
        abort(404);
    }

    // Get the payment and the corresponding merchant_order reported by the IPN.
    if ($_GET["topic"] == 'payment') {
$payment_info = $mp->get("/v1/payments/$payment_id/" . $_GET["id"]);
        $merchant_order_info = $mp->get("/merchant_orders/" . $payment_info["response"]["collection"]["merchant_order_id"]);

        // Get the merchant_order reported by the IPN.

        // get order and link the notification id
        $external_reference_id = $merchant_order_info["response"]["external_reference"];
       //here you must clear unnecessary data in external reference
       ...
       // get order
        $order = Order::findOrFail($external_reference_id);
        // link notification id
        $order->mp_notification_id = $_GET["id"];

        if ($merchant_order_info["status"] == 200) {
            // If the payment's transaction amount is equal (or bigger) than the merchant_order's amount you can release your items

            $paid_amount = 0;

            foreach ($merchant_order_info["response"]["payments"] as $payment) {
                $order->status = $payment['status'];
                if ($payment['status'] == 'approved') {
                    $paid_amount += $payment['transaction_amount'];
                }
            }

            if ($paid_amount >= $merchant_order_info["response"]["total_amount"]) {
                if (count($merchant_order_info["response"]["shipments"]) > 0) { 

                    // The merchant_order has shipments
                    if ($merchant_order_info["response"]["shipments"][0]["status"] == "ready_to_ship") {
                        print_r("Totally paid. Print the label and release your item.");
                    }
                } else {
                    // The merchant_order don't has any shipments
                    print_r("Totally paid. Release your item.");
                }
            } else {
                print_r("Not paid yet. Do not release your item.");
            }
        }

        $order->save();

    }

    return response('OK', 201);
}

Este método es el esqueleto básico que se ofrece en la documentación de mercadopago, con el agregado de que se obtiene el id indicado como referencia externa y con éste podemos obtener nuevamente la orden asociada al pago y realizar todo lo que necesitemos con ella, finalmente debemos responder con un código http 201 si todo se ejecuta en forma correcta.

Obviamente éste código puede mejorarse con algunas refactorizaciones. Pero me pareció buena idea dejar alguna respuesta online a la pregunta inicialmente planteada, ya que yo también tuve esa duda y no encontraba demasiada información al respecto.

Si te gustó este post y queres ayudarme invitame un café Invitame un café en cafecito.app


almost 4 years ago

Lucas Fiege