effective_orders 4.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1004 -0
  4. data/app/assets/images/effective_orders/stripe.png +0 -0
  5. data/app/assets/javascripts/effective_orders.js +6 -0
  6. data/app/assets/javascripts/effective_orders/customers.js.coffee +32 -0
  7. data/app/assets/javascripts/effective_orders/providers/stripe.js.coffee +77 -0
  8. data/app/assets/javascripts/effective_orders/subscriptions.js.coffee +81 -0
  9. data/app/assets/stylesheets/effective_orders.scss +2 -0
  10. data/app/assets/stylesheets/effective_orders/_cart.scss +4 -0
  11. data/app/assets/stylesheets/effective_orders/_order.scss +58 -0
  12. data/app/controllers/admin/customers_controller.rb +24 -0
  13. data/app/controllers/admin/order_items_controller.rb +16 -0
  14. data/app/controllers/admin/orders_controller.rb +223 -0
  15. data/app/controllers/effective/carts_controller.rb +85 -0
  16. data/app/controllers/effective/concerns/purchase.rb +62 -0
  17. data/app/controllers/effective/customers_controller.rb +20 -0
  18. data/app/controllers/effective/orders_controller.rb +162 -0
  19. data/app/controllers/effective/providers/cheque.rb +22 -0
  20. data/app/controllers/effective/providers/free.rb +33 -0
  21. data/app/controllers/effective/providers/mark_as_paid.rb +33 -0
  22. data/app/controllers/effective/providers/moneris.rb +60 -0
  23. data/app/controllers/effective/providers/paypal.rb +33 -0
  24. data/app/controllers/effective/providers/phone.rb +22 -0
  25. data/app/controllers/effective/providers/pretend.rb +26 -0
  26. data/app/controllers/effective/providers/refund.rb +33 -0
  27. data/app/controllers/effective/providers/stripe.rb +72 -0
  28. data/app/controllers/effective/subscripter_controller.rb +18 -0
  29. data/app/controllers/effective/webhooks_controller.rb +109 -0
  30. data/app/datatables/admin/effective_customers_datatable.rb +22 -0
  31. data/app/datatables/admin/effective_orders_datatable.rb +100 -0
  32. data/app/datatables/effective_orders_datatable.rb +79 -0
  33. data/app/helpers/effective_carts_helper.rb +113 -0
  34. data/app/helpers/effective_orders_helper.rb +143 -0
  35. data/app/helpers/effective_paypal_helper.rb +49 -0
  36. data/app/helpers/effective_stripe_helper.rb +85 -0
  37. data/app/helpers/effective_subscriptions_helper.rb +34 -0
  38. data/app/mailers/effective/orders_mailer.rb +196 -0
  39. data/app/models/concerns/acts_as_purchasable.rb +118 -0
  40. data/app/models/concerns/acts_as_subscribable.rb +90 -0
  41. data/app/models/concerns/acts_as_subscribable_buyer.rb +49 -0
  42. data/app/models/effective/access_denied.rb +17 -0
  43. data/app/models/effective/cart.rb +88 -0
  44. data/app/models/effective/cart_item.rb +40 -0
  45. data/app/models/effective/customer.rb +92 -0
  46. data/app/models/effective/order.rb +541 -0
  47. data/app/models/effective/order_item.rb +63 -0
  48. data/app/models/effective/product.rb +23 -0
  49. data/app/models/effective/sold_out_validator.rb +7 -0
  50. data/app/models/effective/subscripter.rb +185 -0
  51. data/app/models/effective/subscription.rb +95 -0
  52. data/app/models/effective/tax_rate_calculator.rb +48 -0
  53. data/app/views/admin/customers/_actions.html.haml +2 -0
  54. data/app/views/admin/customers/index.html.haml +6 -0
  55. data/app/views/admin/customers/show.html.haml +6 -0
  56. data/app/views/admin/order_items/index.html.haml +3 -0
  57. data/app/views/admin/orders/_datatable_actions.html.haml +18 -0
  58. data/app/views/admin/orders/_form.html.haml +35 -0
  59. data/app/views/admin/orders/_form_note_internal.html.haml +7 -0
  60. data/app/views/admin/orders/_order_actions.html.haml +9 -0
  61. data/app/views/admin/orders/_order_item_fields.html.haml +14 -0
  62. data/app/views/admin/orders/checkout.html.haml +3 -0
  63. data/app/views/admin/orders/edit.html.haml +6 -0
  64. data/app/views/admin/orders/index.html.haml +6 -0
  65. data/app/views/admin/orders/new.html.haml +4 -0
  66. data/app/views/admin/orders/show.html.haml +4 -0
  67. data/app/views/effective/carts/_cart.html.haml +28 -0
  68. data/app/views/effective/carts/_cart_actions.html.haml +3 -0
  69. data/app/views/effective/carts/show.html.haml +17 -0
  70. data/app/views/effective/customers/_customer.html.haml +72 -0
  71. data/app/views/effective/customers/_form.html.haml +21 -0
  72. data/app/views/effective/customers/edit.html.haml +4 -0
  73. data/app/views/effective/customers/update.js.erb +5 -0
  74. data/app/views/effective/orders/_checkout_actions.html.haml +3 -0
  75. data/app/views/effective/orders/_checkout_step1.html.haml +4 -0
  76. data/app/views/effective/orders/_checkout_step2.html.haml +37 -0
  77. data/app/views/effective/orders/_datatable_actions.html.haml +2 -0
  78. data/app/views/effective/orders/_fields.html.haml +31 -0
  79. data/app/views/effective/orders/_fields_note.html.haml +7 -0
  80. data/app/views/effective/orders/_fields_terms.html.haml +8 -0
  81. data/app/views/effective/orders/_order.html.haml +11 -0
  82. data/app/views/effective/orders/_order_actions.html.haml +18 -0
  83. data/app/views/effective/orders/_order_deferred.html.haml +9 -0
  84. data/app/views/effective/orders/_order_footer.html.haml +1 -0
  85. data/app/views/effective/orders/_order_header.html.haml +23 -0
  86. data/app/views/effective/orders/_order_items.html.haml +72 -0
  87. data/app/views/effective/orders/_order_notes.html.haml +17 -0
  88. data/app/views/effective/orders/_order_payment.html.haml +24 -0
  89. data/app/views/effective/orders/_order_shipping.html.haml +30 -0
  90. data/app/views/effective/orders/_orders_table.html.haml +23 -0
  91. data/app/views/effective/orders/cheque/_form.html.haml +4 -0
  92. data/app/views/effective/orders/declined.html.haml +12 -0
  93. data/app/views/effective/orders/deferred.html.haml +13 -0
  94. data/app/views/effective/orders/deferred/_form.html.haml +16 -0
  95. data/app/views/effective/orders/edit.html.haml +3 -0
  96. data/app/views/effective/orders/free/_form.html.haml +5 -0
  97. data/app/views/effective/orders/index.html.haml +3 -0
  98. data/app/views/effective/orders/mark_as_paid/_form.html.haml +23 -0
  99. data/app/views/effective/orders/moneris/_form.html.haml +47 -0
  100. data/app/views/effective/orders/new.html.haml +3 -0
  101. data/app/views/effective/orders/paypal/_form.html.haml +5 -0
  102. data/app/views/effective/orders/phone/_form.html.haml +4 -0
  103. data/app/views/effective/orders/pretend/_form.html.haml +8 -0
  104. data/app/views/effective/orders/purchased.html.haml +11 -0
  105. data/app/views/effective/orders/refund/_form.html.haml +5 -0
  106. data/app/views/effective/orders/show.html.haml +6 -0
  107. data/app/views/effective/orders/stripe/_element.html.haml +8 -0
  108. data/app/views/effective/orders/stripe/_form.html.haml +31 -0
  109. data/app/views/effective/orders_mailer/order_error.html.haml +11 -0
  110. data/app/views/effective/orders_mailer/order_receipt_to_admin.html.haml +2 -0
  111. data/app/views/effective/orders_mailer/order_receipt_to_buyer.html.haml +2 -0
  112. data/app/views/effective/orders_mailer/payment_request_to_buyer.html.haml +13 -0
  113. data/app/views/effective/orders_mailer/pending_order_invoice_to_buyer.html.haml +13 -0
  114. data/app/views/effective/orders_mailer/refund_notification_to_admin.html.haml +15 -0
  115. data/app/views/effective/orders_mailer/subscription_canceled.html.haml +9 -0
  116. data/app/views/effective/orders_mailer/subscription_created.html.haml +13 -0
  117. data/app/views/effective/orders_mailer/subscription_event_to_admin.html.haml +13 -0
  118. data/app/views/effective/orders_mailer/subscription_payment_failed.html.haml +9 -0
  119. data/app/views/effective/orders_mailer/subscription_payment_succeeded.html.haml +9 -0
  120. data/app/views/effective/orders_mailer/subscription_trial_expired.html.haml +5 -0
  121. data/app/views/effective/orders_mailer/subscription_trialing.html.haml +7 -0
  122. data/app/views/effective/orders_mailer/subscription_updated.html.haml +13 -0
  123. data/app/views/effective/subscripter/_form.html.haml +60 -0
  124. data/app/views/effective/subscripter/_plan.html.haml +23 -0
  125. data/app/views/layouts/effective_orders_mailer_layout.html.haml +25 -0
  126. data/config/effective_orders.rb +279 -0
  127. data/config/routes.rb +70 -0
  128. data/db/migrate/01_create_effective_orders.rb.erb +137 -0
  129. data/lib/effective_orders.rb +243 -0
  130. data/lib/effective_orders/engine.rb +60 -0
  131. data/lib/effective_orders/version.rb +3 -0
  132. data/lib/generators/effective_orders/install_generator.rb +63 -0
  133. data/lib/generators/templates/effective_orders_mailer_preview.rb +120 -0
  134. data/lib/tasks/effective_orders_tasks.rake +69 -0
  135. metadata +276 -0
@@ -0,0 +1,70 @@
1
+ EffectiveOrders::Engine.routes.draw do
2
+ scope :module => 'effective' do
3
+ resources :orders, except: [:destroy] do
4
+ member do
5
+ get :purchased
6
+ get :deferred
7
+ get :declined
8
+ get :send_buyer_receipt
9
+
10
+ post :free if EffectiveOrders.free?
11
+ post :mark_as_paid if EffectiveOrders.mark_as_paid?
12
+ post :cheque if EffectiveOrders.cheque?
13
+ post :phone if EffectiveOrders.phone?
14
+ post :pretend if EffectiveOrders.pretend?
15
+ post :refund if EffectiveOrders.refund?
16
+ post :stripe if EffectiveOrders.stripe?
17
+ end
18
+
19
+ collection do
20
+ post :bulk_send_buyer_receipt
21
+
22
+ post :moneris_postback if EffectiveOrders.moneris?
23
+ post :paypal_postback if EffectiveOrders.paypal?
24
+ end
25
+ end
26
+
27
+ post 'orders/:id', to: 'orders#update'
28
+
29
+ if EffectiveOrders.subscriptions?
30
+ match 'subscribe', to: 'subscripter#update', via: :post, as: :subscripter
31
+
32
+ match 'customer/settings', to: 'customers#edit', as: :customer_settings, via: [:get]
33
+ match 'customer/settings', to: 'customers#update', via: [:patch, :put, :post]
34
+ match 'webhooks/stripe', to: 'webhooks#stripe', via: [:get, :post, :put]
35
+ end
36
+
37
+ match 'cart', to: 'carts#show', as: 'cart', via: :get
38
+ match 'cart', to: 'carts#destroy', via: :delete
39
+
40
+ # If you Tweak this route, please update EffectiveOrdersHelper too
41
+ match 'cart/:purchasable_type/:purchasable_id', to: 'carts#add_to_cart', via: [:get, :post], as: 'add_to_cart'
42
+ match 'cart/:id', to: 'carts#remove_from_cart', via: [:delete], as: 'remove_from_cart'
43
+ end
44
+
45
+ namespace :admin do
46
+ resources :customers, only: [:index, :show]
47
+
48
+ resources :orders do
49
+ member do
50
+ post :send_payment_request
51
+
52
+ get :checkout
53
+ post :checkout
54
+ patch :checkout
55
+ end
56
+
57
+ collection do
58
+ post :bulk_send_payment_request
59
+ end
60
+ end
61
+
62
+ post 'orders/:id', to: 'orders#update'
63
+
64
+ resources :order_items, only: [:index]
65
+ end
66
+ end
67
+
68
+ Rails.application.routes.draw do
69
+ mount EffectiveOrders::Engine => '/', as: 'effective_orders'
70
+ end
@@ -0,0 +1,137 @@
1
+ class CreateEffectiveOrders < ActiveRecord::Migration[4.2]
2
+ def self.up
3
+ create_table <%= @orders_table_name %> do |t|
4
+ t.integer :user_id
5
+
6
+ t.integer :parent_id
7
+ t.string :parent_type
8
+
9
+ t.string :state
10
+ t.datetime :purchased_at
11
+
12
+ t.text :note
13
+ t.text :note_to_buyer
14
+ t.text :note_internal
15
+
16
+ t.string :billing_name
17
+ t.string :email
18
+ t.string :cc
19
+
20
+ t.text :payment
21
+ t.string :payment_provider
22
+ t.string :payment_card
23
+
24
+ t.decimal :tax_rate, :precision => 6, :scale => 3
25
+
26
+ t.integer :subtotal
27
+ t.integer :tax
28
+ t.integer :total
29
+
30
+ t.timestamps
31
+ end
32
+
33
+ add_index <%= @orders_table_name %>, :user_id
34
+
35
+
36
+ create_table <%= @order_items_table_name %> do |t|
37
+ t.integer :order_id
38
+ t.string :purchasable_type
39
+ t.integer :purchasable_id
40
+
41
+ t.string :name
42
+ t.integer :quantity
43
+ t.integer :price, :default => 0
44
+ t.boolean :tax_exempt
45
+
46
+ t.timestamps
47
+ end
48
+
49
+ add_index <%= @order_items_table_name %>, :order_id
50
+ add_index <%= @order_items_table_name %>, :purchasable_id
51
+ add_index <%= @order_items_table_name %>, [:purchasable_type, :purchasable_id]
52
+
53
+
54
+ create_table <%= @carts_table_name %> do |t|
55
+ t.integer :user_id
56
+ t.integer :cart_items_count, :default => 0
57
+ t.timestamps
58
+ end
59
+
60
+ add_index <%= @carts_table_name %>, :user_id
61
+
62
+ create_table <%= @cart_items_table_name %> do |t|
63
+ t.integer :cart_id
64
+ t.string :purchasable_type
65
+ t.integer :purchasable_id
66
+
67
+ t.string :unique
68
+ t.integer :quantity
69
+
70
+ t.timestamps
71
+ end
72
+
73
+ add_index <%= @cart_items_table_name %>, :cart_id
74
+ add_index <%= @cart_items_table_name %>, :purchasable_id
75
+ add_index <%= @cart_items_table_name %>, [:purchasable_type, :purchasable_id]
76
+
77
+ create_table <%= @customers_table_name %> do |t|
78
+ t.integer :user_id
79
+
80
+ t.string :stripe_customer_id
81
+ t.string :payment_method_id
82
+ t.string :active_card
83
+ t.string :status
84
+
85
+ t.integer :subscriptions_count, :default => 0
86
+
87
+ t.timestamps
88
+ end
89
+
90
+ add_index <%= @customers_table_name %>, :user_id
91
+
92
+ create_table <%= @subscriptions_table_name %> do |t|
93
+ t.integer :customer_id
94
+
95
+ t.integer :subscribable_id
96
+ t.string :subscribable_type
97
+
98
+ t.string :stripe_plan_id
99
+ t.string :stripe_subscription_id
100
+
101
+ t.string :name
102
+ t.string :description
103
+ t.string :interval
104
+ t.integer :quantity
105
+ t.string :status
106
+
107
+ t.timestamps
108
+ end
109
+
110
+ add_index <%= @subscriptions_table_name %>, :customer_id
111
+ add_index <%= @subscriptions_table_name %>, :subscribable_id
112
+ add_index <%= @subscriptions_table_name %>, [:subscribable_type, :subscribable_id]
113
+
114
+ create_table <%= @products_table_name %> do |t|
115
+ t.integer :purchased_order_id
116
+
117
+ t.string :name
118
+ t.integer :price
119
+ t.boolean :tax_exempt, :default => false
120
+
121
+ t.string :qb_item_name
122
+
123
+ t.timestamps
124
+ end
125
+
126
+ end
127
+
128
+ def self.down
129
+ drop_table <%= @orders_table_name %>
130
+ drop_table <%= @order_items_table_name %>
131
+ drop_table <%= @carts_table_name %>
132
+ drop_table <%= @cart_items_table_name %>
133
+ drop_table <%= @customers_table_name %>
134
+ drop_table <%= @subscriptions_table_name %>
135
+ drop_table <%= @products_table_name %>
136
+ end
137
+ end
@@ -0,0 +1,243 @@
1
+ require 'effective_addresses'
2
+ require 'effective_orders/engine'
3
+ require 'effective_orders/version'
4
+
5
+ module EffectiveOrders
6
+ PENDING = 'pending'.freeze # New orders are created in a pending state
7
+ CONFIRMED = 'confirmed'.freeze # Once the order has passed checkout step 1
8
+ DEFERRED = 'deferred'.freeze # Deferred providers. Cheque or Phone was selected.
9
+ PURCHASED = 'purchased'.freeze # Purchased by provider
10
+ DECLINED = 'declined'.freeze # Declined by provider
11
+
12
+ STATES = { PENDING => PENDING, CONFIRMED => CONFIRMED, DEFERRED => DEFERRED, PURCHASED => PURCHASED, DECLINED => DECLINED }
13
+
14
+ # Subscription statuses (as per stripe)
15
+ ACTIVE = 'active'.freeze
16
+ PAST_DUE = 'past_due'.freeze
17
+ CANCELED = 'canceled'.freeze
18
+
19
+ STATUSES = { ACTIVE => ACTIVE, PAST_DUE => PAST_DUE, CANCELED => CANCELED }
20
+
21
+ # The following are all valid config keys
22
+ mattr_accessor :orders_table_name
23
+ mattr_accessor :order_items_table_name
24
+ mattr_accessor :carts_table_name
25
+ mattr_accessor :cart_items_table_name
26
+ mattr_accessor :customers_table_name
27
+ mattr_accessor :subscriptions_table_name
28
+ mattr_accessor :products_table_name
29
+
30
+ mattr_accessor :authorization_method
31
+
32
+ mattr_accessor :layout
33
+ mattr_accessor :mailer
34
+
35
+ mattr_accessor :orders_collection_scope
36
+ mattr_accessor :order_tax_rate_method
37
+
38
+ mattr_accessor :obfuscate_order_ids
39
+ mattr_accessor :billing_address
40
+ mattr_accessor :shipping_address
41
+ mattr_accessor :use_address_full_name
42
+
43
+ mattr_accessor :collect_note
44
+ mattr_accessor :collect_note_required
45
+ mattr_accessor :collect_note_message
46
+
47
+ mattr_accessor :terms_and_conditions
48
+ mattr_accessor :terms_and_conditions_label
49
+
50
+ mattr_accessor :minimum_charge
51
+
52
+ # Features
53
+ mattr_accessor :free_enabled
54
+ mattr_accessor :mark_as_paid_enabled
55
+ mattr_accessor :pretend_enabled
56
+ mattr_accessor :pretend_message
57
+
58
+ # Payment processors. false or Hash
59
+ mattr_accessor :cheque
60
+ mattr_accessor :moneris
61
+ mattr_accessor :paypal
62
+ mattr_accessor :phone
63
+ mattr_accessor :refund
64
+ mattr_accessor :stripe
65
+ mattr_accessor :subscriptions # Stripe subscriptions
66
+ mattr_accessor :trial # Trial mode
67
+
68
+ def self.setup
69
+ yield self
70
+ end
71
+
72
+ def self.authorized?(controller, action, resource)
73
+ @_exceptions ||= [Effective::AccessDenied, (CanCan::AccessDenied if defined?(CanCan)), (Pundit::NotAuthorizedError if defined?(Pundit))].compact
74
+
75
+ return !!authorization_method unless authorization_method.respond_to?(:call)
76
+ controller = controller.controller if controller.respond_to?(:controller)
77
+
78
+ begin
79
+ !!(controller || self).instance_exec((controller || self), action, resource, &authorization_method)
80
+ rescue *@_exceptions
81
+ false
82
+ end
83
+ end
84
+
85
+ def self.authorize!(controller, action, resource)
86
+ raise Effective::AccessDenied.new('Access Denied', action, resource) unless authorized?(controller, action, resource)
87
+ end
88
+
89
+ def self.permitted_params
90
+ [
91
+ :cc, :note, :terms_and_conditions, :confirmed_checkout,
92
+ billing_address: EffectiveAddresses.permitted_params,
93
+ shipping_address: EffectiveAddresses.permitted_params,
94
+ subscripter: [:stripe_plan_id, :stripe_token]
95
+ ]
96
+ end
97
+
98
+ def self.cheque?
99
+ cheque.kind_of?(Hash)
100
+ end
101
+
102
+ def self.free?
103
+ free_enabled == true
104
+ end
105
+
106
+ def self.deferred?
107
+ deferred_providers.present?
108
+ end
109
+
110
+ def self.mark_as_paid?
111
+ mark_as_paid_enabled == true
112
+ end
113
+
114
+ def self.moneris?
115
+ moneris.kind_of?(Hash)
116
+ end
117
+
118
+ def self.paypal?
119
+ paypal.kind_of?(Hash)
120
+ end
121
+
122
+ def self.phone?
123
+ phone.kind_of?(Hash)
124
+ end
125
+
126
+ def self.pretend?
127
+ pretend_enabled == true
128
+ end
129
+
130
+ def self.refund?
131
+ refund.kind_of?(Hash)
132
+ end
133
+
134
+ def self.stripe?
135
+ stripe.kind_of?(Hash)
136
+ end
137
+
138
+ def self.subscriptions?
139
+ subscriptions.kind_of?(Hash)
140
+ end
141
+
142
+ def self.trial?
143
+ trial.kind_of?(Hash)
144
+ end
145
+
146
+ def self.single_payment_processor?
147
+ [moneris?, paypal?, stripe?].select { |enabled| enabled }.length == 1
148
+ end
149
+
150
+ # The Effective::Order.payment_provider value must be in this collection
151
+ def self.payment_providers
152
+ [
153
+ ('cheque' if cheque?),
154
+ ('credit card' if mark_as_paid?),
155
+ ('free' if free?),
156
+ ('moneris' if moneris?),
157
+ ('paypal' if paypal?),
158
+ ('phone' if phone?),
159
+ ('pretend' if pretend?),
160
+ ('refund' if refund?),
161
+ ('stripe' if stripe?),
162
+ ('other' if mark_as_paid?),
163
+ 'none'
164
+ ].compact
165
+ end
166
+
167
+ def self.deferred_providers
168
+ [('cheque' if cheque?), ('phone' if phone?)].compact
169
+ end
170
+
171
+ def self.can_skip_checkout_step1?
172
+ return false if require_billing_address
173
+ return false if require_shipping_address
174
+ return false if collect_note
175
+ return false if terms_and_conditions
176
+ true
177
+ end
178
+
179
+ def self.stripe_plans
180
+ return [] unless (stripe? && subscriptions?)
181
+
182
+ @stripe_plans ||= (
183
+ Rails.logger.info '[STRIPE] index plans'
184
+
185
+ plans = begin
186
+ Stripe::Plan.respond_to?(:all) ? Stripe::Plan.all : Stripe::Plan.list
187
+ rescue => e
188
+ raise e if Rails.env.production?
189
+ Rails.logger.info "[STRIPE ERROR]: #{e.message}"
190
+ Rails.logger.info "[STRIPE ERROR]: effective_orders continuing with empty stripe plans. This would fail loudly in Rails.env.production."
191
+ []
192
+ end
193
+
194
+ plans = plans.map do |plan|
195
+ description = ("$#{'%0.2f' % (plan.amount / 100.0)}" + ' ' + plan.currency.upcase + '/' + plan.interval.to_s)
196
+
197
+ {
198
+ id: plan.id,
199
+ product_id: plan.product,
200
+ name: plan.nickname || description,
201
+ description: description,
202
+ amount: plan.amount,
203
+ currency: plan.currency,
204
+ interval: plan.interval,
205
+ interval_count: plan.interval_count,
206
+ trial_period_days: plan.trial_period_days
207
+ }
208
+ end.sort do |x, y|
209
+ val ||= (x[:interval] <=> y[:interval])
210
+ val = nil if val == 0
211
+
212
+ val ||= (x[:amount] <=> y[:amount])
213
+ val = nil if val == 0
214
+
215
+ val ||= (x[:name] <=> y[:name])
216
+ val = nil if val == 0
217
+
218
+ val || (x[:id] <=> y[:id])
219
+ end
220
+
221
+ # Calculate savings for any yearly per user plans, based on their matching monthly plans
222
+ plans.select { |plan| plan[:interval] == 'year' }.each do |yearly|
223
+ monthly_name = yearly[:name].downcase.gsub('year', 'month')
224
+ monthly = plans.find { |plan| plan[:interval] == 'month' && plan[:name].downcase == monthly_name }
225
+ next unless monthly
226
+
227
+ savings = (monthly[:amount].to_i * 12) - yearly[:amount].to_i
228
+ next unless savings > 0
229
+
230
+ yearly[:savings] = savings
231
+ end
232
+
233
+ plans
234
+ )
235
+ end
236
+
237
+ def self.stripe_plans_collection
238
+ stripe_plans.map { |plan| [plan[:name], plan[:id]] }
239
+ end
240
+
241
+ class SoldOutException < Exception; end
242
+ class AlreadyPurchasedException < Exception; end
243
+ end