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.
@@ -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
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *.sw*
6
+ cucumber.yml
7
+ tags
8
+ rerun.txt
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
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in relish-billing.gemspec
4
+ gemspec
@@ -0,0 +1,9 @@
1
+ guard 'cucumber', cli: '--format pretty' do
2
+ watch(%r{^features/.+\.feature$})
3
+ watch(%r{^features/.+\.rb$}) { 'features' }
4
+ end
5
+
6
+ guard 'rspec', version: 2 do
7
+ watch(%r{^spec/.+_spec\.rb$})
8
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
9
+ end
@@ -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.
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :default => ['rspec', 'cucumber', 'ctags']
4
+
5
+ task(:rspec) { sh 'rspec' }
6
+ task(:cucumber) { sh 'cucumber --format progress -t ~@todo' }
7
+ task(:ctags) { sh 'ctags -R' }
@@ -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,11 @@
1
+ if ENV['HEADLESS'] == 'true'
2
+ require 'headless'
3
+
4
+ headless = Headless.new
5
+ headless.start
6
+
7
+ at_exit do
8
+ headless.destroy
9
+ end
10
+ end
11
+
@@ -0,0 +1,3 @@
1
+ Relish::Billing::RecurlyApiDriver.configure do |config|
2
+ config.environment = :test
3
+ 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
+