relish-billing 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Guardfile +9 -0
- data/README.md +14 -0
- data/Rakefile +7 -0
- data/features/cancel_subscription.feature +8 -0
- data/features/create_new_account.feature +17 -0
- data/features/see_account_status.feature +43 -0
- data/features/step_definitions/steps.rb +89 -0
- data/features/support/dsl.rb +113 -0
- data/features/support/headless.rb +11 -0
- data/features/support/recurly.rb +3 -0
- data/features/update_card_details.feature +15 -0
- data/features/upgrade_account.feature +10 -0
- data/lib/relish/billing.rb +38 -0
- data/lib/relish/billing/account.rb +37 -0
- data/lib/relish/billing/entities.rb +107 -0
- data/lib/relish/billing/fake_driver.rb +101 -0
- data/lib/relish/billing/recurly_api_driver.rb +340 -0
- data/lib/relish/billing/testing_helper.rb +27 -0
- data/lib/relish/billing/version.rb +5 -0
- data/relish-billing.gemspec +30 -0
- data/script/ci +17 -0
- data/spec/relish/billing/fake_driver_spec.rb +15 -0
- data/spec/relish/billing/recurly_api_driver_spec.rb +66 -0
- data/spec/relish/billing/user_profile_spec.rb +28 -0
- data/spec/support/driver_examples.rb +169 -0
- metadata +212 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d1b1be88caf4798b4983d474584b4f2dd15fd303
|
4
|
+
data.tar.gz: e186dd758c0ace91a9cc75110d4b14805b432eff
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fce9f8fd37a1af4a808f3d0a4b8302b12f8c385c431ba21f66b9957327faa2a33cd803a93a075ec8bc924acbd0d71a900dcb2afebf647fd0625618c8c67e9b5f
|
7
|
+
data.tar.gz: 9e71a693e922b5a0f491fdafdf276765cd3d4f66053b314c2ab5a14925885125026a57811c57d1c9fa848be1aad291adf95a8ae4e443492bd56b8b46322f9547
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.9.3-p194@relish-billing --create
|
data/Gemfile
ADDED
data/Guardfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Relish Billing
|
2
|
+
|
3
|
+
This gem provides the billing subsystem to the main Relish application.
|
4
|
+
|
5
|
+
See the features directory for details.
|
6
|
+
|
7
|
+
# Some notes on SagePay's API
|
8
|
+
|
9
|
+
Recurly uses SagePage on the back-end. Their API has the following mandatory fields:
|
10
|
+
- address line 1
|
11
|
+
- address city
|
12
|
+
- address state (for US address)
|
13
|
+
|
14
|
+
However, I can disable address checking via the admin interface, then just send dummy values for these fields.
|
data/Rakefile
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
Feature: Cancel subscription
|
2
|
+
|
3
|
+
The customer no longer wishes to use Relish, so cancels their subscription
|
4
|
+
|
5
|
+
Scenario: Publisher decides to cancel their subscription
|
6
|
+
Given I have started a paid subscription on the pro plan
|
7
|
+
When I choose to cancel the subscription
|
8
|
+
Then I should see a confirmation message that my subscription has been cancelled
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Feature: Create new billing account
|
2
|
+
|
3
|
+
As part of the sign-up process, once we've created their user or organization profile,
|
4
|
+
we need to create a billing account if they've opted for a paid plan.
|
5
|
+
|
6
|
+
Scenario: Create a billing account with a free trial
|
7
|
+
Given I have an account on the free plan
|
8
|
+
When I choose a paid plan, entering valid card details
|
9
|
+
Then I should see that my plan has been set
|
10
|
+
And I should see that my card details have been saved
|
11
|
+
|
12
|
+
Scenario: Try to create a billing account with a duff card
|
13
|
+
Given I have an account on the free plan
|
14
|
+
When I choose a paid plan, entering invalid card details
|
15
|
+
Then I should see that my plan has been set
|
16
|
+
And I should see that my card details were invalid
|
17
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
Feature: See account status
|
2
|
+
|
3
|
+
When the user goes to their 'my account' page, they'll expect to be able to
|
4
|
+
see details like:
|
5
|
+
|
6
|
+
* what plan they're currently on
|
7
|
+
* next billing date
|
8
|
+
* some details of current card
|
9
|
+
* messaging if they've cancelled their account or their card has become invalid
|
10
|
+
|
11
|
+
We let you sign up and choose your plan without forcing you to enter credit card details.
|
12
|
+
|
13
|
+
You can then go and set them from the account page.
|
14
|
+
|
15
|
+
Scenario: New subscriber sees they need to enter credit card details
|
16
|
+
Given I have started a paid subscription
|
17
|
+
When I view my account status
|
18
|
+
Then I should see that my account is not active
|
19
|
+
When I enter a valid credit card
|
20
|
+
Then I should see that my card details have been saved
|
21
|
+
When I view my account status
|
22
|
+
And I should see that my account is active
|
23
|
+
|
24
|
+
Scenario: Existing subscriber sees their billing details
|
25
|
+
Given I have started a paid subscription on the pro plan
|
26
|
+
And I have entered a valid credit card
|
27
|
+
When I view my account status
|
28
|
+
Then I should see that I am on the pro plan
|
29
|
+
And I should see that my account is active
|
30
|
+
And I should see my masked card number
|
31
|
+
And I should see my next billing date
|
32
|
+
|
33
|
+
Scenario: Free plan user sees account status
|
34
|
+
Given I have an account on the free plan
|
35
|
+
When I view my account status
|
36
|
+
Then I should see that my account is not active
|
37
|
+
|
38
|
+
Scenario: Subscriber who has cancelled can access their details until the end of their billing cycle
|
39
|
+
Given I have started a paid subscription
|
40
|
+
And I have entered a valid credit card
|
41
|
+
When I choose to cancel the subscription
|
42
|
+
And I view my account status
|
43
|
+
Then I should be told that my account will not be renewed at the next billing date
|
@@ -0,0 +1,89 @@
|
|
1
|
+
Given /^I have an account on the free plan$/ do
|
2
|
+
create_user plan: 'free'
|
3
|
+
end
|
4
|
+
|
5
|
+
When /^I choose a paid plan, entering valid card details$/ do
|
6
|
+
choose_plan 'pro'
|
7
|
+
save_card valid_credit_card
|
8
|
+
end
|
9
|
+
|
10
|
+
When /^I choose a paid plan, entering invalid card details$/ do
|
11
|
+
choose_plan 'pro'
|
12
|
+
save_card invalid_credit_card
|
13
|
+
end
|
14
|
+
|
15
|
+
Given /^I have started a paid subscription(?: on the pro plan)?$/ do
|
16
|
+
create_user plan: 'pro'
|
17
|
+
end
|
18
|
+
|
19
|
+
Given /^I (?:enter|have entered) a valid credit card$/ do
|
20
|
+
save_card valid_credit_card
|
21
|
+
end
|
22
|
+
|
23
|
+
When /^I try to change to an invalid card$/ do
|
24
|
+
save_card invalid_credit_card
|
25
|
+
end
|
26
|
+
|
27
|
+
When /^I (?:choose|change to) the starter plan$/ do
|
28
|
+
choose_plan 'starter'
|
29
|
+
end
|
30
|
+
|
31
|
+
Then /^I should now be on the starter plan$/ do
|
32
|
+
current_plan.should == 'starter'
|
33
|
+
end
|
34
|
+
|
35
|
+
When /^I choose to update which card is billed$/ do
|
36
|
+
save_card other_valid_credit_card
|
37
|
+
end
|
38
|
+
|
39
|
+
When /^I (?:choose|have chosen) to cancel the subscription$/ do
|
40
|
+
cancel_subscription
|
41
|
+
end
|
42
|
+
|
43
|
+
When /^I view my account status$/ do
|
44
|
+
view_account_status
|
45
|
+
end
|
46
|
+
|
47
|
+
Then /^I should see that I am on the (\w+) plan$/ do |plan_id|
|
48
|
+
account_status.plan_id.should == plan_id
|
49
|
+
end
|
50
|
+
|
51
|
+
Then /^I should see that my account is not active$/ do
|
52
|
+
account_status.should_not be_active
|
53
|
+
end
|
54
|
+
|
55
|
+
And /^I should see that my account is active$/ do
|
56
|
+
account_status.should be_active
|
57
|
+
end
|
58
|
+
|
59
|
+
Then /^I should see my masked card number$/ do
|
60
|
+
account_status.card_details.last_four.should == current_card.number[12..-1]
|
61
|
+
end
|
62
|
+
|
63
|
+
Then /^I should see my next billing date$/ do
|
64
|
+
account_status.next_billing_date.should_not be_nil
|
65
|
+
end
|
66
|
+
|
67
|
+
Then /^I should be told that my account will not be renewed at the next billing date$/ do
|
68
|
+
account_status.should be_non_renewing
|
69
|
+
end
|
70
|
+
|
71
|
+
Then /^I should see that my plan has been set$/ do
|
72
|
+
ui.shows(:plan_set)
|
73
|
+
end
|
74
|
+
|
75
|
+
Then /^I should see that my card details have been saved$/ do
|
76
|
+
ui.shows(:card_saved)
|
77
|
+
end
|
78
|
+
|
79
|
+
Then /^I should see that my card has been delined$/ do
|
80
|
+
ui.shows(:transaction_declined)
|
81
|
+
end
|
82
|
+
|
83
|
+
Then /^I should see a confirmation message that my subscription has been cancelled$/ do
|
84
|
+
ui.shows(:subscription_cancelled)
|
85
|
+
end
|
86
|
+
|
87
|
+
Then /^I should see that my card details were invalid$/ do
|
88
|
+
ui.shows(:transaction_declined)
|
89
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
$LOAD_PATH << File.expand_path(File.dirname(__FILE__) + '/../../lib')
|
2
|
+
require 'relish/billing'
|
3
|
+
require 'relish/billing/testing_helper'
|
4
|
+
require 'securerandom'
|
5
|
+
module Relish
|
6
|
+
module Billing
|
7
|
+
|
8
|
+
module AcceptanceTestDsl
|
9
|
+
|
10
|
+
include TestingHelper
|
11
|
+
|
12
|
+
def valid_credit_card
|
13
|
+
fixture :valid_credit_card
|
14
|
+
end
|
15
|
+
|
16
|
+
def invalid_credit_card
|
17
|
+
fixture :invalid_credit_card
|
18
|
+
end
|
19
|
+
|
20
|
+
def other_valid_credit_card
|
21
|
+
fixture :other_valid_credit_card
|
22
|
+
end
|
23
|
+
|
24
|
+
def choose_plan(plan_id)
|
25
|
+
change_users_plan_to(plan_id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def current_plan
|
29
|
+
view_account_status
|
30
|
+
account_status.plan_id
|
31
|
+
end
|
32
|
+
|
33
|
+
def cancel_subscription
|
34
|
+
account.cancel ui
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :current_card
|
38
|
+
def save_card(card)
|
39
|
+
account.save_card card, ui
|
40
|
+
@current_card = card
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :account_status
|
44
|
+
def view_account_status
|
45
|
+
@account_status = account.status
|
46
|
+
end
|
47
|
+
|
48
|
+
def ui
|
49
|
+
@ui ||= FakeUI.new
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def account
|
55
|
+
Account.new(user_profile, driver)
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_user(attributes)
|
59
|
+
@user_profile = create_user_profile(attributes)
|
60
|
+
end
|
61
|
+
|
62
|
+
def user_profile
|
63
|
+
@user_profile || raise("No user profile defined")
|
64
|
+
end
|
65
|
+
|
66
|
+
def change_users_plan_to(new_plan)
|
67
|
+
@user_profile = user_profile.with_plan(new_plan)
|
68
|
+
account.plan_changed ui
|
69
|
+
end
|
70
|
+
|
71
|
+
def driver
|
72
|
+
@driver ||= Billing.driver
|
73
|
+
end
|
74
|
+
|
75
|
+
class FakeUI
|
76
|
+
attr_reader :status
|
77
|
+
|
78
|
+
def method_missing(message, *args)
|
79
|
+
return super unless in_protocol?(message)
|
80
|
+
messages << message
|
81
|
+
end
|
82
|
+
|
83
|
+
def shows(message)
|
84
|
+
return if messages.include?(message)
|
85
|
+
raise RSpec::Expectations::ExpectationNotMetError, "The expected message #{message.inspect} is not one of #{messages.inspect}"
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def messages
|
91
|
+
@messages ||= []
|
92
|
+
end
|
93
|
+
|
94
|
+
def in_protocol?(message)
|
95
|
+
[
|
96
|
+
:plan_set,
|
97
|
+
:card_saved,
|
98
|
+
:transaction_declined,
|
99
|
+
:subscription_cancelled,
|
100
|
+
].include?(message)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
World(Relish::Billing::AcceptanceTestDsl)
|
110
|
+
|
111
|
+
Relish::Billing.configure do |config|
|
112
|
+
config.driver = :recurly if ENV['HIT_THE_WEB']
|
113
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Feature: Update card details
|
2
|
+
|
3
|
+
Need to be able to update my card, e.g. if I decide
|
4
|
+
to use a different one for billing.
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given I have started a paid subscription on the pro plan
|
8
|
+
|
9
|
+
Scenario: Update card details successfully
|
10
|
+
When I choose to update which card is billed
|
11
|
+
Then I should see that my card details have been saved
|
12
|
+
|
13
|
+
Scenario: Use a bad card
|
14
|
+
When I try to change to an invalid card
|
15
|
+
Then I should see that my card has been declined
|
@@ -0,0 +1,10 @@
|
|
1
|
+
Feature: Upgrading an account
|
2
|
+
|
3
|
+
The existing publisher is so happy with the service that they decide to upgrade
|
4
|
+
|
5
|
+
Scenario: Publisher upgrades from a paid plan to a better paid plan
|
6
|
+
Given I have started a paid subscription on the pro plan
|
7
|
+
And I have entered a valid credit card
|
8
|
+
When I change to the starter plan
|
9
|
+
Then I should see that my plan has been set
|
10
|
+
And I should now be on the starter plan
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "relish/billing/version"
|
2
|
+
require "relish/billing/account"
|
3
|
+
require 'relish/billing/recurly_api_driver'
|
4
|
+
require 'relish/billing/fake_driver'
|
5
|
+
|
6
|
+
module Relish
|
7
|
+
module Billing
|
8
|
+
class << self
|
9
|
+
def account_for(user_profile)
|
10
|
+
Account.new(user_profile, driver)
|
11
|
+
end
|
12
|
+
|
13
|
+
def configure
|
14
|
+
yield config
|
15
|
+
end
|
16
|
+
|
17
|
+
def driver
|
18
|
+
@driver ||= {
|
19
|
+
fake: FakeDriver,
|
20
|
+
recurly: RecurlyApiDriver
|
21
|
+
}.fetch(config.driver).new
|
22
|
+
end
|
23
|
+
|
24
|
+
def config
|
25
|
+
@config ||= Config.new(driver: :fake)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
Config = Class.new do
|
31
|
+
attr_accessor :driver
|
32
|
+
def initialize(attributes)
|
33
|
+
@driver = attributes.fetch(:driver)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Relish
|
2
|
+
module Billing
|
3
|
+
class Account
|
4
|
+
def initialize(user_profile, driver)
|
5
|
+
raise ArgumentError.new("please pass a UserProfile") unless user_profile.is_a?(UserProfile)
|
6
|
+
@user_profile, @driver = user_profile, driver
|
7
|
+
end
|
8
|
+
|
9
|
+
def status
|
10
|
+
@driver.account_status(@user_profile)
|
11
|
+
end
|
12
|
+
|
13
|
+
def save_card(card, listener)
|
14
|
+
@driver.save_card @user_profile, card
|
15
|
+
listener.card_saved
|
16
|
+
rescue CardDeclinedError => exception
|
17
|
+
listener.transaction_declined(exception.reason)
|
18
|
+
ensure
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def cancel(listener)
|
23
|
+
@driver.cancel_subscription @user_profile
|
24
|
+
listener.subscription_cancelled
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def plan_changed(listener)
|
29
|
+
@driver.set_plan(@user_profile)
|
30
|
+
listener.plan_set @user_profile.plan
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|