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.
- package/README.md +128 -0
- package/bin/claude-framework +3 -0
- package/framework/agents/design-lead.md +240 -0
- package/framework/agents/product-owner.md +179 -0
- package/framework/agents/tech-lead.md +226 -0
- package/framework/commands/ayuda.md +127 -0
- package/framework/commands/a/303/261adir.md +98 -0
- package/framework/commands/backup.md +397 -0
- package/framework/commands/cambiar.md +110 -0
- package/framework/commands/cloud.md +457 -0
- package/framework/commands/code.md +142 -0
- package/framework/commands/debug.md +334 -0
- package/framework/commands/deploy.md +383 -0
- package/framework/commands/deshacer.md +120 -0
- package/framework/commands/estado.md +218 -0
- package/framework/commands/explica.md +227 -0
- package/framework/commands/feature.md +120 -0
- package/framework/commands/git.md +427 -0
- package/framework/commands/historial.md +202 -0
- package/framework/commands/learn.md +408 -0
- package/framework/commands/movil.md +245 -0
- package/framework/commands/nuevo.md +118 -0
- package/framework/commands/plan.md +134 -0
- package/framework/commands/prd.md +113 -0
- package/framework/commands/probar.md +148 -0
- package/framework/commands/revisar.md +208 -0
- package/framework/commands/seeds.md +230 -0
- package/framework/commands/seguridad.md +226 -0
- package/framework/commands/tasks.md +157 -0
- package/framework/skills/architecture/algorithms.md +970 -0
- package/framework/skills/architecture/clean-code.md +1080 -0
- package/framework/skills/architecture/design-patterns.md +1984 -0
- package/framework/skills/architecture/functional-programming.md +972 -0
- package/framework/skills/architecture/solid.md +991 -0
- package/framework/skills/cloud/cloud-aws.md +848 -0
- package/framework/skills/cloud/cloud-azure.md +931 -0
- package/framework/skills/cloud/cloud-gcp.md +848 -0
- package/framework/skills/cloud/message-queues.md +1229 -0
- package/framework/skills/core/accessibility.md +401 -0
- package/framework/skills/core/api.md +474 -0
- package/framework/skills/core/authentication.md +306 -0
- package/framework/skills/core/authorization.md +388 -0
- package/framework/skills/core/background-jobs.md +341 -0
- package/framework/skills/core/caching.md +473 -0
- package/framework/skills/core/code-review.md +341 -0
- package/framework/skills/core/controllers.md +290 -0
- package/framework/skills/core/cua.md +285 -0
- package/framework/skills/core/documentation.md +472 -0
- package/framework/skills/core/file-uploads.md +351 -0
- package/framework/skills/core/hotwire-native.md +296 -0
- package/framework/skills/core/hotwire.md +278 -0
- package/framework/skills/core/i18n.md +334 -0
- package/framework/skills/core/imports-exports.md +750 -0
- package/framework/skills/core/infrastructure.md +337 -0
- package/framework/skills/core/models.md +228 -0
- package/framework/skills/core/notifications.md +672 -0
- package/framework/skills/core/payments.md +581 -0
- package/framework/skills/core/performance.md +361 -0
- package/framework/skills/core/rails-scaffold.md +131 -0
- package/framework/skills/core/search.md +518 -0
- package/framework/skills/core/security.md +565 -0
- package/framework/skills/core/seeds.md +307 -0
- package/framework/skills/core/seo.md +542 -0
- package/framework/skills/core/testing.md +393 -0
- package/framework/skills/core/views.md +260 -0
- package/framework/skills/core/websockets.md +564 -0
- package/framework/skills/data/advanced-sql.md +1204 -0
- package/framework/skills/data/nosql.md +1141 -0
- package/framework/skills/devops/containers-advanced.md +1237 -0
- package/framework/skills/devops/debugging.md +834 -0
- package/framework/skills/devops/git-workflow.md +752 -0
- package/framework/skills/devops/networking.md +932 -0
- package/framework/skills/devops/shell-scripting.md +1132 -0
- package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
- package/framework/sub-agents/cloud-agent.md +677 -0
- package/framework/sub-agents/data.md +504 -0
- package/framework/sub-agents/debugging-agent.md +554 -0
- package/framework/sub-agents/devops.md +483 -0
- package/framework/sub-agents/docs.md +176 -0
- package/framework/sub-agents/frontend-dev.md +349 -0
- package/framework/sub-agents/git-workflow-agent.md +697 -0
- package/framework/sub-agents/integrations.md +630 -0
- package/framework/sub-agents/native-dev.md +434 -0
- package/framework/sub-agents/qa.md +138 -0
- package/framework/sub-agents/rails-dev.md +375 -0
- package/framework/sub-agents/security.md +526 -0
- package/framework/sub-agents/ui.md +437 -0
- package/framework/sub-agents/ux.md +284 -0
- package/framework/templates/api-spec.md +500 -0
- package/framework/templates/component-spec.md +248 -0
- package/framework/templates/feature.json +13 -0
- package/framework/templates/model-spec.md +318 -0
- package/framework/templates/prd-template.md +80 -0
- package/framework/templates/task-plan.md +122 -0
- package/framework/templates/task-user-story.md +52 -0
- package/framework/templates/technical-spec.md +260 -0
- package/framework/templates/user-story.md +95 -0
- package/package.json +42 -0
- package/project-templates/CLAUDE.md +42 -0
- package/project-templates/contexts/architecture.md +25 -0
- package/project-templates/contexts/conventions.md +46 -0
- package/project-templates/contexts/design-system.md +47 -0
- package/project-templates/contexts/requirements.md +38 -0
- package/project-templates/contexts/stack.md +30 -0
- package/project-templates/history/active/models.md +11 -0
- package/project-templates/history/changelog.md +15 -0
- package/project-templates/workspace/.gitkeep +0 -0
- package/src/cli.js +52 -0
- package/src/init.js +104 -0
- package/src/status.js +75 -0
- 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
|