elsabro 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/commands/elsabro/add-phase.md +17 -0
  2. package/commands/elsabro/add-todo.md +111 -53
  3. package/commands/elsabro/audit-milestone.md +19 -0
  4. package/commands/elsabro/check-todos.md +210 -31
  5. package/commands/elsabro/complete-milestone.md +20 -1
  6. package/commands/elsabro/debug.md +19 -0
  7. package/commands/elsabro/discuss-phase.md +18 -1
  8. package/commands/elsabro/execute.md +496 -52
  9. package/commands/elsabro/insert-phase.md +18 -1
  10. package/commands/elsabro/list-phase-assumptions.md +17 -0
  11. package/commands/elsabro/new-milestone.md +19 -0
  12. package/commands/elsabro/new.md +19 -0
  13. package/commands/elsabro/pause-work.md +75 -0
  14. package/commands/elsabro/plan-milestone-gaps.md +20 -1
  15. package/commands/elsabro/plan.md +264 -36
  16. package/commands/elsabro/progress.md +203 -79
  17. package/commands/elsabro/quick.md +19 -0
  18. package/commands/elsabro/remove-phase.md +17 -0
  19. package/commands/elsabro/research-phase.md +18 -1
  20. package/commands/elsabro/resume-work.md +130 -2
  21. package/commands/elsabro/start.md +365 -98
  22. package/commands/elsabro/verify-work.md +271 -12
  23. package/package.json +1 -1
  24. package/references/SYSTEM_INDEX.md +241 -0
  25. package/references/command-flow.md +352 -0
  26. package/references/enforcement-rules.md +331 -0
  27. package/references/error-handling-instructions.md +26 -12
  28. package/references/state-sync.md +381 -0
  29. package/references/task-dispatcher.md +388 -0
  30. package/references/tasks-integration.md +380 -0
  31. package/skills/api-microservice.md +765 -0
  32. package/skills/api-setup.md +76 -3
  33. package/skills/auth-setup.md +46 -6
  34. package/skills/chrome-extension.md +584 -0
  35. package/skills/cicd-setup.md +1206 -0
  36. package/skills/cli-tool.md +884 -0
  37. package/skills/database-setup.md +41 -5
  38. package/skills/desktop-app.md +1351 -0
  39. package/skills/expo-app.md +35 -2
  40. package/skills/full-stack-app.md +543 -0
  41. package/skills/mobile-app.md +813 -0
  42. package/skills/nextjs-app.md +33 -2
  43. package/skills/payments-setup.md +76 -1
  44. package/skills/saas-starter.md +639 -0
  45. package/skills/sentry-setup.md +41 -7
  46. package/skills/testing-setup.md +1218 -0
@@ -0,0 +1,639 @@
1
+ ---
2
+ name: saas-starter
3
+ description: Template SaaS completo con pagos Stripe, subscripciones, dashboard de usuario y panel de admin. Basado en full-stack-app con features adicionales de monetizacion.
4
+ tags: [saas, stripe, subscriptions, payments, dashboard, admin, nextjs, prisma]
5
+ difficulty: advanced
6
+ estimated_time: 60min
7
+ extends: full-stack-app
8
+ ---
9
+
10
+ # Skill: SaaS Starter
11
+
12
+ <when_to_use>
13
+ Usar cuando el usuario menciona:
14
+ - "crear SaaS"
15
+ - "app con pagos"
16
+ - "subscripciones"
17
+ - "Stripe"
18
+ - "monetizar app"
19
+ - "dashboard con billing"
20
+ </when_to_use>
21
+
22
+ <pre_requisites>
23
+ ## Pre-requisitos
24
+
25
+ - Todo lo de full-stack-app
26
+ - Cuenta de Stripe (test mode)
27
+ - Stripe CLI instalado
28
+ </pre_requisites>
29
+
30
+ <additional_structure>
31
+ ## Estructura Adicional (sobre full-stack-app)
32
+
33
+ ```
34
+ my-saas/
35
+ ├── src/
36
+ │ ├── app/
37
+ │ │ ├── (dashboard)/
38
+ │ │ │ ├── billing/page.tsx # Gestion de subscripcion
39
+ │ │ │ └── settings/page.tsx # Configuracion de cuenta
40
+ │ │ ├── (admin)/
41
+ │ │ │ ├── layout.tsx # Layout admin
42
+ │ │ │ ├── page.tsx # Dashboard admin
43
+ │ │ │ └── users/page.tsx # Gestion de usuarios
44
+ │ │ ├── api/
45
+ │ │ │ ├── stripe/
46
+ │ │ │ │ └── webhook/route.ts # Webhooks de Stripe
47
+ │ │ │ └── admin/
48
+ │ │ │ └── users/route.ts
49
+ │ │ └── pricing/page.tsx # Pagina de precios
50
+ │ ├── lib/
51
+ │ │ └── stripe.ts # Cliente Stripe
52
+ │ └── actions/
53
+ │ └── billing.ts # Server Actions para billing
54
+ ├── prisma/
55
+ │ └── schema.prisma # + Subscription, Price models
56
+ └── stripe/
57
+ └── fixtures/ # Productos y precios de prueba
58
+ ```
59
+ </additional_structure>
60
+
61
+ <setup_steps>
62
+ ## Pasos de Setup
63
+
64
+ ### Paso 1: Completar full-stack-app primero
65
+
66
+ Ejecutar todos los pasos de `/elsabro:skill full-stack-app` primero.
67
+
68
+ ### Paso 2: Instalar dependencias de Stripe
69
+
70
+ ```bash
71
+ npm install stripe @stripe/stripe-js
72
+ ```
73
+
74
+ ### Paso 3: Configurar variables de Stripe
75
+
76
+ Agregar a `.env.local`:
77
+
78
+ ```env
79
+ STRIPE_SECRET_KEY=sk_test_...
80
+ STRIPE_PUBLISHABLE_KEY=pk_test_...
81
+ STRIPE_WEBHOOK_SECRET=whsec_...
82
+ STRIPE_PRICE_ID_BASIC=price_...
83
+ STRIPE_PRICE_ID_PRO=price_...
84
+ ```
85
+
86
+ ### Paso 4: Actualizar schema de Prisma
87
+
88
+ Agregar a `prisma/schema.prisma`:
89
+
90
+ ```prisma
91
+ model User {
92
+ id String @id @default(cuid())
93
+ name String?
94
+ email String @unique
95
+ emailVerified DateTime?
96
+ password String?
97
+ image String?
98
+ accounts Account[]
99
+ sessions Session[]
100
+
101
+ // Stripe fields
102
+ stripeCustomerId String? @unique
103
+ subscription Subscription?
104
+
105
+ // Admin
106
+ role Role @default(USER)
107
+
108
+ createdAt DateTime @default(now())
109
+ updatedAt DateTime @updatedAt
110
+ }
111
+
112
+ enum Role {
113
+ USER
114
+ ADMIN
115
+ }
116
+
117
+ model Subscription {
118
+ id String @id @default(cuid())
119
+ userId String @unique
120
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
121
+
122
+ stripeSubscriptionId String @unique
123
+ stripePriceId String
124
+ stripeCurrentPeriodEnd DateTime
125
+ status SubscriptionStatus
126
+
127
+ createdAt DateTime @default(now())
128
+ updatedAt DateTime @updatedAt
129
+ }
130
+
131
+ enum SubscriptionStatus {
132
+ ACTIVE
133
+ CANCELED
134
+ PAST_DUE
135
+ UNPAID
136
+ TRIALING
137
+ }
138
+ ```
139
+
140
+ ### Paso 5: Crear cliente de Stripe
141
+
142
+ Crear `src/lib/stripe.ts`:
143
+
144
+ ```typescript
145
+ import Stripe from "stripe";
146
+
147
+ export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
148
+ apiVersion: "2024-12-18.acacia",
149
+ typescript: true,
150
+ });
151
+
152
+ export const PLANS = {
153
+ basic: {
154
+ name: "Basic",
155
+ priceId: process.env.STRIPE_PRICE_ID_BASIC!,
156
+ price: 9,
157
+ features: ["5 proyectos", "1GB almacenamiento", "Soporte email"],
158
+ },
159
+ pro: {
160
+ name: "Pro",
161
+ priceId: process.env.STRIPE_PRICE_ID_PRO!,
162
+ price: 29,
163
+ features: ["Proyectos ilimitados", "10GB almacenamiento", "Soporte prioritario", "API access"],
164
+ },
165
+ };
166
+
167
+ export async function createCheckoutSession(
168
+ userId: string,
169
+ priceId: string,
170
+ email: string
171
+ ) {
172
+ const session = await stripe.checkout.sessions.create({
173
+ mode: "subscription",
174
+ payment_method_types: ["card"],
175
+ line_items: [{ price: priceId, quantity: 1 }],
176
+ success_url: `${process.env.NEXTAUTH_URL}/dashboard?success=true`,
177
+ cancel_url: `${process.env.NEXTAUTH_URL}/pricing?canceled=true`,
178
+ customer_email: email,
179
+ metadata: { userId },
180
+ });
181
+
182
+ return session;
183
+ }
184
+
185
+ export async function createBillingPortalSession(customerId: string) {
186
+ const session = await stripe.billingPortal.sessions.create({
187
+ customer: customerId,
188
+ return_url: `${process.env.NEXTAUTH_URL}/dashboard/billing`,
189
+ });
190
+
191
+ return session;
192
+ }
193
+ ```
194
+
195
+ ### Paso 6: Crear Server Actions para billing
196
+
197
+ Crear `src/actions/billing.ts`:
198
+
199
+ ```typescript
200
+ "use server";
201
+
202
+ import { auth } from "@/lib/auth";
203
+ import { db } from "@/lib/db";
204
+ import { createCheckoutSession, createBillingPortalSession } from "@/lib/stripe";
205
+ import { redirect } from "next/navigation";
206
+
207
+ export async function subscribe(priceId: string) {
208
+ const session = await auth();
209
+
210
+ if (!session?.user?.id) {
211
+ redirect("/login");
212
+ }
213
+
214
+ const user = await db.user.findUnique({
215
+ where: { id: session.user.id },
216
+ include: { subscription: true },
217
+ });
218
+
219
+ if (!user) {
220
+ throw new Error("Usuario no encontrado");
221
+ }
222
+
223
+ // Si ya tiene subscripcion activa, redirigir al portal
224
+ if (user.subscription?.status === "ACTIVE") {
225
+ const portalSession = await createBillingPortalSession(user.stripeCustomerId!);
226
+ redirect(portalSession.url);
227
+ }
228
+
229
+ // Crear checkout session
230
+ const checkoutSession = await createCheckoutSession(
231
+ user.id,
232
+ priceId,
233
+ user.email
234
+ );
235
+
236
+ redirect(checkoutSession.url!);
237
+ }
238
+
239
+ export async function manageBilling() {
240
+ const session = await auth();
241
+
242
+ if (!session?.user?.id) {
243
+ redirect("/login");
244
+ }
245
+
246
+ const user = await db.user.findUnique({
247
+ where: { id: session.user.id },
248
+ });
249
+
250
+ if (!user?.stripeCustomerId) {
251
+ redirect("/pricing");
252
+ }
253
+
254
+ const portalSession = await createBillingPortalSession(user.stripeCustomerId);
255
+ redirect(portalSession.url);
256
+ }
257
+ ```
258
+
259
+ ### Paso 7: Crear webhook de Stripe
260
+
261
+ Crear `src/app/api/stripe/webhook/route.ts`:
262
+
263
+ ```typescript
264
+ import { headers } from "next/headers";
265
+ import { NextResponse } from "next/server";
266
+ import { stripe } from "@/lib/stripe";
267
+ import { db } from "@/lib/db";
268
+ import Stripe from "stripe";
269
+
270
+ export async function POST(request: Request) {
271
+ const body = await request.text();
272
+ const signature = headers().get("stripe-signature")!;
273
+
274
+ let event: Stripe.Event;
275
+
276
+ try {
277
+ event = stripe.webhooks.constructEvent(
278
+ body,
279
+ signature,
280
+ process.env.STRIPE_WEBHOOK_SECRET!
281
+ );
282
+ } catch (error) {
283
+ console.error("Webhook signature verification failed");
284
+ return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
285
+ }
286
+
287
+ switch (event.type) {
288
+ case "checkout.session.completed": {
289
+ const session = event.data.object as Stripe.Checkout.Session;
290
+
291
+ await db.user.update({
292
+ where: { id: session.metadata!.userId },
293
+ data: { stripeCustomerId: session.customer as string },
294
+ });
295
+
296
+ const subscription = await stripe.subscriptions.retrieve(
297
+ session.subscription as string
298
+ );
299
+
300
+ await db.subscription.create({
301
+ data: {
302
+ userId: session.metadata!.userId,
303
+ stripeSubscriptionId: subscription.id,
304
+ stripePriceId: subscription.items.data[0].price.id,
305
+ stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
306
+ status: "ACTIVE",
307
+ },
308
+ });
309
+ break;
310
+ }
311
+
312
+ case "customer.subscription.updated": {
313
+ const subscription = event.data.object as Stripe.Subscription;
314
+
315
+ await db.subscription.update({
316
+ where: { stripeSubscriptionId: subscription.id },
317
+ data: {
318
+ stripePriceId: subscription.items.data[0].price.id,
319
+ stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
320
+ status: subscription.status.toUpperCase() as any,
321
+ },
322
+ });
323
+ break;
324
+ }
325
+
326
+ case "customer.subscription.deleted": {
327
+ const subscription = event.data.object as Stripe.Subscription;
328
+
329
+ await db.subscription.update({
330
+ where: { stripeSubscriptionId: subscription.id },
331
+ data: { status: "CANCELED" },
332
+ });
333
+ break;
334
+ }
335
+ }
336
+
337
+ return NextResponse.json({ received: true });
338
+ }
339
+ ```
340
+
341
+ ### Paso 8: Crear pagina de precios
342
+
343
+ Crear `src/app/pricing/page.tsx`:
344
+
345
+ ```typescript
346
+ import { auth } from "@/lib/auth";
347
+ import { PLANS } from "@/lib/stripe";
348
+ import { subscribe } from "@/actions/billing";
349
+ import { Button } from "@/components/ui/button";
350
+
351
+ export default async function PricingPage() {
352
+ const session = await auth();
353
+
354
+ return (
355
+ <div className="min-h-screen bg-gray-50 py-16">
356
+ <div className="max-w-5xl mx-auto px-4">
357
+ <div className="text-center mb-12">
358
+ <h1 className="text-4xl font-bold mb-4">Planes y Precios</h1>
359
+ <p className="text-gray-600 text-lg">
360
+ Elige el plan que mejor se adapte a tus necesidades
361
+ </p>
362
+ </div>
363
+
364
+ <div className="grid md:grid-cols-2 gap-8">
365
+ {Object.entries(PLANS).map(([key, plan]) => (
366
+ <div
367
+ key={key}
368
+ className="bg-white rounded-2xl shadow-lg p-8 border-2 border-transparent hover:border-blue-500 transition-colors"
369
+ >
370
+ <h2 className="text-2xl font-bold mb-2">{plan.name}</h2>
371
+ <div className="mb-6">
372
+ <span className="text-4xl font-bold">${plan.price}</span>
373
+ <span className="text-gray-600">/mes</span>
374
+ </div>
375
+
376
+ <ul className="space-y-3 mb-8">
377
+ {plan.features.map((feature) => (
378
+ <li key={feature} className="flex items-center gap-2">
379
+ <svg className="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
380
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd"/>
381
+ </svg>
382
+ {feature}
383
+ </li>
384
+ ))}
385
+ </ul>
386
+
387
+ <form action={subscribe.bind(null, plan.priceId)}>
388
+ <Button className="w-full" type="submit">
389
+ {session ? "Subscribirse" : "Empezar"}
390
+ </Button>
391
+ </form>
392
+ </div>
393
+ ))}
394
+ </div>
395
+ </div>
396
+ </div>
397
+ );
398
+ }
399
+ ```
400
+
401
+ ### Paso 9: Crear pagina de billing
402
+
403
+ Crear `src/app/(dashboard)/billing/page.tsx`:
404
+
405
+ ```typescript
406
+ import { auth } from "@/lib/auth";
407
+ import { db } from "@/lib/db";
408
+ import { redirect } from "next/navigation";
409
+ import { manageBilling } from "@/actions/billing";
410
+ import { Button } from "@/components/ui/button";
411
+ import { PLANS } from "@/lib/stripe";
412
+
413
+ export default async function BillingPage() {
414
+ const session = await auth();
415
+
416
+ if (!session?.user?.id) {
417
+ redirect("/login");
418
+ }
419
+
420
+ const user = await db.user.findUnique({
421
+ where: { id: session.user.id },
422
+ include: { subscription: true },
423
+ });
424
+
425
+ const currentPlan = user?.subscription
426
+ ? Object.values(PLANS).find(p => p.priceId === user.subscription?.stripePriceId)
427
+ : null;
428
+
429
+ return (
430
+ <div className="max-w-2xl mx-auto py-8 px-4">
431
+ <h1 className="text-2xl font-bold mb-8">Facturacion</h1>
432
+
433
+ <div className="bg-white rounded-xl shadow p-6">
434
+ {user?.subscription ? (
435
+ <>
436
+ <div className="mb-6">
437
+ <h2 className="text-lg font-semibold mb-2">Plan Actual</h2>
438
+ <p className="text-3xl font-bold text-blue-600">
439
+ {currentPlan?.name || "Plan"}
440
+ </p>
441
+ <p className="text-gray-600">
442
+ ${currentPlan?.price}/mes
443
+ </p>
444
+ </div>
445
+
446
+ <div className="mb-6">
447
+ <h3 className="font-medium mb-2">Estado</h3>
448
+ <span className={`px-3 py-1 rounded-full text-sm ${
449
+ user.subscription.status === "ACTIVE"
450
+ ? "bg-green-100 text-green-800"
451
+ : "bg-yellow-100 text-yellow-800"
452
+ }`}>
453
+ {user.subscription.status}
454
+ </span>
455
+ </div>
456
+
457
+ <div className="mb-6">
458
+ <h3 className="font-medium mb-2">Proximo cobro</h3>
459
+ <p className="text-gray-600">
460
+ {user.subscription.stripeCurrentPeriodEnd.toLocaleDateString()}
461
+ </p>
462
+ </div>
463
+
464
+ <form action={manageBilling}>
465
+ <Button type="submit" variant="outline">
466
+ Gestionar Subscripcion
467
+ </Button>
468
+ </form>
469
+ </>
470
+ ) : (
471
+ <div className="text-center py-8">
472
+ <p className="text-gray-600 mb-4">
473
+ No tienes una subscripcion activa
474
+ </p>
475
+ <a href="/pricing">
476
+ <Button>Ver Planes</Button>
477
+ </a>
478
+ </div>
479
+ )}
480
+ </div>
481
+ </div>
482
+ );
483
+ }
484
+ ```
485
+
486
+ ### Paso 10: Crear panel de admin
487
+
488
+ Crear `src/app/(admin)/layout.tsx`:
489
+
490
+ ```typescript
491
+ import { auth } from "@/lib/auth";
492
+ import { db } from "@/lib/db";
493
+ import { redirect } from "next/navigation";
494
+
495
+ export default async function AdminLayout({
496
+ children,
497
+ }: {
498
+ children: React.ReactNode;
499
+ }) {
500
+ const session = await auth();
501
+
502
+ if (!session?.user?.id) {
503
+ redirect("/login");
504
+ }
505
+
506
+ const user = await db.user.findUnique({
507
+ where: { id: session.user.id },
508
+ });
509
+
510
+ if (user?.role !== "ADMIN") {
511
+ redirect("/dashboard");
512
+ }
513
+
514
+ return (
515
+ <div className="min-h-screen bg-gray-100">
516
+ <nav className="bg-gray-900 text-white">
517
+ <div className="max-w-7xl mx-auto px-4 py-3 flex gap-6">
518
+ <a href="/admin" className="font-medium hover:text-gray-300">
519
+ Dashboard
520
+ </a>
521
+ <a href="/admin/users" className="font-medium hover:text-gray-300">
522
+ Usuarios
523
+ </a>
524
+ <a href="/dashboard" className="ml-auto text-gray-400 hover:text-white">
525
+ Volver a la app
526
+ </a>
527
+ </div>
528
+ </nav>
529
+ <main className="max-w-7xl mx-auto px-4 py-8">{children}</main>
530
+ </div>
531
+ );
532
+ }
533
+ ```
534
+
535
+ Crear `src/app/(admin)/page.tsx`:
536
+
537
+ ```typescript
538
+ import { db } from "@/lib/db";
539
+
540
+ export default async function AdminDashboard() {
541
+ const [totalUsers, activeSubscriptions, revenue] = await Promise.all([
542
+ db.user.count(),
543
+ db.subscription.count({ where: { status: "ACTIVE" } }),
544
+ db.subscription.findMany({
545
+ where: { status: "ACTIVE" },
546
+ select: { stripePriceId: true },
547
+ }),
548
+ ]);
549
+
550
+ return (
551
+ <div>
552
+ <h1 className="text-2xl font-bold mb-8">Admin Dashboard</h1>
553
+
554
+ <div className="grid md:grid-cols-3 gap-6">
555
+ <div className="bg-white rounded-xl shadow p-6">
556
+ <h3 className="text-gray-600 text-sm">Total Usuarios</h3>
557
+ <p className="text-3xl font-bold">{totalUsers}</p>
558
+ </div>
559
+
560
+ <div className="bg-white rounded-xl shadow p-6">
561
+ <h3 className="text-gray-600 text-sm">Subscripciones Activas</h3>
562
+ <p className="text-3xl font-bold">{activeSubscriptions}</p>
563
+ </div>
564
+
565
+ <div className="bg-white rounded-xl shadow p-6">
566
+ <h3 className="text-gray-600 text-sm">MRR Estimado</h3>
567
+ <p className="text-3xl font-bold">
568
+ ${activeSubscriptions * 19}
569
+ </p>
570
+ </div>
571
+ </div>
572
+ </div>
573
+ );
574
+ }
575
+ ```
576
+
577
+ ### Paso 11: Ejecutar migraciones
578
+
579
+ ```bash
580
+ npx prisma migrate dev --name add-stripe-fields
581
+ npx prisma generate
582
+ ```
583
+
584
+ ### Paso 12: Configurar Stripe webhook local
585
+
586
+ ```bash
587
+ stripe listen --forward-to localhost:3000/api/stripe/webhook
588
+ ```
589
+ </setup_steps>
590
+
591
+ <verification>
592
+ ## Verificacion
593
+
594
+ ### 1. Probar checkout flow
595
+ 1. Ir a /pricing
596
+ 2. Click en "Subscribirse"
597
+ 3. Usar tarjeta de test: 4242 4242 4242 4242
598
+ 4. Verificar redireccion a dashboard
599
+
600
+ ### 2. Verificar webhook
601
+ - Revisar logs de `stripe listen`
602
+ - Verificar que subscription se creo en DB
603
+
604
+ ### 3. Probar billing portal
605
+ - Ir a /dashboard/billing
606
+ - Click "Gestionar Subscripcion"
607
+ - Verificar acceso al portal de Stripe
608
+
609
+ ### 4. Probar admin
610
+ - Cambiar role a ADMIN en DB
611
+ - Ir a /admin
612
+ - Verificar metricas
613
+ </verification>
614
+
615
+ <stripe_test_cards>
616
+ ## Tarjetas de Test
617
+
618
+ | Escenario | Numero |
619
+ |-----------|--------|
620
+ | Exito | 4242 4242 4242 4242 |
621
+ | Requiere auth | 4000 0025 0000 3155 |
622
+ | Declinada | 4000 0000 0000 0002 |
623
+ | Fondos insuficientes | 4000 0000 0000 9995 |
624
+ </stripe_test_cards>
625
+
626
+ <common_issues>
627
+ ## Problemas Comunes
628
+
629
+ ### "Webhook signature verification failed"
630
+ - Verificar STRIPE_WEBHOOK_SECRET
631
+ - Usar el secret de `stripe listen`
632
+
633
+ ### "Customer not found"
634
+ - El checkout debe completarse primero
635
+ - Verificar que el webhook creo el customer
636
+
637
+ ### "Cannot access admin"
638
+ - Verificar que el user tiene role=ADMIN en DB
639
+ </common_issues>