Paypal y Rails: ¿Cómo cobrar desde tu aplicación web? [ESPECIAL]


NOTAS: Yo uso MongoDB como mi base de datos, entonces este tutorial está hecho asumiendo que tu backend es Mongoid y no ActiveRecord, por ejemplo aquí no verás migraciones. También es importante aclarar que en el caso de este post "card_bundle" es lo que vendemos y "card_bundle_sale" es nuestro "carrito" u "orden"

Este es un tutorial especial que voy a escribir mientras estoy programando una webapp para un cliente ya que considero que este es uno de los temas que generan más dudas entre los desarrolladores web allá afuera. Paypal puede ser intimidante, ¡Díganmelo a mi! Así que vamos paso por paso juntos:

NOTA: Aquí no enseñaré cómo implementar una aplicación de compra-venta en Rails, ya que esta es una cuestión genérica que no se puede aglomerar en un solo tutorial para todos los casos. El propósito de este tutorial más bien es entregar un método lo más flexible posible para que cualquiera que esté construyendo una app de e-commerce según su caso aplicado allá afuera pueda integrar cobros por Paypal en su aplicación web sin depender de un diseño o workflow previamente fijado.

So, tienes una aplicación Rails donde quieres de hecho cobrar dinero. En mi caso estoy construyendo una plataforma para ventas de viajes estilo BestDay para una agencia de reciente comienzo y la idea es poder vender directamente algunos paquetes desde la página web a las personas usando sus tarjetas de crédito. El workflow "más básico" de un sitio de e-commerce es el siguiente:

Ente a vender => Órdenes => Objeto de Venta (Carrito)

Por ejemplo, una aplicación donde se vendan playeras, la gente pueda elegir las que le gusten seleccionándolas con un checkbox (armando en el background un objeto de "orden") y finalmente se les muestra "un carrito" u objeto de venta que se utilizará como "la cuenta" del pedido. Los datos de este último son los que debemos pasarle a paypal, veamos cómo, paso por paso:

1) Crea 2 cuentas en la sandbox de Paypal

Necesitaremos una de vendedor y una de comprador, a ambas podemos ponerles el dinero "de mentiras" que queramos y otros atributos que consideremos necesarios. Las podemos crear en Paypal Developers en el apartado de Applications > Sanbox Test Accounts. Una vez creadas podemos acceder a ellas desde el Paypal Sandbox para hacer las pruebas y corroboraciones pertinentes.


2) Website Payments Standard

Vamos a integrar Paypal al checkout de nuestro carrito en la aplicación Rails, la manera más fácil sería por medio de Website Payments Standard y por ahí comenzaremos. So, en mi app yo tengo una sección donde la persona puede escoger de una lista de varios items y darles una cantidad a cada uno para comprar más de uno de ese item. Lo que quiero hacer es que cada que alguien termine de armar una orden se le lleve a pagar esa orden con Paypal inmediatamente, ¿Cómo se hace?


Ciertamente no es difícil, en mi caso específico yo vendo un ente llamado "card_bundle" por medio de "card_bundle_sales" (órdenes, si quieres llamarlo así) y guardo el proceso de venta en la base de datos a manera de "sales" para tener una especie de registro de ventas hechas automáticamente. En mi caso lo que hice fue dejar un botón de submit normal en la parte de la aplicación donde la gente compra (obviamente sin diseño ni nada allá arriba) que al ser pulsado crea una venta nueva en mi registro de sales y por tanto popula un registro de "card_bundle_sale" que puedo utilizar para pasarle las variables adecuadas a paypal con respecto a la orden. Veamos el código usado para lograr esto:

Controlador Sales:


De aquí nos interesa lo que aparece en la línea 29, básicamente le estoy diciendo al controlador que si una nueva venta se crea redireccione al usuario a la "paypal_url" especificando como argumento la URL de retorno que quiero para cuando la compra está completada (dentro del paréntesis).

Modelo CardBundleSale

Para definir esa "Paypal URL" ocupo hacerlo en el modelo de mi ente que represente "una orden" en el sistema, en este caso es CardBundleSale. el código para ese método queda más o menos así:



Aquí es donde la cosa se puede poner "intimidante" pues hay que checar la documentación de paypal, verán: El método que ven ahí arriba no hace otra cosa más que crear una URL de Paypal que podemos usar para cobrar. Daría lo mismo si esta data se le alimenta a Paypal por este método o bien, si usáramos un botón embedido nadamás, la data sería la misma. Cabe destacar que los valores en azul son valores que en mi opinión se deben pasar en cualquier caso, son "los de cajón" (por cierto, cmd siempre ha de estar definido como '_xclick' para que la cosa funcione) Mientras que el método que itera sobre el array de items que estoy vendiendo es código muy específico para mi aplicación, tu tendrías que crear tu propio método iterante según el caso específico de tu escenario. A final de cuentas construimos nuestra URL y listo!

Vista CardBundles#Index

Por acá sólo tenemos el submit y ya:


Como verán, nada de otro mundo en esta vista, creamos nuestro objeto "venta" (que a su vez popula una "orden" o "card_bundle_sale" y listo.

Con este "arreglo" ya podemos empezar a vender desde nuestra app, Genial! Hagamos una prueba:



Como verán, el pago procesa y todo, ¡Yay! pero aún no podemos cantar victoria...

3) IPN

Con las 3 cosas que preparamos arriba ya podemos hacer ventas en Paypal, pero esta implementación tiene grandes fallas: Para empezar, el hecho de registrar una venta en este caso (osea, generar una cotización que más tarde puede ser pagada en Paypal) no quiere decir necesariamente que la venta se haya realizado, por lo tanto, tenemos que tener una manera de rectificar que el pago de una "cotización" fue exitoso. Ahora, en mi caso uso un carrito volátil basado en sesiones, por lo que siempre que creo una venta éste se vacía por sí solo... En la mayoría de los casos si tu app tiene una fuerte integración con paypal o una implementación de cart tradicional esto no será así, por lo que debes generar un método que te permita limpiar el carro en el momento que  una orden fue pagada de manera que si el cliente quiere hacer otra los items anteriores no se le sumen a su total. Empecemos por reparar estas cuestiones:

Para hacer que todo esto funcione como debe, tenemos que usar el servicio de Instant Payment Notification o IPN de Paypal y para esto, crearemos un nuevo recurso en nuestra aplicación:

rails g scaffold payment_notification hash_params:hash card_bundle_sale_id:string status:string transaction_id:string

Borramos todas las vistas de este recurso y nos vamos a su controlador, (mismo que tenemos que vaciar) para poner este código:


Recuerda pasar correctamente los atributos de tu cart_id (card_bundle_sale_id en este caso) y el hash de tus parámetros según tu caso práctico... Si estuviéramos usando ActiveRecord esto del hash se tendría que hacer serializando el atributo params del modelo para poder tener los parámetros que paypal nos suelte en un hash cuando lo ocupemos. En Mongoid no, porque MongoDB almacena todo en JSON Objects.

Ahora configuramos nuestra notificación de pago en su modelo y el código queda más o menos así:



Aquí lo que estoy haciendo yo es crear un callback que hará que una vez creada una notificación de pago (al tener una compra exitosa por medio de paypal) marcará dicha compra como "pagada a tal hora" entonces en mi listado de ventas del que les hablé antes aparecerá la fecha y hora del pago y eso me permitirá saber 3 cosas: 1) si una orden está pagada 2) cuantas órdenes en promedio se pagan por c/u que se arma en la web y 3) cuándo le puedo contar al cliente como pagado su pedido. Para que esto fuera posible tuve que modificar mi modelo de Card Bundle Sale un poco y quedó así:



Lo que hice aquí fue añadirle las Mongoid Timestaps al modelo para poder tener los atributos del campo "purchased_at" funcionando y le añadí dicho campo al modelo, también lo añadí a la vista de Sales#Index donde se registran todas mis "ventas" (o más bien "cotizaciones") para un control más estructurado y que los administradores puedan ver cuando algo ha sido pagado y exactamente en qué momento.

Ahora necesitamos Limpiar el carro cada que se haga una compra exitosa. En mi caso esto no es necesario ya que mi carrito es "volátil" pero para aquellos que tengan un cart "hard_coded" algo como esto podría ayudarles, aclaro que esta es una de esas cosas que es muy específica de cada aplicación/contexto y puede que en tu caso esto aplique/te sirva o no, veamos:

En el application_controller:

def current_cart

  if session[:cart_id]
    @current_cart ||= Cart.find(session[:cart_id])
    session[:cart_id] = nil if @current_cart.purchased_at
  end
  if session[:cart_id].nil?
    @current_cart = Cart.create!
    session[:cart_id] = @current_cart.id
  end
  @current_cart
end

Sólo nos queda hacer los cambios pertinentes a nuestro workflow paypal y en mi caso fue así:

Añadí el parámetro y argumento de notify_url a mi método "paypal_url" en mi modelo de "orden" (card_bundle_sale en este caso):



Luego en mi acción create de sale, le pasé la nueva ruta al redirect_to:


Y finalmente, lo testeamos desde consola fingiendo una notificación directo al servidor con:

curl -d "txn_id=ID_TXN&invoice=ID_CART&payment_status=Completed" http://localhost:3000/payment_notifications

Donde ID_TXN es el id de transacción que te genera paypal al pagar e ID_CART es el id de tu objeto cart, (en este caso card_bundle_sale) asociado a la venta en cuestión, veamos un ejemplo en vivo:



¡Bien! Pero no hemos acabado todavía jajaajaja!

4) Encriptación de operaciones

Por muy bonito que se sienta el haber armado esto, todavía no hemos terminado, ahora tenemos que evitar que alguien pueda alterar las transacciones pasando los parámetros de forma encriptada en lugar de por la pura URL. ¿Porqué? Porque si no lo hacemos así, nos pueden cambiar el precio de un producto y "llevárselo" por $0 y no queremos eso... Veamos pues:

Lo primero que haremos será correr una secuencia de comandos para generar unos certificados SSL que usaremos para identificar nuestra webapp con Paypal:

1. cd myapp
2. mkdir certs
3. cd certs
4. openssl genrsa -out app_key.pem 1024
5. openssl req -new -key app_key.pem -x509 -days 365 -out app_cert.pem
6. mv ~/Downloads/paypal_cert_pem.txt paypal_cert.pem

NOTA: Necesitas tener OpenSSL instalado

("myapp" allá arriba se refiere al nombre de la carpeta de tu aplicación) El 5to comando nos pedirá unos cuantos datos, los rellenamos y proseguimos lléndonos a "Perfil>Certificados para Sitio Web" (se puede llamar ligeramente la opción en cada una de las interfaces de paypal pero buscamos la de los pagos codificados para ser exactos). En esta ventana añadiremos nuestro archivo app_cert.pem, anotaremos el id de certificado que paypal le dé y finalmente descargaremos el certificado público de paypal y lo guardamos en nuestro directorio "certs" de la aplicación, renombrándolo a "paypal_cert.pem":


Ahora cambiaremos un poco nuestro proceso de checkout y configuración de la aplicación. Al momento que estoy haciendo esto (pues es en vivo mientras trabajo) decidimos sí mostrar una página extra de confirmación de orden antes de enviar al cliente a pagar en esta app (hasta el momento no lo hemos hecho) puesto que paypal permite muy pocos parámetros de personalización para la página donde se muestra la información de la orden y eso no propicia una buena experiencia de usuario; Veamos entonces cómo quedó todo esto:

Archivo YAML

Usaremos un archivo de configuración global YAML para pasar algunas de las variables que ocupamos para nuestro checkout de manera transparente y uniforme en nuestra aplicación. Para hacer esto, creamos 2 archivos:

config/initializers/load_app_config.rb:


config/app_config.yaml:


El primer archivo simplemente le dice Rails donde encontrar al segundo y qué hacer con él. El segundo es un archivo a donde se mueven todas las variables usadas (o las más importantes pues) en el proceso de checkout, de manera que evitemos repetición y todo esté mejor organizado y limpio. Nótese que en el entorno de producción la URL de paypal se pasa a la normal y ya no a la del sandbox.

NOTA: El "secret" puede ser lo que tú quieras.

Cambiando el proceso de Checkout

Ahora voy a cambiar mi proceso de checkout. Para esto, simplemente cambiaré el condicional de mi acción create de sale (esto en mi caso específico) para que una vez registrada una orden/venta en lugar de llevar el cliente a pagar directo a paypal lo lleve a revisar su orden en la vista show de la misma (la del card_bundle_sale que genera el cliente al armar su orden en nuestro caso):


Y luego, en esa vista a donde mando el cliente crearé un botón de checkout especial que lo que hará será mandar un form con campos ocultos para poder generar el ente de checkout que necesitamos:


Aquí lo que hago es hacer una forma para armar la nueva paypal_url encriptada, toma especialmente 2 campos que ocupa: cmd y encrypted. Puedes usar la misma forma en cualquier situación pero sólo recuerda cambiar mi "cart_bundle_sale" por el objeto que represente tu orden en tu escenario.

Más tarde debemos cambiar los métodos y callbacks especiales de nuestro modelo que represente las órdenes (card_bundle_sales en este caso) por algo como esto:



Como verán, es básicamente el mismo archivo, pero ahora el método se llama "paypal_encrypted" en lugar de "paypal_url" y los parámetros que tenemos en el YAML son llamados como parámetros de la constante [APP_CONFIG], También añadimos el campo de "cert_id" que no teníamos antes y al término de nuestro método iterante llamamos otro método que se llama "encrypt_for_paypal" pasándole como argumento el resultado de nuestra iteración. Más abajo está definido el método encriptador y ése lo puedes copiar y pegar si quieres, funciona bien para todos los casos. Guardamos ese archivo y proseguimos:

Accediendo a las variables del YAML

Ahora sólo nos queda cambiar nuestro callback en el modelo de payment_notifications para que revise bien los atributos más importantes de la transacción y si es que todas nuestras ammm... "validaciones" pasan sin errores, haga su trabajo, (también podríamos programarlo para que si no pasa, nos mande un correo avisándonos o como quieran, pero yo lo dejé así), De igual manera ahora está configurado para usar nuestras variables globales, el dichoso callback queda así:



Una vez que hemos configurado esto, ¡hemos terminado oficialmente! Tenemos una integración completa, funcional y segura de Paypal en nuestro sitio/aplicación web...

Tuitea Esto:
Integrar #Paypal con #Rails y no morir en el intento #Mongoid #DesarrolloWeb - http://bit.ly/XBYowb vía: @xenodesystems - Tweet!

P.D. Este tutorial está basado en las lecciones del increíble Ryan Bates de railscasts.com, si quieres aprender más sobre la integración de paypal (o cualquier tema rails) te recomiendo que le eches un ojo a su sitio web, por cierto, este post se agregará a nuestro curso gratuito de Ruby on Rails para que lo chequen desde el principio y aprendan Rails si quieren.