stripe-ruby-mock 2.4.0 → 2.5.8

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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/.env +2 -0
  3. data/.travis.yml +8 -4
  4. data/README.md +12 -5
  5. data/lib/stripe_mock/api/account_balance.rb +14 -0
  6. data/lib/stripe_mock/api/client.rb +4 -4
  7. data/lib/stripe_mock/api/conversion_rate.rb +14 -0
  8. data/lib/stripe_mock/api/errors.rb +25 -14
  9. data/lib/stripe_mock/api/instance.rb +6 -6
  10. data/lib/stripe_mock/api/webhooks.rb +5 -1
  11. data/lib/stripe_mock/client.rb +18 -2
  12. data/lib/stripe_mock/data/list.rb +14 -2
  13. data/lib/stripe_mock/data.rb +217 -56
  14. data/lib/stripe_mock/instance.rb +93 -8
  15. data/lib/stripe_mock/request_handlers/accounts.rb +34 -2
  16. data/lib/stripe_mock/request_handlers/balance.rb +17 -0
  17. data/lib/stripe_mock/request_handlers/balance_transactions.rb +18 -2
  18. data/lib/stripe_mock/request_handlers/charges.rb +25 -17
  19. data/lib/stripe_mock/request_handlers/coupons.rb +5 -4
  20. data/lib/stripe_mock/request_handlers/customers.rb +26 -8
  21. data/lib/stripe_mock/request_handlers/ephemeral_key.rb +13 -0
  22. data/lib/stripe_mock/request_handlers/external_accounts.rb +55 -0
  23. data/lib/stripe_mock/request_handlers/helpers/bank_account_helpers.rb +1 -1
  24. data/lib/stripe_mock/request_handlers/helpers/card_helpers.rb +9 -7
  25. data/lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb +10 -6
  26. data/lib/stripe_mock/request_handlers/helpers/external_account_helpers.rb +49 -0
  27. data/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb +55 -15
  28. data/lib/stripe_mock/request_handlers/helpers/token_helpers.rb +2 -2
  29. data/lib/stripe_mock/request_handlers/invoices.rb +93 -14
  30. data/lib/stripe_mock/request_handlers/orders.rb +5 -5
  31. data/lib/stripe_mock/request_handlers/payouts.rb +32 -0
  32. data/lib/stripe_mock/request_handlers/plans.rb +1 -0
  33. data/lib/stripe_mock/request_handlers/products.rb +43 -0
  34. data/lib/stripe_mock/request_handlers/refunds.rb +10 -7
  35. data/lib/stripe_mock/request_handlers/subscription_items.rb +36 -0
  36. data/lib/stripe_mock/request_handlers/subscriptions.rb +95 -34
  37. data/lib/stripe_mock/request_handlers/tax_rates.rb +36 -0
  38. data/lib/stripe_mock/request_handlers/tokens.rb +9 -3
  39. data/lib/stripe_mock/request_handlers/transfers.rb +11 -5
  40. data/lib/stripe_mock/request_handlers/validators/param_validators.rb +10 -1
  41. data/lib/stripe_mock/server.rb +14 -1
  42. data/lib/stripe_mock/test_strategies/base.rb +7 -5
  43. data/lib/stripe_mock/test_strategies/live.rb +5 -0
  44. data/lib/stripe_mock/test_strategies/mock.rb +8 -0
  45. data/lib/stripe_mock/util.rb +8 -2
  46. data/lib/stripe_mock/version.rb +1 -1
  47. data/lib/stripe_mock/webhook_fixtures/account.updated.json +1 -1
  48. data/lib/stripe_mock/webhook_fixtures/charge.dispute.funds_reinstated.json +88 -0
  49. data/lib/stripe_mock/webhook_fixtures/charge.dispute.funds_withdrawn.json +88 -0
  50. data/lib/stripe_mock/webhook_fixtures/charge.updated.json +58 -0
  51. data/lib/stripe_mock/webhook_fixtures/customer.subscription.created.json +40 -10
  52. data/lib/stripe_mock/webhook_fixtures/customer.subscription.deleted.json +39 -10
  53. data/lib/stripe_mock/webhook_fixtures/customer.subscription.trial_will_end.json +39 -10
  54. data/lib/stripe_mock/webhook_fixtures/customer.subscription.updated.json +40 -11
  55. data/lib/stripe_mock/webhook_fixtures/invoice.created.json +3 -2
  56. data/lib/stripe_mock/webhook_fixtures/invoice.payment_failed.json +1 -1
  57. data/lib/stripe_mock/webhook_fixtures/invoice.payment_succeeded.json +92 -85
  58. data/lib/stripe_mock/webhook_fixtures/invoice.updated.json +3 -2
  59. data/lib/stripe_mock/webhook_fixtures/plan.created.json +1 -1
  60. data/lib/stripe_mock/webhook_fixtures/plan.deleted.json +1 -1
  61. data/lib/stripe_mock/webhook_fixtures/plan.updated.json +1 -1
  62. data/lib/stripe_mock.rb +13 -0
  63. data/spec/instance_spec.rb +54 -4
  64. data/spec/integration_examples/prepare_error_examples.rb +6 -6
  65. data/spec/list_spec.rb +16 -3
  66. data/spec/readme_spec.rb +2 -0
  67. data/spec/server_spec.rb +6 -2
  68. data/spec/shared_stripe_examples/account_examples.rb +35 -0
  69. data/spec/shared_stripe_examples/balance_examples.rb +11 -0
  70. data/spec/shared_stripe_examples/balance_transaction_examples.rb +28 -0
  71. data/spec/shared_stripe_examples/bank_examples.rb +28 -1
  72. data/spec/shared_stripe_examples/card_examples.rb +10 -3
  73. data/spec/shared_stripe_examples/charge_examples.rb +73 -17
  74. data/spec/shared_stripe_examples/coupon_examples.rb +8 -2
  75. data/spec/shared_stripe_examples/customer_examples.rb +54 -2
  76. data/spec/shared_stripe_examples/dispute_examples.rb +19 -8
  77. data/spec/shared_stripe_examples/ephemeral_key_examples.rb +17 -0
  78. data/spec/shared_stripe_examples/error_mock_examples.rb +3 -3
  79. data/spec/shared_stripe_examples/external_account_examples.rb +170 -0
  80. data/spec/shared_stripe_examples/extra_features_examples.rb +2 -0
  81. data/spec/shared_stripe_examples/invoice_examples.rb +314 -51
  82. data/spec/shared_stripe_examples/payout_examples.rb +68 -0
  83. data/spec/shared_stripe_examples/plan_examples.rb +37 -5
  84. data/spec/shared_stripe_examples/product_example.rb +65 -0
  85. data/spec/shared_stripe_examples/recipient_examples.rb +7 -7
  86. data/spec/shared_stripe_examples/refund_examples.rb +17 -11
  87. data/spec/shared_stripe_examples/subscription_examples.rb +463 -33
  88. data/spec/shared_stripe_examples/subscription_items_examples.rb +75 -0
  89. data/spec/shared_stripe_examples/tax_rate_examples.rb +42 -0
  90. data/spec/shared_stripe_examples/transfer_examples.rb +72 -23
  91. data/spec/shared_stripe_examples/webhook_event_examples.rb +74 -5
  92. data/spec/spec_helper.rb +7 -6
  93. data/spec/stripe_mock_spec.rb +16 -3
  94. data/spec/support/stripe_examples.rb +7 -1
  95. data/spec/util_spec.rb +35 -1
  96. data/stripe-ruby-mock.gemspec +1 -1
  97. metadata +34 -7
  98. data/ChangeLog.rdoc +0 -4
@@ -28,7 +28,7 @@ module StripeMock
28
28
  if token.nil? || @card_tokens[token].nil?
29
29
  # TODO: Make this strict
30
30
  msg = "Invalid token id: #{token}"
31
- raise Stripe::InvalidRequestError.new(msg, 'tok', 404)
31
+ raise Stripe::InvalidRequestError.new(msg, 'tok', http_status: 404)
32
32
  else
33
33
  @card_tokens.delete(token)
34
34
  end
@@ -36,7 +36,7 @@ module StripeMock
36
36
 
37
37
  def get_card_or_bank_by_token(token)
38
38
  token_id = token['id'] || token
39
- @card_tokens[token_id] || @bank_tokens[token_id] || raise(Stripe::InvalidRequestError.new("Invalid token id: #{token_id}", 'tok', 404))
39
+ @card_tokens[token_id] || @bank_tokens[token_id] || raise(Stripe::InvalidRequestError.new("Invalid token id: #{token_id}", 'tok', http_status: 404))
40
40
  end
41
41
 
42
42
  end
@@ -52,29 +52,98 @@ module StripeMock
52
52
  def pay_invoice(route, method_url, params, headers)
53
53
  route =~ method_url
54
54
  assert_existence :invoice, $1, invoices[$1]
55
- invoices[$1].merge!(:paid => true, :attempted => true, :charge => 'ch_1fD6uiR9FAA2zc')
55
+ charge = invoice_charge(invoices[$1])
56
+ invoices[$1].merge!(:paid => true, :attempted => true, :charge => charge[:id])
56
57
  end
57
58
 
58
59
  def upcoming_invoice(route, method_url, params, headers)
59
60
  route =~ method_url
60
- raise Stripe::InvalidRequestError.new('Missing required param: customer', nil, 400) if params[:customer].nil?
61
+ raise Stripe::InvalidRequestError.new('Missing required param: customer', nil, http_status: 400) if params[:customer].nil?
62
+ raise Stripe::InvalidRequestError.new('When previewing changes to a subscription, you must specify either `subscription` or `subscription_items`', nil, http_status: 400) if !params[:subscription_proration_date].nil? && params[:subscription].nil? && params[:subscription_plan].nil?
63
+ raise Stripe::InvalidRequestError.new('Cannot specify proration date without specifying a subscription', nil, http_status: 400) if !params[:subscription_proration_date].nil? && params[:subscription].nil?
61
64
 
62
65
  customer = customers[params[:customer]]
63
66
  assert_existence :customer, params[:customer], customer
64
67
 
65
- raise Stripe::InvalidRequestError.new("No upcoming invoices for customer: #{customer[:id]}", nil, 404) if customer[:subscriptions][:data].length == 0
68
+ raise Stripe::InvalidRequestError.new("No upcoming invoices for customer: #{customer[:id]}", nil, http_status: 404) if customer[:subscriptions][:data].length == 0
66
69
 
67
- most_recent = customer[:subscriptions][:data].min_by { |sub| sub[:current_period_end] }
68
- invoice_item = get_mock_subscription_line_item(most_recent)
70
+ subscription =
71
+ if params[:subscription]
72
+ customer[:subscriptions][:data].select{|s|s[:id] == params[:subscription]}.first
73
+ else
74
+ customer[:subscriptions][:data].min_by { |sub| sub[:current_period_end] }
75
+ end
69
76
 
70
- id = new_id('in')
71
- invoices[id] = Data.mock_invoice([invoice_item],
72
- id: id,
77
+ if params[:subscription_proration_date] && !((subscription[:current_period_start]..subscription[:current_period_end]) === params[:subscription_proration_date])
78
+ raise Stripe::InvalidRequestError.new('Cannot specify proration date outside of current subscription period', nil, http_status: 400)
79
+ end
80
+
81
+ prorating = false
82
+ subscription_proration_date = nil
83
+ subscription_plan_id = params[:subscription_plan] || subscription[:plan][:id]
84
+ subscription_quantity = params[:subscription_quantity] || subscription[:quantity]
85
+ if subscription_plan_id != subscription[:plan][:id] || subscription_quantity != subscription[:quantity]
86
+ prorating = true
87
+ invoice_date = Time.now.to_i
88
+ subscription_plan = assert_existence :plan, subscription_plan_id, plans[subscription_plan_id.to_s]
89
+ preview_subscription = Data.mock_subscription
90
+ preview_subscription = resolve_subscription_changes(preview_subscription, [subscription_plan], customer, { trial_end: params[:subscription_trial_end] })
91
+ preview_subscription[:id] = subscription[:id]
92
+ preview_subscription[:quantity] = subscription_quantity
93
+ subscription_proration_date = params[:subscription_proration_date] || Time.now
94
+ else
95
+ preview_subscription = subscription
96
+ invoice_date = subscription[:current_period_end]
97
+ end
98
+
99
+ invoice_lines = []
100
+
101
+ if prorating
102
+ unused_amount = subscription[:plan][:amount] * subscription[:quantity] * (subscription[:current_period_end] - subscription_proration_date.to_i) / (subscription[:current_period_end] - subscription[:current_period_start])
103
+ invoice_lines << Data.mock_line_item(
104
+ id: new_id('ii'),
105
+ amount: -unused_amount,
106
+ description: 'Unused time',
107
+ plan: subscription[:plan],
108
+ period: {
109
+ start: subscription_proration_date.to_i,
110
+ end: subscription[:current_period_end]
111
+ },
112
+ quantity: subscription[:quantity],
113
+ proration: true
114
+ )
115
+
116
+ preview_plan = assert_existence :plan, params[:subscription_plan], plans[params[:subscription_plan]]
117
+ if preview_plan[:interval] == subscription[:plan][:interval] && preview_plan[:interval_count] == subscription[:plan][:interval_count] && params[:subscription_trial_end].nil?
118
+ remaining_amount = preview_plan[:amount] * subscription_quantity * (subscription[:current_period_end] - subscription_proration_date.to_i) / (subscription[:current_period_end] - subscription[:current_period_start])
119
+ invoice_lines << Data.mock_line_item(
120
+ id: new_id('ii'),
121
+ amount: remaining_amount,
122
+ description: 'Remaining time',
123
+ plan: preview_plan,
124
+ period: {
125
+ start: subscription_proration_date.to_i,
126
+ end: subscription[:current_period_end]
127
+ },
128
+ quantity: subscription_quantity,
129
+ proration: true
130
+ )
131
+ end
132
+ end
133
+
134
+ subscription_line = get_mock_subscription_line_item(preview_subscription)
135
+ invoice_lines << subscription_line
136
+
137
+ Data.mock_invoice(invoice_lines,
138
+ id: new_id('in'),
73
139
  customer: customer[:id],
74
- subscription: most_recent[:id],
75
- period_start: most_recent[:current_period_start],
76
- period_end: most_recent[:current_period_end],
77
- next_payment_attempt: most_recent[:current_period_end] + 3600 )
140
+ discount: customer[:discount],
141
+ created: invoice_date,
142
+ starting_balance: customer[:account_balance],
143
+ subscription: preview_subscription[:id],
144
+ period_start: prorating ? invoice_date : preview_subscription[:current_period_start],
145
+ period_end: prorating ? invoice_date : preview_subscription[:current_period_end],
146
+ next_payment_attempt: preview_subscription[:current_period_end] + 3600 )
78
147
  end
79
148
 
80
149
  private
@@ -84,15 +153,25 @@ module StripeMock
84
153
  id: subscription[:id],
85
154
  type: "subscription",
86
155
  plan: subscription[:plan],
87
- amount: subscription[:plan][:amount],
156
+ amount: subscription[:status] == 'trialing' ? 0 : subscription[:plan][:amount] * subscription[:quantity],
88
157
  discountable: true,
89
- quantity: 1,
158
+ quantity: subscription[:quantity],
90
159
  period: {
91
160
  start: subscription[:current_period_end],
92
161
  end: get_ending_time(subscription[:current_period_start], subscription[:plan], 2)
93
162
  })
94
163
  end
95
164
 
165
+ ## charge the customer on the invoice, if one does not exist, create
166
+ #anonymous charge
167
+ def invoice_charge(invoice)
168
+ begin
169
+ new_charge(nil, nil, {customer: invoice[:customer]["id"], amount: invoice[:amount_due], currency: StripeMock.default_currency}, nil)
170
+ rescue Stripe::InvalidRequestError
171
+ new_charge(nil, nil, {source: generate_card_token, amount: invoice[:amount_due], currency: StripeMock.default_currency}, nil)
172
+ end
173
+ end
174
+
96
175
  end
97
176
  end
98
177
  end
@@ -15,16 +15,16 @@ module StripeMock
15
15
  order_items = []
16
16
 
17
17
  unless params[:currency].to_s.size == 3
18
- raise Stripe::InvalidRequestError.new('You must supply a currency', nil, 400)
18
+ raise Stripe::InvalidRequestError.new('You must supply a currency', nil, http_status: 400)
19
19
  end
20
20
 
21
21
  if params[:items]
22
22
  unless params[:items].is_a? Array
23
- raise Stripe::InvalidRequestError.new('You must supply a list of items', nil, 400)
23
+ raise Stripe::InvalidRequestError.new('You must supply a list of items', nil, http_status: 400)
24
24
  end
25
25
 
26
26
  unless params[:items].first.is_a? Hash
27
- raise Stripe::InvalidRequestError.new('You must supply an item', nil, 400)
27
+ raise Stripe::InvalidRequestError.new('You must supply an item', nil, http_status: 400)
28
28
  end
29
29
  end
30
30
 
@@ -45,7 +45,7 @@ module StripeMock
45
45
  end
46
46
  end
47
47
 
48
- if [:created, :paid, :canceled, :fulfilled, :returned].includes? params[:status]
48
+ if %w(created paid canceled fulfilled returned).include? params[:status]
49
49
  order[:status] = params[:status]
50
50
  end
51
51
  order
@@ -61,7 +61,7 @@ module StripeMock
61
61
  order = assert_existence :order, $1, orders[$1]
62
62
 
63
63
  if params[:source].blank? && params[:customer].blank?
64
- raise Stripe::InvalidRequestError.new('You must supply a source or customer', nil, 400)
64
+ raise Stripe::InvalidRequestError.new('You must supply a source or customer', nil, http_status: 400)
65
65
  end
66
66
 
67
67
  charge_id = new_id('ch')
@@ -0,0 +1,32 @@
1
+ module StripeMock
2
+ module RequestHandlers
3
+ module Payouts
4
+
5
+ def Payouts.included(klass)
6
+ klass.add_handler 'post /v1/payouts', :new_payout
7
+ klass.add_handler 'get /v1/payouts', :list_payouts
8
+ klass.add_handler 'get /v1/payouts/(.*)', :get_payout
9
+ end
10
+
11
+ def new_payout(route, method_url, params, headers)
12
+ id = new_id('po')
13
+
14
+ unless params[:amount].is_a?(Integer) || (params[:amount].is_a?(String) && /^\d+$/.match(params[:amount]))
15
+ raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400)
16
+ end
17
+
18
+ payouts[id] = Data.mock_payout(params.merge :id => id)
19
+ end
20
+
21
+ def list_payouts(route, method_url, params, headers)
22
+ Data.mock_list_object(payouts.clone.values, params)
23
+ end
24
+
25
+ def get_payout(route, method_url, params, headers)
26
+ route =~ method_url
27
+ assert_existence :payout, $1, payouts[$1]
28
+ payouts[$1] ||= Data.mock_payout(:id => $1)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -11,6 +11,7 @@ module StripeMock
11
11
  end
12
12
 
13
13
  def new_plan(route, method_url, params, headers)
14
+ params[:id] ||= new_id('plan')
14
15
  validate_create_plan_params(params)
15
16
  plans[ params[:id] ] = Data.mock_plan(params)
16
17
  end
@@ -0,0 +1,43 @@
1
+ module StripeMock
2
+ module RequestHandlers
3
+ module Products
4
+ def self.included(base)
5
+ base.add_handler 'post /v1/products', :create_product
6
+ base.add_handler 'get /v1/products/(.*)', :retrieve_product
7
+ base.add_handler 'post /v1/products/(.*)', :update_product
8
+ base.add_handler 'get /v1/products', :list_products
9
+ base.add_handler 'delete /v1/products/(.*)', :destroy_product
10
+ end
11
+
12
+ def create_product(_route, _method_url, params, _headers)
13
+ params[:id] ||= new_id('prod')
14
+ products[params[:id]] = Data.mock_product(params)
15
+ end
16
+
17
+ def retrieve_product(route, method_url, _params, _headers)
18
+ id = method_url.match(route).captures.first
19
+ assert_existence :product, id, products[id]
20
+ end
21
+
22
+ def update_product(route, method_url, params, _headers)
23
+ id = method_url.match(route).captures.first
24
+ product = assert_existence :product, id, products[id]
25
+
26
+ product.merge!(params)
27
+ end
28
+
29
+ def list_products(_route, _method_url, params, _headers)
30
+ limit = params[:limit] || 10
31
+ Data.mock_list_object(products.values.take(limit), params)
32
+ end
33
+
34
+ def destroy_product(route, method_url, _params, _headers)
35
+ id = method_url.match(route).captures.first
36
+ assert_existence :product, id, products[id]
37
+
38
+ products.delete(id)
39
+ { id: id, object: 'product', deleted: true }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -10,12 +10,15 @@ module StripeMock
10
10
  end
11
11
 
12
12
  def new_refund(route, method_url, params, headers)
13
- if params[:idempotency_key] && refunds.any?
14
- original_refund = refunds.values.find { |c| c[:idempotency_key] == params[:idempotency_key]}
15
- return refunds[original_refund[:id]] if original_refund
13
+ if headers && headers[:idempotency_key]
14
+ params[:idempotency_key] = headers[:idempotency_key]
15
+ if refunds.any?
16
+ original_refund = refunds.values.find { |c| c[:idempotency_key] == headers[:idempotency_key]}
17
+ return refunds[original_refund[:id]] if original_refund
18
+ end
16
19
  end
17
20
 
18
- charge = get_charge(route, method_url, params, headers)
21
+ charge = assert_existence :charge, params[:charge], charges[params[:charge]]
19
22
  params[:amount] ||= charge[:amount]
20
23
  id = new_id('re')
21
24
  bal_trans_params = {
@@ -47,7 +50,7 @@ module StripeMock
47
50
  allowed = allowed_refund_params(params)
48
51
  disallowed = params.keys - allowed
49
52
  if disallowed.count > 0
50
- raise Stripe::InvalidRequestError.new("Received unknown parameters: #{disallowed.join(', ')}" , '', 400)
53
+ raise Stripe::InvalidRequestError.new("Received unknown parameters: #{disallowed.join(', ')}" , '', http_status: 400)
51
54
  end
52
55
 
53
56
  refunds[id] = Util.rmerge(refund, params)
@@ -72,9 +75,9 @@ module StripeMock
72
75
 
73
76
  def ensure_refund_required_params(params)
74
77
  if non_integer_charge_amount?(params)
75
- raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', 400)
78
+ raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400)
76
79
  elsif non_positive_charge_amount?(params)
77
- raise Stripe::InvalidRequestError.new('Invalid positive integer', 'amount', 400)
80
+ raise Stripe::InvalidRequestError.new('Invalid positive integer', 'amount', http_status: 400)
78
81
  elsif params[:charge].nil?
79
82
  raise Stripe::InvalidRequestError.new('Must provide the identifier of the charge to refund.', nil)
80
83
  end
@@ -0,0 +1,36 @@
1
+ module StripeMock
2
+ module RequestHandlers
3
+ module SubscriptionItems
4
+
5
+ def SubscriptionItems.included(klass)
6
+ klass.add_handler 'get /v1/subscription_items', :retrieve_subscription_items
7
+ klass.add_handler 'post /v1/subscription_items/([^/]*)', :update_subscription_item
8
+ klass.add_handler 'post /v1/subscription_items', :create_subscription_items
9
+ end
10
+
11
+ def retrieve_subscription_items(route, method_url, params, headers)
12
+ route =~ method_url
13
+
14
+ require_param(:subscription) unless params[:subscription]
15
+
16
+ Data.mock_list_object(subscriptions_items, params)
17
+ end
18
+
19
+ def create_subscription_items(route, method_url, params, headers)
20
+ params[:id] ||= new_id('si')
21
+
22
+ require_param(:subscription) unless params[:subscription]
23
+ require_param(:plan) unless params[:plan]
24
+
25
+ subscriptions_items[params[:id]] = Data.mock_subscription_item(params.merge(plan: plans[params[:plan]]))
26
+ end
27
+
28
+ def update_subscription_item(route, method_url, params, headers)
29
+ route =~ method_url
30
+
31
+ subscription_item = assert_existence :subscription_item, $1, subscriptions_items[$1]
32
+ subscription_item.merge!(params.merge(plan: plans[params[:plan]]))
33
+ end
34
+ end
35
+ end
36
+ end
@@ -35,9 +35,7 @@ module StripeMock
35
35
  def create_customer_subscription(route, method_url, params, headers)
36
36
  route =~ method_url
37
37
 
38
- plan_id = params[:plan].to_s
39
- plan = assert_existence :plan, plan_id, plans[plan_id]
40
-
38
+ subscription_plans = get_subscription_plans_from_params(params)
41
39
  customer = assert_existence :customer, $1, customers[$1]
42
40
 
43
41
  if params[:source]
@@ -46,11 +44,12 @@ module StripeMock
46
44
  customer[:default_source] = new_card[:id]
47
45
  end
48
46
 
49
- # Ensure customer has card to charge if plan has no trial and is not free
50
- verify_card_present(customer, plan, params)
51
-
52
47
  subscription = Data.mock_subscription({ id: (params[:id] || new_id('su')) })
53
- subscription.merge!(custom_subscription_params(plan, customer, params))
48
+ subscription = resolve_subscription_changes(subscription, subscription_plans, customer, params)
49
+
50
+ # Ensure customer has card to charge if plan has no trial and is not free
51
+ # Note: needs updating for subscriptions with multiple plans
52
+ verify_card_present(customer, subscription_plans.first, subscription, params)
54
53
 
55
54
  if params[:coupon]
56
55
  coupon_id = params[:coupon]
@@ -61,9 +60,9 @@ module StripeMock
61
60
  coupon = coupons[coupon_id]
62
61
 
63
62
  if coupon
64
- subscription[:discount] = Stripe::Util.convert_to_stripe_object({ coupon: coupon }, {})
63
+ add_coupon_to_object(subscription, coupon)
65
64
  else
66
- raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', 400)
65
+ raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400)
67
66
  end
68
67
  end
69
68
 
@@ -74,25 +73,50 @@ module StripeMock
74
73
  end
75
74
 
76
75
  def create_subscription(route, method_url, params, headers)
76
+ if headers && headers[:idempotency_key]
77
+ if subscriptions.any?
78
+ original_subscription = subscriptions.values.find { |c| c[:idempotency_key] == headers[:idempotency_key]}
79
+ puts original_subscription
80
+ return subscriptions[original_subscription[:id]] if original_subscription
81
+ end
82
+ end
77
83
  route =~ method_url
78
84
 
79
- plan_id = params[:plan].to_s
80
- plan = assert_existence :plan, plan_id, plans[plan_id]
85
+ subscription_plans = get_subscription_plans_from_params(params)
81
86
 
82
- customer_id = params[:customer].to_s
87
+ customer = params[:customer]
88
+ customer_id = customer.is_a?(Stripe::Customer) ? customer[:id] : customer.to_s
83
89
  customer = assert_existence :customer, customer_id, customers[customer_id]
84
90
 
91
+ if subscription_plans && customer
92
+ subscription_plans.each do |plan|
93
+ unless customer[:currency].to_s == plan[:currency].to_s
94
+ raise Stripe::InvalidRequestError.new("Customer's currency of #{customer[:currency]} does not match plan's currency of #{plan[:currency]}", 'currency', http_status: 400)
95
+ end
96
+ end
97
+ end
98
+
85
99
  if params[:source]
86
100
  new_card = get_card_by_token(params.delete(:source))
87
101
  add_card_to_object(:customer, new_card, customer)
88
102
  customer[:default_source] = new_card[:id]
89
103
  end
90
104
 
91
- # Ensure customer has card to charge if plan has no trial and is not free
92
- verify_card_present(customer, plan, params)
105
+ allowed_params = %w(customer application_fee_percent coupon items metadata plan quantity source tax_percent trial_end trial_period_days current_period_start created prorate billing_cycle_anchor billing days_until_due idempotency_key)
106
+ unknown_params = params.keys - allowed_params.map(&:to_sym)
107
+ if unknown_params.length > 0
108
+ raise Stripe::InvalidRequestError.new("Received unknown parameter: #{unknown_params.join}", unknown_params.first.to_s, http_status: 400)
109
+ end
93
110
 
94
111
  subscription = Data.mock_subscription({ id: (params[:id] || new_id('su')) })
95
- subscription.merge!(custom_subscription_params(plan, customer, params))
112
+ subscription = resolve_subscription_changes(subscription, subscription_plans, customer, params)
113
+ if headers[:idempotency_key]
114
+ subscription[:idempotency_key] = headers[:idempotency_key]
115
+ end
116
+
117
+ # Ensure customer has card to charge if plan has no trial and is not free
118
+ # Note: needs updating for subscriptions with multiple plans
119
+ verify_card_present(customer, subscription_plans.first, subscription, params)
96
120
 
97
121
  if params[:coupon]
98
122
  coupon_id = params[:coupon]
@@ -103,9 +127,9 @@ module StripeMock
103
127
  coupon = coupons[coupon_id]
104
128
 
105
129
  if coupon
106
- subscription[:discount] = Stripe::Util.convert_to_stripe_object({ coupon: coupon }, {})
130
+ add_coupon_to_object(subscription, coupon)
107
131
  else
108
- raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', 400)
132
+ raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400)
109
133
  end
110
134
  end
111
135
 
@@ -145,11 +169,12 @@ module StripeMock
145
169
  customer[:default_source] = new_card[:id]
146
170
  end
147
171
 
148
- # expand the plan for addition to the customer object
149
- plan_name =
150
- params[:plan].is_a?(String) ? params[:plan] : subscription[:plan][:id]
172
+ subscription_plans = get_subscription_plans_from_params(params)
151
173
 
152
- plan = plans[plan_name]
174
+ # subscription plans are not being updated but load them for the response
175
+ if subscription_plans.empty?
176
+ subscription_plans = subscription[:items][:data].map { |item| item[:plan] }
177
+ end
153
178
 
154
179
  if params[:coupon]
155
180
  coupon_id = params[:coupon]
@@ -159,17 +184,14 @@ module StripeMock
159
184
 
160
185
  coupon = coupons[coupon_id]
161
186
  if coupon
162
- subscription[:discount] = Stripe::Util.convert_to_stripe_object({ coupon: coupon }, {})
187
+ add_coupon_to_object(subscription, coupon)
163
188
  elsif coupon_id == ""
164
- subscription[:discount] = Stripe::Util.convert_to_stripe_object(nil, {})
189
+ subscription[:discount] = nil
165
190
  else
166
- raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', 400)
191
+ raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400)
167
192
  end
168
193
  end
169
-
170
- assert_existence :plan, plan_name, plan
171
- params[:plan] = plan if params[:plan]
172
- verify_card_present(customer, plan)
194
+ verify_card_present(customer, subscription_plans.first, subscription)
173
195
 
174
196
  if subscription[:cancel_at_period_end]
175
197
  subscription[:cancel_at_period_end] = false
@@ -177,7 +199,8 @@ module StripeMock
177
199
  end
178
200
 
179
201
  params[:current_period_start] = subscription[:current_period_start]
180
- subscription.merge!(custom_subscription_params(plan, customer, params))
202
+ params[:trial_end] = params[:trial_end] || subscription[:trial_end]
203
+ subscription = resolve_subscription_changes(subscription, subscription_plans, customer, params)
181
204
 
182
205
  # delete the old subscription, replace with the new subscription
183
206
  customer[:subscriptions][:data].reject! { |sub| sub[:id] == subscription[:id] }
@@ -216,11 +239,49 @@ module StripeMock
216
239
 
217
240
  private
218
241
 
219
- def verify_card_present(customer, plan, params={})
220
- if customer[:default_source].nil? && customer[:trial_end].nil? && plan[:trial_period_days].nil? &&
221
- plan[:amount] != 0 && plan[:trial_end].nil? && params[:trial_end].nil?
222
- raise Stripe::InvalidRequestError.new('You must supply a valid card xoxo', nil, 400)
242
+ def get_subscription_plans_from_params(params)
243
+ plan_ids = if params[:plan]
244
+ [params[:plan].to_s]
245
+ elsif params[:items]
246
+ items = params[:items]
247
+ items = items.values if items.respond_to?(:values)
248
+ items.map { |item| item[:plan].to_s if item[:plan] }
249
+ else
250
+ []
251
+ end
252
+ plan_ids.each do |plan_id|
253
+ assert_existence :plan, plan_id, plans[plan_id]
223
254
  end
255
+ plan_ids.map { |plan_id| plans[plan_id] }
256
+ end
257
+
258
+ # Ensure customer has card to charge unless one of the following criterias is met:
259
+ # 1) is in trial
260
+ # 2) is free
261
+ # 3) has billing set to send invoice
262
+ def verify_card_present(customer, plan, subscription, params={})
263
+ return if customer[:default_source]
264
+ return if customer[:trial_end]
265
+ return if params[:trial_end]
266
+
267
+ plan_trial_period_days = plan[:trial_period_days] || 0
268
+ plan_has_trial = plan_trial_period_days != 0 || plan[:amount] == 0 || plan[:trial_end]
269
+ return if plan && plan_has_trial
270
+
271
+ return if subscription && subscription[:trial_end] && subscription[:trial_end] != 'now'
272
+
273
+ if subscription[:items]
274
+ trial = subscription[:items][:data].none? do |item|
275
+ plan = item[:plan]
276
+ (plan[:trial_period_days].nil? || plan[:trial_period_days] == 0) &&
277
+ (plan[:trial_end].nil? || plan[:trial_end] == 'now')
278
+ end
279
+ return if trial
280
+ end
281
+
282
+ return if params[:billing] == 'send_invoice'
283
+
284
+ raise Stripe::InvalidRequestError.new('You must supply a valid card xoxo', nil, http_status: 400)
224
285
  end
225
286
 
226
287
  def verify_active_status(subscription)
@@ -228,7 +289,7 @@ module StripeMock
228
289
 
229
290
  if status == 'canceled'
230
291
  message = "No such subscription: #{id}"
231
- raise Stripe::InvalidRequestError.new(message, 'subscription', 404)
292
+ raise Stripe::InvalidRequestError.new(message, 'subscription', http_status: 404)
232
293
  end
233
294
  end
234
295
  end
@@ -0,0 +1,36 @@
1
+ module StripeMock
2
+ module RequestHandlers
3
+ module TaxRates
4
+ def TaxRates.included(klass)
5
+ klass.add_handler 'post /v1/tax_rates', :new_tax_rate
6
+ klass.add_handler 'post /v1/tax_rates/([^/]*)', :update_tax_rate
7
+ klass.add_handler 'get /v1/tax_rates/([^/]*)', :get_tax_rate
8
+ klass.add_handler 'get /v1/tax_rates', :list_tax_rates
9
+ end
10
+
11
+ def update_tax_rate(route, method_url, params, headers)
12
+ route =~ method_url
13
+ rate = assert_existence :tax_rate, $1, tax_rates[$1]
14
+ rate.merge!(params)
15
+ rate
16
+ end
17
+
18
+ def new_tax_rate(route, method_url, params, headers)
19
+ params[:id] ||= new_id('txr')
20
+ tax_rates[ params[:id] ] = Data.mock_tax_rate(params)
21
+ tax_rates[ params[:id] ]
22
+ end
23
+
24
+ def list_tax_rates(route, method_url, params, headers)
25
+ Data.mock_list_object(tax_rates.values, params)
26
+ end
27
+
28
+ def get_tax_rate(route, method_url, params, headers)
29
+ route =~ method_url
30
+ tax_rate = assert_existence :tax_rate, $1, tax_rates[$1]
31
+ tax_rate.clone
32
+ end
33
+ end
34
+ end
35
+ end
36
+
@@ -9,7 +9,7 @@ module StripeMock
9
9
 
10
10
  def create_token(route, method_url, params, headers)
11
11
  if params[:customer].nil? && params[:card].nil? && params[:bank_account].nil?
12
- raise Stripe::InvalidRequestError.new('You must supply either a card, customer, or bank account to create a token.', nil, 400)
12
+ raise Stripe::InvalidRequestError.new('You must supply either a card, customer, or bank account to create a token.', nil, http_status: 400)
13
13
  end
14
14
 
15
15
  cus_id = params[:customer]
@@ -31,6 +31,12 @@ module StripeMock
31
31
  params[:card][:fingerprint] = StripeMock::Util.fingerprint(params[:card][:number])
32
32
  params[:card][:last4] = params[:card][:number][-4,4]
33
33
  customer_card = params[:card]
34
+ elsif params[:bank_account].is_a?(String)
35
+ customer = assert_existence :customer, cus_id, customers[cus_id]
36
+
37
+ # params[:bank_account] is an id; grab it from the db
38
+ bank_account = verify_bank_account(customer, params[:bank_account])
39
+ assert_existence :bank_account, params[:bank_account], bank_account
34
40
  elsif params[:bank_account]
35
41
  # params[:card] is a hash of cc info; "Sanitize" the card number
36
42
  bank_account = params[:bank_account]
@@ -40,12 +46,12 @@ module StripeMock
40
46
  end
41
47
 
42
48
  if bank_account
43
- token_id = generate_bank_token(bank_account)
49
+ token_id = generate_bank_token(bank_account.dup)
44
50
  bank_account = @bank_tokens[token_id]
45
51
 
46
52
  Data.mock_bank_account_token(params.merge :id => token_id, :bank_account => bank_account)
47
53
  else
48
- token_id = generate_card_token(customer_card)
54
+ token_id = generate_card_token(customer_card.dup)
49
55
  card = @card_tokens[token_id]
50
56
 
51
57
  Data.mock_card_token(params.merge :id => token_id, :card => card)