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.

Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -715
  3. data/app/controllers/pay/webhooks/braintree_controller.rb +10 -3
  4. data/app/controllers/pay/webhooks/paddle_controller.rb +7 -8
  5. data/app/controllers/pay/webhooks/stripe_controller.rb +6 -3
  6. data/app/jobs/pay/{email_sync_job.rb → customer_sync_job.rb} +3 -4
  7. data/app/models/pay/application_record.rb +1 -5
  8. data/app/models/pay/charge.rb +54 -17
  9. data/app/models/pay/customer.rb +87 -0
  10. data/app/models/pay/merchant.rb +19 -0
  11. data/app/models/pay/payment_method.rb +41 -0
  12. data/app/models/pay/subscription.rb +42 -30
  13. data/app/models/pay/webhook.rb +36 -0
  14. data/app/views/layouts/pay/application.html.erb +2 -3
  15. data/app/views/pay/payments/show.html.erb +108 -81
  16. data/app/views/pay/user_mailer/receipt.html.erb +2 -2
  17. data/app/views/pay/user_mailer/refund.html.erb +2 -2
  18. data/config/locales/en.yml +1 -1
  19. data/db/migrate/1_create_pay_tables.rb +72 -0
  20. data/lib/generators/active_record/templates/billable_migration.rb +1 -0
  21. data/lib/pay/attributes.rb +74 -0
  22. data/lib/pay/billable/sync_customer.rb +30 -0
  23. data/lib/pay/braintree/billable.rb +133 -110
  24. data/lib/pay/braintree/payment_method.rb +42 -0
  25. data/lib/pay/braintree/subscription.rb +9 -12
  26. data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -1
  27. data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +4 -4
  28. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
  29. data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -1
  30. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +2 -2
  31. data/lib/pay/braintree/webhooks/subscription_went_active.rb +1 -1
  32. data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +1 -1
  33. data/lib/pay/braintree.rb +3 -2
  34. data/lib/pay/engine.rb +6 -1
  35. data/lib/pay/fake_processor/billable.rb +45 -21
  36. data/lib/pay/fake_processor/payment_method.rb +21 -0
  37. data/lib/pay/fake_processor/subscription.rb +11 -8
  38. data/lib/pay/fake_processor.rb +2 -1
  39. data/lib/pay/nano_id.rb +13 -0
  40. data/lib/pay/paddle/billable.rb +18 -48
  41. data/lib/pay/paddle/charge.rb +5 -5
  42. data/lib/pay/paddle/payment_method.rb +60 -0
  43. data/lib/pay/paddle/response.rb +0 -0
  44. data/lib/pay/paddle/subscription.rb +49 -8
  45. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +6 -3
  46. data/lib/pay/paddle/webhooks/subscription_created.rb +1 -40
  47. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
  48. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +26 -28
  49. data/lib/pay/paddle/webhooks/subscription_updated.rb +2 -2
  50. data/lib/pay/paddle.rb +7 -3
  51. data/lib/pay/payment.rb +1 -1
  52. data/lib/pay/receipts.rb +35 -7
  53. data/lib/pay/stripe/billable.rb +75 -76
  54. data/lib/pay/stripe/charge.rb +44 -17
  55. data/lib/pay/stripe/merchant.rb +10 -10
  56. data/lib/pay/stripe/payment_method.rb +61 -0
  57. data/lib/pay/stripe/subscription.rb +55 -22
  58. data/lib/pay/stripe/webhooks/account_updated.rb +2 -3
  59. data/lib/pay/stripe/webhooks/charge_refunded.rb +1 -1
  60. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -2
  61. data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +3 -1
  62. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +3 -1
  63. data/lib/pay/stripe/webhooks/customer_deleted.rb +7 -15
  64. data/lib/pay/stripe/webhooks/customer_updated.rb +10 -3
  65. data/lib/pay/stripe/webhooks/payment_action_required.rb +2 -2
  66. data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +6 -8
  67. data/lib/pay/stripe/webhooks/payment_method_attached.rb +15 -0
  68. data/lib/pay/stripe/webhooks/payment_method_detached.rb +12 -0
  69. data/lib/pay/stripe/webhooks/payment_method_updated.rb +10 -4
  70. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
  71. data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -1
  72. data/lib/pay/stripe/webhooks/subscription_renewing.rb +12 -2
  73. data/lib/pay/stripe.rb +6 -3
  74. data/lib/pay/version.rb +1 -1
  75. data/lib/pay/webhooks/delegator.rb +4 -0
  76. data/lib/pay/webhooks/process_job.rb +9 -0
  77. data/lib/pay/webhooks.rb +1 -0
  78. data/lib/pay.rb +7 -78
  79. data/lib/tasks/pay.rake +20 -0
  80. metadata +23 -36
  81. data/app/models/pay.rb +0 -5
  82. data/db/migrate/20170205020145_create_pay_subscriptions.rb +0 -17
  83. data/db/migrate/20170727235816_create_pay_charges.rb +0 -18
  84. data/db/migrate/20190816015720_add_status_to_pay_subscriptions.rb +0 -14
  85. data/db/migrate/20200603134434_add_data_to_pay_models.rb +0 -6
  86. data/db/migrate/20210309004259_add_data_to_pay_billable.rb +0 -10
  87. data/db/migrate/20210406215234_add_currency_to_pay_charges.rb +0 -5
  88. data/db/migrate/20210406215506_add_application_fee_to_pay_models.rb +0 -7
  89. data/lib/pay/billable/sync_email.rb +0 -40
  90. 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@^1.0/dist/tailwind.min.css" rel="stylesheet">
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-200 leading-normal p-4 h-full">
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 id="app" class="h-screen md:flex md:justify-center md:items-center">
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="flex items-center mb-4 bg-red-100 border border-red-200 px-5 py-2 rounded-lg text-red-500" v-if="errorMessage">
5
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="flex-shrink-0 w-6 h-6">
6
- <path class="fill-current text-red-300" d="M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20z"/>
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">{{ errorMessage }}</span>
14
+ <span data-payment-intent-target="errorMessage" class="ml-3"></span>
11
15
  </p>
12
16
 
13
- <p class="flex items-center mb-4 bg-green-100 border border-green-200 px-5 py-4 rounded-lg text-green-700" v-if="paymentProcessed && successMessage">
14
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="flex-shrink-0 w-6 h-6">
15
- <circle cx="12" cy="12" r="10" class="fill-current text-green-300"/>
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">{{ successMessage }}</span>
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" v-if="! paymentProcessed">
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 v-show="status == 'requires_payment_method'">
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="inline-block text-black bg-gray-200 border border-gray-400 rounded-lg w-full px-4 py-3 mb-3 focus:outline-none" v-model="name">
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="bg-gray-200 border border-gray-400 rounded-lg p-4 mb-6"></div>
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 id="card-button" class="inline-block w-full px-4 py-3 mb-4 text-white rounded-lg bg-blue-400 hover:bg-blue-500" :class="{ 'bg-blue-400': paymentProcessing, 'bg-blue-600': ! paymentProcessing }" @click="confirmPayment" :disabled="paymentProcessing">
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-200 hover:bg-gray-300 text-center text-gray-700 rounded-lg" %>
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
- // We can trigger SCA immediately if this payment requires action
84
- // This makes sure a new SCA subscription doesn't have to put in their card twice
85
- if (this.status == "requires_action") {
86
- this.paymentProcessing = true
87
- this.paymentProcessed = false
88
- stripe.confirmCardPayment(this.clientSecret).then(this.handleConfirmResult.bind(this))
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
- // Setup elements in case the authentication fails and user needs to put in a new card
92
- const elements = stripe.elements();
93
- this.cardElement = elements.create('card');
94
- this.cardElement.mount('#card-element');
95
- },
96
-
97
- methods: {
98
- confirmPayment: function () {
99
- this.paymentProcessing = true
100
- this.paymentProcessed = false
101
- this.successMessage = ''
102
- this.errorMessage = ''
103
-
104
- stripe.confirmCardPayment(
105
- this.clientSecret,
106
- {
107
- payment_method: {
108
- card: this.cardElement,
109
- billing_details: { name: this.name }
110
- },
111
- save_payment_method: true,
112
- setup_future_usage: 'off_session',
113
- }
114
- ).then(this.handleConfirmResult.bind(this))
115
- },
116
-
117
- handleConfirmResult(result) {
118
- this.paymentProcessing = false;
119
- if (result.error) {
120
- if (result.error.code === 'parameter_invalid_empty' &&
121
- result.error.param === 'payment_method_data[billing_details][name]') {
122
- this.errorMessage = '<%=t "requires_action.name_missing" %>'
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.paymentProcessed = true;
129
- this.successMessage = '<%=t "requires_action.success" %>'
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/>
@@ -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: 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