pay 2.7.1 → 3.0.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pay might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +34 -715
- data/app/controllers/pay/webhooks/braintree_controller.rb +10 -3
- data/app/controllers/pay/webhooks/paddle_controller.rb +7 -8
- data/app/controllers/pay/webhooks/stripe_controller.rb +6 -3
- data/app/jobs/pay/{email_sync_job.rb → customer_sync_job.rb} +3 -4
- data/app/models/pay/application_record.rb +1 -5
- data/app/models/pay/charge.rb +54 -17
- data/app/models/pay/customer.rb +87 -0
- data/app/models/pay/merchant.rb +19 -0
- data/app/models/pay/payment_method.rb +41 -0
- data/app/models/pay/subscription.rb +42 -30
- data/app/models/pay/webhook.rb +36 -0
- data/app/views/layouts/pay/application.html.erb +2 -3
- data/app/views/pay/payments/show.html.erb +108 -81
- data/app/views/pay/user_mailer/receipt.html.erb +2 -2
- data/app/views/pay/user_mailer/refund.html.erb +2 -2
- data/config/locales/en.yml +1 -1
- data/db/migrate/1_create_pay_tables.rb +72 -0
- data/lib/generators/active_record/templates/billable_migration.rb +1 -0
- data/lib/pay/attributes.rb +74 -0
- data/lib/pay/billable/sync_customer.rb +30 -0
- data/lib/pay/braintree/billable.rb +133 -110
- data/lib/pay/braintree/payment_method.rb +42 -0
- data/lib/pay/braintree/subscription.rb +9 -12
- data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +4 -4
- data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +2 -2
- data/lib/pay/braintree/webhooks/subscription_went_active.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +1 -1
- data/lib/pay/braintree.rb +3 -2
- data/lib/pay/engine.rb +6 -1
- data/lib/pay/fake_processor/billable.rb +45 -21
- data/lib/pay/fake_processor/payment_method.rb +21 -0
- data/lib/pay/fake_processor/subscription.rb +11 -8
- data/lib/pay/fake_processor.rb +2 -1
- data/lib/pay/nano_id.rb +13 -0
- data/lib/pay/paddle/billable.rb +18 -48
- data/lib/pay/paddle/charge.rb +5 -5
- data/lib/pay/paddle/payment_method.rb +60 -0
- data/lib/pay/paddle/response.rb +0 -0
- data/lib/pay/paddle/subscription.rb +49 -8
- data/lib/pay/paddle/webhooks/subscription_cancelled.rb +6 -3
- data/lib/pay/paddle/webhooks/subscription_created.rb +1 -40
- data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
- data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +26 -28
- data/lib/pay/paddle/webhooks/subscription_updated.rb +2 -2
- data/lib/pay/paddle.rb +7 -3
- data/lib/pay/payment.rb +1 -1
- data/lib/pay/receipts.rb +35 -7
- data/lib/pay/stripe/billable.rb +75 -76
- data/lib/pay/stripe/charge.rb +44 -17
- data/lib/pay/stripe/merchant.rb +10 -10
- data/lib/pay/stripe/payment_method.rb +61 -0
- data/lib/pay/stripe/subscription.rb +55 -22
- data/lib/pay/stripe/webhooks/account_updated.rb +2 -3
- data/lib/pay/stripe/webhooks/charge_refunded.rb +1 -1
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -2
- data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +3 -1
- data/lib/pay/stripe/webhooks/checkout_session_completed.rb +3 -1
- data/lib/pay/stripe/webhooks/customer_deleted.rb +7 -15
- data/lib/pay/stripe/webhooks/customer_updated.rb +10 -3
- data/lib/pay/stripe/webhooks/payment_action_required.rb +2 -2
- data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +6 -8
- data/lib/pay/stripe/webhooks/payment_method_attached.rb +15 -0
- data/lib/pay/stripe/webhooks/payment_method_detached.rb +12 -0
- data/lib/pay/stripe/webhooks/payment_method_updated.rb +10 -4
- data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
- data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -1
- data/lib/pay/stripe/webhooks/subscription_renewing.rb +12 -2
- data/lib/pay/stripe.rb +6 -3
- data/lib/pay/version.rb +1 -1
- data/lib/pay/webhooks/delegator.rb +4 -0
- data/lib/pay/webhooks/process_job.rb +9 -0
- data/lib/pay/webhooks.rb +1 -0
- data/lib/pay.rb +7 -78
- data/lib/tasks/pay.rake +20 -0
- metadata +23 -36
- data/app/models/pay.rb +0 -5
- data/db/migrate/20170205020145_create_pay_subscriptions.rb +0 -17
- data/db/migrate/20170727235816_create_pay_charges.rb +0 -18
- data/db/migrate/20190816015720_add_status_to_pay_subscriptions.rb +0 -14
- data/db/migrate/20200603134434_add_data_to_pay_models.rb +0 -6
- data/db/migrate/20210309004259_add_data_to_pay_billable.rb +0 -10
- data/db/migrate/20210406215234_add_currency_to_pay_charges.rb +0 -5
- data/db/migrate/20210406215506_add_application_fee_to_pay_models.rb +0 -7
- data/lib/pay/billable/sync_email.rb +0 -40
- data/lib/pay/billable.rb +0 -172
@@ -0,0 +1,36 @@
|
|
1
|
+
module Pay
|
2
|
+
class Webhook < Pay::ApplicationRecord
|
3
|
+
validates :processor, presence: true
|
4
|
+
validates :event_type, presence: true
|
5
|
+
validates :event, presence: true
|
6
|
+
|
7
|
+
def process!
|
8
|
+
Pay::Webhooks.instrument type: "#{processor}.#{event_type}", event: rehydrated_event
|
9
|
+
|
10
|
+
# Remove after successfully processing
|
11
|
+
destroy
|
12
|
+
end
|
13
|
+
|
14
|
+
# Events have already been verified by the webhook, so we just store the raw data
|
15
|
+
# Then we can rehydrate as webhook objects for each payment processor
|
16
|
+
def rehydrated_event
|
17
|
+
case processor
|
18
|
+
when "braintree"
|
19
|
+
Pay.braintree_gateway.webhook_notification.parse(event["bt_signature"], event["bt_payload"])
|
20
|
+
when "paddle"
|
21
|
+
to_recursive_ostruct(event)
|
22
|
+
when "stripe"
|
23
|
+
::Stripe::Event.construct_from(event)
|
24
|
+
else
|
25
|
+
event
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_recursive_ostruct(hash)
|
30
|
+
result = hash.each_with_object({}) do |(key, val), memo|
|
31
|
+
memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
|
32
|
+
end
|
33
|
+
OpenStruct.new(result)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -8,11 +8,10 @@
|
|
8
8
|
<%#= javascript_include_tag "pay/application" %>
|
9
9
|
<%= csrf_meta_tags %>
|
10
10
|
|
11
|
-
<link href="https://unpkg.com/tailwindcss@^
|
12
|
-
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
|
11
|
+
<link href="https://unpkg.com/tailwindcss@^2.2/dist/tailwind.min.css" rel="stylesheet">
|
13
12
|
<script src="https://js.stripe.com/v3"></script>
|
14
13
|
</head>
|
15
|
-
<body class="font-sans text-gray-600 bg-gray-
|
14
|
+
<body class="font-sans text-gray-600 bg-gray-100 leading-normal p-4 h-full">
|
16
15
|
|
17
16
|
<%= yield %>
|
18
17
|
|
@@ -1,22 +1,25 @@
|
|
1
|
-
<div
|
1
|
+
<div
|
2
|
+
data-controller="payment-intent"
|
3
|
+
data-payment-intent-client-secret-value="<%= @payment.client_secret %>"
|
4
|
+
data-payment-intent-status-value="<%= @payment.status %>"
|
5
|
+
data-payment-intent-customer-value="<%= @payment.customer %>"
|
6
|
+
class="h-screen md:flex md:justify-center md:items-center">
|
2
7
|
<div class="w-full max-w-lg">
|
3
8
|
<!-- Status Messages -->
|
4
|
-
<p class="
|
5
|
-
<svg xmlns="http://www.w3.org/2000/svg"
|
6
|
-
<path
|
7
|
-
<path class="fill-current text-red-500" d="M12 18a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm1-5.9c-.13 1.2-1.88 1.2-2 0l-.5-5a1 1 0 0 1 1-1.1h1a1 1 0 0 1 1 1.1l-.5 5z"/>
|
9
|
+
<p data-payment-intent-target="error" class="hidden items-center mb-4 bg-red-100 border border-red-200 px-5 py-2 rounded-lg text-red-500">
|
10
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 text-red-500 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
11
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
8
12
|
</svg>
|
9
13
|
|
10
|
-
<span class="ml-3"
|
14
|
+
<span data-payment-intent-target="errorMessage" class="ml-3"></span>
|
11
15
|
</p>
|
12
16
|
|
13
|
-
<p class="
|
14
|
-
<svg xmlns="http://www.w3.org/2000/svg"
|
15
|
-
<
|
16
|
-
<path class="fill-current text-green-500" d="M10 14.59l6.3-6.3a1 1 0 0 1 1.4 1.42l-7 7a1 1 0 0 1-1.4 0l-3-3a1 1 0 0 1 1.4-1.42l2.3 2.3z"/>
|
17
|
+
<p data-payment-intent-target="success" class="hidden items-center mb-4 bg-green-100 border border-green-200 px-5 py-4 rounded-lg text-green-700">
|
18
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 text-green-500 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
19
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
17
20
|
</svg>
|
18
21
|
|
19
|
-
<span class="ml-3"
|
22
|
+
<span data-payment-intent-target="successMessage" class="ml-3"></span>
|
20
23
|
</p>
|
21
24
|
|
22
25
|
<div class="bg-white rounded-lg shadow-xl p-4 sm:py-6 sm:px-10 mb-5">
|
@@ -29,29 +32,29 @@
|
|
29
32
|
<p class="mb-6"><%=t "cancelled.description" %></p>
|
30
33
|
|
31
34
|
<% else %>
|
32
|
-
<div id="payment-elements"
|
35
|
+
<div data-payment-intent-target="form" id="payment-elements">
|
33
36
|
<!-- Instructions -->
|
34
37
|
<h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "requires_action.header", amount: number_to_currency(@payment.amount / 100.0) %></h1>
|
35
38
|
<p class="mb-6"><%=t "requires_action.description" %></p>
|
36
39
|
|
37
|
-
<div
|
40
|
+
<div data-payment-intent-target="cardFields" class="hidden">
|
38
41
|
<!-- Name -->
|
39
42
|
<label for="cardholder-name" class="inline-block text-sm text-gray-700 font-semibold mb-2"><%=t "requires_action.full_name" %></label>
|
40
|
-
<input id="cardholder-name" type="text" placeholder="Jane Doe" required class="
|
43
|
+
<input data-payment-intent-target="name" id="cardholder-name" type="text" placeholder="Jane Doe" required autofocus class="mt-1 mb-6 block w-full px-3 py-2 rounded-md border border-gray-300 shadow-sm focus:outline-none focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
|
41
44
|
|
42
45
|
<!-- Card -->
|
43
46
|
<label for="card-element" class="inline-block text-sm text-gray-700 font-semibold mb-2"><%=t "requires_action.card" %></label>
|
44
|
-
<div id="card-element" class="
|
47
|
+
<div data-payment-intent-target="card" id="card-element" class="mt-1 mb-6 block w-full px-3 py-4 rounded-md border border-gray-300 shadow-sm focus:outline-none focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"></div>
|
45
48
|
</div>
|
46
49
|
|
47
50
|
<!-- Pay Button -->
|
48
|
-
<button
|
51
|
+
<button data-payment-intent-target="button" data-action="click->payment-intent#confirmPayment" class="inline-block w-full px-4 py-3 mb-4 text-white rounded-lg bg-blue-400 hover:bg-blue-500">
|
49
52
|
<%=t "requires_action.button", amount: number_to_currency(@payment.amount / 100.0) %>
|
50
53
|
</button>
|
51
54
|
</div>
|
52
55
|
<% end %>
|
53
56
|
|
54
|
-
<%= link_to t("back"), @redirect_to, class: "inline-block w-full px-4 py-3 bg-gray-
|
57
|
+
<%= link_to t("back"), @redirect_to, class: "inline-block w-full px-4 py-3 bg-gray-100 hover:bg-gray-200 text-center text-gray-600 rounded-lg" %>
|
55
58
|
</div>
|
56
59
|
|
57
60
|
<p class="text-center text-gray-500 text-sm">
|
@@ -60,75 +63,99 @@
|
|
60
63
|
</div>
|
61
64
|
</div>
|
62
65
|
|
63
|
-
<script>
|
66
|
+
<script type="module">
|
64
67
|
window.stripe = Stripe('<%= Pay::Stripe.public_key %>');
|
65
|
-
var app = new Vue({
|
66
|
-
el: '#app',
|
67
|
-
data: {
|
68
|
-
clientSecret: '<%= @payment.client_secret %>',
|
69
|
-
status: '<%= @payment.status %>',
|
70
|
-
name: '',
|
71
|
-
cardElement: null,
|
72
|
-
paymentProcessing: false,
|
73
|
-
paymentProcessed: false,
|
74
|
-
successMessage: '',
|
75
|
-
errorMessage: ''
|
76
|
-
},
|
77
|
-
|
78
|
-
mounted: function () {
|
79
|
-
if (this.status == "succeeded" || this.status == "canceled") {
|
80
|
-
return
|
81
|
-
}
|
82
68
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
69
|
+
import { Application, Controller } from 'https://cdn.skypack.dev/stimulus'
|
70
|
+
const application = Application.start()
|
71
|
+
|
72
|
+
application.register('payment-intent', class extends Controller {
|
73
|
+
static targets = ["error", "errorMessage", "success", "successMessage", "form", "button", "name", "card", "cardFields"]
|
74
|
+
static values = {
|
75
|
+
clientSecret: String,
|
76
|
+
customer: String,
|
77
|
+
status: String,
|
78
|
+
errorMessage: String,
|
79
|
+
successMessage: String,
|
80
|
+
complete: Boolean,
|
81
|
+
processing: Boolean
|
82
|
+
}
|
83
|
+
|
84
|
+
connect() {
|
85
|
+
if (this.hasCardTarget) {
|
86
|
+
this.elements = stripe.elements()
|
87
|
+
this.cardElement = this.elements.create("card")
|
88
|
+
this.cardElement.mount(this.cardTarget)
|
89
89
|
}
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
this.
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
} else {
|
124
|
-
this.errorMessage = result.error.message
|
125
|
-
this.status = result.error.payment_intent.status
|
126
|
-
}
|
90
|
+
}
|
91
|
+
|
92
|
+
statusValueChanged() {
|
93
|
+
switch(this.statusValue) {
|
94
|
+
case "requires_action":
|
95
|
+
stripe.confirmCardPayment(this.clientSecretValue).then(this.handleConfirmResult.bind(this))
|
96
|
+
break;
|
97
|
+
case "requires_payment_method":
|
98
|
+
this.cardFieldsTarget.classList.toggle("hidden", false)
|
99
|
+
break;
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
confirmPayment() {
|
104
|
+
this.processingValue = true
|
105
|
+
this.completeValue = false
|
106
|
+
stripe.confirmCardPayment(this.clientSecretValue, {
|
107
|
+
payment_method: {
|
108
|
+
card: this.cardElement,
|
109
|
+
billing_details: { name: this.nameTarget.value }
|
110
|
+
},
|
111
|
+
save_payment_method: this.customerValue != "",
|
112
|
+
setup_future_usage: 'off_session',
|
113
|
+
}).then(this.handleConfirmResult.bind(this))
|
114
|
+
}
|
115
|
+
|
116
|
+
handleConfirmResult(result) {
|
117
|
+
this.processingValue = false
|
118
|
+
|
119
|
+
if (result.error) {
|
120
|
+
if (result.error.code === 'parameter_invalid_empty' &&
|
121
|
+
result.error.param === 'payment_method_data[billing_details][name]') {
|
122
|
+
this.errorMessageValue = '<%=t "requires_action.name_missing" %>'
|
127
123
|
} else {
|
128
|
-
this.
|
129
|
-
this.
|
124
|
+
this.errorMessageValue = result.error.message
|
125
|
+
this.statusValue = result.error.payment_intent.status
|
130
126
|
}
|
127
|
+
} else {
|
128
|
+
this.completeValue = true;
|
129
|
+
this.successMessageValue = '<%=t "requires_action.success" %>'
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
completeValueChanged() {
|
134
|
+
if (this.hasFormTarget) {
|
135
|
+
this.formTarget.classList.toggle("hidden", this.completeValue)
|
136
|
+
}
|
137
|
+
}
|
138
|
+
|
139
|
+
processingValueChanged() {
|
140
|
+
if (this.hasButtonTarget) {
|
141
|
+
this.buttonTarget.disabled = this.processingValue
|
142
|
+
this.buttonTarget.classList.toggle("bg-blue-400", this.processingValue)
|
143
|
+
this.buttonTarget.classList.toggle("bg-blue-500", !this.processingValue)
|
131
144
|
}
|
132
|
-
}
|
145
|
+
}
|
146
|
+
|
147
|
+
errorMessageValueChanged() {
|
148
|
+
this.errorMessageTarget.textContent = this.errorMessageValue
|
149
|
+
const enabled = (this.errorMessageValue != '')
|
150
|
+
this.errorTarget.classList.toggle("flex", enabled)
|
151
|
+
this.errorTarget.classList.toggle("hidden", !enabled)
|
152
|
+
}
|
153
|
+
|
154
|
+
successMessageValueChanged() {
|
155
|
+
this.successMessageTarget.textContent = this.successMessageValue
|
156
|
+
const enabled = (this.successMessageValue != '')
|
157
|
+
this.successTarget.classList.toggle("flex", enabled)
|
158
|
+
this.successTarget.classList.toggle("hidden", !enabled)
|
159
|
+
}
|
133
160
|
})
|
134
161
|
</script>
|
@@ -10,8 +10,8 @@ Amount: USD <%= ActionController::Base.helpers.number_to_currency(params[:charge
|
|
10
10
|
Charged to: <%= params[:charge].charged_to %><br/>
|
11
11
|
Transaction ID: <%= params[:charge].id %><br/>
|
12
12
|
Date: <%= params[:charge].created_at %><br/>
|
13
|
-
<% if params[:charge].owner.extra_billing_info? %>
|
14
|
-
<%= params[:charge].owner.extra_billing_info %><br/>
|
13
|
+
<% if params[:charge].customer.owner.extra_billing_info? %>
|
14
|
+
<%= params[:charge].customer.owner.extra_billing_info %><br/>
|
15
15
|
<% end %>
|
16
16
|
<br/>
|
17
17
|
<br/>
|
@@ -11,8 +11,8 @@ Amount: USD <%= ActionController::Base.helpers.number_to_currency(params[:charge
|
|
11
11
|
Refunded to: <%= params[:charge].charged_to %><br/>
|
12
12
|
Transaction ID: <%= params[:charge].id %><br/>
|
13
13
|
Date: <%= params[:charge].created_at %><br/>
|
14
|
-
<% if params[:charge].owner.extra_billing_info? %>
|
15
|
-
<%= params[:charge].owner.extra_billing_info %><br/>
|
14
|
+
<% if params[:charge].customer.owner.extra_billing_info? %>
|
15
|
+
<%= params[:charge].customer.owner.extra_billing_info %><br/>
|
16
16
|
<% end %>
|
17
17
|
<br/>
|
18
18
|
<br/>
|
data/config/locales/en.yml
CHANGED
@@ -9,7 +9,7 @@ en:
|
|
9
9
|
header: Confirm your %{amount} payment
|
10
10
|
description: Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.
|
11
11
|
full_name: Full name
|
12
|
-
card:
|
12
|
+
card: Credit or debit card
|
13
13
|
button: Pay %{amount}
|
14
14
|
name_missing: Please provide your name.
|
15
15
|
success: The payment was successful.
|
@@ -0,0 +1,72 @@
|
|
1
|
+
class CreatePayTables < ActiveRecord::Migration[6.0]
|
2
|
+
def change
|
3
|
+
create_table :pay_customers do |t|
|
4
|
+
t.belongs_to :owner, polymorphic: true, index: false
|
5
|
+
t.string :processor, null: false
|
6
|
+
t.string :processor_id
|
7
|
+
t.boolean :default
|
8
|
+
t.public_send Pay::Adapter.json_column_type, :data
|
9
|
+
t.datetime :deleted_at
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
add_index :pay_customers, [:owner_type, :owner_id, :deleted_at, :default], name: :pay_customer_owner_index
|
13
|
+
add_index :pay_customers, [:processor, :processor_id], unique: true
|
14
|
+
|
15
|
+
create_table :pay_merchants do |t|
|
16
|
+
t.belongs_to :owner, polymorphic: true, index: false
|
17
|
+
t.string :processor, null: false
|
18
|
+
t.string :processor_id
|
19
|
+
t.boolean :default
|
20
|
+
t.public_send Pay::Adapter.json_column_type, :data
|
21
|
+
t.timestamps
|
22
|
+
end
|
23
|
+
add_index :pay_merchants, [:owner_type, :owner_id, :processor]
|
24
|
+
|
25
|
+
create_table :pay_payment_methods do |t|
|
26
|
+
t.belongs_to :customer, foreign_key: {to_table: :pay_customers}, null: false, index: false
|
27
|
+
t.string :processor_id, null: false
|
28
|
+
t.boolean :default
|
29
|
+
t.string :type
|
30
|
+
t.public_send Pay::Adapter.json_column_type, :data
|
31
|
+
t.timestamps
|
32
|
+
end
|
33
|
+
add_index :pay_payment_methods, [:customer_id, :processor_id], unique: true
|
34
|
+
|
35
|
+
create_table :pay_subscriptions do |t|
|
36
|
+
t.belongs_to :customer, foreign_key: {to_table: :pay_customers}, null: false, index: false
|
37
|
+
t.string :name, null: false
|
38
|
+
t.string :processor_id, null: false
|
39
|
+
t.string :processor_plan, null: false
|
40
|
+
t.integer :quantity, default: 1, null: false
|
41
|
+
t.string :status, null: false
|
42
|
+
t.datetime :trial_ends_at
|
43
|
+
t.datetime :ends_at
|
44
|
+
t.decimal :application_fee_percent, precision: 8, scale: 2
|
45
|
+
t.public_send Pay::Adapter.json_column_type, :metadata
|
46
|
+
t.public_send Pay::Adapter.json_column_type, :data
|
47
|
+
t.timestamps
|
48
|
+
end
|
49
|
+
add_index :pay_subscriptions, [:customer_id, :processor_id], unique: true
|
50
|
+
|
51
|
+
create_table :pay_charges do |t|
|
52
|
+
t.belongs_to :customer, foreign_key: {to_table: :pay_customers}, null: false, index: false
|
53
|
+
t.belongs_to :subscription, foreign_key: {to_table: :pay_subscriptions}, null: true
|
54
|
+
t.string :processor_id, null: false
|
55
|
+
t.integer :amount, null: false
|
56
|
+
t.string :currency
|
57
|
+
t.integer :application_fee_amount
|
58
|
+
t.integer :amount_refunded
|
59
|
+
t.public_send Pay::Adapter.json_column_type, :metadata
|
60
|
+
t.public_send Pay::Adapter.json_column_type, :data
|
61
|
+
t.timestamps
|
62
|
+
end
|
63
|
+
add_index :pay_charges, [:customer_id, :processor_id], unique: true
|
64
|
+
|
65
|
+
create_table :pay_webhooks do |t|
|
66
|
+
t.string :processor
|
67
|
+
t.string :event_type
|
68
|
+
t.public_send Pay::Adapter.json_column_type, :event
|
69
|
+
t.timestamps
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -5,6 +5,7 @@ class AddPayBillableTo<%= table_name.camelize %> < ActiveRecord::Migration<%= mi
|
|
5
5
|
change_table :<%= table_name %>, bulk: true do |t|
|
6
6
|
t.string :processor
|
7
7
|
t.string :processor_id
|
8
|
+
t.public_send(Pay::Adapter.json_column_type, :pay_data)
|
8
9
|
t.datetime :trial_ends_at
|
9
10
|
t.string :card_type
|
10
11
|
t.string :card_last4
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Pay
|
2
|
+
# Adds Pay methods to ActiveRecord models
|
3
|
+
|
4
|
+
module Attributes
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module CustomerExtension
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
has_many :pay_customers, class_name: "Pay::Customer", as: :owner, inverse_of: :owner
|
12
|
+
has_many :charges, through: :pay_customers, class_name: "Pay::Charge"
|
13
|
+
has_many :subscriptions, through: :pay_customers, class_name: "Pay::Subscription"
|
14
|
+
has_one :payment_processor, -> { where(default: true, deleted_at: nil) }, class_name: "Pay::Customer", as: :owner, inverse_of: :owner
|
15
|
+
|
16
|
+
after_commit :cancel_active_pay_subscriptions!, on: [:destroy]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Changes a user's payment processor
|
20
|
+
#
|
21
|
+
# This has several effects:
|
22
|
+
# - Finds or creates a Pay::Customer for the process and marks it as default
|
23
|
+
# - Removes the default flag from all other Pay::Customers
|
24
|
+
# - Removes the default flag from all Pay::PaymentMethods
|
25
|
+
def set_payment_processor(processor_name, allow_fake: false, **attributes)
|
26
|
+
raise Pay::Error, "Processor `#{processor_name}` is not allowed" if processor_name.to_s == "fake_processor" && !allow_fake
|
27
|
+
|
28
|
+
ActiveRecord::Base.transaction do
|
29
|
+
pay_customers.update_all(default: false)
|
30
|
+
pay_customer = pay_customers.active.where(processor: processor_name).first_or_initialize
|
31
|
+
pay_customer.update!(attributes.merge(default: true))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return new payment processor
|
35
|
+
reload_payment_processor
|
36
|
+
end
|
37
|
+
|
38
|
+
def cancel_active_pay_subscriptions!
|
39
|
+
subscriptions.active.each(&:cancel_now!)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module MerchantExtension
|
44
|
+
extend ActiveSupport::Concern
|
45
|
+
|
46
|
+
included do
|
47
|
+
has_many :pay_merchants, class_name: "Pay::Merchant", as: :owner, inverse_of: :owner
|
48
|
+
has_one :merchant_processor, -> { where(default: true) }, class_name: "Pay::Merchant", as: :owner, inverse_of: :owner
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_merchant_processor(processor_name, **attributes)
|
52
|
+
ActiveRecord::Base.transaction do
|
53
|
+
pay_merchants.update_all(default: false)
|
54
|
+
pay_merchant = pay_merchants.where(processor: processor_name).first_or_initialize
|
55
|
+
pay_merchant.update!(attributes.merge(default: true))
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return new payment processor
|
59
|
+
reload_merchant_processor
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class_methods do
|
64
|
+
def pay_customer
|
65
|
+
include Billable::SyncCustomer
|
66
|
+
include CustomerExtension
|
67
|
+
end
|
68
|
+
|
69
|
+
def pay_merchant
|
70
|
+
include MerchantExtension
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|