Pagos con Stripe en Laravel 13
Creación de una aplicación en Laravel 13 que permite realizar pagos en Stripe usando IA (Laravel Boost, Claude Code y Gemini)
1. Creación del VPS
Servidor VPS en DigitalOcean.com: LEMP
Getting started after deploying LEMP
2. Configuración del servidor virtual
El servidor VPS debe tener instalada la versión mínima de PHP (8.3) y las extensiones necesarias.
Conexión al droplet por ssh ( con un usuario diferente a root:):
ssh usuario@alumnoportada.com.es
Instalar paquetes de PHP
sudo apt update sudo apt install curl -y sudo apt install php php-common php-gd php-mysql php-curl php-intl php-mbstring php-bcmath php-xml php-zip -y
Server configuration for Nginx
server {
listen 80;
listen [::]:80;
server_name example.com;
root /srv/example.com/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
sudo nano /etc/nginx/sites-available/laravelia
# /etc/nginx/sites-available/laravelia
server {
# sustituir usuario por el nombre de usuario usado
root /home/paco/notesAppIA/public;
# Add index.php to the list if you are using PHP
index index.php index.html;
# sustituir alumno.me por el dominio usado
server_name laravelia.alumnoportada.com.es;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# versión 8.5 de PHP
location ~ .php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.5-fpm.sock;
}
}
enlazar sites-available con sites-enabled
sudo ln -s /etc/nginx/sites-available/laravelia /etc/nginx/sites-enabled/
comprobar la configuración de Nginx y reiniciarlo
sudo nginx -t sudo systemctl restart nginx sudo systemctl status nginx
instalar el certificado de Let’s Encrypt
sudo certbot --nginx -d laravelia.alumnoportada.com.es
fichero de configuración completo
server {
# Set root directive with your /public Laravel directory
root /home/paco/notesAppIA/public;
# Set index directive with index.php
index index.php;
# Set server_name directive with the hostname
server_name laravelia.alumnoportada.com.es;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/var/run/php/php8.5-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/laravelia.alumnoportada.com.es/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/laravelia.alumnoportada.com.es/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = laravel.alumnoportada.com.es) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name laravel.alumnoportada.com.es;
return 404; # managed by Certbot
}
3. Instalación de Composer y node.js
sudo apt update sudo apt install nodejs npm
4. Base de datos
Crear la base de datos notesDBIA y el usuario user_notesDBIA con la password Malaga2526
Conectarse al servidor de bases de datos
sudo mysql
o
mysql -u usuario -p
Ejecutar estos comandos:
CREATE DATABASE notesDBIA; CREATE USER 'user_notesDBIA'@'localhost' IDENTIFIED BY 'Malaga2526'; GRANT ALL PRIVILEGES ON notesDBIA.* TO 'user_notesDBIA'@'localhost'; FLUSH PRIVILEGES; exit
5. Instalación de Laravel
(Eliminar la carpeta notesAppIA, si ya estaba creada)
cd ~ # borrar la carpeta notesAppIA, si ya existe rm -r notesAppIA
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Crear el proyecto
composer create-project laravel/laravel notesAppIA cd notesAppIA composer require laravel/jetstream php artisan jetstream:install livewire php artisan install:api
¿Qué es Laravel Jetstream y cómo empezar?
Modificar la conexión a la base de datos en el fichero .env:
nano .env
Información de conexión a la base de datos:
# DB_CONNECTION=sqlite DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=notesDBIA DB_USERNAME=user_notesDBIA DB_PASSWORD=Malaga2526 DB_COLLATION=utf8mb4_unicode_ci
Configuración del correo
nano .env
fichero .env (en la carpeta del proyecto de Laravel):
MAIL_MAILER=smtp MAIL_HOST=127.0.0.1 MAIL_PORT=25 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_FROM_ADDRESS="admin@alumnoportada.com.es" MAIL_FROM_NAME="${APP_NAME}"
Modificar el fichero config/mail
nano config/mail.php
fichero config/mail.php
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 25),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
'verify_peer' => false,
],
6. Laravel Boost
Laravel Boost is the official Laravel 13 tool that integrates MCP (Model Context Protocol), guidelines, and skills to enhance AI-assisted development. It provides an MCP server that allows AI agents to interact directly with your application, offering tools to inspect routes, database schemas, logs, and execute code via Tinker.
Cómo usar Laravel Boost para potenciar con IA tus proyectos Laravel
Más información:
How To Boost Your Laravel App In Minutes
Laravel Boost: La documentación que tu proyecto Laravel necesita
Laravel Skills: a public directory of “AI agent skills” for Laravel & PHP
Step 1: Instalar Laravel Boost
composer require laravel/boost –dev</ br>
Step 2: Configurar Laravel Boost
php artisan boost:install
Step 3: Usar un MCP
-
Gemini support is typically enabled automatically, but if you find it isn’t
-
Open a shell in the project’s directory
-
Run gemini mcp add -s project -t stdio laravel-boost php artisan boost:mcp
Updates: Use php artisan boost:update to refresh guidelines and skills, or add it to your Composer scripts for automatic updates.
Customization: Developers can create custom skills by adding a SKILL.md file to their package or project, following the Agent Skills format with YAML frontmatter.
Instalación de Boost:
composer require laravel/boost --dev php artisan boost:install
██████╗ ██████╗ ██████╗ ███████╗ ████████╗
██╔══██╗ ██╔═══██╗ ██╔═══██╗ ██╔════╝ ╚══██╔══╝
██████╔╝ ██║ ██║ ██║ ██║ ███████╗ ██║
██╔══██╗ ██║ ██║ ██║ ██║ ╚════██║ ██║
██████╔╝ ╚██████╔╝ ╚██████╔╝ ███████║ ██║
╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝
✦ Laravel Boost :: Install :: We Must Ship ✦
Let’s give Laravel a Boost
┌ Which Boost features would you like to configure? ───────────┐
│ ◼ AI Guidelines │
│ ◼ Agent Skills │
│ › ◼ Boost MCP Server Configuration │
└──────────────────────────────────────────────────────────────┘
This will override the current guidelines, skills, and MCP configuration
┌ Which integrations would you like to configure for Boost? ───┐
│ › ◻ Laravel Cloud │
└──────────────────────────────────────────────────────────────┘
Selected integrations will have their MCP servers or skills automatically configured
┌ Which AI agents would you like to configure? ────────────────┐
│ ◻ Amp │
│ ◼ Claude Code │
│ ◻ Codex │
│ ◻ Cursor │
│ › ◼ Gemini CLI │
│ ◻ GitHub Copilot │
│ ◻ Junie │
│ ◻ Kiro │
│ ◻ OpenCode │
└──────────────────────────────────────────────────────────────┘
Use the space bar to select options.
Adding 9 guidelines to your selected agents
┌───────┬──────────────┬────────────┬──────────────┬───────────────┐
│ boost │ deployments │ foundation │ laravel/core │ livewire/core │
├───────┼──────────────┼────────────┼──────────────┼───────────────┤
│ php │ phpunit/core │ pint/core │ tests │ │
└───────┴──────────────┴────────────┴──────────────┴───────────────┘
Claude Code… ✓
Gemini CLI … ✓
Syncing 4 skills for skills-capable agents
┌──────────────────────┬─────────────────────────┐
│ fortify-development │ laravel-best-practices │
├──────────────────────┼─────────────────────────┤
│ livewire-development │ tailwindcss-development │
└──────────────────────┴─────────────────────────┘
7. Instalación de Claude Code en consola
Claude Code para Laravel: Guía Completa 2026
Instalación en consola
curl -fsSL https://claude.ai/install.sh | bash
✔ Claude Code successfully installed!
Version: 2.1.139
Location: ~/.local/bin/claude
Claude is enabled automatically, but if you find it isn’t
Open a shell in the project’s directory
Run
claude mcp add -s local -t stdio laravel-boost php artisan boost:mcp
~/.local/bin/claude mcp add -s local -t stdio laravel-boost php artisan boost:mcp
Error: installing claude code native build latest… bash: line 151: 8889 killed «$binary_path» install ${target:+»$target»}
killed) para evitar que el servidor se congele.
Si falla la instalación, se puede usar la extensión de Visual Studio Code:
Ayuda:
https://code.claude.com/docs/en/quickstart
8. Instalación de Gemini en consola
instalar node y npm:
Cómo instalar Node.js y npm en Windows, macOS y Linux
sudo apt install nodejs npm
Instalar Gemini cli
npm install -g @google/gemini-cli
Usar gemini
gemini mcp add -s project -t stdio laravel-boost php artisan boost:mcp gemini
Si falla la instalación, se puede usar la extensión de Gemini para Visual Studio Code:
9. Montar los archivos remotos para revisarlos
Ejecutar en el equipo local:
(mkdir servidor) (sudo apt install sshfs) sshfs usuario@alumnoportada.com.es:/home/usuario/notesAppIA/ servidor -p 22
Abrir la carpeta con el IDE Visual Studio Code
cd servidor code .
o usar la extensión ssh-fs
10. Generar la app con Claude
usar claude en consola:
cd ~/notesAppIA ~/.local/bin/claude
▐▛███▜▌ Claude Code v2.1.139
▝▜█████▛▘ Sonnet 4.6 · Claude Pro
▘▘ ▝▝ ~/notesAppIA
/login
Mi workflow: Plan → Tareas → Ejecución supervisada
Para tareas complejas —nuevas features, refactorizaciones grandes, migraciones— uso un workflow que funciona extraordinariamente bien:
1. Trazar el plan
Inicio Claude Code y describo la feature completa. Le pido que no implemente nada todavía, solo que cree un plan detallado:
Necesito implementar [descripción de la feature].
No implementes nada todavía. Crea un archivo plan.md con:
– Análisis de lo que hay que hacer
– Lista de tareas ordenadas
– Archivos que se van a crear o modificar
– Consideraciones técnicas
Claude genera un plan.md con todas las tareas numeradas y detalladas.
2. Ejecutar tarea por tarea
Aquí está la clave: cada tarea en una nueva ventana de Claude Code.
Abro una nueva sesión y le digo:
Revisa plan.md y continúa con la siguiente tarea pendiente.
Márcala como completada cuando termines.
Claude lee el plan, identifica la siguiente tarea, la implementa, y actualiza el archivo marcándola como hecha.
1. Plan
Prompt
Quiero crear [una app en Laravel 13 donde cada usuario pueda manejar sus propias tareas. El modelo de tarea es: title: char(255), required, description: text, limit: date (date of termination), link: url price: float, required En la app cada usuario puede hacer login o registrarse (verificando su email) desde un navegador. Cuando acceda, puede manejar sus tareas (CRUD) : crear, editar o eliminar sus tareas. También quiero crear un api rest donde el usuario pueda hacer, login, registrarse y realizar crud de tareas desde una aplicación en el móvil. ] No implementes nada todavía. Crea un archivo plan.md con: - Análisis de lo que hay que hacer - Lista de tareas ordenadas - Archivos que se van a crear o modificar - Consideraciones técnicas
2. Ejecución
Prompt
Revisa plan.md y continúa con la siguiente tarea pendiente. Márcala como completada cuando termines.
11. Prueba
(php artisan migrate)
Cambiar el propietario de la carpeta storage y bootstrap/cache:
cd ~/notesAppIA sudo chown -R www-data:www-data storage sudo chown -R www-data:www-data bootstrap/cache
Permisos en bootstrap/cache y storage/logs/laravel.log:
sudo chmod 777 bootstrap/cache sudo chmod 777 storage/logs/laravel.log
Ejecutar
npm install npm run build
Comprobación:
laravelia.alumnoportada.com.es
12. Correcciones con Gemini
cd ~/notesAppIA gemini
Elegir el modelo
/model
gemini-3.1-flash-lite-preview
Prompt:
when i make login, i get the error: [Whoops! Something went wrong.hese credentials do not match our records.], can you fix it?
13. Repositorio en GitHub
Crear el repositorio notesAppLaravelIA en GitHub y subir los archivos
cd ~/notesAppIA echo "# Notes App with Laravel and IA" >> README.md git init git branch -M main git remote add origin https://TOKEN@github.com/paco-portada/notesAppLaravelIA.git git add . git commit -m "first commit" git push -u origin main
crear rama para realizar pagos con Stripe
git checkout -b "feature/pagos"
14. Laravel y Stripe
How to Integrate Stripe Payment Gateway in Laravel
cd ~/notesAppIA composer require stripe/stripe-php
API Keys
Next, you should configure your Stripe API keys in your application’s .env file. You can retrieve your Stripe API keys from the Stripe control panel:
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_CURRENCY=EU
nano .env
Añadir al fichero .env las claves
STRIPE_KEY=pk_test_51TWBt9Q5 . . . STRIPE_SECRET=sk_test_51TWBt9Q5 . . . STRIPE_CURRENCY=EU
situarse en la nueva rama
git switch feature/pagos
iniciar Claude
~/.local/bin/claude /login
Prompt
in the Laravel app add a button (with edit and delete) in the task for payment using stripe
Migration — paid_at (timestamp) and stripe_payment_id (string) columns on tasks.
Fichero app/models/Task.php
<?php
namespace App\Models;
use Database\Factories\TaskFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Orchid\Filters\Filterable;
use Orchid\Filters\Types\Like;
use Orchid\Filters\Types\Where;
use Orchid\Filters\Types\WhereDateStartEnd;
use Orchid\Screen\AsSource;
class Task extends Model
{
/** @use HasFactory<TaskFactory> */
use AsSource, Filterable, HasFactory;
protected $fillable = [
'user_id',
'title',
'description',
'limit',
'link',
'price',
'paid_at',
'stripe_payment_id',
];
/**
* The attributes for which you can use filters in url.
*
* @var array
*/
protected $allowedFilters = [
'id' => Where::class,
'title' => Like::class,
'user_id' => Where::class,
'price' => Where::class,
'limit' => WhereDateStartEnd::class,
'paid_at' => WhereDateStartEnd::class,
];
/**
* The attributes for which can use sort in url.
*
* @var array
*/
protected $allowedSorts = [
'id',
'title',
'price',
'limit',
'paid_at',
'updated_at',
'created_at',
];
protected function casts(): array
{
return [
'limit' => 'date',
'price' => 'float',
'paid_at' => 'datetime',
];
}
public function isPaid(): bool
{
return $this->paid_at !== null;
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Fichero database/migrations/create_tasks_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->char('title', 255);
$table->text('description')->nullable();
$table->date('limit')->nullable();
$table->string('link')->nullable();
$table->float('price')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('tasks');
}
};
Fichero add_paid_at_to_tasks_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->timestamp('paid_at')->nullable()->after('price');
$table->string('stripe_payment_id')->nullable()->after('paid_at');
});
}
public function down(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropColumn(['paid_at', 'stripe_payment_id']);
});
}
};
ejecutar la migración
php artisan migrate
añadir el botón de pago en la vista
Task list view — each task with a price now shows a green «Pay $X.XX» button (POST form → Stripe Checkout) or a «✓ Paid» badge once paid. A success flash banner appears on return.
Fichero resources/views/livewire/tasks/task-list.blade.php
<!-- Actions -->
<div class="flex justify-between items-center pt-2">
<div>
@if ($editingTaskId && $price && ! \App\Models\Task::find($editingTaskId)?->isPaid())
<a href="{{ route('tasks.checkout', $editingTaskId) }}"
onclick="event.preventDefault(); document.getElementById('modal-payment-form').submit();"
class="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition">
Pay ${{ number_format((float)$price, 2) }}
</a>
@endif
</div>
<div class="flex gap-3">
<button type="button" wire:click="closeModal"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition">
Cancel
</button>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition">
<span wire:loading.remove wire:target="save">
{{ $editingTaskId ? 'Update Task' : 'Create Task' }}
</span>
<span wire:loading wire:target="save">Saving…</span>
</button>
</div>
</div>
app/Services/StripeService.php — wraps the Stripe SDK;
createCheckoutSession(), builds a Checkout Session,
retrieveSession() fetches it for the success callback.
Fichero app/Services/StripeService.php
<?php
namespace App\Services;
use App\Models\Task;
use Stripe\Checkout\Session;
use Stripe\Stripe;
class StripeService
{
public function __construct()
{
Stripe::setApiKey(config('services.stripe.secret'));
}
public function createCheckoutSession(Task $task, string $successUrl, string $cancelUrl): object
{
return Session::create([
'payment_method_types' => ['card'],
'line_items' => [[
'price_data' => [
'currency' => config('services.stripe.currency', 'eur'),
'product_data' => ['name' => $task->title],
'unit_amount' => (int) round($task->price * 100),
],
'quantity' => 1,
]],
'mode' => 'payment',
'success_url' => $successUrl,
'cancel_url' => $cancelUrl,
]);
}
public function retrieveSession(string $sessionId): object
{
return Session::retrieve($sessionId);
}
}
app/Http/Controllers/PaymentController.php — two actions:
– checkout — authorizes the user, creates a Stripe session, redirects there
– success — verifies payment_status === ‘paid’, stamps paid_at + stripe_payment_id
Fichero app/Http/Controllers/PaymentController.php
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use App\Services\StripeService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PaymentController extends Controller
{
use AuthorizesRequests;
public function checkout(Task $task, StripeService $stripe): RedirectResponse
{
$this->authorize('update', $task);
abort_if($task->price === null, 400, 'This task has no price set.');
abort_if($task->isPaid(), 400, 'This task has already been paid.');
$session = $stripe->createCheckoutSession(
$task,
route('tasks.payment.success', $task).'?session_id={CHECKOUT_SESSION_ID}',
route('tasks'),
);
return redirect()->away($session->url);
}
public function success(Task $task, Request $request, StripeService $stripe): RedirectResponse
{
$this->authorize('update', $task);
$sessionId = $request->query('session_id');
if ($sessionId) {
$session = $stripe->retrieveSession($sessionId);
if ($session->payment_status === 'paid') {
$task->update([
'paid_at' => now(),
'stripe_payment_id' => $session->id,
]);
}
}
return redirect()->route('tasks')->with('payment_success', $task->title);
}
}
routes/web.php — two new auth-protected routes:
POST /tasks/{task}/checkout
GET /tasks/{task}/payment/success.
Fichero routes/web.php
<?php
use App\Http\Controllers\PaymentController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::middleware([
'auth:sanctum',
config('jetstream.auth_session'),
'verified',
])->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
Route::get('/tasks', fn () => view('tasks.index'))->name('tasks');
Route::post('/tasks/{task}/checkout', [PaymentController::class, 'checkout'])->name('tasks.checkout');
Route::get('/tasks/{task}/payment/success', [PaymentController::class, 'success'])->name('tasks.payment.success');
});
Si hay errores, corregirlos con Gemini:
gemini
Prompt
I get the error [ ], Can you fix it?
15. Prueba
laravelia.alumnoportada.com.es
16. Instalación del panel de administración orchid
Admin Panel para Laravel 11: Orchid
Instalación
composer require orchid/platform php artisan orchid:install
Crear usuario de administración
php artisan orchid:admin admin admin@alumnoportada.com.es malaga2526
Uso de Gemini
gemini
corregir un error:
Prompt
i can not access to the orchid panel with the email admin@alumnoportada.com.es and password malaga2526, can you fix it?
Prompt
add a resource for managing all the tasks in the orchid panel
Prompt
Add the route Admin in the Welcome view(near Login and Register) to the orchid panel
Repositorio en GitHub (ramas main y feature/pagos)







Deja una respuesta
Lo siento, debes estar conectado para publicar un comentario.