Published on

Como integrar Stripe con Laravel 9

Authors

Stripe es una poderosa pasarela de pago que permite ser integrada de forma muy sencilla en tú web. Permitiendo procesar pagos únicos, suscripciones a productos, generación de facturas, pagos en distintas monedas, etc. Todo eso integrado con un framework tan poderoso como Laravel, te permite crear aplicaciones robustas, fiables y sencillas. 

¿Qué es Stripe?


Stripe es un sistema de pago muy parecido a Paypal, por la que se pueden procesar pagos y suscripciones a través de internet. Todo eso se integra a través de una poderosa API que se puede implementar prácticamente en cualquier aplicación o plataforma digital. Algunas de las ventajas principales de Stripe:

  • Fácil implementación  acompañada de una gran documentación de su API.
  • El usuario que pague no debe tener cuenta en Stripe (como si sucede con Paypal)
  • Tú no tendrás que guardas ningún dato sensible respecto a número de tarjeta, etc. Stripe se encarga de eso.
  • Puedes integrar su propio formulario de pago con javascript y olvidarte de tener crear uno propio.
  • La documentación te muestra la forma de integrar las peticiones en tú Backend, en distintas tecnologías (PHP, Node, Java, Rubi, Go, .NET, etc) 

Pero para ver lo fácil que es de implementar, vamos a crear un proyecto sencillo que replique una web de venta de libros. Podremos realizar pagos únicos y pagos por medio de suscripción (mensual / anual) para el acceso a todos los libros. Para ese proceso de compra integraremos un sistema de pago que obviamente será Stripe.


Empezaremos con los comandos necesarios para crear nuestro proyecto. Las versiones en la que los construiremos la aplicación será en la versión 9.1 de Laravel y 8.0.2 de PHP.

  • Crear proyecto Laravel (composer create-project  laravel/laravel laravelwithstripe)
  • Laravel-cashier (composer require laravel/cashier)

Con este último comando, instalaremos una dependencia que nos facilitara la conexión  e iteración con la API de Stripe. Pero te recomiendo que te leas la documentación de laravel/cashier y veas todos lo que se puede hacer con ella. Desde saber si un usuario tiene la suscripción activa a poder generar facturas de los pagos realizados.


El siguiente paso, será que te crees una cuenta en Stripe si todavía no la tienes. Además la creación de la cuenta es gratuita. 

Una vez que estés registrado en Stripe, accede al dashboard de desarrollador y veras dos claves que necesitaras para conectar tú aplicación con la API de Stripe.



Pero ya que estamos aquí, vamos aprovechar el viaje. Crearemos dos productos. Un plan de suscripción mensual y otro plan de suscripción anual.

Para eso accederemos a la sección producto que se encuentra dentro del dashboard, y pincharemos en "Añadir producto".

Tiene varios campos a rellenar. En nuestro caso lo registraremos así:



Guardaremos el producto. Al crear un producto, se crea una Id necesario más adelante. Lo podremos ver si pinchamos en el producto que aparecerá en la lista de la vista producto. En la sección TARIFA, hay un campo que se llama "ID DE API"


Este id, lo necesitaremos para añadirlo en nuestro proyecto. Porque cuando un usuario se suscriba a nuestro plan (Que hace referencia al producto creado en Stripe), tendremos que pasarle este id. Por último crearemos el plan de suscripción Anual. El proceso es el mismo que hemos visto anteriormente. Con esto, terminaría las acciones necesarias en la web de Stripe.

Comentarte también otras opciones sobre productos. Stripe te da la posibilidad de poder crear productos desde la API, o crear un producto con dos precios distintos. Por ejemplo, podrías crear un mismo producto, pero con un precio en euros y otro en dólares.

Ya en nuestro proyecto, añadimos cinco variables en nuestro fichero .env .  La URL de la API de Stripe, la KEY,  el SECRET y los dos Id´s de los productos creados

                            STRIPE_BASE_URI=https://api.stripe.com
                            STRIPE_KEY=pk_test_51IkU4WAK2uIyILzIxlYAjTrkoMOr
                            STRIPE_SECRET=sk_test_51IkU4WAK2uIyILzI5C7O
                            STRIPE_MENSUAL_PLAN=price_uIyILzIkMM
                            STRIPE_ANUAL_PLAN=price_uIyILzIZm7ZF
                        

Ahora configuraremos el "services.php" que está en la ruta "config/services.php". Crearemos un array "stripe" y le añadimos 5 campos. Los tres primeros hacen referencia a la URL de la API de Stripe, la KEY , el SECRET y el último que es una campo tipo array, hace referencia a los id´s de nuestros planes que guardamos en el fichero .env

A tener en cuenta, las key del array plans, deben coincidir con el valor de la columna stripe_plan, de la tabla "plans"

 'stripe' => [
        'base_uri'  => env('STRIPE_BASE_URI'),
        'key'       => env('STRIPE_KEY'),
        'secret'    => env('STRIPE_SECRET'),
        'class'     => App\App\Services\StripeService::class,
        'plans' => [
            'monthly' => env('STRIPE_MENSUAL_PLAN'),
            'yearly' => env('STRIPE_ANUAL_PLAN'),
        ],
    ],

Porque creamos esta configuración y no usamos las variables generadas en el fichero .env. Porque las variables creadas en el archivo .env no son aptas para usarlas en tiempo de ejecución. 

Migraciones, factories y seeders

Pasemos a crear las migraciones necesarias. Crearemos tres tablas más. Books, Images  y Plans. El código sería el siguiente:


Migración para Books

return new class extends Migration
{

    public function up()
    {
        Schema::create('books', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->longText('description');
            $table->float('price');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('books');
    }
};

Tabla images

return new class extends Migration
{

    public function up()
    {
        Schema::create('images', function (Blueprint $table) {
            $table->id();
            $table->string('url');
            $table->unsignedBigInteger('imageable_id');
            $table->string('imageable_type');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('images');
    }
};

Tabla Plans

return new class extends Migration
{

    public function up()
    {
        Schema::create('plans', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->string('stripe_plan');
            $table->float('cost');
            $table->text('description')->nullable();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('plans');
    }
};

Configuraremos los factories. El único que viene ya creado es el del modelo users. Empezaremos por el modelo Book

<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;

class BookFactory extends Factory
{

    public function definition()
    {
        return [
            'name'        => $this->faker->sentence(),
            'description' => $this->faker->sentence(),
            'price'       => random_int(8, 15),
        ];
    }
}

El modelo Image:

<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;

class ImageFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'url' => 'public/books/fake_book.jpg',
        ];
    }
}

Por último modificaremos el fichero DatabaseSeeder.php, para tener una serie de datos en nuestra base de datos, cada vez que ejecutemos las migraciones.

El seeder

<?php
namespace Database\Seeders;
use App\Models\User;
use App\Models\Book;
use App\Models\Image;
use App\Models\Plan;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Storage;
class DatabaseSeeder extends Seeder
{

    public function run()
    {
    
      User::factory()->create([
        'name'=> 'User',
        'email'=> 'user@mail.com',
        'password'=> \bcrypt('12345678'),
      ]);
      
      Plan::factory()->create([
'name' => 'Monthly', 'slug' => 'monthly', 'stripe_plan' => 'monthly', 'cost' => 30, 'description' => 'One month access to the entire library'
      ]);
      Plan::factory()->create([
'name' => 'Yearly', 'slug' => 'yearly', 'stripe_plan' => 'yearly', 'cost' => 60, 'description' => 'One-year access to the entire library'
      ]);
      
      $books = Book::factory(10)->create();
      foreach ($books as $book) {
        Image::factory(1)->create([
            'imageable_id' => $book->id,
            'imageable_type' =>'App\Models\Book',
        ]);
      }

    
    }
}

Para correr las  migraciones con los seeders o para construirla de nuevo, usaríamos el siguiente comando:

php artisan migrate:refresh --seed

El código de nuestro fichero de rutas:

<?php
use App\Http\Controllers\BookController;
use App\Http\Controllers\PlanController;
use App\Http\Controllers\PaymentController;
use App\Http\Controllers\SubscriptionController;
use Illuminate\Support\Facades\Route;

Route::get('/',  [BookController::class, 'index'])->name('bookstore');
Route::get('/cart/{id}',  [PaymentController::class, 'show'])->middleware('auth')->name('purchase');
Route::post('/payments/pay',  [PaymentController::class, 'pay'])->middleware('auth')->name('pay');
Route::get('/payments/approval',  [PaymentController::class, 'approval'])->middleware('auth')->name('approval');
Route::get('/payments/cancelled',  [PaymentController::class, 'cancelled'])->middleware('auth')->name('cancelled');
Route::prefix('subscribe')->middleware('auth')->name('subscribe.')
->group(function(){
    Route::get('/', [SubscriptionController::class, 'show'])->name('plans');
    Route::post('/', [SubscriptionController::class, 'store'])->name('store');
    Route::post('/approval', [SubscriptionController::class, 'approval'])->name('approval');
    Route::post('/cancelled', [SubscriptionController::class, 'cancelled'])->name('cancelled');
});
Route::get('/dashboard', function () {
    return view('dashboard');
})->middleware(['auth'])->name('dashboard');
require __DIR__.'/auth.php';

Ahora vamos a crear el servicio de Stripe, que nos permitirá interactuar con su API. Creamos una archivo llamado StripeService y su ruta será "app\Services\StripeServices.php"

Lo esencial de este archivo:

  • En el controlador cargamos las variables del fichero config
  • Instanciamos la clase StripeClient que instalamos con la dependecia de Laravel-cashier
  • El método "handleSuscription", se encarga de llamar al endpoint de Stripe que primero crea un cliente de pago y luego lo asocia a producto. En este caso una suscripción mensual o anual.
  • Los métodos "createIntent" y "confirmPayment", llevan el flujo para pagos únicos o no recurrentes. Primero se debe crear una intención de pago para luego terminar confirmandolo.
<?php
namespace App\Services;
use Illuminate\Http\Request;
use App\Traits\ExternalServices;
class StripeService
{
    use ExternalServices;
    protected $key;
    protected $secret;
    protected $baseUri;
    protected $stripe;
    protected $plans;
    public function __construct()
    {
        $this->baseUri = config('services.stripe.base_uri');
        $this->key = config('services.stripe.key');
        $this->secret = config('services.stripe.secret');
        $this->stripe = new \Stripe\StripeClient(
            config('services.stripe.secret')
          );
        $this->plans = config('services.stripe.plans');
    }
    
    public function handleSuscription(Request $request)
    {
        try{
            $customer = $this->createCustomer($request->name, $request->email, $request->payment_method);
            $price_select_plan= $this->plans[$request->plan];
            $subscription = $this->createSubscription($customer->id,  $request->payment_method,  $price_select_plan);
        }catch(\Exception $exception) {
            throw new \Exception($exception->getMessage(), 1);
            
        }
        return  $subscription;
    }

    /**
     * Create a payment intention
     */
    public function createIntent(float $price, string $currency = 'eur', string $paymentMethod )
    {
         return  $this->stripe->paymentIntents->create([
            'amount' => $price*100,
            'currency' => $currency,
            'payment_method_types' => ['card'],
            'payment_method'        => $paymentMethod,
          ]);
      
    }
    /**
     * Confirm an intention to pay
     */
    public function confirmPayment($paymentIntentId, $paymentMethod)
    {
        $this->stripe->paymentIntents->confirm(
            $paymentIntentId,
            ['payment_method' => $paymentMethod]
          );
    }
    
    /**
     * Create a new customer
     */
    public function createCustomer(string $name, string $email, string $paymentMethod)
    {
        return    $this->stripe->customers->create([
                'name' => $name,
                'email' => $email,
                'payment_method' => $paymentMethod
            ]);
        }

     /**
     * Create a new subscription
     */
    public function createSubscription( $customerId, $paymentMethod, $priceId)
    {
        return $this->stripe->subscriptions->create([
            'customer' =>  $customerId,
            'items' => [
              ['price' => $priceId],
            ],
            'default_payment_method' =>  $paymentMethod
          ]);
    }
    
}

Controladores

Ahora crearemos los controladores necesarios para trabajar con nuestra vistas. 

El comando de ejecución y el código de cada uno sería el siguiente:

Desde BookController, nos cargara el catalogo de libros

php artisan make:controller BookController
<?php
namespace App\Http\Controllers;
use App\Models\Book;
use Illuminate\Http\Request;
class BookController extends Controller
{
    public function index()
    {
        $books = Book::all();
        return view('bookstore', compact('books'));
    }

}

El controlador PaymentController, se encargara de procesar los pagos únicos.

php artisan make:controller PaymentController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\StripeService;
use App\Models\Book;
use Stripe;
use Session;
class PaymentController extends Controller
{
    protected $stripe;
    public function __construct(StripeService $stripe)
    {
        $this->stripe = $stripe;
    }
    public function show($id)
    {
          $book = Book::find($id);
          return view('Purchase', compact('book'));
    }
    /**
     * Creates an intention to pay and its confirmation
     * 
     */
    public function pay(Request $request)
    {
        $rules = [
            'price' => ['required', 'numeric', 'min:5']
        ];
       
        $request->validate($rules);
       
        
        try{
            $payment_intent = $this->stripe->createIntent($request->price, 'eur', $request->payment_method );
            $this->approval($payment_intent->id, $request->payment_method);
       }catch(\Excepction $exception) {
            
            return redirect()->back()->withErrors('No se ha podido completar el pago de su pedido')->withInput();
       }
       $book = json_decode($request->book, true);
       
       return view('confirmation_page', compact('book'));
    }
    /**
     * create payment confirmation
     * 
     */
    public function approval($payment_intent_id, $payment_method)
    {
        try{
            $this->stripe->confirmPayment($payment_intent_id, $payment_method);
        }catch(\Excepction $exception){
            return false;
        }
    }
    /**
     * Cancel a payment
     */
    public function cancelled()
    {
        return redirect()
        ->route('bookstore')
        ->withErrors('Se ha cancelado el pago');
    }
}

Y por último, desde el SubscriptionController, gestionaremos las suscripciones.

php artisan make:controller SubscriptionController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Plan;
use App\Services\StripeService;
class SubscriptionController extends Controller
{
    protected $stripe;
    public function __construct(StripeService $stripe)
    {
        $this->stripe =  $stripe;
    }
    public function show() 
    {
        $plans =  Plan::all();
        return view('plans', compact('plans'));
    }

    public function store(Request $request) 
    {
        $rules = [
            'plan'  => ['required', 'exists:plans,slug'],
            'name'  => ['required'],
            'email' => ['required'],
        ];
        $request->validate($rules);
        try{
            $this->stripe->handleSuscription($request);
        }catch(\Exception $exception) {
            throw new \Exception($exception->getMessage(), 1);
            
        }
        return view('confirmation_page');
    }

   
}

Con todo esto, el resultado final sería el siguiente:

Recuerda que esto es solo una parte de todo lo que se puede hacer con Stripe y Laravel-cashier. También recordarte que en el repositorio tienes todo el código disponible para revisar, bajar o clonar.  Para  probarlo, solo debes añadir tus claves de Stripe, correr las migraciones con el comando que te he indicado más arriba.

Por último si tienes alguna duda o sugerencia. Puedes contactarme a través del formulario de contacto o por medio de mis perfiles en redes sociales.

Espero que este articulo te haya sido de utilidad y no dudes en compartirlo si crees que puede ser de utilidad para otras personas.

feliz código a tod@s!!

1992