Integración de los métodos de pago de Stripe y PayPal en Ruby on Rails

Publicado: 2022-03-11

Una característica clave para las grandes empresas de comercio electrónico como AliExpress, Ebay y Amazon es una forma segura de manejar los pagos, lo cual es esencial para su negocio. Si esta característica falla, las consecuencias serían devastadoras. Esto se aplica a los líderes de la industria y a los desarrolladores de Ruby on Rails que trabajan en aplicaciones de comercio electrónico.

La ciberseguridad es esencial para prevenir ataques, y una forma de hacer que el proceso de transacción sea más seguro es pedirle a un servicio de terceros que lo maneje. Incluir pasarelas de pago en su aplicación es una forma de lograr este objetivo, ya que brindan autorización de usuario, encriptación de datos y un panel para que pueda seguir el estado de la transacción sobre la marcha.

Hay una variedad de servicios de pasarela de pago en la web, pero en este artículo me centraré en la integración de Stripe y PayPal en una aplicación de Rails. Por mencionar algunos otros: Amazon Payments, Square, SecurePay, WorldPay, Authorize.Net, 2Checkout.com, Braintree, Amazon o BlueSnap.

Cómo funciona la integración de la pasarela de pago

Representación general para transacciones que involucran pasarelas de pago
Representación general para transacciones que involucran pasarelas de pago

En general, habrá un formulario/botón en su aplicación donde el usuario puede iniciar sesión/insertar los datos de la tarjeta de crédito. PayPal y Stripe ya hacen que este primer paso sea más seguro mediante el uso de formularios iframe o popups que evitan que su aplicación almacene información confidencial de la tarjeta de crédito del usuario, ya que devolverán un token que representa esta transacción. Es posible que algunos usuarios ya se sientan más seguros para procesar pagos al saber que un servicio de terceros está manejando el proceso de transacción, por lo que esto también puede ser una atracción para su aplicación.

Después de autenticar la información del usuario, una pasarela de pago confirmará el pago poniéndose en contacto con un procesador de pagos que se comunica con los bancos para liquidar los pagos. Esto asegura que la transacción se carga/abona correctamente.

Stripe utiliza un formulario de tarjeta de crédito que solicita el número de tarjeta de crédito, cvv y fecha de vencimiento. Entonces, el usuario debe completar la información de la tarjeta de crédito en las entradas seguras de Stripe. Después de proporcionar esta información, el back-end de su aplicación procesa este pago a través de un token.

A diferencia de Stripe, PayPal redirige al usuario a la página de inicio de sesión de PayPal. El usuario autoriza y selecciona el método de pago a través de PayPal y, nuevamente, su back-end manejará tokens en lugar de datos confidenciales del usuario.

Es importante mencionar que, para estas dos pasarelas de pago, su back-end debe solicitar la ejecución de la transacción en curso a través de las API de Stripe o PayPal, lo que dará una respuesta OK/NOK, por lo que su aplicación debería redirigir al usuario a una página correcta o de error en consecuencia.

La intención de este artículo es proporcionar una guía rápida para integrar estas dos pasarelas de pago en una sola aplicación. Para todas las pruebas, utilizaremos sandboxes y cuentas de prueba proporcionadas por Stripe y PayPal para simular pagos.

Configuración

Antes de integrar las pasarelas de pago, realizaremos una configuración para inicializar la aplicación agregando gemas, tablas de base de datos y una página de índice. Este proyecto fue creado usando Rails versión 5.2.3 y Ruby 2.6.3.

Nota: Puede consultar las nuevas características de Rails 6 en nuestro artículo reciente.

Paso 1: inicialice una aplicación Rails.

Inicialice el proyecto ejecutando la inicialización del proyecto con el comando rails con el nombre de su aplicación:

 rails new YOUR_APP_NAME

Y cd en la carpeta de tu aplicación.

Paso 2: Instala gemas.

Además de las gemas de Stripe y PayPal, se agregaron algunas otras gemas:

  • devise : utilizado para la autenticación y autorización del usuario
  • haml : herramienta de plantillas para renderizar páginas de usuario
  • jquery-rails : para jquery en los scripts front-end
  • money-rails : para mostrar valores de dinero formateados

Agregue a su Gemfile :

 gem "devise", ">= 4.7.1" gem "haml" gem "jquery-rails" gem "money-rails"

Después de agregarlo, ejecute en su CLI:

 bundle install

Paso 3: inicializa las gemas.

Algunas de estas gemas requerirán inicialización además de instalarlas a través de bundle .

Dispositivo de instalación:

 rails g devise:install

Inicializando money-rails :

 rails g money_rails:initializer

Inicialice jquery-rails agregando al final de app/assets/javascripts/application.js lo siguiente:

 //= require jquery //= require jquery_ujs

Paso 4: Tablas y migraciones

En este proyecto se utilizarán tres tablas Usuarios , Productos y Pedidos .

  • Users : se generará a través del dispositivo
  • Columnas de Products :
    • name
    • price_cents
    • Stripe_plan_name : ID que representa un plan de suscripción creado en Stripe, para que los usuarios puedan suscribirse. Este campo solo es obligatorio para productos asociados a un plan Stripe.
    • paypal_plan_name : Lo mismo que stripe_plan_name pero para PayPal
  • Columnas Orders :
    • product_id
    • user_id
    • status : Esto informará si el pedido está pendiente, fallido o pagado.
    • token : este es un token generado a partir de las API (ya sea Stripe o PayPal) para inicializar una transacción.
    • price_cents : Similar al producto, pero se usa para que este valor sea persistente en el registro del pedido.
    • payment_gateway : Almacena qué pasarela de pago se está utilizando para el pedido PayPal o Stripe
    • customer_id : Esto se usará para Stripe para almacenar el cliente de Stripe para una suscripción, y se explicará con más detalle en una sección posterior.

Para generar estas tablas, se deben generar algunas migraciones:

Para crear la tabla de Usuarios . Correr:

 rails g devise User

Para crear la tabla Productos . Genere una migración ejecutando:

 rails generate migration CreateProducts name:string stripe_plan_name:string paypal_plan_name:string

Abra su archivo de migración creado, que debe estar ubicado en db/migrate/ , y realice cambios para que su migración se vea similar a esto:

 class CreateProducts < ActiveRecord::Migration[5.2] def change create_table :products do |t| t.string :name t.string :stripe_plan_name t.string :paypal_plan_name end add_money :products, :price, currency: { present: true } end end

Para crear la tabla Pedidos . Genere una migración ejecutando:

 rails generate migration CreateOrders product_id:integer user_id:integer status:integer token:string charge_id:string error_message:string customer_id:string payment_gateway:integer

Nuevamente, abra su archivo de migración creado que debe estar ubicado en db/migrate/ y realice cambios en ese archivo para que se vea similar a esto:

 class CreateOrders < ActiveRecord::Migration[5.2] def change create_table :orders do |t| t.integer :product_id t.integer :user_id t.integer :status, default: 0 t.string :token t.string :charge_id t.string :error_message t.string :customer_id t.integer :payment_gateway t.timestamps end add_money :orders, :price, currency: { present: false } end end

Ejecute migraciones de bases de datos ejecutando:

 rails db:migrate

Paso 5: Crear modelos.

El modelo de usuario ya se creó a partir de la instalación del dispositivo y no se requerirán cambios en él. Además de eso, se crearán dos modelos para Producto y Pedido .

Producto. Agrega un nuevo archivo, app/models/product.rb , con:

 class Product < ActiveRecord::Base monetize :price_cents has_many :orders end

Pedido. Agrega un nuevo archivo, app/models/order.rb , con:

 class Order < ApplicationRecord enum status: { pending: 0, failed: 1, paid: 2, paypal_executed: 3} enum payment_gateway: { stripe: 0, paypal: 1 } belongs_to :product belongs_to :user scope :recently_created, -> { where(created_at: 1.minutes.ago..DateTime.now) } def set_paid self.status = Order.statuses[:paid] end def set_failed self.status = Order.statuses[:failed] end def set_paypal_executed self.status = Order.statuses[:paypal_executed] end end

Paso 6: Complete la base de datos.

Se creará un usuario y dos productos en la consola. Los registros de pedidos se crearán de acuerdo con las pruebas de pago.

  • Ejecutar rails s
  • En su navegador, visite http://localhost:3000
  • Se le redirigirá a una página de registro.
  • Registre un usuario completando su dirección de correo electrónico y contraseña.
  • En su terminal, aparecerán los siguientes registros que muestran que se creó un usuario en su base de datos:
 User Create (0.1ms) INSERT INTO "users" ("email", "encrypted_password", "created_at", "updated_at") VALUES (?, ?, ?, ?) …
  • Cree dos productos sin suscripciones ejecutando rails c y agregando:
    • Product.create(name: "Awesome T-Shirt", price_cents: 3000)
    • Product.create(name: "Awesome Sneakers", price_cents: 5000)

Paso 7: crea una página de índice

La página principal del proyecto incluye la selección de productos para compras o suscripciones. Adicionalmente, también cuenta con un apartado para la selección del método de pago (Stripe o PayPal). También se utiliza un botón de envío para cada tipo de pasarela de pago, ya que para PayPal agregaremos su propio diseño de botón a través de su biblioteca de JavaScript.

Primero, cree las rutas para el index y submit en config/routes.rb .

 Rails.application.routes.draw do devise_for :users get '/', to: 'orders#index' post '/orders/submit', to: 'orders#submit' end

Cree y agregue un index acciones y submit en el controlador de pedidos app/controllers/orders_controller.rb . La acción orders#index almacena dos variables para ser consumidas en el front-end: @products_purchase que tiene una lista de productos sin planes y @products_subscription que tiene productos con planes de PayPal y Stripe.

 class OrdersController < ApplicationController before_action :authenticate_user! def index products = Product.all @products_purchase = products.where(stripe_plan_name:nil, paypal_plan_name:nil) @products_subscription = products - @products_purchase end def submit end end

Cree un archivo en app/views/orders/index.html.haml . Este archivo contiene todas las entradas que enviaremos a nuestro back-end a través del método de envío y la interacción para las pasarelas de pago y la selección de productos. Aquí hay algunos atributos de nombre de entrada:

  • Orders[product_id] almacena la identificación del producto.
  • Orders[payment_gateway] contiene la pasarela de pago con valores de Stripe o PayPal para la otra.
 %div %h1 List of products = form_tag({:controller => "orders", :action => "submit" }, {:id => 'order-details'}) do %input{id:'order-type', :type=>"hidden", :value=>"stripe", :name=>'orders[payment_gateway]'} .form_row %h4 Charges/Payments - @products_purchase.each do |product| %div{'data-charges-and-payments-section': true} = radio_button_tag 'orders[product_id]', product.id, @products_purchase.first == product %span{id: "radioButtonName#{product.id}"} #{product.name} %span{id: "radioButtonPrice#{product.id}", :'data-price' => "#{product.price_cents}"} #{humanized_money_with_symbol product.price} %br %h4 Subscriptions - @products_subscription.each do |product| %div = radio_button_tag 'orders[product_id]', product.id, false %span{id: "radioButtonName#{product.id}"} #{product.name} %span{id: "radioButtonPrice#{product.id}", :'data-price' => "#{product.price_cents}"} #{humanized_money_with_symbol product.price} %br %hr %h1 Payment Method .form_row %div = radio_button_tag 'payment-selection', 'stripe', true, onclick: "changeTab();" %span Stripe %br %div = radio_button_tag 'payment-selection', 'paypal', false, onclick: "changeTab();" %span Paypal %br %br %div{id:'tab-stripe', class:'paymentSelectionTab active'} %div{id:'card-element'} %div{id:'card-errors', role:"alert"} %br %br = submit_tag "Buy it!", id: "submit-stripe" %div{id:'tab-paypal', class:'paymentSelectionTab'} %div{id: "submit-paypal"} %br %br %hr :javascript function changeTab() { var newActiveTabID = $('input[name="payment-selection"]:checked').val(); $('.paymentSelectionTab').removeClass('active'); $('#tab-' + newActiveTabID).addClass('active'); } :css #card-element { width:500px; } .paymentSelectionTab { display: none; } .paymentSelectionTab.active { display: block !important; }

Si ejecuta su aplicación con rails s y visita su página en http://localhost:3000 . Debería poder ver la página de la siguiente manera:

Página de índice sin procesar sin integración de Stripe y PayPal
Página de índice sin procesar sin integración de Stripe y PayPal

Almacenamiento de credenciales de pasarela de pago

Las claves de PayPal y Stripe se almacenarán en un archivo que Git no rastreará. Hay dos tipos de claves almacenadas en este archivo para cada pasarela de pago y, por ahora, usaremos un valor ficticio para ellas. En secciones posteriores se presentan instrucciones adicionales para crear estas claves.

Paso 1: Agrega esto en .gitignore .

 /config/application.yml

Paso 2: Cree un archivo con sus credenciales en config/application.yml . Debe contener todas sus claves de prueba/sandbox de PayPal y Stripe para acceder a estas API.

 test: &default PAYPAL_ENV: sandbox PAYPAL_CLIENT_ID: YOUR_CREDENTIAL_HERE PAYPAL_CLIENT_SECRET: YOUR_CREDENTIAL_HERE STRIPE_PUBLISHABLE_KEY: YOUR_CREDENTIAL_HERE STRIPE_SECRET_KEY: YOUR_CREDENTIAL_HERE development: <<: *default

Paso 3: para almacenar las variables del archivo config/application.yml cuando se inicia la aplicación, agregue estas líneas en config/application.rb dentro de la clase de Application para que estén disponibles en ENV .

 config_file = Rails.application.config_for(:application) config_file.each do |key,value| ENV[key] = value end unless config_file.nil?

Configuración de bandas

Agregaremos una joya para usar la API de Stripe: stripe-rails . También es necesario crear una cuenta de Stripe para que se puedan procesar los cargos y las suscripciones. Si es necesario, puede consultar los métodos API para Stripe API en la documentación oficial.

Paso 1: agregue la gema de rieles de rayas a su proyecto.

La gema stripe-rails proporcionará una interfaz para todas las solicitudes de API utilizadas en este proyecto.

Agregue esto en el Gemfile :

 gem 'stripe-rails'

Correr:

 bundle install

Paso 2: Genere sus claves API.

Para tener las claves API para comunicarse con Stripe, deberá crear una cuenta en Stripe. Para probar la aplicación, es posible usar el modo de prueba, por lo que no es necesario completar información comercial real en el proceso de creación de la cuenta de Stripe.

  • Crea una cuenta en Stripe si no tienes una (https://dashboard.stripe.com/).
  • Mientras aún está en el panel de control de Stripe, después de iniciar sesión, active Ver datos de prueba .
  • En https://dashboard.stripe.com/test/apikeys, reemplace YOUR_CREDENTIAL_HERE por los valores STRIPE_PUBLISHABLE_KEY y STRIPE_SECRET_KEY en /config/application.yml con el contenido de Publishable Key y Secret key .

Paso 3: Inicializa el módulo Stripe

Además de reemplazar las claves, todavía necesitamos inicializar el módulo Stripe, para que use las claves ya configuradas en nuestro ENV .

Cree un archivo en config/initializers/stripe.rb con:

 Rails.application.configure do config.stripe.secret_key = ENV["STRIPE_SECRET_KEY"] config.stripe.publishable_key = ENV["STRIPE_PUBLISHABLE_KEY"] end

Paso 4: Integra Stripe en el front-end.

Agregaremos la biblioteca Stripe JavaScript y la lógica para enviar un token que represente la información de la tarjeta de crédito del usuario y se procesará en nuestro back-end.

En el archivo index.html.haml , agregue esto en la parte superior de su archivo. Esto usará el módulo Stripe (proporcionado por la gema) para agregar la biblioteca de javascript de Stripe a la página del usuario.

 = stripe_javascript_tag

Stripe utiliza campos de entrada seguros que se crean a través de su API. Como se crean en un iframe creado a través de esta API, no tendrá que preocuparse por posibles vulnerabilidades en el manejo de la información de la tarjeta de crédito del usuario. Además, su back-end no podrá procesar/almacenar ningún dato confidencial del usuario y solo recibirá un token que represente esta información.

Estos campos de entrada se crean llamando a stripe.elements().create('card') . Después de eso, solo se requiere llamar al objeto devuelto con mount() pasando como argumento el id/clase del elemento HTML en el que se deben montar estas entradas. Puede encontrar más información en Stripe.

Cuando el usuario presiona el botón Enviar con el método de pago Stripe, se realiza otra llamada a la API que devuelve una promesa en el elemento de la tarjeta Stripe creado:

 stripe.createToken(card).then(function(result)

La variable de result de esta función, si no tiene asignado un error de propiedad, tendrá un token que se puede recuperar accediendo al atributo result.token.id . Este token se enviará al back-end.

Para realizar estos cambios, reemplace el código comentado // YOUR STRIPE AND PAYPAL CODE WILL BE HERE en index.html.haml con:

 (function setupStripe() { //Initialize stripe with publishable key var stripe = Stripe("#{ENV['STRIPE_PUBLISHABLE_KEY']}"); //Create Stripe credit card elements. var elements = stripe.elements(); var card = elements.create('card'); //Add a listener in order to check if card.addEventListener('change', function(event) { //the div card-errors contains error details if any var displayError = document.getElementById('card-errors'); document.getElementById('submit-stripe').disabled = false; if (event.error) { // Display error displayError.textContent = event.error.message; } else { // Clear error displayError.textContent = ''; } }); // Mount Stripe card element in the #card-element div. card.mount('#card-element'); var form = document.getElementById('order-details'); // This will be called when the #submit-stripe button is clicked by the user. form.addEventListener('submit', function(event) { $('#submit-stripe').prop('disabled', true); event.preventDefault(); stripe.createToken(card).then(function(result) { if (result.error) { // Inform that there was an error. var errorElement = document.getElementById('card-errors'); errorElement.textContent = result.error.message; } else { // Now we submit the form. We also add a hidden input storing // the token. So our back-end can consume it. var $form = $("#order-details"); // Add a hidden input orders[token] $form.append($('<input type="hidden" name="orders[token]"/>').val(result.token.id)); // Set order type $('#order-type').val('stripe'); $form.submit(); } }); return false; }); }()); //YOUR PAYPAL CODE WILL BE HERE

Si visita su página, debería tener el siguiente aspecto con los nuevos campos de entrada segura de Stripe:

Página de índice integrada con campos de entrada seguros de Stripe.
Página de índice integrada con campos de entrada seguros de Stripe.

Paso 5: Pruebe su aplicación.

Complete el formulario de tarjeta de crédito con una tarjeta de prueba (https://stripe.com/docs/testing) y envíe la página. Compruebe si la acción de submit se llama con todos los parámetros ( product_id , payment_gateway y token ) en la salida de su servidor.

Cargos de rayas

Los cargos de Stripe representan transacciones únicas. Por lo tanto, después de una transacción de cargo de Stripe, recibiría dinero directamente del cliente. Esto es ideal para vender productos que no están asociados a planes. En una sección posterior, mostraré cómo realizar el mismo tipo de transacción con PayPal, pero el nombre de PayPal para este tipo de transacción es Pago .

En esta sección también proporcionaré todo el esqueleto para manejar y enviar una orden. Creamos un pedido en la acción de submit cuando se envía el formulario de Stripe. Este pedido inicialmente tendrá el estado pendiente , por lo que si algo sale mal mientras se procesa este pedido, seguirá estando pendiente .

Si surge algún error de las llamadas a la API de Stripe, configuramos el pedido en un estado fallido y, si el cargo se completa con éxito, estará en el estado pagado . El usuario también es redirigido de acuerdo con la respuesta de la API de Stripe, como se muestra en el siguiente gráfico:

Transacciones de rayas.
Transacciones de rayas.

Además, cuando se realiza un cargo de Stripe, se devuelve una identificación. Estaremos almacenando esta identificación para que luego pueda buscarla en su panel de control de Stripe si es necesario. Esta identificación también se puede utilizar si el pedido debe ser reembolsado. Tal cosa no será explorada en este artículo.

Paso 1: Crea el servicio Stripe.

Usaremos una clase singleton para representar las operaciones de Stripe usando la API de Stripe. Para crear un cargo, se llama al método Stripe::Charge.create y el atributo ID del objeto devuelto se almacenará en el registro de pedido charge_id . Esta función de create se llama pasando el token originado en el front-end, el precio del pedido y una descripción.

Por lo tanto, cree una nueva carpeta app/services/orders y agregue un servicio Stripe: app/services/orders/stripe.rb que contenga la clase Orders::Stripe singleton, que tiene una entrada en el método execute .

 class Orders::Stripe INVALID_STRIPE_OPERATION = 'Invalid Stripe Operation' def self.execute(order:, user:) product = order.product # Check if the order is a plan if product.stripe_plan_name.blank? charge = self.execute_charge(price_cents: product.price_cents, description: product.name, card_token: order.token) else #SUBSCRIPTIONS WILL BE HANDLED HERE end unless charge&.id.blank? # If there is a charge with id, set order paid. order.charge_id = charge.id order.set_paid end rescue Stripe::StripeError => e # If a Stripe error is raised from the API, # set status failed and an error message order.error_message = INVALID_STRIPE_OPERATION order.set_failed end private def self.execute_charge(price_cents:, description:, card_token:) Stripe::Charge.create({ amount: price_cents.to_s, currency: "usd", description: description, source: card_token }) end end

Paso 2: implemente la acción de envío y llame al servicio Stripe.

En orders_controller.rb , agregue lo siguiente en la acción de submit , que básicamente llamará al servicio Orders::Stripe.execute . Tenga en cuenta que también se agregaron dos nuevas funciones privadas: prepare_new_order y order_params .

 def submit @order = nil #Check which type of order it is if order_params[:payment_gateway] == "stripe" prepare_new_order Orders::Stripe.execute(order: @order, user: current_user) elsif order_params[:payment_gateway] == "paypal" #PAYPAL WILL BE HANDLED HERE end ensure if @order&.save if @order.paid? # Success is rendered when order is paid and saved return render html: SUCCESS_MESSAGE elsif @order.failed? && [email protected]_message.blank? # Render error only if order failed and there is an error_message return render html: @order.error_message end end render html: FAILURE_MESSAGE end private # Initialize a new order and and set its user, product and price. def prepare_new_order @order = Order.new(order_params) @order.user_id = current_user.id @product = Product.find(@order.product_id) @order.price_cents = @product.price_cents end def order_params params.require(:orders).permit(:product_id, :token, :payment_gateway, :charge_id) end

Paso 3: Pruebe su aplicación.

Compruebe si la acción de envío, cuando se llama con una tarjeta de prueba válida, realiza una redirección a un mensaje correcto. Además, verifique en su panel de control de Stripe si también se muestra el pedido.

Suscripciones de banda

Se pueden crear suscripciones o planes para pagos recurrentes. Con este tipo de productos se cobra al usuario de forma diaria, semanal, mensual o anual de forma automática según la configuración del plan. En esta sección, usaremos el campo para el producto stripe_plan_name para almacenar la ID del plan; en realidad, podemos elegir la ID, y lo llamaremos premium-plan que se usará para crear el relación customer <-> subscription .

También crearemos una nueva columna para la tabla de usuarios llamada stripe_customer_id que se completará con la propiedad id de un objeto de cliente de Stripe. Un cliente de Stripe se crea cuando se llama a la función Stripe::Customer.create , y también puede consultar los clientes creados y vinculados a su cuenta en (https://dashboard.stripe.com/test/customers). Los clientes se crean pasando un parámetro de source que, en nuestro caso, es el token generado en el front-end que se envía cuando se envía el formulario.

El objeto de cliente obtenido de la última llamada API de Stripe mencionada también se usa para crear una suscripción que se realiza llamando a customer.subscriptions.create y pasando la ID del plan como parámetro.

Además, la gema stripe-rails proporciona la interfaz para recuperar y actualizar un cliente desde Stripe, lo que se hace llamando a Stripe::Customer.retrieve y Stripe::Customer.update , respectivamente.

Entonces, cuando un registro de usuario ya tiene un stripe_customer_id , en lugar de crear un nuevo cliente usando Stripe::Customer.create , llamaremos a Stripe::Customer.retrieve pasando stripe_customer_id como parámetro, seguido de Stripe::Customer.update , y en este caso, pasando al token un parámetro.

En primer lugar, crearemos un plan con la API de Stripe para que podamos crear un nuevo producto de suscripción con el campo stripe_plan_name . Posteriormente, haremos modificaciones en orders_controller y en el servicio de Stripe para que se maneje la creación y ejecución de suscripciones de Stripe.

Paso 1: crea un plan con la API de Stripe.

Abra su consola usando los rails c . Crea una suscripción para tu cuenta de Stripe con:

 Stripe::Plan.create({ amount: 10000, interval: 'month', product: { name: 'Premium plan', }, currency: 'usd', id: 'premium-plan', })

Si el resultado devuelto en este paso es verdadero, significa que el plan se creó correctamente y puede acceder a él en su panel de Stripe.

Paso 2: Cree un producto en la base de datos con el conjunto de campos stripe_plan_name .

Ahora, cree un producto con stripe_plan_name establecido como premium-plan en la base de datos:

 Product.create(price_cents: 10000, name: 'Premium Plan', stripe_plan_name: 'premium-plan')

Paso 3: Genere una migración para agregar una columna stripe_customer_id en la tabla de users .

Ejecuta lo siguiente en la terminal:

 rails generate migration AddStripeCustomerIdToUser stripe_customer_id:string rails db:migrate

Paso 4: implementar la lógica de suscripción en la clase de servicio Stripe.

Agregue dos funciones más en los métodos privados de app/services/orders/stripe.rb : execute_subscription es responsable de crear las suscripciones en el objeto del cliente. La función find_or_create_customer se encarga de devolver el cliente ya creado o devolver un cliente recién creado.

 def self.execute_subscription(plan:, token:, customer:) customer.subscriptions.create({ plan: plan }) end def self.find_or_create_customer(card_token:, customer_id:, email:) if customer_id stripe_customer = Stripe::Customer.retrieve({ id: customer_id }) if stripe_customer stripe_customer = Stripe::Customer.update(stripe_customer.id, { source: card_token}) end else stripe_customer = Stripe::Customer.create({ email: email, source: card_token }) end stripe_customer end

Finalmente, en la función de execute en el mismo archivo ( app/services/orders/stripe.rb ), primero llamaremos a find_or_create_customer y luego ejecutaremos la suscripción llamando a execute_subscription pasando el cliente recuperado/creado anterior. Por lo tanto, reemplace el comentario #SUBSCRIPTIONS WILL BE HANDLED HERE en el método de execute con el siguiente código:

 customer = self.find_or_create_customer(card_token: order.token, customer_id: user.stripe_customer_id, email: user.email) if customer user.update(stripe_customer_id: customer.id) order.customer_id = customer.id charge = self.execute_subscription(plan: product.stripe_plan_name, customer: customer)

Paso 5: Pruebe su aplicación.

Visite su sitio web, seleccione el Premium Plan del producto de suscripción y complete una tarjeta de prueba válida. Después de enviar, debería redirigirte a una página exitosa. Además, verifique en su panel de Stripe si la suscripción se creó con éxito.

Configuración de PayPal

Como hicimos en Stripe, también agregaremos una joya para usar la API de PayPal: paypal-sdk-rest , y también se requiere crear una cuenta de PayPal. Se puede consultar un flujo de trabajo descriptivo para PayPal usando esta gema en la documentación oficial de la API de PayPal.

Paso 1: agregue la gema paypal-sdk-rest a su proyecto.

Agregue esto en el Gemfile :

 gem 'paypal-sdk-rest'

Correr:

 bundle install

Paso 2: Genere sus claves API.

Para tener las claves API para comunicarse con PayPal, deberá crear una cuenta de PayPal. Entonces:

  • Cree una cuenta (o use su cuenta de PayPal) en https://developer.paypal.com/.
  • Aún conectado a su cuenta, cree dos cuentas de sandbox en https://developer.paypal.com/developer/accounts/:
    • Personal (cuenta de comprador): se utilizará en sus pruebas para realizar pagos y suscripciones.
    • Business (Cuenta de comerciante): se vinculará a la aplicación, que tendrá las claves API que estamos buscando. Además de eso, todas las transacciones se pueden seguir en esta cuenta.
  • Cree una aplicación en https://developer.paypal.com/developer/applications usando la cuenta de sandbox comercial anterior.
  • Después de este paso, recibirá dos claves para PayPal: Client ID y Secret .
  • En config/application.yml , reemplace YOUR_CREDENTIAL_HERE de PAYPAL_CLIENT_ID y PAYPAL_CLIENT_SECRET con las claves que acaba de recibir.

Paso 3: Inicialice el módulo de PayPal.

Similar a Stripe, además de reemplazar las claves en application.yml , aún necesitamos inicializar el módulo de PayPal para que pueda usar las claves ya configuradas en nuestra variable ENV . Para ello, cree un archivo en config/initializers/paypal.rb con:

 PayPal::SDK.configure( mode: ENV['PAYPAL_ENV'], client_id: ENV['PAYPAL_CLIENT_ID'], client_secret: ENV['PAYPAL_CLIENT_SECRET'], ) PayPal::SDK.logger.level = Logger::INFO

Paso 4: integre PayPal en el front-end.

En index.html.haml agregue esto en la parte superior del archivo:

 %script(src="https://www.paypal.com/sdk/js?client-id=#{ENV['PAYPAL_CLIENT_ID']}")

A diferencia de Stripe, PayPal usa solo un botón que, cuando se hace clic, abre una ventana emergente segura donde el usuario puede iniciar sesión y proceder al pago/suscripción. Este botón se puede representar llamando al método paypal.Button(PARAM1).render(PARAM2) .

  • PARAM1 es un objeto con la configuración del entorno y dos funciones de devolución de llamada como propiedades: createOrder y onApprove .
  • PARAM2 indica el identificador del elemento HTML al que debe adjuntarse el botón de PayPal.

Entonces, aún en el mismo archivo, reemplace el código comentado YOUR PAYPAL CODE WILL BE HERE con:

 (function setupPaypal() { function isPayment() { return $('[data-charges-and-payments-section] input[name="orders[product_id]"]:checked').length } function submitOrderPaypal(chargeID) { var $form = $("#order-details"); // Add a hidden input orders[charge_id] $form.append($('<input type="hidden" name="orders[charge_id]"/>').val(chargeID)); // Set order type $('#order-type').val('paypal'); $form.submit(); } paypal.Buttons({ env: "#{ENV['PAYPAL_ENV']}", createOrder: function() { }, onApprove: function(data) { } }).render('#submit-paypal'); }());

Paso 5: Pruebe su aplicación.

Visite su página y verifique si el botón de PayPal aparece cuando selecciona PayPal como método de pago.

Transacciones de PayPal

La lógica de las transacciones de PayPal, a diferencia de Stripe, es un poco más compleja al involucrar más solicitudes que se originan desde el front-end hasta el back-end. Por eso existe esta sección. Explicaré más o menos (sin ningún código) cómo se implementarán las funciones descritas en los métodos createOrder y onApprove y qué se espera en los procesos de back-end también.

Paso 1: cuando el usuario hace clic en el botón Enviar de PayPal, se abre una ventana emergente de PayPal que solicita las credenciales del usuario, pero se está cargando. Se llama a la función callback createOrder .

Ventana emergente de PayPal, estado de carga
Ventana emergente de PayPal, estado de carga

Paso 2: en esta función, realizaremos una solicitud a nuestro back-end que creará un pago/suscripción. Este es el comienzo de una transacción y aún no se aplicarán cargos, por lo que la transacción se encuentra en estado pendiente . Nuestro back-end debería devolvernos un token, que se generará utilizando el módulo de PayPal (proporcionado a través de la gema paypal-rest-sdk ).

Paso 3: aún en la devolución de llamada de createOrder , devolvemos este token generado en nuestro back-end, y si todo está bien, la ventana emergente de PayPal mostrará lo siguiente, solicitando las credenciales de usuario:

Ventana emergente de PayPal, credenciales de usuario
Ventana emergente de PayPal, credenciales de usuario

Paso 4: después de que el usuario haya iniciado sesión y seleccionado el método de pago, la ventana emergente cambiará su estado a lo siguiente:

Ventana emergente de PayPal, transacción autorizada
Ventana emergente de PayPal, transacción autorizada

Paso 5: Ahora se llama a la devolución de llamada de la función onApprove . Lo hemos definido como lo siguiente: onApprove: function(data) . El objeto data tendrá la información de pago para poder ejecutarlo. En esta devolución de llamada, se realizará otra solicitud a nuestra función de back-end, esta vez pasando el objeto de datos para ejecutar la orden de PayPal.

Paso 6: Nuestro back-end ejecuta esta transacción y devuelve 200 (si tiene éxito).

Paso 7: cuando nuestro back-end regresa, enviamos el formulario. Esta es la tercera solicitud que hacemos a nuestro back-end.

Tenga en cuenta que, a diferencia de Stripe, se realizan tres solicitudes a nuestro back-end en este proceso. Y mantendremos el estado de nuestro registro de pedidos sincronizado en consecuencia:

  • devolución de llamada createOrder : se crea una transacción y también se crea un registro de pedido; por lo tanto, está en estado pendiente por defecto.
  • onApprove callback: la transacción se ejecuta y nuestro pedido se establecerá como paypal_executed .
  • Se envía la página del pedido: la transacción ya se ejecutó, por lo que no cambia nada. El registro del pedido cambiará su estado a pagado .

Todo este proceso se describe en el siguiente gráfico:

transacciones de paypal
transacciones de paypal

Pagos de PayPal

Los pagos de PayPal siguen la misma lógica que los Cargos de Stripe, por lo que representan transacciones únicas, pero como se mencionó en la sección anterior, tienen una lógica de flujo diferente. Estos son los cambios que deberán realizarse para manejar los pagos de PayPal:

Paso 1: Cree nuevas rutas para PayPal y ejecute los pagos.

Add the following routes in config/routes.rb :

 post 'orders/paypal/create_payment' => 'orders#paypal_create_payment', as: :paypal_create_payment post 'orders/paypal/execute_payment' => 'orders#paypal_execute_payment', as: :paypal_execute_payment

This will create two new routes for creating and executing payments which will be handled in the paypal_create_payment and paypal_execute_payment orders controller methods.

Step 2: Create the PayPal service.

Add the singleton class Orders::Paypal at: app/services/orders/paypal.rb .

This service will initially have three responsibilities:

  • The create_payment method creates a payment by calling PayPal::SDK::REST::Payment.new . A token is generated and returned to the front-end.
  • The execute_payment method executes the payment by first finding the previous created payment object through PayPal::SDK::REST::Payment.find(payment_id) which uses the payment_id as an argument which has the same value as the charge_id stored in the previous step in the order object. After that, we call execute in the payment object with a given payer as the parameter. This payer is given by the front end after the user has provided credentials and selected a payment method in the popup.
  • The finish method finds an order by a specific charge_id querying for recently created orders in the paypal_executed state. If a record is found, it is marked as paid.
 class Orders::Paypal def self.finish(charge_id) order = Order.paypal_executed.recently_created.find_by(charge_id: charge_id) return nil if order.nil? order.set_paid order end def self.create_payment(order:, product:) payment_price = (product.price_cents/100.0).to_s currency = "USD" payment = PayPal::SDK::REST::Payment.new({ intent: "sale", payer: { payment_method: "paypal" }, redirect_urls: { return_url: "/", cancel_url: "/" }, transactions: [{ item_list: { items: [{ name: product.name, sku: product.name, price: payment_price, currency: currency, quantity: 1 } ] }, amount: { total: payment_price, currency: currency }, description: "Payment for: #{product.name}" }] }) if payment.create order.token = payment.token order.charge_id = payment.id return payment.token if order.save end end def self.execute_payment(payment_id:, payer_id:) order = Order.recently_created.find_by(charge_id: payment_id) return false unless order payment = PayPal::SDK::REST::Payment.find(payment_id) if payment.execute( payer_id: payer_id ) order.set_paypal_executed return order.save end end

Step 3: Call the PayPal service in the controller in the submit action.

Add a callback for prepare_new_order before the action paypal_create_payment (which will be added in the next step) is requested by adding the following in the file app/controllers/orders_controller.rb :

 class OrdersController < ApplicationController before_action :authenticate_user! before_action :prepare_new_order, only: [:paypal_create_payment] ...

Again, in the same file, call PayPal service in the submit action by replacing the commented code #PAYPAL WILL BE HANDLED HERE. con lo siguiente:

 ... elsif order_params[:payment_gateway] == "paypal" @order = Orders::Paypal.finish(order_params[:token]) end ...

Step 4: Create the actions for handling requests.

Still, in the app/controllers/orders_controller.rb file, create two new actions (which should be public) for handling requests to paypal_create_payment and paypal_execute_payment routes:

  • The paypal_create_payment method: Will call our service method create_payment . If that returns successfully, it will return the order token created by Orders::Paypal.create_payment .
  • The paypal_execute_payment method: Will call our service method execute_payment (which executes our payments). If the payment is performed successfully, it returns 200.
 ... def paypal_create_payment result = Orders::Paypal.create_payment(order: @order, product: @product) if result render json: { token: result }, status: :ok else render json: {error: FAILURE_MESSAGE}, status: :unprocessable_entity end end def paypal_execute_payment if Orders::Paypal.execute_payment(payment_id: params[:paymentID], payer_id: params[:payerID]) render json: {}, status: :ok else render json: {error: FAILURE_MESSAGE}, status: :unprocessable_entity end end ...

Step 5: Implement the front-end callback functions for createOrder and onApprove .

Make your paypal.Button.render call look like this:

 paypal.Buttons({ env: "#{ENV['PAYPAL_ENV']}", createOrder: function() { $('#order-type').val("paypal"); if (isPayment()) { return $.post("#{paypal_create_payment_url}", $('#order-details').serialize()).then(function(data) { return data.token; }); } else { } }, onApprove: function(data) { if (isPayment()) { return $.post("#{paypal_execute_payment_url}", { paymentID: data.paymentID, payerID: data.payerID }).then(function() { submitOrderPaypal(data.paymentID) }); } else { } } }).render('#submit-paypal');

As mentioned in the previous section, we call paypal_create_payment_url for the createOrder callback and paypal_execute_payment_url for the onApprove callback. Notice that if the last request returns success, we submit the order, which is the third request made to the server.

In the createOrder function handler, we return a token (obtained from the back end). In the onApprove callback, we have two properties passed down to our back-end paymentID and payerID . These will be used in order to execute the payment.

Finally, notice that we have two empty else clauses as I'm leaving room for the next section where we will be adding PayPal subscriptions.

If you visit your page after integrating the front-end JavaScript section and select PayPal as the payment method, it should look like the following:

Index page after integration with PayPal
Index page after integration with PayPal

Step 6: Test your application.

  • Visite la página de índice.
  • Seleccione un producto de pago/cobro y PayPal como método de pago.
  • Haga clic en el botón enviar PayPal.
  • En la ventana emergente de PayPal:
    • Utilice las credenciales de la cuenta de comprador que creó.
    • Inicie sesión y confirme su pedido.
    • La ventana emergente debería cerrarse.
  • Compruebe si se le redirige a una página de éxito.
  • Finalmente, verifique si el pedido se realizó en la cuenta de PayPal iniciando sesión con su cuenta comercial en https://www.sandbox.paypal.com/signin y revisando el tablero https://www.sandbox.paypal.com/listing /actas.

Suscripciones de PayPal

Los planes/acuerdos/suscripciones de PayPal siguen la misma lógica que las suscripciones de Stripe y se crean para pagos recurrentes. Con este tipo de productos se cobra al usuario de forma diaria, semanal, mensual o anual de forma automática según su configuración.

Usaremos el campo del producto paypal_plan_name para almacenar el ID del plan proporcionado por PayPal. En este caso, a diferencia de Stripe, no elegimos el ID, y PayPal devuelve este valor al que se utilizará para actualizar el último producto creado en nuestra base de datos.

Para crear una suscripción, no se requiere información del customer en ningún paso, ya que el método onApprove probablemente maneja este vínculo en su implementación subyacente. Así que nuestras mesas seguirán siendo las mismas.

Paso 1: Cree un plan utilizando la API de PayPal.

Abra su consola usando los rails c . Crea una suscripción para tu cuenta PayPal con:

 plan = PayPal::SDK::REST::Plan.new({ name: 'Premium Plan', description: 'Premium Plan', type: 'fixed', payment_definitions: [{ name: 'Premium Plan', type: 'REGULAR', frequency_interval: '1', frequency: 'MONTH', cycles: '12', amount: { currency: 'USD', value: '100.00' } }], merchant_preferences: { cancel_url: 'http://localhost:3000/', return_url: 'http://localhost:3000/', max_fail_attempts: '0', auto_bill_amount: 'YES', initial_fail_amount_action: 'CONTINUE' } }) plan.create plan_update = { op: 'replace', path: '/', value: { state: 'ACTIVE' } } plan.update(plan_update)

Paso 2: actualice el último producto en la base de datos paypal_plan_name con el plan.id devuelto.

Correr:

 Product.last.update(paypal_plan_name: plan.id)

Paso 3: agregue rutas para la suscripción de PayPal.

Agregue dos nuevas rutas en config/routes.rb :

 post 'orders/paypal/create_subscription' => 'orders#paypal_create_subscription', as: :paypal_create_subscription post 'orders/paypal/execute_subscription' => 'orders#paypal_execute_subscription', as: :paypal_execute_subscription

Paso 4: Manejar la creación y ejecución en el servicio de PayPal.

Agregue dos funciones más para crear y ejecutar suscripciones en Orders::Paypal de app/services/orders/paypal.rb :

 def self.create_subscription(order:, product:) agreement = PayPal::SDK::REST::Agreement.new({ name: product.name, description: "Subscription for: #{product.name}", start_date: (Time.now.utc + 1.minute).iso8601, payer: { payment_method: "paypal" }, plan: { id: product.paypal_plan_name } }) if agreement.create order.token = agreement.token return agreement.token if order.save end end def self.execute_subscription(token:) order = Order.recently_created.find_by(token: token) return false unless order agreement = PayPal::SDK::REST::Agreement.new agreement.token = token if agreement.execute order.charge_id = agreement.id order.set_paypal_executed return order.charge_id if order.save end end

En create_subscription , inicializamos un acuerdo llamando al método PayPal::SDK::REST::Agreement.new y pasando product.paypal_plan_name como uno de sus atributos. Luego lo creamos, y ahora se establecerá un token para este último objeto. También devolvemos el token al front-end.

En execute_subscription encontramos el registro de order creado en la llamada anterior. Después de eso, inicializamos un nuevo acuerdo, configuramos el token de este objeto anterior y lo ejecutamos. Si este último paso se realiza con éxito, el estado del pedido se establece en paypal_ejecutado . Y ahora devolvemos al front-end el ID del acuerdo que también se almacena en order.chager_id .

Paso 5: agregue acciones para crear y ejecutar suscripciones en orders_controller .

Cambie app/controllers/orders_controller.rb . En la parte superior de la Clase, en primer lugar, y luego actualice la devolución de llamada prepare_new_order para que también se ejecute antes de llamar a paypal_create_subscription :

 class OrdersController < ApplicationController before_action :authenticate_user! before_action :prepare_new_order, only: [:paypal_create_payment, :paypal_create_subscription]

Además, en el mismo archivo agregue las dos funciones públicas para que llamen al servicio Orders::Paypal con un flujo similar al que ya tenemos en los pagos de PayPal:

 ... def paypal_create_subscription result = Orders::Paypal.create_subscription(order: @order, product: @product) if result render json: { token: result }, status: :ok else render json: {error: FAILURE_MESSAGE}, status: :unprocessable_entity end end def paypal_execute_subscription result = Orders::Paypal.execute_subscription(token: params[:subscriptionToken]) if result render json: { id: result}, status: :ok else render json: {error: FAILURE_MESSAGE}, status: :unprocessable_entity end end ...

Paso 6: Adición de controladores de suscripción para las devoluciones de llamadas createOrder y onApprove en el front-end.

Finalmente, en index.html.haml , reemplace la función paypal.Buttons con lo siguiente, que llenará los dos vacíos else teníamos antes:

 paypal.Buttons({ env: "#{ENV['PAYPAL_ENV']}", createOrder: function() { $('#order-type').val("paypal"); if (isPayment()) { return $.post("#{paypal_create_payment_url}", $('#order-details').serialize()).then(function(data) { return data.token; }); } else { return $.post("#{paypal_create_subscription_url}", $('#order-details').serialize()).then(function(data) { return data.token; }); } }, onApprove: function(data) { if (isPayment()) { return $.post("#{paypal_execute_payment_url}", { paymentID: data.paymentID, payerID: data.payerID }).then(function() { submitOrderPaypal(data.paymentID) }); } else { return $.post("#{paypal_execute_subscription_url}", { subscriptionToken: data.orderID }).then(function(executeData) { submitOrderPaypal(executeData.id) }); } } }).render('#submit-paypal');

La creación y ejecución de suscripciones tiene una lógica similar a la utilizada para los pagos. Una diferencia es que, al ejecutar pagos, los datos de la función de devolución de llamada onApprove ya tienen un paymentID . de pago que representa el charge_id de cargo para enviar el formulario a través submitOrderPaypal(data.paymentID) . Para suscripciones, obtenemos charge_id solo después de ejecutarlo solicitando un POST en paypal_execute_subscription_url , por lo que podemos llamar a submitOrderPaypal(executeData.id) .

Paso 7: Pruebe su aplicación.

  • Visite la página de índice.
  • Seleccione un producto de suscripción y PayPal como método de pago.
  • Haga clic en el botón enviar PayPal.
  • En la ventana emergente de PayPal:
    • Utilice las credenciales de la cuenta de comprador que creó.
    • Inicie sesión y confirme su pedido.
    • La ventana emergente debería cerrarse.
  • Compruebe si se le redirige a una página de éxito.
  • Finalmente, verifique si el pedido se realizó en la cuenta de PayPal iniciando sesión con su cuenta comercial en https://www.sandbox.paypal.com/signin y revisando el tablero https://www.sandbox.paypal.com/listing/ actas.

Conclusión

Después de leer este artículo, debería poder integrar pagos/cargos, así como transacciones de suscripciones para PayPal y Stripe en su aplicación Rails. Hay muchos puntos que podrían mejorarse y que no agregué en este artículo en aras de la brevedad. Organicé todo en base a una suposición de dificultad:

  • Más fácil:
    • Use Transport Layer Security (TLS) para que sus solicitudes usen HTTPS.
    • Implemente configuraciones de entorno de producción para PayPal y Stripe.
    • Agregue una nueva página para que los usuarios puedan acceder a un historial de pedidos anteriores.
  • Medio:
    • Reembolsar o cancelar suscripciones.
    • Proporcionar una solución para los pagos de usuarios no registrados.
  • Más difícil:
    • Proporcione una forma de eliminar cuentas y conservar su token y customer_id si el usuario desea volver. Pero después de una cierta cantidad de días, elimine estos datos para que su aplicación sea más compatible con PCI.
    • Mover a la API de la versión 2 de PayPal en el lado del servidor (https://developer.paypal.com/docs/api/payments/v2/) La gema que usamos en este tutorial paypal-sdk-rest , solo tiene una versión beta para la versión 2, por lo que podría usarse con precaución (https://github.com/paypal/PayPal-Ruby-SDK/tree/2.0-beta).
    • Incluir solicitudes idempotentes.
      • Banda: https://stripe.com/docs/api/idempotent_requests
      • PayPal: https://developer.paypal.com/docs/api-basics/#api-idempotency

También recomiendo leer sobre el elemento Stripe Checkout, que es otra forma de integrar Stripe en el front-end. A diferencia de Stripe Elements, que usamos en este tutorial, Stripe Checkout abre una ventana emergente después de hacer clic en un botón (similar a PayPal) donde el usuario completa la información de la tarjeta de crédito O elige pagar con Google Pay/Apple Pay https://stripe.com /docs/web.

Una recomendación de segunda lectura son las páginas de seguridad de ambas pasarelas de pago.

  • Para rayas
  • para paypal

Finalmente, ¡gracias por leer este artículo! También puede consultar mi proyecto de GitHub utilizado para este ejemplo de proyecto. Allí, también agregué pruebas de rspec durante el desarrollo.