relish-billing 0.0.3
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.
- 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
|
+
|