relish-billing 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,107 @@
1
+ module Relish
2
+ module Billing
3
+
4
+ class CardDetails
5
+ attr_reader :first_name, :last_name, :address1, :address2, :city, :state, :zip, :country, :number, :expiry_month, :expiry_year, :cvv
6
+ def initialize(params = {})
7
+ @first_name = params.fetch :first_name, ''
8
+ @last_name = params.fetch :last_name, ''
9
+ @address1 = params.fetch :address1, ''
10
+ @address2 = params.fetch :address2, ''
11
+ @city = params.fetch :city, ''
12
+ @state = params.fetch :state, ''
13
+ @zip = params.fetch :zip, ''
14
+ @country = params.fetch :country, ''
15
+ @number = params.fetch :number, ''
16
+ @expiry_month = params.fetch :expiry_month, ''
17
+ @expiry_year = params.fetch :expiry_year, ''
18
+ @cvv = params.fetch :cvv, ''
19
+ end
20
+ end
21
+
22
+ class MaskedCardDetails
23
+ attr_reader :last_four
24
+
25
+ def initialize(params)
26
+ @last_four = params.fetch :last_four
27
+ end
28
+ end
29
+
30
+ require 'date'
31
+ class UserProfile
32
+ attr_reader :email, :id, :plan
33
+ def initialize(params)
34
+ @email = params.fetch :email
35
+ @id = params.fetch :id
36
+ @plan = params.fetch :plan
37
+ @trial_end_date = params.fetch :trial_end_date
38
+ end
39
+
40
+ def trial_end_date
41
+ realistic_trial_end_date
42
+ end
43
+
44
+ def with_plan(new_plan)
45
+ UserProfile.new(
46
+ email: email,
47
+ id: id,
48
+ plan: new_plan,
49
+ trial_end_date: trial_end_date)
50
+ end
51
+
52
+ private
53
+
54
+ def realistic_trial_end_date
55
+ return Date.today if @trial_end_date < Date.today
56
+ @trial_end_date
57
+ end
58
+ end
59
+
60
+ class AccountStatus
61
+ attr_reader :plan_id, :card_details, :next_billing_date
62
+ def initialize(params)
63
+ @plan_id = params.fetch :plan_id
64
+ @card_details = params.fetch :card_details
65
+ @next_billing_date = params.fetch :next_billing_date
66
+ @non_renewing = params.fetch :non_renewing
67
+ @active = params.fetch :active
68
+ end
69
+
70
+ # If this flag is true, it means that the user has
71
+ # cancelled their subscription, but not yet hit the end
72
+ # of their billing period.
73
+ #
74
+ # So they can still access their account for the time being,
75
+ # but they should be told that their account will go
76
+ # back to free plan at the end of their billing cycle.
77
+ def non_renewing?
78
+ @non_renewing
79
+ end
80
+
81
+ # This this flag is true, it means the user has a
82
+ # paid account that's all up and running normally
83
+ def active?
84
+ @active
85
+ end
86
+
87
+ end
88
+
89
+ CardDeclinedError = Class.new(StandardError) do
90
+ attr_reader :reason
91
+
92
+ def initialize(message = nil)
93
+ @reason = Reason.new(message)
94
+ end
95
+
96
+ Reason = Struct.new(:message) do
97
+ def append_to(string)
98
+ string << message if message
99
+ end
100
+ end
101
+ end
102
+
103
+ InvalidPlanError = Class.new(StandardError)
104
+ AccountNotFoundError = Class.new(StandardError)
105
+
106
+ end
107
+ end
@@ -0,0 +1,101 @@
1
+ require_relative 'entities'
2
+
3
+ module Relish
4
+ module Billing
5
+ class FakeDriver
6
+ def fixtures
7
+ default = { expiry_month: '', expiry_year: '', cvv: '', country: 'BE' }
8
+ @fixtures ||= {
9
+ valid_credit_card: CardDetails.new(default.merge(number: '1111111111111111')),
10
+ invalid_credit_card: CardDetails.new(default.merge(number: '*INVALIDINVALID*')),
11
+ other_valid_credit_card: CardDetails.new(default.merge(number: '1111222233334444')),
12
+ }
13
+ end
14
+
15
+ def clear_test_data
16
+ @plans = nil
17
+ @cards = nil
18
+ end
19
+
20
+ def set_plan(profile)
21
+ ensure_account_for(profile)
22
+ plans[profile.id] = profile.plan
23
+ self
24
+ end
25
+
26
+ def save_card(profile, card)
27
+ ensure_account_for(profile)
28
+ raise CardDeclinedError if invalid?(card)
29
+ cards[profile.id] = card
30
+ self
31
+ end
32
+
33
+ def cancel_subscription(profile_id)
34
+ plans.delete(profile_id)
35
+ cards.delete(profile_id)
36
+ self
37
+ end
38
+
39
+ def account_status(profile)
40
+ ensure_account_for(profile)
41
+ plan = plans.fetch(profile.id)
42
+ if active?(profile)
43
+ card_details = mask(cards[profile.id])
44
+ else
45
+ card_details = CardDetails.new
46
+ end
47
+ AccountStatus.new plan_id: plan,
48
+ card_details: card_details,
49
+ next_billing_date: next_billing_date(profile),
50
+ non_renewing: true,
51
+ active: active?(profile)
52
+ end
53
+
54
+ private
55
+
56
+ def next_billing_date(profile)
57
+ if active?(profile) && (profile.trial_end_date == Date.today)
58
+ Date.today.next_month
59
+ else
60
+ profile.trial_end_date
61
+ end
62
+ end
63
+
64
+ def ensure_account_for(profile)
65
+ return if plans.key?(profile.id)
66
+ plans[profile.id] = profile.plan
67
+ end
68
+
69
+ def invalid?(card)
70
+ card == fixtures.fetch(:invalid_credit_card)
71
+ end
72
+
73
+ def active?(profile)
74
+ cards.key?(profile.id)
75
+ end
76
+
77
+ def mask(card)
78
+ MaskedCardDetails.new(
79
+ last_four: last_four_from(card))
80
+ end
81
+
82
+ def last_four_from(card)
83
+ card.number[12..-1]
84
+ end
85
+
86
+ def cards
87
+ @cards ||= {}
88
+ end
89
+
90
+ def plans
91
+ @plans ||= {}
92
+ end
93
+
94
+ class NullCard
95
+ def number
96
+ '?' * 16
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,340 @@
1
+ require 'recurly'
2
+ require_relative 'entities'
3
+
4
+ module Relish
5
+ module Billing
6
+ class RecurlyApiDriver
7
+
8
+ def self.configure
9
+ yield config
10
+ end
11
+
12
+ def self.config
13
+ @config ||= Config.new
14
+ end
15
+
16
+ def clear_test_data
17
+ self.class.config.environment.clear_test_data
18
+ end
19
+
20
+ def fixtures
21
+ card = lambda do |number|
22
+ CardDetails.new(first_name: 'Test first name',
23
+ last_name: 'Test last name',
24
+ number: number,
25
+ cvv: '123',
26
+ expiry_month: 11,
27
+ expiry_year: 2015)
28
+ end
29
+
30
+ {
31
+ valid_credit_card: card['4111111111111111'],
32
+ invalid_credit_card: card['4000000000000002'],
33
+ other_valid_credit_card: card['378282246310005'],
34
+ }
35
+ end
36
+
37
+ def set_plan(profile)
38
+ ensure_account(profile) do |account|
39
+ ensure_subscription(profile, account) do |subscription|
40
+ begin
41
+ subscription.update_attributes! plan_code: profile.plan
42
+ rescue Recurly::Resource::Invalid
43
+ raise InvalidPlanError
44
+ end
45
+ end
46
+ end
47
+ self
48
+ end
49
+
50
+ def save_card(profile, card)
51
+ ensure_account(profile) do |account|
52
+ account.billing_info = build.billing_info(card)
53
+ account.billing_info.save!
54
+ end
55
+ set_plan(profile)
56
+ rescue Recurly::Transaction::DeclinedError,
57
+ Recurly::Resource::Invalid => exception
58
+ raise CardDeclinedError, exception.message
59
+ rescue ArgumentError,
60
+ NoMethodError
61
+ raise CardDeclinedError
62
+ ensure
63
+ self
64
+ end
65
+
66
+ def cancel_subscription(profile)
67
+ ensure_account(profile) do |account|
68
+ ensure_subscription(profile, account) do |subscription|
69
+ subscription.cancel
70
+ end
71
+ end
72
+ self
73
+ end
74
+
75
+ def account_status(profile)
76
+ ensure_account(profile) do |account|
77
+ ensure_subscription(profile, account) do |subscription|
78
+ return build.account_status(subscription, card_details(account))
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def card_details(account)
86
+ return CardDetails.new unless account.billing_info
87
+ build.masked_card(account.billing_info)
88
+ end
89
+
90
+ def ensure_account(profile)
91
+ yield Recurly::Account.find(profile.id)
92
+ rescue Recurly::Resource::NotFound
93
+ yield Recurly::Account.create! build.account(profile)
94
+ ensure
95
+ self
96
+ end
97
+
98
+ def ensure_subscription(profile, account)
99
+ unless account.billing_info
100
+ yield NoSubscription.new(profile) if block_given?
101
+ return self
102
+ end
103
+
104
+ unless account.subscriptions.any?
105
+ Recurly::Subscription.create! build.subscription(profile, account)
106
+ account.reload
107
+ end
108
+
109
+ yield account.subscriptions.first if block_given?
110
+ self
111
+ end
112
+
113
+ def build
114
+ Module.new do
115
+ extend self
116
+
117
+ def account(profile)
118
+ {
119
+ account_code: profile.id,
120
+ email: profile.email
121
+ }
122
+ end
123
+
124
+ def subscription(profile, account)
125
+ {
126
+ plan_code: profile.plan,
127
+ currency: 'USD',
128
+ account: account,
129
+ trial_ends_at: profile.trial_end_date
130
+ }
131
+ end
132
+
133
+ def billing_info(card)
134
+ {
135
+ first_name: card.first_name,
136
+ last_name: card.last_name,
137
+ number: card.number,
138
+ month: card.expiry_month,
139
+ year: card.expiry_year,
140
+ verification_value: card.cvv,
141
+ address1: card.address1,
142
+ address2: card.address2,
143
+ city: card.city,
144
+ state: card.state,
145
+ zip: card.zip,
146
+ country: card.country
147
+ }
148
+ end
149
+
150
+ def masked_card(billing_info)
151
+ MaskedCardDetails.new(
152
+ last_four: billing_info.last_four
153
+ )
154
+ end
155
+
156
+ def account_status(subscription, card_details)
157
+ AccountStatus.new(
158
+ plan_id: subscription.plan.plan_code,
159
+ card_details: card_details,
160
+ next_billing_date: subscription.current_period_ends_at.to_date,
161
+ non_renewing: subscription.state == 'canceled',
162
+ active: subscription.state == 'active')
163
+ end
164
+
165
+ end
166
+ end
167
+
168
+ class NoSubscription
169
+ def initialize(profile)
170
+ @profile = profile
171
+ end
172
+
173
+ def plan
174
+ NoPlan.new
175
+ end
176
+
177
+ def update_attributes!(*)
178
+ # noop
179
+ end
180
+
181
+ def cancel
182
+ # noop
183
+ end
184
+
185
+ def current_period_ends_at
186
+ @profile.trial_end_date
187
+ end
188
+
189
+ def state
190
+ 'non-existent'
191
+ end
192
+ end
193
+
194
+ class NoPlan
195
+ def plan_code
196
+ '????'
197
+ end
198
+ end
199
+
200
+ def upgrade_plan_to(new_plan_code, account_code)
201
+ new_plan = {
202
+ plan_code: new_plan_code,
203
+ timeframe: 'now'
204
+ }
205
+ subscription_for_account(account_code).update_attributes! new_plan
206
+ self
207
+ rescue Recurly::Resource::Invalid
208
+ raise InvalidPlanError
209
+ end
210
+
211
+ def subscription_for_account(account_code)
212
+ account = retrieve_account(account_code)
213
+ account.subscriptions.first
214
+ end
215
+
216
+ def retrieve_account(account_code)
217
+ Recurly::Account.find(account_code.to_s)
218
+ rescue Recurly::Resource::NotFound
219
+ raise AccountNotFoundError.new(account_code)
220
+ end
221
+
222
+ class Config
223
+ attr_reader :environment
224
+
225
+ def environment=(name)
226
+ @environment = env(name).configure
227
+ self
228
+ end
229
+
230
+ def enable_logging
231
+ require 'logger'
232
+ Recurly.logger = Logger.new STDOUT
233
+ end
234
+
235
+ private
236
+
237
+ def env(name)
238
+ envs = {
239
+ test: Test,
240
+ production: Production
241
+ }
242
+ envs.fetch(name).new
243
+ end
244
+
245
+ class Environment
246
+ def configure
247
+ Recurly.api_key = api_key
248
+ Recurly.js.private_key = js_private_key
249
+ self
250
+ end
251
+ end
252
+
253
+ # Production
254
+ # matt@mattwynne.net
255
+ # Recurly.api_key = '843cf121e28149309292c9b500e34e1b'
256
+ # Recurly.js.private_key = 'a716ce2fcc904310b2ace41809ed9eb9'
257
+ class Production < Environment
258
+ VariableNotSet = Class.new(StandardError) do
259
+ def message
260
+ "Environment variable '#{super}' not set. Other values are: #{ENV.inspect}"
261
+ end
262
+ end
263
+ AttemptToClearProductionData = Class.new(StandardError)
264
+
265
+ def clear_test_data
266
+ raise AttemptToClearProductionData
267
+ end
268
+
269
+ private
270
+
271
+ def api_key
272
+ get_env_var('RECURLY_API_KEY')
273
+ end
274
+
275
+ def js_private_key
276
+ get_env_var('RECURLY_JS_PRIVATE_KEY')
277
+ end
278
+
279
+ def get_env_var(name)
280
+ ENV[name] || raise(VariableNotSet.new(name))
281
+ end
282
+ end
283
+
284
+ # Test
285
+ # support@relishapp.com / relishapp
286
+ # Recurly.api_key = '300041d517734cddaa84c705522d7354'
287
+ # Recurly.js.private_key = '9e052cad35774060ab4003b0c71315d9'
288
+ class Test < Environment
289
+
290
+ def clear_test_data
291
+ recurly_website.clear_test_data
292
+ end
293
+
294
+ private
295
+
296
+ def api_key
297
+ '300041d517734cddaa84c705522d7354'
298
+ end
299
+
300
+ def js_private_key
301
+ '9e052cad35774060ab4003b0c71315d9'
302
+ end
303
+
304
+ private
305
+
306
+ def recurly_website
307
+ @recurly_website ||= RecurlyWebsite.new
308
+ end
309
+
310
+ class RecurlyWebsite
311
+ def clear_test_data
312
+ require 'capybara/webkit'
313
+ Capybara.default_wait_time = (ENV['WAIT_FOR_RECURLY'] || 60).to_i
314
+ puts "Calling Recurly to clear test data..."
315
+ session.visit('https://relish-test.recurly.com/configuration/test_data')
316
+ session.execute_script 'window.confirm = function () { return true }'
317
+ session.click_on 'Delete All Test Data'
318
+ session.visit 'https://relish-test.recurly.com/accounts'
319
+ session.assert_no_selector('.account')
320
+ end
321
+
322
+ private
323
+
324
+ def session
325
+ @session ||= begin
326
+ session = Capybara::Session.new(:webkit)
327
+ session.visit('https://relish-test.recurly.com/configuration/test_data')
328
+ session.fill_in 'Email', with: 'support@relishapp.com'
329
+ session.fill_in 'Password', with: 'relishapp'
330
+ session.click_button 'Log In'
331
+ session
332
+ end
333
+ end
334
+ end
335
+ end
336
+
337
+ end
338
+ end
339
+ end
340
+ end