effective_orders 4.5.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.
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