stripe-ruby-mock 3.1.0 → 4.1.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec_tests.yml +7 -10
  3. data/CHANGELOG.md +24 -0
  4. data/Gemfile +0 -5
  5. data/README.md +3 -3
  6. data/bin/stripe-mock-server +1 -0
  7. data/lib/stripe_mock/api/client.rb +1 -1
  8. data/lib/stripe_mock/data.rb +53 -9
  9. data/lib/stripe_mock/instance.rb +4 -2
  10. data/lib/stripe_mock/request_handlers/charges.rb +20 -1
  11. data/lib/stripe_mock/request_handlers/checkout_session.rb +12 -11
  12. data/lib/stripe_mock/request_handlers/customers.rb +12 -1
  13. data/lib/stripe_mock/request_handlers/helpers/search_helpers.rb +67 -0
  14. data/lib/stripe_mock/request_handlers/invoices.rb +18 -3
  15. data/lib/stripe_mock/request_handlers/payment_intents.rb +11 -2
  16. data/lib/stripe_mock/request_handlers/payment_methods.rb +5 -1
  17. data/lib/stripe_mock/request_handlers/payouts.rb +10 -3
  18. data/lib/stripe_mock/request_handlers/prices.rb +13 -4
  19. data/lib/stripe_mock/request_handlers/products.rb +14 -5
  20. data/lib/stripe_mock/request_handlers/subscriptions.rb +11 -2
  21. data/lib/stripe_mock/request_handlers/tax_ids.rb +66 -0
  22. data/lib/stripe_mock/server.rb +9 -4
  23. data/lib/stripe_mock/test_strategies/base.rb +0 -1
  24. data/lib/stripe_mock/version.rb +1 -1
  25. data/lib/stripe_mock.rb +2 -0
  26. data/spec/shared_stripe_examples/bank_token_examples.rb +5 -7
  27. data/spec/shared_stripe_examples/charge_examples.rb +97 -0
  28. data/spec/shared_stripe_examples/checkout_session_examples.rb +0 -5
  29. data/spec/shared_stripe_examples/customer_examples.rb +56 -0
  30. data/spec/shared_stripe_examples/invoice_examples.rb +86 -1
  31. data/spec/shared_stripe_examples/payment_intent_examples.rb +75 -13
  32. data/spec/shared_stripe_examples/payment_method_examples.rb +10 -0
  33. data/spec/shared_stripe_examples/payout_examples.rb +26 -6
  34. data/spec/shared_stripe_examples/price_examples.rb +70 -1
  35. data/spec/shared_stripe_examples/product_examples.rb +71 -0
  36. data/spec/shared_stripe_examples/subscription_examples.rb +65 -6
  37. data/spec/spec_helper.rb +1 -1
  38. data/stripe-ruby-mock.gemspec +3 -3
  39. metadata +22 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b5b1b83df91e8e6fd700456710804749b9b1008e572fd8c74eec4f1519e8a49
4
- data.tar.gz: bf362cf2fe9aa556e155df2b61076248335e3b2a43f69e0e6fc046406d65da57
3
+ metadata.gz: 148628f00d214cfaa405f65529b1b8155da9475063166370b0b287084afcd0d4
4
+ data.tar.gz: ac5f7249f7a83d1fe2ac9f7a1eb4ff007465b404289f1f8bd60d277fe110d4d9
5
5
  SHA512:
6
- metadata.gz: a397bf1b196f62bb7c8d76f79e2582f6d6594d20ddb9473b8dd5e7680c1844620a29456cddd8be8c5d9b785f4317ea1b092b340f841ccdbef92d849c6bb116e7
7
- data.tar.gz: 90f97fe65c4dcd10eaa853aec740bce21c56b8df57bc1a589acc6f13ebf1b65943731b50f820d9f8eaa77954673f78b34bb62b326cbb7c71f5692a0b74a347c0
6
+ metadata.gz: 8627e73ab695f6a0403d8438abb979a3b4b241d1b04419a2e82844225426dbd19a02eef3ac95a719f644ece575090e46f68b504ab83d5a865e46d1e14f353099
7
+ data.tar.gz: 9ea4b6e5b061a0d8342f4459fd558df25df4888bf713142e21263e73029411e6f226baa86eb6341de726891cb4245a34ef0c0d744e21b378d6afe0dcae64eda8
@@ -18,21 +18,18 @@ permissions:
18
18
 
19
19
  jobs:
20
20
  test:
21
-
22
21
  runs-on: ubuntu-latest
23
22
  strategy:
24
23
  matrix:
25
- ruby-version: ['2.6', '2.7', '3.0']
26
-
24
+ ruby: ['3.1', '3.2', '3.3']
27
25
  steps:
28
- - uses: actions/checkout@v3
29
- - name: Set up Ruby
30
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
31
- # change this to (see https://github.com/ruby/setup-ruby#versioning):
32
- # uses: ruby/setup-ruby@v1
33
- uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
26
+ - uses: actions/checkout@v4
27
+ - name: Install Ruby and gems
28
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
29
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
30
+ uses: ruby/setup-ruby@v1
34
31
  with:
35
- ruby-version: ${{ matrix.ruby-version }}
32
+ ruby-version: ${{ matrix.ruby }}
36
33
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
37
34
  - name: Run tests
38
35
  run: bundle exec rspec
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  ### Unreleased
2
2
 
3
+ ### 4.1.0 (2025-04-24)
4
+ - [#920](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/920) Adds DRB as a dependency in preparation to Ruby 3.4.0 [@lucascppessoa](https://github.com/lucascppessoa)
5
+ - [#927](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/927) Allow :cancel_url nil in create Checkout Session [@shu-illy](https://github.com/shu-illy)
6
+ - [#929](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/929) Actions uses only supported ruby versions [@alexmamonchik](https://github.com/alexmamonchik)
7
+ - [#925](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/925) Fix github actions and add missing product attributes [@bettysteger](https://github.com/bettysteger)
8
+ - [#928](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/928) Add tax ID endpoints and data [@lfittl](https://github.com/lfittl)
9
+ - [#835](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/835) Account: replace deprecated verification hash by requirements [@fabianoarruda](https://github.com/fabianoarruda)
10
+ - [#903](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/903) Add require option to require additional file [@stevenharman](https://github.com/stevenharman)
11
+ - [#904](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/904) Add has_more to Subscription.items data [@stevenharman](https://github.com/stevenharman)
12
+ - [#843](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/843) Some updates around subscriptions [@donnguyen](https://github.com/donnguyen)
13
+ - [#917](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/917) Removed charges on PI and added latest_charge [@espen](https://github.com/espen)
14
+ - [#916](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/916) Better tests for payout [@espen](https://github.com/espen)
15
+ - [#862](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/862) Add simple support for us bank accounts [@smtlaissezfaire](https://github.com/smtlaissezfaire)
16
+ - [#907](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/907) Fix calls to use raise instead of throw [@smtlaissezfaire](https://github.com/smtlaissezfaire)
17
+ - [#908](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/908) Add update_payout [@espen](https://github.com/espen)
18
+
19
+ ### 4.0.0 (2024-08-07)
20
+ - [#905](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/905) Allow Stripe SDK v11 by [@stevenharman ](https://github.com/stevenharman)
21
+ - [#830](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/830) Implement search API by [@adamstegman](https://github.com/adamstegman)
22
+ - [#848](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/848) Extending runtime dependency on Stripe gem from version 5 through 11 by [@smakani](https://github.com/smakani)
23
+ - [#893](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/893) Adds support for stripe-ruby v10 by [@fabianoarruda](https://github.com/fabianoarruda)
24
+
25
+
26
+ ### 3.1.0 (2024-01-03)
3
27
  - [#693](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/693) gemspec: add change,issue,source_code URL by [@mtmail](https://github.com/mtmail)
4
28
  - [#700](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/700) update the balance API to respond with instant_available by [@iamnader](https://github.com/iamnader)
5
29
  - [#687](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/687) Add PaymentIntent Webhooks by [@klaustopher](https://github.com/klaustopher)
data/Gemfile CHANGED
@@ -1,10 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- platforms :ruby_19 do
4
- gem 'mime-types', '~> 2.6'
5
- gem 'rest-client', '~> 1.8'
6
- end
7
-
8
3
  group :test do
9
4
  gem 'rake'
10
5
  gem 'dotenv'
data/README.md CHANGED
@@ -12,7 +12,7 @@ This gem has unexpectedly grown in popularity and I've gotten pretty busy, so I'
12
12
 
13
13
  In your gemfile:
14
14
 
15
- gem 'stripe-ruby-mock', '~> 3.1.0', :require => 'stripe_mock'
15
+ gem 'stripe-ruby-mock', :require => 'stripe_mock'
16
16
 
17
17
  ## !!! Important
18
18
 
@@ -29,8 +29,8 @@ version `3.0.0` has [breaking changes](https://github.com/stripe-ruby-mock/strip
29
29
 
30
30
  ### Requirements
31
31
 
32
- * ruby >= 2.6.0
33
- * stripe >= 5.0.0
32
+ * ruby >= 2.7.0
33
+ * stripe > 5 & < 11
34
34
 
35
35
  ### Specifications
36
36
 
@@ -11,6 +11,7 @@ opts = Trollop::options do
11
11
  opt :server, "Server to use", :type => :string, :default => 'thin'
12
12
  opt :debug, "Request and response output", :default => true
13
13
  opt :pid_path, "Location to put server pid file", :type => :string, :default => './stripe-mock-server.pid'
14
+ opt :require, "Extra path to require when loading the server; can be specified multiple times", :type => :string, :multi => true
14
15
  end
15
16
 
16
17
  require 'stripe_mock'
@@ -27,7 +27,7 @@ module StripeMock
27
27
 
28
28
  private
29
29
 
30
- def self.redirect_to_mock_server(method, url, api_key: nil, api_base: nil, params: {}, headers: {})
30
+ def self.redirect_to_mock_server(method, url, api_key: nil, api_base: nil, usage: [], params: {}, headers: {})
31
31
  handler = Instance.handler_for_method_url("#{method} #{url}")
32
32
 
33
33
  if mock_error = client.error_queue.error_for_handler_name(handler[:name])
@@ -34,10 +34,15 @@ module StripeMock
34
34
 
35
35
  ]
36
36
  },
37
- verification: {
38
- fields_needed: [],
39
- due_by: nil,
40
- contacted: false
37
+ requirements: {
38
+ alternatives: [],
39
+ current_deadline: nil,
40
+ currently_due: [],
41
+ disabled_reason: nil,
42
+ errors: [],
43
+ eventually_due: [],
44
+ past_due: [],
45
+ pending_verification: []
41
46
  },
42
47
  transfer_schedule: {
43
48
  delay_days: 7,
@@ -122,6 +127,25 @@ module StripeMock
122
127
  }.merge(params)
123
128
  end
124
129
 
130
+ def self.mock_tax_id(params)
131
+ {
132
+ id: 'test_cus_default',
133
+ object: 'tax_id',
134
+ country: 'DE',
135
+ created: 1559079603,
136
+ customer: nil,
137
+ livemode: false,
138
+ type: 'eu_vat',
139
+ value: 'DE123456789',
140
+ verification: nil,
141
+ owner: {
142
+ type: 'self',
143
+ customer: nil
144
+ },
145
+ metadata: {}
146
+ }.merge(params)
147
+ end
148
+
125
149
  def self.mock_tax_rate(params)
126
150
  {
127
151
  id: 'test_cus_default',
@@ -382,7 +406,8 @@ module StripeMock
382
406
  currency: StripeMock.default_currency
383
407
  },
384
408
  quantity: 1
385
- }]
409
+ }],
410
+ has_more: false
386
411
  },
387
412
  cancel_at_period_end: false,
388
413
  canceled_at: nil,
@@ -401,7 +426,12 @@ module StripeMock
401
426
  default_payment_method: nil,
402
427
  pending_invoice_item_interval: nil,
403
428
  next_pending_invoice_item_invoice: nil,
404
- latest_invoice: nil
429
+ pending_setup_intent: nil,
430
+ latest_invoice: nil,
431
+ application_fee_percent: nil,
432
+ cancel_at: nil,
433
+ end_at: nil,
434
+ pause_collection: nil
405
435
  }, params)
406
436
  end
407
437
 
@@ -642,15 +672,18 @@ module StripeMock
642
672
  attributes:[],
643
673
  caption: nil,
644
674
  created: 1466698000,
675
+ default_price: nil,
645
676
  deactivate_on: [],
646
677
  description: nil,
647
678
  images: [],
679
+ marketing_features: [],
648
680
  livemode: false,
649
681
  metadata: {},
650
682
  name: "The Mock Product",
651
683
  package_dimensions: nil,
652
684
  shippable: nil,
653
685
  statement_descriptor: nil,
686
+ tax_code: nil,
654
687
  type: "service",
655
688
  unit_label: "my_unit",
656
689
  updated: 1537939442,
@@ -1221,7 +1254,8 @@ module StripeMock
1221
1254
  statement_descriptor: nil,
1222
1255
  trial_period_days: nil
1223
1256
  },
1224
- quantity: 2
1257
+ quantity: 2,
1258
+ price: mock_price
1225
1259
  }.merge(params)
1226
1260
  end
1227
1261
 
@@ -1336,7 +1370,17 @@ module StripeMock
1336
1370
  country: 'DE',
1337
1371
  fingerprint: 'FD81kbVPe7M05BMj',
1338
1372
  last4: params.dig(:sepa_debit, :iban)&.[](-4..) || '3000'
1339
- }
1373
+ },
1374
+ us_bank_account: {
1375
+ account_holder_type: "individual",
1376
+ account_type: "checking",
1377
+ bank_name: "STRIPE TEST BANK",
1378
+ financial_connections_account: "fca_0614042384b19afec4474940",
1379
+ fingerprint: "7bc48d016359a45a",
1380
+ last4: "6789",
1381
+ networks: {"preferred"=>"ach", "supported"=>["ach"]},
1382
+ routing_number: "110000000"
1383
+ },
1340
1384
  }
1341
1385
 
1342
1386
  {
@@ -1396,7 +1440,7 @@ module StripeMock
1396
1440
  id: cs_id,
1397
1441
  object: 'checkout.session',
1398
1442
  billing_address_collection: nil,
1399
- cancel_url: 'https://example.com/cancel',
1443
+ cancel_url: nil,
1400
1444
  client_reference_id: nil,
1401
1445
  customer: nil,
1402
1446
  customer_email: nil,
@@ -52,6 +52,7 @@ module StripeMock
52
52
  include StripeMock::RequestHandlers::CountrySpec
53
53
  include StripeMock::RequestHandlers::Payouts
54
54
  include StripeMock::RequestHandlers::EphemeralKey
55
+ include StripeMock::RequestHandlers::TaxIds
55
56
  include StripeMock::RequestHandlers::TaxRates
56
57
  include StripeMock::RequestHandlers::Checkout
57
58
  include StripeMock::RequestHandlers::Checkout::Session
@@ -59,7 +60,7 @@ module StripeMock
59
60
  attr_reader :accounts, :balance, :balance_transactions, :bank_tokens, :charges, :coupons, :customers,
60
61
  :disputes, :events, :invoices, :invoice_items, :orders, :payment_intents, :payment_methods,
61
62
  :setup_intents, :plans, :prices, :promotion_codes, :recipients, :refunds, :transfers, :payouts,
62
- :subscriptions, :country_spec, :subscriptions_items, :products, :tax_rates, :checkout_sessions,
63
+ :subscriptions, :country_spec, :subscriptions_items, :products, :tax_ids, :tax_rates, :checkout_sessions,
63
64
  :checkout_session_line_items
64
65
 
65
66
  attr_accessor :error_queue, :debug, :conversion_rate, :account_balance
@@ -93,6 +94,7 @@ module StripeMock
93
94
  @subscriptions = {}
94
95
  @subscriptions_items = {}
95
96
  @country_spec = {}
97
+ @tax_ids = {}
96
98
  @tax_rates = {}
97
99
  @checkout_sessions = {}
98
100
  @checkout_session_line_items = {}
@@ -109,7 +111,7 @@ module StripeMock
109
111
  @base_strategy = TestStrategies::Base.new
110
112
  end
111
113
 
112
- def mock_request(method, url, api_key: nil, api_base: nil, params: {}, headers: {})
114
+ def mock_request(method, url, api_key: nil, api_base: nil, usage: [], params: {}, headers: {})
113
115
  return {} if method == :xtest
114
116
 
115
117
  api_key ||= (Stripe.api_key || DUMMY_API_KEY)
@@ -5,7 +5,8 @@ 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
@@ -90,6 +91,24 @@ module StripeMock
90
91
  Data.mock_list_object(clone.values, params)
91
92
  end
92
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
+
93
112
  def get_charge(route, method_url, params, headers)
94
113
  route =~ method_url
95
114
  charge_id = $1 || params[:charge]
@@ -12,16 +12,17 @@ module StripeMock
12
12
  def new_session(route, method_url, params, headers)
13
13
  id = params[:id] || new_id('cs')
14
14
 
15
- [:cancel_url, :success_url].each do |p|
16
- require_param(p) if params[p].nil? || params[p].empty?
17
- end
15
+ require_param(:success_url) if params[:success_url].nil? || params[:success_url].empty?
18
16
 
19
17
  line_items = nil
20
18
  if params[:line_items]
21
19
  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]
20
+ unless line_item[:quantity]
21
+ raise Stripe::InvalidRequestError.new("Quantity is required. Add `quantity` to `line_items[#{i}]`", :line_items)
22
+ end
23
+
23
24
  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
+ raise Stripe::InvalidRequestError.new("Price or amount and currency is required. Add `price`, `price_data`, or `amount`, `currency` and `name` to `line_items[#{i}]`", :line_items)
25
26
  end
26
27
  {
27
28
  id: new_id("li"),
@@ -48,11 +49,11 @@ module StripeMock
48
49
  if line_items
49
50
  amount = 0
50
51
 
51
- line_items.each do |line_item|
52
+ line_items.each do |line_item|
52
53
  price = prices[line_item[:price]]
53
54
 
54
55
  if price.nil?
55
- raise StripeMock::StripeMockError.new("Price not found for ID: #{line_item[:price]}")
56
+ raise StripeMock::StripeMockError.new("Price not found for ID: #{line_item[:price]}", :line_items)
56
57
  end
57
58
 
58
59
  amount += (price[:unit_amount] * line_item[:quantity])
@@ -78,7 +79,7 @@ module StripeMock
78
79
  checkout_session_line_items[id] = line_items
79
80
  when "setup"
80
81
  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
+ raise Stripe::InvalidRequestError.new("You cannot pass `line_items` in `setup` mode", :line_items, http_status: 400)
82
83
  end
83
84
  setup_intent = new_setup_intent(nil, nil, {
84
85
  customer: params[:customer],
@@ -91,7 +92,7 @@ module StripeMock
91
92
  require_param(:line_items) if params[:line_items].nil? || params[:line_items].empty?
92
93
  checkout_session_line_items[id] = line_items
93
94
  else
94
- throw Stripe::InvalidRequestError.new("Invalid mode: must be one of payment, setup, or subscription", :mode, http_status: 400)
95
+ raise Stripe::InvalidRequestError.new("Invalid mode: must be one of payment, setup, or subscription", :mode, http_status: 400)
95
96
  end
96
97
 
97
98
  checkout_sessions[id] = {
@@ -156,7 +157,7 @@ module StripeMock
156
157
  price = prices[line_item[:price]].clone
157
158
 
158
159
  if price.nil?
159
- raise StripeMock::StripeMockError.new("Price not found for ID: #{line_item[:price]}")
160
+ raise StripeMock::StripeMockError.new("Price not found for ID: #{line_item[:price]}", :line_items)
160
161
  end
161
162
 
162
163
  {
@@ -170,7 +171,7 @@ module StripeMock
170
171
  }
171
172
  end
172
173
  else
173
- throw Stripe::InvalidRequestError("Only payment and subscription sessions have line items")
174
+ raise Stripe::InvalidRequestError.new("Only payment and subscription sessions have line items", :line_items)
174
175
  end
175
176
  end
176
177
  end
@@ -5,9 +5,10 @@ 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
 
@@ -140,6 +141,16 @@ module StripeMock
140
141
  Data.mock_list_object(customers[stripe_account]&.values, params)
141
142
  end
142
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)
152
+ end
153
+
143
154
  def delete_customer_discount(route, method_url, params, headers)
144
155
  stripe_account = headers && headers[:stripe_account] || Stripe.api_key
145
156
  route =~ method_url
@@ -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
@@ -6,7 +6,8 @@ module StripeMock
6
6
  klass.add_handler 'post /v1/invoices', :new_invoice
7
7
  klass.add_handler 'get /v1/invoices/upcoming', :upcoming_invoice
8
8
  klass.add_handler 'get /v1/invoices/(.*)/lines', :get_invoice_line_items
9
- klass.add_handler 'get /v1/invoices/(.*)', :get_invoice
9
+ klass.add_handler 'get /v1/invoices/((?!search).*)', :get_invoice
10
+ klass.add_handler 'get /v1/invoices/search', :search_invoices
10
11
  klass.add_handler 'get /v1/invoices', :list_invoices
11
12
  klass.add_handler 'post /v1/invoices/(.*)/pay', :pay_invoice
12
13
  klass.add_handler 'post /v1/invoices/(.*)', :update_invoice
@@ -25,6 +26,14 @@ module StripeMock
25
26
  invoices[$1].merge!(params)
26
27
  end
27
28
 
29
+ SEARCH_FIELDS = ["currency", "customer", "number", "receipt_number", "subscription", "total"].freeze
30
+ def search_invoices(route, method_url, params, headers)
31
+ require_param(:query) unless params[:query]
32
+
33
+ results = search_results(invoices.values, params[:query], fields: SEARCH_FIELDS, resource_name: "invoices")
34
+ Data.mock_list_object(results, params)
35
+ end
36
+
28
37
  def list_invoices(route, method_url, params, headers)
29
38
  params[:offset] ||= 0
30
39
  params[:limit] ||= 10
@@ -68,6 +77,9 @@ module StripeMock
68
77
  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?
69
78
  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?
70
79
 
80
+ if params[:subscription] && params[:customer].nil?
81
+ params[:customer] = subscriptions[params[:subscription]][:customer]
82
+ end
71
83
  customer = customers[stripe_account][params[:customer]]
72
84
  assert_existence :customer, params[:customer], customer
73
85
 
@@ -105,8 +117,9 @@ module StripeMock
105
117
  invoice_lines = []
106
118
 
107
119
  if prorating
120
+ plan_amount = subscription[:plan][:amount] || subscription[:plan][:unit_amount]
108
121
  unused_amount = (
109
- subscription[:plan][:amount].to_f *
122
+ plan_amount.to_f *
110
123
  subscription[:quantity] *
111
124
  (subscription[:current_period_end] - subscription_proration_date.to_i) / (subscription[:current_period_end] - subscription[:current_period_start])
112
125
  ).ceil
@@ -160,11 +173,13 @@ module StripeMock
160
173
  private
161
174
 
162
175
  def get_mock_subscription_line_item(subscription)
176
+ plan_amount = subscription[:plan][:amount] || subscription[:plan][:unit_amount]
177
+
163
178
  Data.mock_line_item(
164
179
  id: subscription[:id],
165
180
  type: "subscription",
166
181
  plan: subscription[:plan],
167
- amount: subscription[:status] == 'trialing' ? 0 : subscription[:plan][:amount] * subscription[:quantity],
182
+ amount: subscription[:status] == 'trialing' ? 0 : plan_amount * subscription[:quantity],
168
183
  discountable: true,
169
184
  quantity: subscription[:quantity],
170
185
  period: {
@@ -6,7 +6,8 @@ module StripeMock
6
6
  def PaymentIntents.included(klass)
7
7
  klass.add_handler 'post /v1/payment_intents', :new_payment_intent
8
8
  klass.add_handler 'get /v1/payment_intents', :get_payment_intents
9
- klass.add_handler 'get /v1/payment_intents/(.*)', :get_payment_intent
9
+ klass.add_handler 'get /v1/payment_intents/((?!search).*)', :get_payment_intent
10
+ klass.add_handler 'get /v1/payment_intents/search', :search_payment_intents
10
11
  klass.add_handler 'post /v1/payment_intents/(.*)/confirm', :confirm_payment_intent
11
12
  klass.add_handler 'post /v1/payment_intents/(.*)/capture', :capture_payment_intent
12
13
  klass.add_handler 'post /v1/payment_intents/(.*)/cancel', :cancel_payment_intent
@@ -70,6 +71,14 @@ module StripeMock
70
71
  payment_intent
71
72
  end
72
73
 
74
+ SEARCH_FIELDS = ["amount", "currency", "customer", "status"].freeze
75
+ def search_payment_intents(route, method_url, params, headers)
76
+ require_param(:query) unless params[:query]
77
+
78
+ results = search_results(payment_intents.values, params[:query], fields: SEARCH_FIELDS, resource_name: "payment_intents")
79
+ Data.mock_list_object(results, params)
80
+ end
81
+
73
82
  def capture_payment_intent(route, method_url, params, headers)
74
83
  route =~ method_url
75
84
  payment_intent = assert_existence :payment_intent, $1, payment_intents[$1]
@@ -184,7 +193,7 @@ module StripeMock
184
193
  payment_method: payment_intent[:payment_method]
185
194
  )
186
195
 
187
- payment_intent[:charges][:data] << charges[charge_id].clone
196
+ payment_intent[:latest_charge] = charge_id
188
197
 
189
198
  payment_intent
190
199
  end
@@ -116,8 +116,12 @@ module StripeMock
116
116
  end
117
117
  end
118
118
 
119
+ def valid_types
120
+ %w(card ideal sepa_debit us_bank_account)
121
+ end
122
+
119
123
  def invalid_type?(type)
120
- !%w(card ideal sepa_debit).include?(type)
124
+ !valid_types.include?(type)
121
125
  end
122
126
  end
123
127
  end
@@ -4,18 +4,25 @@ module StripeMock
4
4
 
5
5
  def Payouts.included(klass)
6
6
  klass.add_handler 'post /v1/payouts', :new_payout
7
+ klass.add_handler 'post /v1/payouts/(.*)', :update_payout
7
8
  klass.add_handler 'get /v1/payouts', :list_payouts
8
9
  klass.add_handler 'get /v1/payouts/(.*)', :get_payout
9
10
  end
10
11
 
11
12
  def new_payout(route, method_url, params, headers)
12
- id = new_id('po')
13
+ params[:id] ||= new_id("po")
13
14
 
14
- unless params[:amount].is_a?(Integer) || (params[:amount].is_a?(String) && /^\d+$/.match(params[:amount]))
15
+ unless (params[:amount].is_a?(Integer) && params[:amount].positive?) || (params[:amount].is_a?(String) && /^\d+$/.match(params[:amount]))
15
16
  raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400)
16
17
  end
17
18
 
18
- payouts[id] = Data.mock_payout(params.merge :id => id)
19
+ payouts[params[:id]] = Data.mock_payout(params.merge :id => params[:id])
20
+ end
21
+
22
+ def update_payout(route, method_url, params, headers)
23
+ route =~ method_url
24
+ assert_existence :payout, $1, payouts[$1]
25
+ payouts[$1].merge!(params)
19
26
  end
20
27
 
21
28
  def list_payouts(route, method_url, params, headers)