paddle_rails 0.1.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +294 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/paddle_rails/application.css +16 -0
- data/app/assets/stylesheets/paddle_rails/tailwind.css +1824 -0
- data/app/assets/tailwind/application.css +1 -0
- data/app/controllers/concerns/paddle_rails/paddle_checkout_error_handler.rb +89 -0
- data/app/controllers/concerns/paddle_rails/subscription_owner.rb +16 -0
- data/app/controllers/paddle_rails/application_controller.rb +21 -0
- data/app/controllers/paddle_rails/checkout_controller.rb +121 -0
- data/app/controllers/paddle_rails/dashboard_controller.rb +37 -0
- data/app/controllers/paddle_rails/onboarding_controller.rb +55 -0
- data/app/controllers/paddle_rails/payments_controller.rb +62 -0
- data/app/controllers/paddle_rails/subscriptions_controller.rb +92 -0
- data/app/controllers/paddle_rails/webhooks_controller.rb +78 -0
- data/app/helpers/paddle_rails/application_helper.rb +121 -0
- data/app/helpers/paddle_rails/subscription_owner_helper.rb +14 -0
- data/app/jobs/paddle_rails/application_job.rb +4 -0
- data/app/jobs/paddle_rails/process_webhook_job.rb +38 -0
- data/app/mailers/paddle_rails/application_mailer.rb +6 -0
- data/app/models/concerns/paddle_rails/subscribable.rb +46 -0
- data/app/models/paddle_rails/application_record.rb +5 -0
- data/app/models/paddle_rails/payment.rb +43 -0
- data/app/models/paddle_rails/price.rb +25 -0
- data/app/models/paddle_rails/product.rb +16 -0
- data/app/models/paddle_rails/subscription.rb +87 -0
- data/app/models/paddle_rails/subscription_item.rb +35 -0
- data/app/models/paddle_rails/webhook_event.rb +51 -0
- data/app/presenters/paddle_rails/payment_presenter.rb +96 -0
- data/app/presenters/paddle_rails/product_presenter.rb +178 -0
- data/app/presenters/paddle_rails/subscription_presenter.rb +145 -0
- data/app/views/layouts/paddle_rails/application.html.erb +170 -0
- data/app/views/paddle_rails/checkout/show.html.erb +128 -0
- data/app/views/paddle_rails/dashboard/_change_plan.html.erb +286 -0
- data/app/views/paddle_rails/dashboard/_current_subscription.html.erb +66 -0
- data/app/views/paddle_rails/dashboard/_payment_history.html.erb +79 -0
- data/app/views/paddle_rails/dashboard/_payment_method.html.erb +48 -0
- data/app/views/paddle_rails/dashboard/show.html.erb +47 -0
- data/app/views/paddle_rails/onboarding/show.html.erb +100 -0
- data/app/views/paddle_rails/shared/configuration_error.html.erb +94 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20251124180624_create_paddle_rails_subscription_plans.rb +18 -0
- data/db/migrate/20251124180817_create_paddle_rails_subscription_prices.rb +26 -0
- data/db/migrate/20251127221947_create_paddle_rails_webhook_events.rb +19 -0
- data/db/migrate/20251128135831_create_paddle_rails_subscriptions.rb +21 -0
- data/db/migrate/20251128142327_create_paddle_rails_subscription_items.rb +16 -0
- data/db/migrate/20251128151334_remove_paddle_price_id_from_subscriptions.rb +7 -0
- data/db/migrate/20251128151401_rename_subscription_plans_to_products.rb +6 -0
- data/db/migrate/20251128151402_rename_subscription_plan_id_to_subscription_product_id.rb +13 -0
- data/db/migrate/20251128151453_remove_subscription_price_id_from_subscriptions.rb +8 -0
- data/db/migrate/20251128151501_add_subscription_product_id_to_subscription_items.rb +8 -0
- data/db/migrate/20251128152025_remove_paddle_item_id_from_subscription_items.rb +6 -0
- data/db/migrate/20251128212046_rename_subscription_products_to_products.rb +6 -0
- data/db/migrate/20251128212047_rename_subscription_prices_to_prices.rb +6 -0
- data/db/migrate/20251128212053_rename_subscription_product_id_to_product_id_in_prices.rb +13 -0
- data/db/migrate/20251128212054_rename_fks_in_subscription_items.rb +20 -0
- data/db/migrate/20251128220016_add_scheduled_cancelation_at_to_subscriptions.rb +6 -0
- data/db/migrate/20251129121336_add_payment_method_to_subscriptions.rb +10 -0
- data/db/migrate/20251129222345_create_paddle_rails_payments.rb +24 -0
- data/lib/paddle_rails/checkout.rb +181 -0
- data/lib/paddle_rails/configuration.rb +121 -0
- data/lib/paddle_rails/engine.rb +49 -0
- data/lib/paddle_rails/product_sync.rb +176 -0
- data/lib/paddle_rails/subscription_sync.rb +303 -0
- data/lib/paddle_rails/version.rb +6 -0
- data/lib/paddle_rails/webhook_processor.rb +102 -0
- data/lib/paddle_rails/webhook_verifier.rb +110 -0
- data/lib/paddle_rails.rb +32 -0
- data/lib/tasks/paddle_rails_tasks.rake +15 -0
- metadata +157 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<div class="max-w-2xl mx-auto p-8 md:p-12">
|
|
2
|
+
<div class="bg-red-50 border border-red-200 rounded-lg p-6 md:p-8">
|
|
3
|
+
<!-- Error Icon and Title -->
|
|
4
|
+
<div class="flex items-start gap-4 mb-6">
|
|
5
|
+
<div class="flex-shrink-0">
|
|
6
|
+
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
7
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
8
|
+
</svg>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="flex-1">
|
|
11
|
+
<h1 class="text-xl font-medium text-gray-900 mb-2">PaddleRails Configuration Error</h1>
|
|
12
|
+
<p class="text-gray-600 text-sm leading-relaxed">
|
|
13
|
+
Your PaddleRails integration is not properly configured. Please complete the following steps to set up the gem.
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Checklist -->
|
|
19
|
+
<div class="space-y-4">
|
|
20
|
+
<h2 class="text-sm font-medium text-gray-900 mb-3">Configuration Checklist:</h2>
|
|
21
|
+
|
|
22
|
+
<div class="space-y-3">
|
|
23
|
+
<!-- API Keys / Environment Variables -->
|
|
24
|
+
<div class="flex items-start gap-3">
|
|
25
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
26
|
+
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
27
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
28
|
+
</svg>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="flex-1">
|
|
31
|
+
<h3 class="text-sm font-medium text-gray-900 mb-1">API Keys / Environment Variables</h3>
|
|
32
|
+
<p class="text-sm text-gray-600 mb-2">
|
|
33
|
+
Set the following environment variables in your application:
|
|
34
|
+
</p>
|
|
35
|
+
<ul class="list-disc list-inside text-sm text-gray-600 space-y-1 ml-4">
|
|
36
|
+
<li><code class="bg-gray-100 px-1.5 py-0.5 rounded text-xs font-mono">PADDLE_API_KEY</code> - Your Paddle API key</li>
|
|
37
|
+
<li><code class="bg-gray-100 px-1.5 py-0.5 rounded text-xs font-mono">PADDLE_PUBLIC_TOKEN</code> - Your Paddle public token</li>
|
|
38
|
+
<li><code class="bg-gray-100 px-1.5 py-0.5 rounded text-xs font-mono">PADDLE_ENVIRONMENT</code> - Set to "production" or "sandbox"</li>
|
|
39
|
+
</ul>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Setup subscription_owner -->
|
|
44
|
+
<div class="flex items-start gap-3">
|
|
45
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
46
|
+
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
47
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
48
|
+
</svg>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="flex-1">
|
|
51
|
+
<h3 class="text-sm font-medium text-gray-900 mb-1">Setup subscription_owner</h3>
|
|
52
|
+
<p class="text-sm text-gray-600 mb-2">
|
|
53
|
+
Configure the <code class="bg-gray-100 px-1.5 py-0.5 rounded text-xs font-mono">subscription_owner_authenticator</code> in your PaddleRails initializer:
|
|
54
|
+
</p>
|
|
55
|
+
<pre class="bg-gray-100 rounded p-3 text-xs font-mono text-gray-800 overflow-x-auto"><code>PaddleRails.configure do |config|
|
|
56
|
+
config.subscription_owner_authenticator do
|
|
57
|
+
current_user || warden.authenticate!(scope: :user)
|
|
58
|
+
end
|
|
59
|
+
end</code></pre>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- Add Subscribable concern -->
|
|
64
|
+
<div class="flex items-start gap-3">
|
|
65
|
+
<div class="flex-shrink-0 mt-0.5">
|
|
66
|
+
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
67
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
68
|
+
</svg>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="flex-1">
|
|
71
|
+
<h3 class="text-sm font-medium text-gray-900 mb-1">Add Subscribable concern to subscription owner</h3>
|
|
72
|
+
<p class="text-sm text-gray-600 mb-2">
|
|
73
|
+
Include the <code class="bg-gray-100 px-1.5 py-0.5 rounded text-xs font-mono">PaddleRails::Subscribable</code> concern in your subscription owner model:
|
|
74
|
+
</p>
|
|
75
|
+
<pre class="bg-gray-100 rounded p-3 text-xs font-mono text-gray-800 overflow-x-auto"><code>class User < ApplicationRecord
|
|
76
|
+
include PaddleRails::Subscribable
|
|
77
|
+
end</code></pre>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Documentation Link -->
|
|
84
|
+
<div class="mt-6 pt-6 border-t border-red-200">
|
|
85
|
+
<p class="text-sm text-gray-600">
|
|
86
|
+
For more information, see the
|
|
87
|
+
<a href="https://github.com/kjellberg/paddle_rails" class="text-red-600 hover:text-red-700 font-medium underline" target="_blank" rel="noopener noreferrer">
|
|
88
|
+
PaddleRails documentation
|
|
89
|
+
</a>.
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
PaddleRails::Engine.routes.draw do
|
|
2
|
+
root "dashboard#show"
|
|
3
|
+
get "onboarding", to: "onboarding#show", as: :onboarding
|
|
4
|
+
post "onboarding/checkout", to: "onboarding#create_checkout", as: :onboarding_checkout
|
|
5
|
+
get "checkout", to: "checkout#show", as: :checkout
|
|
6
|
+
get "checkout/check_status/:transaction_id", to: "checkout#check_status", as: :check_transaction_status
|
|
7
|
+
post "checkout/update_payment_method", to: "checkout#update_payment_method", as: :update_payment_method
|
|
8
|
+
post "subscriptions/revoke_cancellation", to: "subscriptions#revoke_cancellation", as: :revoke_subscription_cancellation
|
|
9
|
+
post "subscriptions/cancel", to: "subscriptions#cancel", as: :cancel_subscription
|
|
10
|
+
post "subscriptions/change_plan", to: "subscriptions#change_plan", as: :change_subscription_plan
|
|
11
|
+
get "payments/:id/invoice", to: "payments#view_invoice", as: :view_payment_invoice
|
|
12
|
+
get "payments/:id/download", to: "payments#download_invoice", as: :download_payment_invoice
|
|
13
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class CreatePaddleRailsSubscriptionPlans < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :paddle_rails_subscription_plans do |t|
|
|
4
|
+
t.string :paddle_product_id, null: false
|
|
5
|
+
t.string :name
|
|
6
|
+
t.text :description
|
|
7
|
+
t.string :status
|
|
8
|
+
t.string :type
|
|
9
|
+
t.string :tax_category
|
|
10
|
+
t.string :image_url
|
|
11
|
+
t.json :custom_data
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :paddle_rails_subscription_plans, :paddle_product_id, unique: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class CreatePaddleRailsSubscriptionPrices < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :paddle_rails_subscription_prices do |t|
|
|
4
|
+
t.references :subscription_plan, null: false, foreign_key: { to_table: :paddle_rails_subscription_plans }
|
|
5
|
+
t.string :paddle_price_id, null: false
|
|
6
|
+
t.string :currency
|
|
7
|
+
t.integer :unit_price
|
|
8
|
+
t.string :billing_interval
|
|
9
|
+
t.integer :billing_interval_count
|
|
10
|
+
t.integer :trial_days
|
|
11
|
+
t.string :type
|
|
12
|
+
t.string :name
|
|
13
|
+
t.text :description
|
|
14
|
+
t.string :status
|
|
15
|
+
t.string :tax_mode
|
|
16
|
+
t.integer :quantity_minimum
|
|
17
|
+
t.integer :quantity_maximum
|
|
18
|
+
t.json :trial_period
|
|
19
|
+
t.json :custom_data
|
|
20
|
+
|
|
21
|
+
t.timestamps
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
add_index :paddle_rails_subscription_prices, :paddle_price_id, unique: true
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class CreatePaddleRailsWebhookEvents < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :paddle_rails_webhook_events do |t|
|
|
4
|
+
t.string :external_id, null: false
|
|
5
|
+
t.string :event_type, null: false
|
|
6
|
+
t.json :payload, null: false
|
|
7
|
+
t.string :status, null: false, default: "pending"
|
|
8
|
+
t.text :processing_errors
|
|
9
|
+
t.datetime :processed_at
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :paddle_rails_webhook_events, :external_id, unique: true
|
|
15
|
+
add_index :paddle_rails_webhook_events, :event_type
|
|
16
|
+
add_index :paddle_rails_webhook_events, :status
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class CreatePaddleRailsSubscriptions < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :paddle_rails_subscriptions do |t|
|
|
4
|
+
t.references :owner, polymorphic: true, null: false, index: true
|
|
5
|
+
t.string :paddle_subscription_id, null: false
|
|
6
|
+
t.string :paddle_price_id
|
|
7
|
+
t.string :status
|
|
8
|
+
t.datetime :current_period_end_at
|
|
9
|
+
t.datetime :trial_ends_at
|
|
10
|
+
t.json :raw_payload
|
|
11
|
+
t.references :subscription_price, foreign_key: { to_table: :paddle_rails_subscription_prices }, index: true
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :paddle_rails_subscriptions, :paddle_subscription_id, unique: true
|
|
17
|
+
add_index :paddle_rails_subscriptions, :paddle_price_id
|
|
18
|
+
add_index :paddle_rails_subscriptions, :status
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreatePaddleRailsSubscriptionItems < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :paddle_rails_subscription_items do |t|
|
|
4
|
+
t.references :subscription, foreign_key: { to_table: :paddle_rails_subscriptions }, null: false, index: true
|
|
5
|
+
t.references :subscription_price, foreign_key: { to_table: :paddle_rails_subscription_prices }, null: false, index: true
|
|
6
|
+
t.integer :quantity, default: 1
|
|
7
|
+
t.string :status
|
|
8
|
+
t.boolean :recurring, default: false
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :paddle_rails_subscription_items, [:subscription_id, :subscription_price_id], unique: true, name: "index_subscription_items_on_subscription_and_price"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class RenameSubscriptionPlanIdToSubscriptionProductId < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
# Remove old foreign key
|
|
4
|
+
remove_foreign_key :paddle_rails_subscription_prices, :paddle_rails_subscription_plans if foreign_key_exists?(:paddle_rails_subscription_prices, :paddle_rails_subscription_plans)
|
|
5
|
+
|
|
6
|
+
# Rename the column
|
|
7
|
+
rename_column :paddle_rails_subscription_prices, :subscription_plan_id, :subscription_product_id
|
|
8
|
+
|
|
9
|
+
# Add new foreign key
|
|
10
|
+
add_foreign_key :paddle_rails_subscription_prices, :paddle_rails_subscription_products, column: :subscription_product_id
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class RemoveSubscriptionPriceIdFromSubscriptions < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
remove_foreign_key :paddle_rails_subscriptions, :paddle_rails_subscription_prices, if_exists: true
|
|
4
|
+
remove_index :paddle_rails_subscriptions, :subscription_price_id, if_exists: true
|
|
5
|
+
remove_column :paddle_rails_subscriptions, :subscription_price_id, :integer, if_exists: true
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class RenameSubscriptionProductIdToProductIdInPrices < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
# Remove old foreign key
|
|
4
|
+
remove_foreign_key :paddle_rails_prices, :paddle_rails_subscription_products, if_exists: true
|
|
5
|
+
|
|
6
|
+
# Rename the column
|
|
7
|
+
rename_column :paddle_rails_prices, :subscription_product_id, :product_id
|
|
8
|
+
|
|
9
|
+
# Add new foreign key
|
|
10
|
+
add_foreign_key :paddle_rails_prices, :paddle_rails_products, column: :product_id
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class RenameFksInSubscriptionItems < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
# Remove old foreign keys
|
|
4
|
+
remove_foreign_key :paddle_rails_subscription_items, :paddle_rails_subscription_prices, if_exists: true
|
|
5
|
+
remove_foreign_key :paddle_rails_subscription_items, :paddle_rails_subscription_products, if_exists: true
|
|
6
|
+
|
|
7
|
+
# Rename the columns
|
|
8
|
+
rename_column :paddle_rails_subscription_items, :subscription_price_id, :price_id
|
|
9
|
+
rename_column :paddle_rails_subscription_items, :subscription_product_id, :product_id
|
|
10
|
+
|
|
11
|
+
# Update the unique index
|
|
12
|
+
remove_index :paddle_rails_subscription_items, name: "index_subscription_items_on_subscription_and_price", if_exists: true
|
|
13
|
+
add_index :paddle_rails_subscription_items, [:subscription_id, :price_id], unique: true, name: "index_subscription_items_on_subscription_and_price"
|
|
14
|
+
|
|
15
|
+
# Add new foreign keys
|
|
16
|
+
add_foreign_key :paddle_rails_subscription_items, :paddle_rails_prices, column: :price_id
|
|
17
|
+
add_foreign_key :paddle_rails_subscription_items, :paddle_rails_products, column: :product_id
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class AddPaymentMethodToSubscriptions < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
add_column :paddle_rails_subscriptions, :payment_method_id, :string
|
|
4
|
+
add_column :paddle_rails_subscriptions, :payment_method_type, :string
|
|
5
|
+
add_column :paddle_rails_subscriptions, :payment_method_details, :json
|
|
6
|
+
|
|
7
|
+
add_index :paddle_rails_subscriptions, :payment_method_id
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class CreatePaddleRailsPayments < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :paddle_rails_payments do |t|
|
|
4
|
+
t.references :subscription, null: false, foreign_key: { to_table: :paddle_rails_subscriptions }
|
|
5
|
+
t.references :owner, polymorphic: true, null: false
|
|
6
|
+
t.string :paddle_transaction_id, null: false
|
|
7
|
+
t.string :invoice_id
|
|
8
|
+
t.string :invoice_number
|
|
9
|
+
t.string :status, null: false
|
|
10
|
+
t.string :origin # subscription_recurring, subscription_update, etc.
|
|
11
|
+
t.integer :total # total in cents (incl. tax)
|
|
12
|
+
t.integer :tax
|
|
13
|
+
t.integer :subtotal
|
|
14
|
+
t.string :currency
|
|
15
|
+
t.datetime :billed_at
|
|
16
|
+
t.json :details # store line_items and other details
|
|
17
|
+
t.json :raw_payload
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
add_index :paddle_rails_payments, :paddle_transaction_id, unique: true
|
|
22
|
+
add_index :paddle_rails_payments, [:owner_type, :owner_id]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaddleRails
|
|
4
|
+
# Service object for creating Paddle transactions (checkouts).
|
|
5
|
+
#
|
|
6
|
+
# Wraps {Paddle::Transaction.create} and is responsible for:
|
|
7
|
+
# - Building the items array from a Paddle price ID.
|
|
8
|
+
# - Merging the owner's **signed** GlobalID into the `custom_data` hash under
|
|
9
|
+
# the \"owner_sgid\" key, so the owner reference is tamper‑evident.
|
|
10
|
+
# - Returning the resulting {Paddle::Transaction} instance.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# checkout = PaddleRails::Checkout.create(
|
|
14
|
+
# owner: user,
|
|
15
|
+
# paddle_price_id: "pri_123"
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# redirect_to checkout.details.checkout.url, allow_other_host: true
|
|
19
|
+
#
|
|
20
|
+
# @example With additional custom data
|
|
21
|
+
# checkout = PaddleRails::Checkout.create(
|
|
22
|
+
# owner: user,
|
|
23
|
+
# paddle_price_id: "pri_123",
|
|
24
|
+
# custom_data: { foo: "bar" }
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# # custom_data will be merged with a signed owner SGID:
|
|
28
|
+
# # { \"owner_sgid\" => user.to_sgid_param(for: \"paddle_rails_owner\"), \"foo\" => \"bar\" }
|
|
29
|
+
class Checkout
|
|
30
|
+
attr_reader :owner, :paddle_price_id, :custom_data, :checkout_url
|
|
31
|
+
|
|
32
|
+
# Create a new checkout (transaction) for a given owner and price.
|
|
33
|
+
#
|
|
34
|
+
# @param owner [Object] the subscription owner (e.g. User) – must support `to_gid_param`
|
|
35
|
+
# @param paddle_price_id [String] the Paddle Price ID used for the transaction
|
|
36
|
+
# @param custom_data [Hash] optional extra metadata to merge into `custom_data`
|
|
37
|
+
# @param checkout_url [String, nil] optional URL that Paddle should use for the checkout
|
|
38
|
+
# @return [Paddle::Transaction] the created transaction
|
|
39
|
+
def self.create(owner:, paddle_price_id:, custom_data: {}, checkout_url: nil)
|
|
40
|
+
new(
|
|
41
|
+
owner: owner,
|
|
42
|
+
paddle_price_id: paddle_price_id,
|
|
43
|
+
custom_data: custom_data,
|
|
44
|
+
checkout_url: checkout_url
|
|
45
|
+
).create
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Convenience helper that creates a checkout and returns only the hosted URL.
|
|
49
|
+
#
|
|
50
|
+
# @param owner [Object] the subscription owner (e.g. User)
|
|
51
|
+
# @param paddle_price_id [String] the Paddle Price ID used for the transaction
|
|
52
|
+
# @param custom_data [Hash] optional extra metadata to merge into `custom_data`
|
|
53
|
+
# @param checkout_url [String, nil] optional URL that Paddle should use for the checkout
|
|
54
|
+
# @return [String, nil] the checkout URL, if present
|
|
55
|
+
def self.url_for(owner:, paddle_price_id:, custom_data: {}, checkout_url: nil)
|
|
56
|
+
transaction = create(
|
|
57
|
+
owner: owner,
|
|
58
|
+
paddle_price_id: paddle_price_id,
|
|
59
|
+
custom_data: custom_data,
|
|
60
|
+
checkout_url: checkout_url
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
extract_url(transaction)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param owner [Object]
|
|
67
|
+
# @param paddle_price_id [String]
|
|
68
|
+
# @param custom_data [Hash]
|
|
69
|
+
# @param checkout_url [String, nil]
|
|
70
|
+
def initialize(owner:, paddle_price_id:, custom_data: {}, checkout_url: nil)
|
|
71
|
+
@owner = owner
|
|
72
|
+
@paddle_price_id = paddle_price_id
|
|
73
|
+
@custom_data = custom_data || {}
|
|
74
|
+
@checkout_url = checkout_url
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Perform the transaction creation against the Paddle API.
|
|
78
|
+
#
|
|
79
|
+
# @return [Paddle::Transaction]
|
|
80
|
+
def create
|
|
81
|
+
attrs = {
|
|
82
|
+
items: [{ price_id: paddle_price_id, quantity: 1 }],
|
|
83
|
+
custom_data: merged_custom_data
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if checkout_url
|
|
87
|
+
attrs[:checkout] = { url: normalize_checkout_url_for_paddle(checkout_url) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
Paddle::Transaction.create(**attrs)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def merged_custom_data
|
|
96
|
+
base = { "owner_sgid" => owner_sgid }
|
|
97
|
+
# Stringify keys to keep custom_data consistent with Paddle expectations
|
|
98
|
+
base.merge(stringified_custom_data)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Build a signed GlobalID string for the owner that can be verified later.
|
|
102
|
+
#
|
|
103
|
+
# NOTE: Webhook processing is not implemented yet, but when it is, you
|
|
104
|
+
# should resolve this using:
|
|
105
|
+
#
|
|
106
|
+
# GlobalID::Locator.locate_signed(owner_sgid, for: \"paddle_rails_owner\")
|
|
107
|
+
#
|
|
108
|
+
# @return [String]
|
|
109
|
+
def owner_sgid
|
|
110
|
+
if owner.respond_to?(:to_sgid_param)
|
|
111
|
+
owner.to_sgid_param(for: "paddle_rails_owner")
|
|
112
|
+
elsif defined?(GlobalID::SignedGlobalID)
|
|
113
|
+
GlobalID::SignedGlobalID.create(owner, for: "paddle_rails_owner").to_s
|
|
114
|
+
else
|
|
115
|
+
owner.to_s
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def stringified_custom_data
|
|
120
|
+
custom_data.to_h.transform_keys(&:to_s)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# In development, convert all http:// URLs to https:// for
|
|
124
|
+
# checkout URLs to avoid Paddle's domain approval error.
|
|
125
|
+
#
|
|
126
|
+
# @param url [String, nil]
|
|
127
|
+
# @return [String, nil]
|
|
128
|
+
def normalize_checkout_url_for_paddle(url)
|
|
129
|
+
return url unless url
|
|
130
|
+
return url unless defined?(Rails) && Rails.env.development?
|
|
131
|
+
|
|
132
|
+
url.sub(/\Ahttp:\/\//, "https://")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Extract the hosted checkout URL from a Paddle::Transaction.
|
|
136
|
+
#
|
|
137
|
+
# The Paddle gem currently exposes this via an internal OpenStruct
|
|
138
|
+
# where the raw attributes live in `checkout.table`.
|
|
139
|
+
#
|
|
140
|
+
# @param transaction [Paddle::Transaction, nil]
|
|
141
|
+
# @return [String, nil]
|
|
142
|
+
def self.extract_url(transaction)
|
|
143
|
+
return nil unless transaction
|
|
144
|
+
|
|
145
|
+
url = nil
|
|
146
|
+
|
|
147
|
+
# Preferred: transaction.checkout.table[:url]
|
|
148
|
+
if transaction.respond_to?(:checkout) && transaction.checkout
|
|
149
|
+
checkout = transaction.checkout
|
|
150
|
+
if checkout.respond_to?(:table)
|
|
151
|
+
url = checkout.table[:url] || checkout.table["url"]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
url ||= checkout.url if checkout.respond_to?(:url)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Fallback: nested details.checkout.url (for other Paddle shapes)
|
|
158
|
+
if url.nil? && transaction.respond_to?(:details) && transaction.details&.respond_to?(:checkout)
|
|
159
|
+
nested = transaction.details.checkout
|
|
160
|
+
url = nested.url if nested.respond_to?(:url)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
rewrite_localhost_url(url)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# In development, Paddle may return https URLs which
|
|
167
|
+
# Rails typically serves over plain HTTP. Normalize those so we
|
|
168
|
+
# don't hit mixed-scheme issues in local setups.
|
|
169
|
+
#
|
|
170
|
+
# @param url [String, nil]
|
|
171
|
+
# @return [String, nil]
|
|
172
|
+
def self.rewrite_localhost_url(url)
|
|
173
|
+
return url unless url
|
|
174
|
+
return url unless defined?(Rails) && Rails.env.development?
|
|
175
|
+
|
|
176
|
+
url.sub(/\Ahttps:\/\//, "http://")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
|