stripe-ruby-mock 2.5.8 → 4.0.0

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 (159) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/rspec_tests.yml +38 -0
  3. data/.gitignore +1 -1
  4. data/.rspec +2 -1
  5. data/CHANGELOG.md +77 -0
  6. data/Gemfile +1 -5
  7. data/README.md +19 -11
  8. data/lib/stripe_mock/api/client.rb +2 -2
  9. data/lib/stripe_mock/api/errors.rb +34 -28
  10. data/lib/stripe_mock/api/instance.rb +1 -1
  11. data/lib/stripe_mock/api/webhooks.rb +68 -24
  12. data/lib/stripe_mock/client.rb +2 -1
  13. data/lib/stripe_mock/data/list.rb +42 -9
  14. data/lib/stripe_mock/data.rb +359 -21
  15. data/lib/stripe_mock/instance.rb +23 -5
  16. data/lib/stripe_mock/request_handlers/account_links.rb +15 -0
  17. data/lib/stripe_mock/request_handlers/accounts.rb +17 -6
  18. data/lib/stripe_mock/request_handlers/balance_transactions.rb +2 -2
  19. data/lib/stripe_mock/request_handlers/charges.rb +31 -5
  20. data/lib/stripe_mock/request_handlers/checkout_session.rb +179 -0
  21. data/lib/stripe_mock/request_handlers/customers.rb +47 -19
  22. data/lib/stripe_mock/request_handlers/ephemeral_key.rb +1 -1
  23. data/lib/stripe_mock/request_handlers/events.rb +30 -3
  24. data/lib/stripe_mock/request_handlers/express_login_links.rb +15 -0
  25. data/lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb +6 -0
  26. data/lib/stripe_mock/request_handlers/helpers/search_helpers.rb +67 -0
  27. data/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb +36 -12
  28. data/lib/stripe_mock/request_handlers/helpers/token_helpers.rb +1 -1
  29. data/lib/stripe_mock/request_handlers/invoices.rb +26 -6
  30. data/lib/stripe_mock/request_handlers/payment_intents.rb +202 -0
  31. data/lib/stripe_mock/request_handlers/payment_methods.rb +124 -0
  32. data/lib/stripe_mock/request_handlers/plans.rb +1 -1
  33. data/lib/stripe_mock/request_handlers/prices.rb +71 -0
  34. data/lib/stripe_mock/request_handlers/products.rb +15 -5
  35. data/lib/stripe_mock/request_handlers/promotion_codes.rb +43 -0
  36. data/lib/stripe_mock/request_handlers/refunds.rb +13 -2
  37. data/lib/stripe_mock/request_handlers/setup_intents.rb +100 -0
  38. data/lib/stripe_mock/request_handlers/sources.rb +12 -6
  39. data/lib/stripe_mock/request_handlers/subscriptions.rb +146 -25
  40. data/lib/stripe_mock/request_handlers/tokens.rb +6 -4
  41. data/lib/stripe_mock/request_handlers/transfers.rb +12 -1
  42. data/lib/stripe_mock/request_handlers/validators/param_validators.rb +124 -9
  43. data/lib/stripe_mock/server.rb +2 -2
  44. data/lib/stripe_mock/test_strategies/base.rb +98 -12
  45. data/lib/stripe_mock/test_strategies/live.rb +23 -12
  46. data/lib/stripe_mock/test_strategies/mock.rb +6 -2
  47. data/lib/stripe_mock/version.rb +1 -1
  48. data/lib/stripe_mock/webhook_fixtures/account.updated.json +1 -1
  49. data/lib/stripe_mock/webhook_fixtures/balance.available.json +27 -15
  50. data/lib/stripe_mock/webhook_fixtures/charge.captured.json +143 -0
  51. data/lib/stripe_mock/webhook_fixtures/charge.dispute.created.json +63 -16
  52. data/lib/stripe_mock/webhook_fixtures/charge.failed.json +101 -44
  53. data/lib/stripe_mock/webhook_fixtures/charge.refund.updated.json +35 -0
  54. data/lib/stripe_mock/webhook_fixtures/charge.refunded.json +145 -50
  55. data/lib/stripe_mock/webhook_fixtures/charge.succeeded.json +114 -43
  56. data/lib/stripe_mock/webhook_fixtures/checkout.session.completed.json +79 -0
  57. data/lib/stripe_mock/webhook_fixtures/checkout.session.completed.payment_mode.json +53 -0
  58. data/lib/stripe_mock/webhook_fixtures/checkout.session.completed.setup_mode.json +45 -0
  59. data/lib/stripe_mock/webhook_fixtures/customer.created.json +37 -45
  60. data/lib/stripe_mock/webhook_fixtures/customer.deleted.json +36 -32
  61. data/lib/stripe_mock/webhook_fixtures/customer.source.created.json +31 -22
  62. data/lib/stripe_mock/webhook_fixtures/customer.source.updated.json +36 -25
  63. data/lib/stripe_mock/webhook_fixtures/customer.subscription.created.json +135 -47
  64. data/lib/stripe_mock/webhook_fixtures/customer.subscription.deleted.json +134 -45
  65. data/lib/stripe_mock/webhook_fixtures/customer.subscription.updated.json +135 -56
  66. data/lib/stripe_mock/webhook_fixtures/customer.updated.json +38 -46
  67. data/lib/stripe_mock/webhook_fixtures/invoice.created.json +176 -49
  68. data/lib/stripe_mock/webhook_fixtures/invoice.finalized.json +171 -0
  69. data/lib/stripe_mock/webhook_fixtures/invoice.paid.json +171 -0
  70. data/lib/stripe_mock/webhook_fixtures/invoice.payment_action_required.json +171 -0
  71. data/lib/stripe_mock/webhook_fixtures/invoice.payment_failed.json +149 -83
  72. data/lib/stripe_mock/webhook_fixtures/invoice.payment_succeeded.json +149 -90
  73. data/lib/stripe_mock/webhook_fixtures/invoice.upcoming.json +70 -0
  74. data/lib/stripe_mock/webhook_fixtures/invoice.updated.json +178 -50
  75. data/lib/stripe_mock/webhook_fixtures/invoiceitem.created.json +87 -13
  76. data/lib/stripe_mock/webhook_fixtures/invoiceitem.updated.json +88 -14
  77. data/lib/stripe_mock/webhook_fixtures/mandate.updated.json +34 -0
  78. data/lib/stripe_mock/webhook_fixtures/payment_intent.amount_capturable_updated.json +170 -0
  79. data/lib/stripe_mock/webhook_fixtures/payment_intent.canceled.json +73 -0
  80. data/lib/stripe_mock/webhook_fixtures/payment_intent.created.json +86 -0
  81. data/lib/stripe_mock/webhook_fixtures/payment_intent.payment_failed.json +225 -0
  82. data/lib/stripe_mock/webhook_fixtures/payment_intent.processing.json +162 -0
  83. data/lib/stripe_mock/webhook_fixtures/payment_intent.requires_action.json +191 -0
  84. data/lib/stripe_mock/webhook_fixtures/payment_intent.succeeded.json +196 -0
  85. data/lib/stripe_mock/webhook_fixtures/payment_link.created.json +47 -0
  86. data/lib/stripe_mock/webhook_fixtures/payment_link.updated.json +50 -0
  87. data/lib/stripe_mock/webhook_fixtures/payment_method.attached.json +63 -0
  88. data/lib/stripe_mock/webhook_fixtures/payment_method.detached.json +62 -0
  89. data/lib/stripe_mock/webhook_fixtures/payout.created.json +40 -0
  90. data/lib/stripe_mock/webhook_fixtures/payout.paid.json +40 -0
  91. data/lib/stripe_mock/webhook_fixtures/payout.updated.json +46 -0
  92. data/lib/stripe_mock/webhook_fixtures/plan.created.json +30 -13
  93. data/lib/stripe_mock/webhook_fixtures/plan.deleted.json +30 -13
  94. data/lib/stripe_mock/webhook_fixtures/plan.updated.json +34 -14
  95. data/lib/stripe_mock/webhook_fixtures/price.created.json +42 -0
  96. data/lib/stripe_mock/webhook_fixtures/price.deleted.json +42 -0
  97. data/lib/stripe_mock/webhook_fixtures/price.updated.json +48 -0
  98. data/lib/stripe_mock/webhook_fixtures/product.created.json +40 -0
  99. data/lib/stripe_mock/webhook_fixtures/product.deleted.json +40 -0
  100. data/lib/stripe_mock/webhook_fixtures/product.updated.json +47 -0
  101. data/lib/stripe_mock/webhook_fixtures/quote.accepted.json +92 -0
  102. data/lib/stripe_mock/webhook_fixtures/quote.canceled.json +92 -0
  103. data/lib/stripe_mock/webhook_fixtures/quote.created.json +92 -0
  104. data/lib/stripe_mock/webhook_fixtures/quote.finalized.json +92 -0
  105. data/lib/stripe_mock/webhook_fixtures/setup_intent.canceled.json +46 -0
  106. data/lib/stripe_mock/webhook_fixtures/setup_intent.created.json +51 -0
  107. data/lib/stripe_mock/webhook_fixtures/setup_intent.setup_failed.json +100 -0
  108. data/lib/stripe_mock/webhook_fixtures/setup_intent.succeeded.json +46 -0
  109. data/lib/stripe_mock/webhook_fixtures/subscription_schedule.canceled.json +119 -0
  110. data/lib/stripe_mock/webhook_fixtures/subscription_schedule.created.json +114 -0
  111. data/lib/stripe_mock/webhook_fixtures/subscription_schedule.released.json +111 -0
  112. data/lib/stripe_mock/webhook_fixtures/subscription_schedule.updated.json +125 -0
  113. data/lib/stripe_mock/webhook_fixtures/tax_rate.created.json +32 -0
  114. data/lib/stripe_mock/webhook_fixtures/tax_rate.updated.json +37 -0
  115. data/lib/stripe_mock.rb +11 -0
  116. data/spec/instance_spec.rb +13 -13
  117. data/spec/integration_examples/completing_checkout_sessions_example.rb +37 -0
  118. data/spec/list_spec.rb +38 -0
  119. data/spec/readme_spec.rb +1 -1
  120. data/spec/server_spec.rb +6 -3
  121. data/spec/shared_stripe_examples/account_examples.rb +10 -2
  122. data/spec/shared_stripe_examples/account_link_examples.rb +16 -0
  123. data/spec/shared_stripe_examples/balance_examples.rb +6 -0
  124. data/spec/shared_stripe_examples/balance_transaction_examples.rb +3 -3
  125. data/spec/shared_stripe_examples/bank_examples.rb +3 -3
  126. data/spec/shared_stripe_examples/bank_token_examples.rb +5 -7
  127. data/spec/shared_stripe_examples/card_examples.rb +4 -4
  128. data/spec/shared_stripe_examples/card_token_examples.rb +17 -21
  129. data/spec/shared_stripe_examples/charge_examples.rb +106 -22
  130. data/spec/shared_stripe_examples/checkout_session_examples.rb +99 -0
  131. data/spec/shared_stripe_examples/coupon_examples.rb +1 -1
  132. data/spec/shared_stripe_examples/customer_examples.rb +149 -53
  133. data/spec/shared_stripe_examples/dispute_examples.rb +2 -2
  134. data/spec/shared_stripe_examples/error_mock_examples.rb +8 -7
  135. data/spec/shared_stripe_examples/express_login_link_examples.rb +12 -0
  136. data/spec/shared_stripe_examples/external_account_examples.rb +3 -3
  137. data/spec/shared_stripe_examples/invoice_examples.rb +148 -40
  138. data/spec/shared_stripe_examples/invoice_item_examples.rb +1 -1
  139. data/spec/shared_stripe_examples/payment_intent_examples.rb +283 -0
  140. data/spec/shared_stripe_examples/payment_method_examples.rb +454 -0
  141. data/spec/shared_stripe_examples/payout_examples.rb +2 -2
  142. data/spec/shared_stripe_examples/plan_examples.rb +135 -92
  143. data/spec/shared_stripe_examples/price_examples.rb +292 -0
  144. data/spec/shared_stripe_examples/product_examples.rb +215 -0
  145. data/spec/shared_stripe_examples/promotion_code_examples.rb +68 -0
  146. data/spec/shared_stripe_examples/refund_examples.rb +38 -21
  147. data/spec/shared_stripe_examples/setup_intent_examples.rb +85 -0
  148. data/spec/shared_stripe_examples/subscription_examples.rb +706 -324
  149. data/spec/shared_stripe_examples/subscription_items_examples.rb +3 -2
  150. data/spec/shared_stripe_examples/transfer_examples.rb +16 -7
  151. data/spec/shared_stripe_examples/webhook_event_examples.rb +62 -16
  152. data/spec/spec_helper.rb +8 -5
  153. data/spec/stripe_mock_spec.rb +4 -4
  154. data/spec/support/shared_contexts/stripe_validator_spec.rb +8 -0
  155. data/spec/support/stripe_examples.rb +11 -1
  156. data/stripe-ruby-mock.gemspec +9 -5
  157. metadata +115 -47
  158. data/.travis.yml +0 -28
  159. data/spec/shared_stripe_examples/product_example.rb +0 -65
@@ -5,14 +5,17 @@ module StripeMock
5
5
  def Charges.included(klass)
6
6
  klass.add_handler 'post /v1/charges', :new_charge
7
7
  klass.add_handler 'get /v1/charges', :get_charges
8
- klass.add_handler 'get /v1/charges/(.*)', :get_charge
8
+ klass.add_handler 'get /v1/charges/search', :search_charges
9
+ klass.add_handler 'get /v1/charges/((?!search).*)', :get_charge
9
10
  klass.add_handler 'post /v1/charges/(.*)/capture', :capture_charge
10
11
  klass.add_handler 'post /v1/charges/(.*)/refund', :refund_charge
11
12
  klass.add_handler 'post /v1/charges/(.*)/refunds', :refund_charge
12
13
  klass.add_handler 'post /v1/charges/(.*)', :update_charge
13
14
  end
14
15
 
15
- def new_charge(route, method_url, params, headers)
16
+ def new_charge(route, method_url, params, headers = {})
17
+ stripe_account = headers && headers[:stripe_account] || Stripe.api_key
18
+
16
19
  if headers && headers[:idempotency_key]
17
20
  params[:idempotency_key] = headers[:idempotency_key]
18
21
  if charges.any?
@@ -29,7 +32,7 @@ module StripeMock
29
32
  # card id, not a token. in this case we'll find the card in the customer
30
33
  # object and return that.
31
34
  if params[:customer]
32
- params[:source] = get_card(customers[params[:customer]], params[:source])
35
+ params[:source] = get_card(customers[stripe_account][params[:customer]], params[:source])
33
36
  else
34
37
  params[:source] = get_card_or_bank_by_token(params[:source])
35
38
  end
@@ -37,7 +40,7 @@ module StripeMock
37
40
  raise Stripe::InvalidRequestError.new("Invalid token id: #{params[:source]}", 'card', http_status: 400)
38
41
  end
39
42
  elsif params[:customer]
40
- customer = customers[params[:customer]]
43
+ customer = customers[stripe_account][params[:customer]]
41
44
  if customer && customer[:default_source]
42
45
  params[:source] = get_card(customer, customer[:default_source])
43
46
  end
@@ -88,6 +91,24 @@ module StripeMock
88
91
  Data.mock_list_object(clone.values, params)
89
92
  end
90
93
 
94
+ SEARCH_FIELDS = [
95
+ "amount",
96
+ "currency",
97
+ "customer",
98
+ "payment_method_details.card.brand",
99
+ "payment_method_details.card.exp_month",
100
+ "payment_method_details.card.exp_year",
101
+ "payment_method_details.card.fingerprint",
102
+ "payment_method_details.card.last4",
103
+ "status",
104
+ ].freeze
105
+ def search_charges(route, method_url, params, headers)
106
+ require_param(:query) unless params[:query]
107
+
108
+ results = search_results(charges.values, params[:query], fields: SEARCH_FIELDS, resource_name: "charges")
109
+ Data.mock_list_object(results, params)
110
+ end
111
+
91
112
  def get_charge(route, method_url, params, headers)
92
113
  route =~ method_url
93
114
  charge_id = $1 || params[:charge]
@@ -146,7 +167,7 @@ module StripeMock
146
167
  elsif non_positive_charge_amount?(params)
147
168
  raise Stripe::InvalidRequestError.new('Invalid positive integer', 'amount', http_status: 400)
148
169
  elsif params[:source].nil? && params[:customer].nil?
149
- raise Stripe::InvalidRequestError.new('Must provide source or customer.', http_status: nil)
170
+ raise Stripe::InvalidRequestError.new('Must provide source or customer.', nil, http_status: nil)
150
171
  end
151
172
  end
152
173
 
@@ -169,6 +190,11 @@ module StripeMock
169
190
  params[:refunds].has_key?(:data) && params[:refunds][:data].nil?)
170
191
  allowed << :refunds
171
192
  end
193
+ if params.has_key?(:payment_method_details) && (params[:payment_method_details].empty? ||
194
+ params[:payment_method_details].has_key?(:card) && (params[:payment_method_details][:card].empty? ||
195
+ params[:payment_method_details][:card].has_key?(:checks) && params[:payment_method_details][:card][:checks].empty?))
196
+ allowed << :payment_method_details
197
+ end
172
198
 
173
199
  allowed
174
200
  end
@@ -0,0 +1,179 @@
1
+ module StripeMock
2
+ module RequestHandlers
3
+ module Checkout
4
+ module Session
5
+ def Session.included(klass)
6
+ klass.add_handler 'post /v1/checkout/sessions', :new_session
7
+ klass.add_handler 'get /v1/checkout/sessions', :list_checkout_sessions
8
+ klass.add_handler 'get /v1/checkout/sessions/([^/]*)', :get_checkout_session
9
+ klass.add_handler 'get /v1/checkout/sessions/([^/]*)/line_items', :list_line_items
10
+ end
11
+
12
+ def new_session(route, method_url, params, headers)
13
+ id = params[:id] || new_id('cs')
14
+
15
+ [:cancel_url, :success_url].each do |p|
16
+ require_param(p) if params[p].nil? || params[p].empty?
17
+ end
18
+
19
+ line_items = nil
20
+ if params[:line_items]
21
+ line_items = params[:line_items].each_with_index.map do |line_item, i|
22
+ throw Stripe::InvalidRequestError("Quantity is required. Add `quantity` to `line_items[#{i}]`") unless line_item[:quantity]
23
+ unless line_item[:price] || line_item[:price_data] || (line_item[:amount] && line_item[:currency] && line_item[:name])
24
+ throw Stripe::InvalidRequestError("Price or amount and currency is required. Add `price`, `price_data`, or `amount`, `currency` and `name` to `line_items[#{i}]`")
25
+ end
26
+ {
27
+ id: new_id("li"),
28
+ price: if line_item[:price]
29
+ line_item[:price]
30
+ elsif line_item[:price_data]
31
+ new_price(nil, nil, line_item[:price_data], nil)[:id]
32
+ else
33
+ new_price(nil, nil, {
34
+ unit_amount: line_item[:amount],
35
+ currency: line_item[:currency],
36
+ product_data: {
37
+ name: line_item[:name]
38
+ }
39
+ }, nil)[:id]
40
+ end,
41
+ quantity: line_item[:quantity]
42
+ }
43
+ end
44
+ end
45
+
46
+ amount = nil
47
+ currency = nil
48
+ if line_items
49
+ amount = 0
50
+
51
+ line_items.each do |line_item|
52
+ price = prices[line_item[:price]]
53
+
54
+ if price.nil?
55
+ raise StripeMock::StripeMockError.new("Price not found for ID: #{line_item[:price]}")
56
+ end
57
+
58
+ amount += (price[:unit_amount] * line_item[:quantity])
59
+ end
60
+
61
+ currency = prices[line_items.first[:price]][:currency]
62
+ end
63
+
64
+ payment_status = "unpaid"
65
+ payment_intent = nil
66
+ setup_intent = nil
67
+ case params[:mode]
68
+ when nil, "payment"
69
+ params[:customer] ||= new_customer(nil, nil, {email: params[:customer_email]}, nil)[:id]
70
+ require_param(:line_items) if params[:line_items].nil? || params[:line_items].empty?
71
+ payment_intent = new_payment_intent(nil, nil, {
72
+ amount: amount,
73
+ currency: currency,
74
+ customer: params[:customer],
75
+ payment_method_options: params[:payment_method_options],
76
+ payment_method_types: params[:payment_method_types]
77
+ }.merge(params[:payment_intent_data] || {}), nil)[:id]
78
+ checkout_session_line_items[id] = line_items
79
+ when "setup"
80
+ if !params[:line_items].nil? && !params[:line_items].empty?
81
+ throw Stripe::InvalidRequestError.new("You cannot pass `line_items` in `setup` mode", :line_items, http_status: 400)
82
+ end
83
+ setup_intent = new_setup_intent(nil, nil, {
84
+ customer: params[:customer],
85
+ payment_method_options: params[:payment_method_options],
86
+ payment_method_types: params[:payment_method_types]
87
+ }.merge(params[:setup_intent_data] || {}), nil)[:id]
88
+ payment_status = "no_payment_required"
89
+ when "subscription"
90
+ params[:customer] ||= new_customer(nil, nil, {email: params[:customer_email]}, nil)[:id]
91
+ require_param(:line_items) if params[:line_items].nil? || params[:line_items].empty?
92
+ checkout_session_line_items[id] = line_items
93
+ else
94
+ throw Stripe::InvalidRequestError.new("Invalid mode: must be one of payment, setup, or subscription", :mode, http_status: 400)
95
+ end
96
+
97
+ checkout_sessions[id] = {
98
+ id: id,
99
+ object: "checkout.session",
100
+ allow_promotion_codes: nil,
101
+ amount_subtotal: amount,
102
+ amount_total: amount,
103
+ automatic_tax: {
104
+ enabled: false,
105
+ status: nil
106
+ },
107
+ billing_address_collection: nil,
108
+ cancel_url: params[:cancel_url],
109
+ client_reference_id: nil,
110
+ currency: currency,
111
+ customer: params[:customer],
112
+ customer_details: nil,
113
+ customer_email: params[:customer_email],
114
+ livemode: false,
115
+ locale: nil,
116
+ metadata: params[:metadata],
117
+ mode: params[:mode],
118
+ payment_intent: payment_intent,
119
+ payment_method_options: params[:payment_method_options],
120
+ payment_method_types: params[:payment_method_types],
121
+ payment_status: payment_status,
122
+ setup_intent: setup_intent,
123
+ shipping: nil,
124
+ shipping_address_collection: nil,
125
+ submit_type: nil,
126
+ subscription: nil,
127
+ success_url: params[:success_url],
128
+ total_details: nil,
129
+ url: URI.join(StripeMock.checkout_base, id).to_s
130
+ }
131
+ end
132
+
133
+ def list_checkout_sessions(route, method_url, params, headers)
134
+ Data.mock_list_object(checkout_sessions.values)
135
+ end
136
+
137
+ def get_checkout_session(route, method_url, params, headers)
138
+ route =~ method_url
139
+ checkout_session = assert_existence :checkout_session, $1, checkout_sessions[$1]
140
+
141
+ checkout_session = checkout_session.clone
142
+ if params[:expand]&.include?('setup_intent') && checkout_session[:setup_intent]
143
+ checkout_session[:setup_intent] = setup_intents[checkout_session[:setup_intent]]
144
+ end
145
+ checkout_session
146
+ end
147
+
148
+ def list_line_items(route, method_url, params, headers)
149
+ route =~ method_url
150
+ checkout_session = assert_existence :checkout_session, $1, checkout_sessions[$1]
151
+
152
+ case checkout_session[:mode]
153
+ when "payment", "subscription"
154
+ line_items = assert_existence :checkout_session_line_items, $1, checkout_session_line_items[$1]
155
+ line_items.map do |line_item|
156
+ price = prices[line_item[:price]].clone
157
+
158
+ if price.nil?
159
+ raise StripeMock::StripeMockError.new("Price not found for ID: #{line_item[:price]}")
160
+ end
161
+
162
+ {
163
+ id: line_item[:id],
164
+ object: "item",
165
+ amount_subtotal: price[:unit_amount] * line_item[:quantity],
166
+ amount_total: price[:unit_amount] * line_item[:quantity],
167
+ currency: price[:currency],
168
+ price: price.clone,
169
+ quantity: line_item[:quantity]
170
+ }
171
+ end
172
+ else
173
+ throw Stripe::InvalidRequestError("Only payment and subscription sessions have line items")
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -5,13 +5,15 @@ module StripeMock
5
5
  def Customers.included(klass)
6
6
  klass.add_handler 'post /v1/customers', :new_customer
7
7
  klass.add_handler 'post /v1/customers/([^/]*)', :update_customer
8
- klass.add_handler 'get /v1/customers/([^/]*)', :get_customer
8
+ klass.add_handler 'get /v1/customers/((?!search)[^/]*)', :get_customer
9
9
  klass.add_handler 'delete /v1/customers/([^/]*)', :delete_customer
10
10
  klass.add_handler 'get /v1/customers', :list_customers
11
+ klass.add_handler 'get /v1/customers/search', :search_customers
11
12
  klass.add_handler 'delete /v1/customers/([^/]*)/discount', :delete_customer_discount
12
13
  end
13
14
 
14
15
  def new_customer(route, method_url, params, headers)
16
+ stripe_account = headers && headers[:stripe_account] || Stripe.api_key
15
17
  params[:id] ||= new_id('cus')
16
18
  sources = []
17
19
 
@@ -29,7 +31,8 @@ module StripeMock
29
31
  params[:default_source] = sources.first[:id]
30
32
  end
31
33
 
32
- customers[ params[:id] ] = Data.mock_customer(sources, params)
34
+ customers[stripe_account] ||= {}
35
+ customers[stripe_account][params[:id]] = Data.mock_customer(sources, params)
33
36
 
34
37
  if params[:plan]
35
38
  plan_id = params[:plan].to_s
@@ -40,26 +43,30 @@ module StripeMock
40
43
  end
41
44
 
42
45
  subscription = Data.mock_subscription({ id: new_id('su') })
43
- subscription = resolve_subscription_changes(subscription, [plan], customers[ params[:id] ], params)
44
- add_subscription_to_customer(customers[ params[:id] ], subscription)
46
+ subscription = resolve_subscription_changes(subscription, [plan], customers[stripe_account][params[:id]], params)
47
+ add_subscription_to_customer(customers[stripe_account][params[:id]], subscription)
45
48
  subscriptions[subscription[:id]] = subscription
46
49
  elsif params[:trial_end]
47
50
  raise Stripe::InvalidRequestError.new('Received unknown parameter: trial_end', nil, http_status: 400)
48
51
  end
49
52
 
50
53
  if params[:coupon]
51
- coupon = coupons[ params[:coupon] ]
54
+ coupon = coupons[params[:coupon]]
52
55
  assert_existence :coupon, params[:coupon], coupon
53
-
54
- add_coupon_to_object(customers[params[:id]], coupon)
56
+ add_coupon_to_object(customers[stripe_account][params[:id]], coupon)
55
57
  end
56
58
 
57
- customers[ params[:id] ]
59
+ customers[stripe_account][params[:id]]
58
60
  end
59
61
 
60
62
  def update_customer(route, method_url, params, headers)
63
+ stripe_account = headers && headers[:stripe_account] || Stripe.api_key
61
64
  route =~ method_url
62
- cus = assert_existence :customer, $1, customers[$1]
65
+ cus = assert_existence :customer, $1, customers[stripe_account][$1]
66
+
67
+ # get existing and pending metadata
68
+ metadata = cus.delete(:metadata) || {}
69
+ metadata_updates = params.delete(:metadata) || {}
63
70
 
64
71
  # Delete those params if their value is nil. Workaround of the problematic way Stripe serialize objects
65
72
  params.delete(:sources) if params[:sources] && params[:sources][:data].nil?
@@ -72,10 +79,13 @@ module StripeMock
72
79
  params.delete(:subscriptions) unless params[:subscriptions][:data].any?{ |v| !!v[:type]}
73
80
  end
74
81
  cus.merge!(params)
82
+ cus[:metadata] = {**metadata, **metadata_updates}
75
83
 
76
84
  if params[:source]
77
85
  if params[:source].is_a?(String)
78
86
  new_card = get_card_or_bank_by_token(params.delete(:source))
87
+ elsif params[:source].is_a?(Stripe::Token)
88
+ new_card = get_card_or_bank_by_token(params[:source][:id])
79
89
  elsif params[:source].is_a?(Hash)
80
90
  unless params[:source][:object] && params[:source][:number] && params[:source][:exp_month] && params[:source][:exp_year]
81
91
  raise Stripe::InvalidRequestError.new('You must supply a valid card', nil, http_status: 400)
@@ -87,31 +97,37 @@ module StripeMock
87
97
  end
88
98
 
89
99
  if params[:coupon]
90
- coupon = coupons[ params[:coupon] ]
91
- assert_existence :coupon, params[:coupon], coupon
100
+ if params[:coupon] == ''
101
+ delete_coupon_from_object(cus)
102
+ else
103
+ coupon = coupons[params[:coupon]]
104
+ assert_existence :coupon, params[:coupon], coupon
92
105
 
93
- add_coupon_to_object(cus, coupon)
106
+ add_coupon_to_object(cus, coupon)
107
+ end
94
108
  end
95
109
 
96
110
  cus
97
111
  end
98
112
 
99
113
  def delete_customer(route, method_url, params, headers)
114
+ stripe_account = headers && headers[:stripe_account] || Stripe.api_key
100
115
  route =~ method_url
101
- assert_existence :customer, $1, customers[$1]
116
+ assert_existence :customer, $1, customers[stripe_account][$1]
102
117
 
103
- customers[$1] = {
104
- id: customers[$1][:id],
118
+ customers[stripe_account][$1] = {
119
+ id: customers[stripe_account][$1][:id],
105
120
  deleted: true
106
121
  }
107
122
  end
108
123
 
109
124
  def get_customer(route, method_url, params, headers)
125
+ stripe_account = headers && headers[:stripe_account] || Stripe.api_key
110
126
  route =~ method_url
111
- customer = assert_existence :customer, $1, customers[$1]
127
+ customer = assert_existence :customer, $1, customers[stripe_account][$1]
112
128
 
113
129
  customer = customer.clone
114
- if params[:expand] == ['default_source']
130
+ if params[:expand] == ['default_source'] && customer[:sources][:data]
115
131
  customer[:default_source] = customer[:sources][:data].detect do |source|
116
132
  source[:id] == customer[:default_source]
117
133
  end
@@ -121,12 +137,24 @@ module StripeMock
121
137
  end
122
138
 
123
139
  def list_customers(route, method_url, params, headers)
124
- Data.mock_list_object(customers.values, params)
140
+ stripe_account = headers && headers[:stripe_account] || Stripe.api_key
141
+ Data.mock_list_object(customers[stripe_account]&.values, params)
142
+ end
143
+
144
+ SEARCH_FIELDS = ["email", "name", "phone"].freeze
145
+ def search_customers(route, method_url, params, headers)
146
+ require_param(:query) unless params[:query]
147
+
148
+ stripe_account = headers && headers[:stripe_account] || Stripe.api_key
149
+ all_customers = customers[stripe_account]&.values
150
+ results = search_results(all_customers, params[:query], fields: SEARCH_FIELDS, resource_name: "customers")
151
+ Data.mock_list_object(results, params)
125
152
  end
126
153
 
127
154
  def delete_customer_discount(route, method_url, params, headers)
155
+ stripe_account = headers && headers[:stripe_account] || Stripe.api_key
128
156
  route =~ method_url
129
- cus = assert_existence :customer, $1, customers[$1]
157
+ cus = assert_existence :customer, $1, customers[stripe_account][$1]
130
158
 
131
159
  cus[:discount] = nil
132
160
 
@@ -6,7 +6,7 @@ module StripeMock
6
6
  end
7
7
 
8
8
  def create_ephemeral_key(route, method_url, params, headers)
9
- Data.mock_ephemeral_key(params)
9
+ Data.mock_ephemeral_key(**params)
10
10
  end
11
11
  end
12
12
  end
@@ -4,7 +4,7 @@ module StripeMock
4
4
 
5
5
  def Events.included(klass)
6
6
  klass.add_handler 'get /v1/events/(.*)', :retrieve_event
7
- klass.add_handler 'get /v1/events', :list_events
7
+ klass.add_handler 'get /v1/events', :list_events
8
8
  end
9
9
 
10
10
  def retrieve_event(route, method_url, params, headers)
@@ -13,9 +13,36 @@ module StripeMock
13
13
  end
14
14
 
15
15
  def list_events(route, method_url, params, headers)
16
- Data.mock_list_object(events.values, params)
16
+ values = filter_by_created(events.values, params: params)
17
+ Data.mock_list_object(values, params)
17
18
  end
18
-
19
+
20
+ private
21
+
22
+ def filter_by_created(event_list, params:)
23
+ if params[:created].nil?
24
+ return event_list
25
+ end
26
+
27
+ if params[:created].is_a?(Hash)
28
+ if params[:created][:gt]
29
+ event_list = event_list.select { |event| event[:created] > params[:created][:gt].to_i }
30
+ end
31
+ if params[:created][:gte]
32
+ event_list = event_list.select { |event| event[:created] >= params[:created][:gte].to_i }
33
+ end
34
+ if params[:created][:lt]
35
+ event_list = event_list.select { |event| event[:created] < params[:created][:lt].to_i }
36
+ end
37
+ if params[:created][:lte]
38
+ event_list = event_list.select { |event| event[:created] <= params[:created][:lte].to_i }
39
+ end
40
+ else
41
+ event_list = event_list.select { |event| event[:created] == params[:created].to_i }
42
+ end
43
+ event_list
44
+ end
45
+
19
46
  end
20
47
  end
21
48
  end
@@ -0,0 +1,15 @@
1
+ module StripeMock
2
+ module RequestHandlers
3
+ module ExpressLoginLinks
4
+
5
+ def ExpressLoginLinks.included(klass)
6
+ klass.add_handler 'post /v1/accounts/(.*)/login_links', :new_account_login_link
7
+ end
8
+
9
+ def new_account_login_link(route, method_url, params, headers)
10
+ route =~ method_url
11
+ Data.mock_express_login_link(params)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -7,11 +7,17 @@ module StripeMock
7
7
  attrs[:coupon] = coupon
8
8
  attrs[:start] = Time.now.to_i
9
9
  attrs[:end] = (DateTime.now >> coupon[:duration_in_months].to_i).to_time.to_i if coupon[:duration] == 'repeating'
10
+ attrs[:id] = new_id("di")
10
11
  end
11
12
 
12
13
  object[:discount] = Stripe::Discount.construct_from(discount_attrs)
13
14
  object
14
15
  end
16
+
17
+ def delete_coupon_from_object(object)
18
+ object[:discount] = nil
19
+ object
20
+ end
15
21
  end
16
22
  end
17
23
  end
@@ -0,0 +1,67 @@
1
+ module StripeMock
2
+ module RequestHandlers
3
+ module Helpers
4
+ # Only supports exact matches on a single field, e.g.
5
+ # - 'amount:100'
6
+ # - 'email:"name@domain.com"'
7
+ # - 'name:"Foo Bar"'
8
+ # - 'metadata["foo"]:"bar"'
9
+ QUERYSTRING_PATTERN = /\A(?<field>[\w\.]+)(\[['"](?<metadata_key>[^'"]*)['"]\])?:['"]?(?<value>[^'"]*)['"]?\z/
10
+ def search_results(all_values, querystring, fields: [], resource_name:)
11
+ values = all_values.dup
12
+ query_match = QUERYSTRING_PATTERN.match(querystring)
13
+ raise Stripe::InvalidRequestError.new(
14
+ 'We were unable to parse your search query.' \
15
+ ' Try using the format `metadata["key"]:"value"` to query for metadata or key:"value" to query for other fields.',
16
+ nil,
17
+ http_status: 400,
18
+ ) unless query_match
19
+
20
+ case query_match[:field]
21
+ when *fields
22
+ values = values.select { |resource|
23
+ exact_match?(actual: field_value(resource, field: query_match[:field]), expected: query_match[:value])
24
+ }
25
+ when "metadata"
26
+ values = values.select { |resource|
27
+ resource[:metadata] &&
28
+ exact_match?(actual: resource[:metadata][query_match[:metadata_key].to_sym], expected: query_match[:value])
29
+ }
30
+ else
31
+ raise Stripe::InvalidRequestError.new(
32
+ "Field `#{query_match[:field]}` is an unsupported search field for resource `#{resource_name}`." \
33
+ " See http://stripe.com/docs/search#query-fields-for-#{resource_name.gsub('_', '-')} for a list of supported fields.",
34
+ nil,
35
+ http_status: 400,
36
+ )
37
+ end
38
+
39
+ values
40
+ end
41
+
42
+ def exact_match?(actual:, expected:)
43
+ # allow comparisons of integers
44
+ if actual.respond_to?(:to_i) && actual.to_i == actual
45
+ expected = expected.to_i
46
+ end
47
+ # allow comparisons of boolean
48
+ case expected
49
+ when "true"
50
+ expected = true
51
+ when "false"
52
+ expected = false
53
+ end
54
+
55
+ actual == expected
56
+ end
57
+
58
+ def field_value(resource, field:)
59
+ value = resource
60
+ field.split('.').each do |segment|
61
+ value = value[segment.to_sym]
62
+ end
63
+ value
64
+ end
65
+ end
66
+ end
67
+ end
@@ -11,12 +11,17 @@ module StripeMock
11
11
  items = options[:items]
12
12
  items = items.values if items.respond_to?(:values)
13
13
  subscription[:items][:data] = plans.map do |plan|
14
- if items && items.size == plans.size
15
- quantity = items &&
16
- items.detect { |item| item[:plan] == plan[:id] }[:quantity] || 1
17
- Data.mock_subscription_item({ plan: plan, quantity: quantity })
14
+ matching_item = items && items.detect { |item| [item[:price], item[:plan]].include? plan[:id] }
15
+ if matching_item
16
+ matching_item[:quantity] ||= 1
17
+ matching_item[:id] ||= new_id('si')
18
+ params = matching_item.merge(plan: plan)
19
+ params[:price] = plan if plan[:object] == "price"
20
+ Data.mock_subscription_item(params)
18
21
  else
19
- Data.mock_subscription_item({ plan: plan })
22
+ params = { plan: plan, id: new_id('si') }
23
+ params[:price] = plan if plan[:object] == "price"
24
+ Data.mock_subscription_item(params)
20
25
  end
21
26
  end
22
27
  subscription
@@ -32,7 +37,7 @@ module StripeMock
32
37
  start_time = options[:current_period_start] || now
33
38
  params = { customer: cus[:id], current_period_start: start_time, created: created_time }
34
39
  params.merge!({ :plan => (plans.size == 1 ? plans.first : nil) })
35
- keys_to_merge = /application_fee_percent|quantity|metadata|tax_percent|billing|days_until_due/
40
+ keys_to_merge = /application_fee_percent|quantity|metadata|tax_percent|billing|days_until_due|default_tax_rates|pending_invoice_item_interval|default_payment_method|collection_method/
36
41
  params.merge! options.select {|k,v| k =~ keys_to_merge}
37
42
 
38
43
  if options[:cancel_at_period_end] == true
@@ -45,10 +50,10 @@ module StripeMock
45
50
 
46
51
  if (((plan && plan[:trial_period_days]) || 0) == 0 && options[:trial_end].nil?) || options[:trial_end] == "now"
47
52
  end_time = options[:billing_cycle_anchor] || get_ending_time(start_time, plan)
48
- params.merge!({status: 'active', current_period_end: end_time, trial_start: nil, trial_end: nil, billing_cycle_anchor: options[:billing_cycle_anchor]})
53
+ params.merge!({status: 'active', current_period_end: end_time, trial_start: nil, trial_end: nil, billing_cycle_anchor: options[:billing_cycle_anchor] || created_time})
49
54
  else
50
55
  end_time = options[:trial_end] || (Time.now.utc.to_i + plan[:trial_period_days]*86400)
51
- params.merge!({status: 'trialing', current_period_end: end_time, trial_start: start_time, trial_end: end_time, billing_cycle_anchor: nil})
56
+ params.merge!({status: 'trialing', current_period_end: end_time, trial_start: start_time, trial_end: end_time, billing_cycle_anchor: options[:billing_cycle_anchor] || created_time})
52
57
  end
53
58
 
54
59
  params
@@ -85,11 +90,13 @@ module StripeMock
85
90
  def get_ending_time(start_time, plan, intervals = 1)
86
91
  return start_time unless plan
87
92
 
88
- case plan[:interval]
93
+ interval = plan[:interval] || plan.dig(:recurring, :interval)
94
+ interval_count = plan[:interval_count] || plan.dig(:recurring, :interval_count) || 1
95
+ case interval
89
96
  when "week"
90
- start_time + (604800 * (plan[:interval_count] || 1) * intervals)
97
+ start_time + (604800 * (interval_count) * intervals)
91
98
  when "month"
92
- (Time.at(start_time).to_datetime >> ((plan[:interval_count] || 1) * intervals)).to_time.to_i
99
+ (Time.at(start_time).to_datetime >> ((interval_count) * intervals)).to_time.to_i
93
100
  when "year"
94
101
  (Time.at(start_time).to_datetime >> (12 * intervals)).to_time.to_i # max period is 1 year
95
102
  else
@@ -111,9 +118,26 @@ module StripeMock
111
118
 
112
119
  def total_items_amount(items)
113
120
  total = 0
114
- items.each { |i| total += (i[:quantity] || 1) * i[:plan][:amount] }
121
+ items.each do |item|
122
+ quantity = item[:quantity] || 1
123
+ amount = item[:plan][:unit_amount] || item[:plan][:amount]
124
+ total += quantity * amount
125
+ end
115
126
  total
116
127
  end
128
+
129
+ def filter_by_timestamp(subscriptions, field:, value:)
130
+ if value.is_a?(Hash)
131
+ operator_mapping = { gt: :>, gte: :>=, lt: :<, lte: :<= }
132
+ subscriptions.filter do |sub|
133
+ sub[field].public_send(operator_mapping[value.keys[0]], value.values[0])
134
+ end
135
+ else
136
+ subscriptions.filter do |sub|
137
+ sub[field] == value
138
+ end
139
+ end
140
+ end
117
141
  end
118
142
  end
119
143
  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', http_status: 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