claude-agent-framework 1.0.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 (111) hide show
  1. package/README.md +128 -0
  2. package/bin/claude-framework +3 -0
  3. package/framework/agents/design-lead.md +240 -0
  4. package/framework/agents/product-owner.md +179 -0
  5. package/framework/agents/tech-lead.md +226 -0
  6. package/framework/commands/ayuda.md +127 -0
  7. package/framework/commands/a/303/261adir.md +98 -0
  8. package/framework/commands/backup.md +397 -0
  9. package/framework/commands/cambiar.md +110 -0
  10. package/framework/commands/cloud.md +457 -0
  11. package/framework/commands/code.md +142 -0
  12. package/framework/commands/debug.md +334 -0
  13. package/framework/commands/deploy.md +383 -0
  14. package/framework/commands/deshacer.md +120 -0
  15. package/framework/commands/estado.md +218 -0
  16. package/framework/commands/explica.md +227 -0
  17. package/framework/commands/feature.md +120 -0
  18. package/framework/commands/git.md +427 -0
  19. package/framework/commands/historial.md +202 -0
  20. package/framework/commands/learn.md +408 -0
  21. package/framework/commands/movil.md +245 -0
  22. package/framework/commands/nuevo.md +118 -0
  23. package/framework/commands/plan.md +134 -0
  24. package/framework/commands/prd.md +113 -0
  25. package/framework/commands/probar.md +148 -0
  26. package/framework/commands/revisar.md +208 -0
  27. package/framework/commands/seeds.md +230 -0
  28. package/framework/commands/seguridad.md +226 -0
  29. package/framework/commands/tasks.md +157 -0
  30. package/framework/skills/architecture/algorithms.md +970 -0
  31. package/framework/skills/architecture/clean-code.md +1080 -0
  32. package/framework/skills/architecture/design-patterns.md +1984 -0
  33. package/framework/skills/architecture/functional-programming.md +972 -0
  34. package/framework/skills/architecture/solid.md +991 -0
  35. package/framework/skills/cloud/cloud-aws.md +848 -0
  36. package/framework/skills/cloud/cloud-azure.md +931 -0
  37. package/framework/skills/cloud/cloud-gcp.md +848 -0
  38. package/framework/skills/cloud/message-queues.md +1229 -0
  39. package/framework/skills/core/accessibility.md +401 -0
  40. package/framework/skills/core/api.md +474 -0
  41. package/framework/skills/core/authentication.md +306 -0
  42. package/framework/skills/core/authorization.md +388 -0
  43. package/framework/skills/core/background-jobs.md +341 -0
  44. package/framework/skills/core/caching.md +473 -0
  45. package/framework/skills/core/code-review.md +341 -0
  46. package/framework/skills/core/controllers.md +290 -0
  47. package/framework/skills/core/cua.md +285 -0
  48. package/framework/skills/core/documentation.md +472 -0
  49. package/framework/skills/core/file-uploads.md +351 -0
  50. package/framework/skills/core/hotwire-native.md +296 -0
  51. package/framework/skills/core/hotwire.md +278 -0
  52. package/framework/skills/core/i18n.md +334 -0
  53. package/framework/skills/core/imports-exports.md +750 -0
  54. package/framework/skills/core/infrastructure.md +337 -0
  55. package/framework/skills/core/models.md +228 -0
  56. package/framework/skills/core/notifications.md +672 -0
  57. package/framework/skills/core/payments.md +581 -0
  58. package/framework/skills/core/performance.md +361 -0
  59. package/framework/skills/core/rails-scaffold.md +131 -0
  60. package/framework/skills/core/search.md +518 -0
  61. package/framework/skills/core/security.md +565 -0
  62. package/framework/skills/core/seeds.md +307 -0
  63. package/framework/skills/core/seo.md +542 -0
  64. package/framework/skills/core/testing.md +393 -0
  65. package/framework/skills/core/views.md +260 -0
  66. package/framework/skills/core/websockets.md +564 -0
  67. package/framework/skills/data/advanced-sql.md +1204 -0
  68. package/framework/skills/data/nosql.md +1141 -0
  69. package/framework/skills/devops/containers-advanced.md +1237 -0
  70. package/framework/skills/devops/debugging.md +834 -0
  71. package/framework/skills/devops/git-workflow.md +752 -0
  72. package/framework/skills/devops/networking.md +932 -0
  73. package/framework/skills/devops/shell-scripting.md +1132 -0
  74. package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
  75. package/framework/sub-agents/cloud-agent.md +677 -0
  76. package/framework/sub-agents/data.md +504 -0
  77. package/framework/sub-agents/debugging-agent.md +554 -0
  78. package/framework/sub-agents/devops.md +483 -0
  79. package/framework/sub-agents/docs.md +176 -0
  80. package/framework/sub-agents/frontend-dev.md +349 -0
  81. package/framework/sub-agents/git-workflow-agent.md +697 -0
  82. package/framework/sub-agents/integrations.md +630 -0
  83. package/framework/sub-agents/native-dev.md +434 -0
  84. package/framework/sub-agents/qa.md +138 -0
  85. package/framework/sub-agents/rails-dev.md +375 -0
  86. package/framework/sub-agents/security.md +526 -0
  87. package/framework/sub-agents/ui.md +437 -0
  88. package/framework/sub-agents/ux.md +284 -0
  89. package/framework/templates/api-spec.md +500 -0
  90. package/framework/templates/component-spec.md +248 -0
  91. package/framework/templates/feature.json +13 -0
  92. package/framework/templates/model-spec.md +318 -0
  93. package/framework/templates/prd-template.md +80 -0
  94. package/framework/templates/task-plan.md +122 -0
  95. package/framework/templates/task-user-story.md +52 -0
  96. package/framework/templates/technical-spec.md +260 -0
  97. package/framework/templates/user-story.md +95 -0
  98. package/package.json +42 -0
  99. package/project-templates/CLAUDE.md +42 -0
  100. package/project-templates/contexts/architecture.md +25 -0
  101. package/project-templates/contexts/conventions.md +46 -0
  102. package/project-templates/contexts/design-system.md +47 -0
  103. package/project-templates/contexts/requirements.md +38 -0
  104. package/project-templates/contexts/stack.md +30 -0
  105. package/project-templates/history/active/models.md +11 -0
  106. package/project-templates/history/changelog.md +15 -0
  107. package/project-templates/workspace/.gitkeep +0 -0
  108. package/src/cli.js +52 -0
  109. package/src/init.js +104 -0
  110. package/src/status.js +75 -0
  111. package/src/update.js +88 -0
@@ -0,0 +1,581 @@
1
+ # Skill: Payments (Stripe)
2
+
3
+ ## Purpose
4
+
5
+ Integrar pagos con Stripe en aplicaciones Rails, incluyendo pagos únicos, suscripciones, y manejo de webhooks.
6
+
7
+ ## Setup
8
+
9
+ ### Instalación
10
+
11
+ ```ruby
12
+ # Gemfile
13
+ gem "stripe"
14
+ gem "pay", "~> 7.0" # Opcional: abstracción sobre Stripe
15
+ ```
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
20
+
21
+ ### Configuración
22
+
23
+ ```bash
24
+ # Agregar a credentials
25
+ rails credentials:edit
26
+ ```
27
+
28
+ ```yaml
29
+ # config/credentials.yml.enc
30
+ stripe:
31
+ publishable_key: pk_test_xxx
32
+ secret_key: sk_test_xxx
33
+ webhook_secret: whsec_xxx
34
+ ```
35
+
36
+ ```ruby
37
+ # config/initializers/stripe.rb
38
+ Stripe.api_key = Rails.application.credentials.dig(:stripe, :secret_key)
39
+ ```
40
+
41
+ ## Pagos únicos (Checkout Session)
42
+
43
+ ### Controller
44
+
45
+ ```ruby
46
+ # app/controllers/checkouts_controller.rb
47
+ class CheckoutsController < ApplicationController
48
+ before_action :authenticate_user!
49
+
50
+ def create
51
+ session = Stripe::Checkout::Session.create(
52
+ customer_email: current_user.email,
53
+ payment_method_types: ["card"],
54
+ line_items: [{
55
+ price_data: {
56
+ currency: "eur",
57
+ product_data: {
58
+ name: params[:product_name],
59
+ description: params[:description]
60
+ },
61
+ unit_amount: params[:amount].to_i * 100 # En centavos
62
+ },
63
+ quantity: 1
64
+ }],
65
+ mode: "payment",
66
+ success_url: success_checkout_url + "?session_id={CHECKOUT_SESSION_ID}",
67
+ cancel_url: cancel_checkout_url,
68
+ metadata: {
69
+ user_id: current_user.id,
70
+ product_id: params[:product_id]
71
+ }
72
+ )
73
+
74
+ redirect_to session.url, allow_other_host: true
75
+ end
76
+
77
+ def success
78
+ @session = Stripe::Checkout::Session.retrieve(params[:session_id])
79
+ end
80
+
81
+ def cancel
82
+ flash[:alert] = t(".cancelled")
83
+ redirect_to root_path
84
+ end
85
+ end
86
+ ```
87
+
88
+ ### Routes
89
+
90
+ ```ruby
91
+ # config/routes.rb
92
+ resources :checkouts, only: [:create] do
93
+ collection do
94
+ get :success
95
+ get :cancel
96
+ end
97
+ end
98
+ ```
99
+
100
+ ### Vista de checkout
101
+
102
+ ```erb
103
+ <%# app/views/products/show.html.erb %>
104
+ <%= button_to "Comprar por #{number_to_currency(@product.price)}",
105
+ checkouts_path(
106
+ product_name: @product.name,
107
+ amount: @product.price,
108
+ product_id: @product.id
109
+ ),
110
+ class: "btn btn-primary",
111
+ data: { turbo: false } %>
112
+ ```
113
+
114
+ ## Suscripciones
115
+
116
+ ### Modelos
117
+
118
+ ```ruby
119
+ # db/migrate/xxx_add_stripe_fields_to_users.rb
120
+ class AddStripeFieldsToUsers < ActiveRecord::Migration[8.0]
121
+ def change
122
+ add_column :users, :stripe_customer_id, :string
123
+ add_column :users, :stripe_subscription_id, :string
124
+ add_column :users, :subscription_status, :string, default: "inactive"
125
+ add_column :users, :subscription_ends_at, :datetime
126
+
127
+ add_index :users, :stripe_customer_id, unique: true
128
+ end
129
+ end
130
+
131
+ # app/models/user.rb
132
+ class User < ApplicationRecord
133
+ def create_or_get_stripe_customer
134
+ return stripe_customer_id if stripe_customer_id.present?
135
+
136
+ customer = Stripe::Customer.create(
137
+ email: email,
138
+ name: name,
139
+ metadata: { user_id: id }
140
+ )
141
+
142
+ update!(stripe_customer_id: customer.id)
143
+ customer.id
144
+ end
145
+
146
+ def active_subscription?
147
+ subscription_status == "active" &&
148
+ (subscription_ends_at.nil? || subscription_ends_at > Time.current)
149
+ end
150
+
151
+ def subscribed?
152
+ %w[active trialing].include?(subscription_status)
153
+ end
154
+ end
155
+ ```
156
+
157
+ ### Controller de suscripciones
158
+
159
+ ```ruby
160
+ # app/controllers/subscriptions_controller.rb
161
+ class SubscriptionsController < ApplicationController
162
+ before_action :authenticate_user!
163
+
164
+ PLANS = {
165
+ "basic" => "price_xxx",
166
+ "pro" => "price_yyy",
167
+ "enterprise" => "price_zzz"
168
+ }.freeze
169
+
170
+ def new
171
+ @plans = PLANS
172
+ end
173
+
174
+ def create
175
+ price_id = PLANS[params[:plan]]
176
+ return redirect_to new_subscription_path, alert: t(".invalid_plan") unless price_id
177
+
178
+ session = Stripe::Checkout::Session.create(
179
+ customer: current_user.create_or_get_stripe_customer,
180
+ payment_method_types: ["card"],
181
+ line_items: [{
182
+ price: price_id,
183
+ quantity: 1
184
+ }],
185
+ mode: "subscription",
186
+ success_url: subscription_success_url + "?session_id={CHECKOUT_SESSION_ID}",
187
+ cancel_url: subscription_cancel_url,
188
+ metadata: {
189
+ user_id: current_user.id
190
+ }
191
+ )
192
+
193
+ redirect_to session.url, allow_other_host: true
194
+ end
195
+
196
+ def success
197
+ @session = Stripe::Checkout::Session.retrieve(
198
+ params[:session_id],
199
+ expand: ["subscription"]
200
+ )
201
+
202
+ flash[:notice] = t(".success")
203
+ end
204
+
205
+ def cancel
206
+ flash[:alert] = t(".cancelled")
207
+ redirect_to pricing_path
208
+ end
209
+
210
+ def portal
211
+ # Portal de cliente de Stripe para gestionar suscripción
212
+ session = Stripe::BillingPortal::Session.create(
213
+ customer: current_user.stripe_customer_id,
214
+ return_url: dashboard_url
215
+ )
216
+
217
+ redirect_to session.url, allow_other_host: true
218
+ end
219
+ end
220
+ ```
221
+
222
+ ## Webhooks
223
+
224
+ ### Controller de webhooks
225
+
226
+ ```ruby
227
+ # app/controllers/webhooks/stripe_controller.rb
228
+ module Webhooks
229
+ class StripeController < ApplicationController
230
+ skip_before_action :verify_authenticity_token
231
+ skip_before_action :authenticate_user!
232
+
233
+ def create
234
+ payload = request.body.read
235
+ sig_header = request.env["HTTP_STRIPE_SIGNATURE"]
236
+ webhook_secret = Rails.application.credentials.dig(:stripe, :webhook_secret)
237
+
238
+ begin
239
+ event = Stripe::Webhook.construct_event(
240
+ payload, sig_header, webhook_secret
241
+ )
242
+ rescue JSON::ParserError
243
+ render json: { error: "Invalid payload" }, status: :bad_request
244
+ return
245
+ rescue Stripe::SignatureVerificationError
246
+ render json: { error: "Invalid signature" }, status: :bad_request
247
+ return
248
+ end
249
+
250
+ handle_event(event)
251
+
252
+ render json: { received: true }
253
+ end
254
+
255
+ private
256
+
257
+ def handle_event(event)
258
+ case event.type
259
+ when "checkout.session.completed"
260
+ handle_checkout_completed(event.data.object)
261
+
262
+ when "customer.subscription.created"
263
+ handle_subscription_created(event.data.object)
264
+
265
+ when "customer.subscription.updated"
266
+ handle_subscription_updated(event.data.object)
267
+
268
+ when "customer.subscription.deleted"
269
+ handle_subscription_deleted(event.data.object)
270
+
271
+ when "invoice.paid"
272
+ handle_invoice_paid(event.data.object)
273
+
274
+ when "invoice.payment_failed"
275
+ handle_payment_failed(event.data.object)
276
+
277
+ else
278
+ Rails.logger.info "Unhandled Stripe event: #{event.type}"
279
+ end
280
+ end
281
+
282
+ def handle_checkout_completed(session)
283
+ return unless session.mode == "payment"
284
+
285
+ user = User.find(session.metadata.user_id)
286
+ product_id = session.metadata.product_id
287
+
288
+ # Crear orden/compra
289
+ Order.create!(
290
+ user: user,
291
+ product_id: product_id,
292
+ stripe_session_id: session.id,
293
+ amount: session.amount_total / 100.0,
294
+ status: "completed"
295
+ )
296
+
297
+ # Enviar email de confirmación
298
+ OrderMailer.confirmation(user, product_id).deliver_later
299
+ end
300
+
301
+ def handle_subscription_created(subscription)
302
+ user = User.find_by(stripe_customer_id: subscription.customer)
303
+ return unless user
304
+
305
+ user.update!(
306
+ stripe_subscription_id: subscription.id,
307
+ subscription_status: subscription.status,
308
+ subscription_ends_at: Time.at(subscription.current_period_end)
309
+ )
310
+ end
311
+
312
+ def handle_subscription_updated(subscription)
313
+ user = User.find_by(stripe_customer_id: subscription.customer)
314
+ return unless user
315
+
316
+ user.update!(
317
+ subscription_status: subscription.status,
318
+ subscription_ends_at: subscription.cancel_at_period_end ?
319
+ Time.at(subscription.current_period_end) : nil
320
+ )
321
+ end
322
+
323
+ def handle_subscription_deleted(subscription)
324
+ user = User.find_by(stripe_customer_id: subscription.customer)
325
+ return unless user
326
+
327
+ user.update!(
328
+ stripe_subscription_id: nil,
329
+ subscription_status: "cancelled",
330
+ subscription_ends_at: Time.current
331
+ )
332
+
333
+ SubscriptionMailer.cancelled(user).deliver_later
334
+ end
335
+
336
+ def handle_invoice_paid(invoice)
337
+ user = User.find_by(stripe_customer_id: invoice.customer)
338
+ return unless user
339
+
340
+ # Registrar pago
341
+ Payment.create!(
342
+ user: user,
343
+ stripe_invoice_id: invoice.id,
344
+ amount: invoice.amount_paid / 100.0,
345
+ status: "paid"
346
+ )
347
+ end
348
+
349
+ def handle_payment_failed(invoice)
350
+ user = User.find_by(stripe_customer_id: invoice.customer)
351
+ return unless user
352
+
353
+ # Notificar al usuario
354
+ PaymentMailer.failed(user, invoice.id).deliver_later
355
+ end
356
+ end
357
+ end
358
+ ```
359
+
360
+ ### Routes para webhooks
361
+
362
+ ```ruby
363
+ # config/routes.rb
364
+ namespace :webhooks do
365
+ post "stripe", to: "stripe#create"
366
+ end
367
+ ```
368
+
369
+ ## Stripe Elements (formulario embebido)
370
+
371
+ ### JavaScript
372
+
373
+ ```javascript
374
+ // app/javascript/controllers/stripe_controller.js
375
+ import { Controller } from "@hotwired/stimulus"
376
+
377
+ export default class extends Controller {
378
+ static targets = ["card", "errors", "submit"]
379
+ static values = {
380
+ publishableKey: String,
381
+ clientSecret: String
382
+ }
383
+
384
+ connect() {
385
+ this.stripe = Stripe(this.publishableKeyValue)
386
+ this.elements = this.stripe.elements()
387
+
388
+ this.card = this.elements.create("card", {
389
+ style: {
390
+ base: {
391
+ fontSize: "16px",
392
+ color: "#32325d",
393
+ fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif"
394
+ }
395
+ }
396
+ })
397
+
398
+ this.card.mount(this.cardTarget)
399
+
400
+ this.card.on("change", (event) => {
401
+ this.errorsTarget.textContent = event.error ? event.error.message : ""
402
+ })
403
+ }
404
+
405
+ async submit(event) {
406
+ event.preventDefault()
407
+ this.submitTarget.disabled = true
408
+
409
+ const { error, paymentIntent } = await this.stripe.confirmCardPayment(
410
+ this.clientSecretValue,
411
+ {
412
+ payment_method: {
413
+ card: this.card
414
+ }
415
+ }
416
+ )
417
+
418
+ if (error) {
419
+ this.errorsTarget.textContent = error.message
420
+ this.submitTarget.disabled = false
421
+ } else {
422
+ // Redirigir a success
423
+ window.location.href = `/payments/success?payment_intent=${paymentIntent.id}`
424
+ }
425
+ }
426
+
427
+ disconnect() {
428
+ this.card.destroy()
429
+ }
430
+ }
431
+ ```
432
+
433
+ ### Vista con Stripe Elements
434
+
435
+ ```erb
436
+ <%# app/views/payments/new.html.erb %>
437
+ <div data-controller="stripe"
438
+ data-stripe-publishable-key-value="<%= Rails.application.credentials.dig(:stripe, :publishable_key) %>"
439
+ data-stripe-client-secret-value="<%= @client_secret %>">
440
+
441
+ <%= form_with url: payments_path, data: { action: "submit->stripe#submit" } do |f| %>
442
+ <div class="mb-4">
443
+ <label class="block text-sm font-medium text-gray-700 mb-2">
444
+ <%= t(".card_details") %>
445
+ </label>
446
+ <div data-stripe-target="card"
447
+ class="p-3 border border-gray-300 rounded-lg"></div>
448
+ <p data-stripe-target="errors"
449
+ class="mt-1 text-sm text-red-600"></p>
450
+ </div>
451
+
452
+ <%= f.submit t(".pay"),
453
+ data: { stripe_target: "submit" },
454
+ class: "w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700" %>
455
+ <% end %>
456
+ </div>
457
+
458
+ <%# Incluir Stripe.js %>
459
+ <script src="https://js.stripe.com/v3/"></script>
460
+ ```
461
+
462
+ ## Testing
463
+
464
+ ### Tarjetas de prueba
465
+
466
+ | Número | Resultado |
467
+ |--------|-----------|
468
+ | 4242 4242 4242 4242 | Éxito |
469
+ | 4000 0000 0000 0002 | Rechazada |
470
+ | 4000 0000 0000 3220 | Requiere 3D Secure |
471
+ | 4000 0027 6000 3184 | Requiere autenticación |
472
+
473
+ ### Specs
474
+
475
+ ```ruby
476
+ # spec/requests/webhooks/stripe_spec.rb
477
+ RSpec.describe "Webhooks::Stripe", type: :request do
478
+ let(:webhook_secret) { "whsec_test" }
479
+ let(:user) { create(:user, stripe_customer_id: "cus_xxx") }
480
+
481
+ before do
482
+ allow(Rails.application.credentials).to receive(:dig)
483
+ .with(:stripe, :webhook_secret)
484
+ .and_return(webhook_secret)
485
+ end
486
+
487
+ def generate_signature(payload)
488
+ timestamp = Time.current.to_i
489
+ signed_payload = "#{timestamp}.#{payload}"
490
+ signature = OpenSSL::HMAC.hexdigest(
491
+ "SHA256",
492
+ webhook_secret,
493
+ signed_payload
494
+ )
495
+ "t=#{timestamp},v1=#{signature}"
496
+ end
497
+
498
+ describe "subscription events" do
499
+ it "handles subscription created" do
500
+ payload = {
501
+ type: "customer.subscription.created",
502
+ data: {
503
+ object: {
504
+ id: "sub_xxx",
505
+ customer: user.stripe_customer_id,
506
+ status: "active",
507
+ current_period_end: 1.month.from_now.to_i
508
+ }
509
+ }
510
+ }.to_json
511
+
512
+ post webhooks_stripe_path,
513
+ params: payload,
514
+ headers: {
515
+ "Content-Type" => "application/json",
516
+ "Stripe-Signature" => generate_signature(payload)
517
+ }
518
+
519
+ expect(response).to have_http_status(:ok)
520
+ expect(user.reload.subscription_status).to eq("active")
521
+ end
522
+ end
523
+ end
524
+ ```
525
+
526
+ ## Manejo de errores
527
+
528
+ ```ruby
529
+ # app/services/stripe_service.rb
530
+ class StripeService
531
+ class << self
532
+ def create_checkout_session(params)
533
+ Stripe::Checkout::Session.create(params)
534
+ rescue Stripe::CardError => e
535
+ handle_card_error(e)
536
+ rescue Stripe::RateLimitError
537
+ Rails.logger.error "Stripe rate limit exceeded"
538
+ raise
539
+ rescue Stripe::InvalidRequestError => e
540
+ Rails.logger.error "Invalid Stripe request: #{e.message}"
541
+ raise
542
+ rescue Stripe::AuthenticationError
543
+ Rails.logger.error "Stripe authentication failed"
544
+ raise
545
+ rescue Stripe::APIConnectionError
546
+ Rails.logger.error "Stripe API connection error"
547
+ raise
548
+ rescue Stripe::StripeError => e
549
+ Rails.logger.error "Stripe error: #{e.message}"
550
+ raise
551
+ end
552
+
553
+ private
554
+
555
+ def handle_card_error(error)
556
+ case error.code
557
+ when "card_declined"
558
+ { error: I18n.t("stripe.errors.card_declined") }
559
+ when "expired_card"
560
+ { error: I18n.t("stripe.errors.expired_card") }
561
+ when "incorrect_cvc"
562
+ { error: I18n.t("stripe.errors.incorrect_cvc") }
563
+ else
564
+ { error: error.message }
565
+ end
566
+ end
567
+ end
568
+ end
569
+ ```
570
+
571
+ ## Checklist
572
+
573
+ - [ ] Stripe gems instaladas
574
+ - [ ] Credentials configuradas (publishable, secret, webhook_secret)
575
+ - [ ] Webhook endpoint configurado en Stripe Dashboard
576
+ - [ ] Eventos de webhook manejados
577
+ - [ ] Tarjetas de prueba funcionando
578
+ - [ ] Customer portal configurado (para suscripciones)
579
+ - [ ] Emails transaccionales configurados
580
+ - [ ] Manejo de errores implementado
581
+ - [ ] Tests escritos