From 1120c22c4760bfa2c8af79dd507f1ad8a6779530 Mon Sep 17 00:00:00 2001 From: Dave Urban Date: Mon, 27 Jul 2020 21:02:33 -0400 Subject: [PATCH 01/28] A succeeded PaymentIntent gets a persistent Charge --- .../request_handlers/payment_intents.rb | 8 ++- .../payment_intent_examples.rb | 64 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/lib/stripe_mock/request_handlers/payment_intents.rb b/lib/stripe_mock/request_handlers/payment_intents.rb index 3309e4879..57dcd1522 100644 --- a/lib/stripe_mock/request_handlers/payment_intents.rb +++ b/lib/stripe_mock/request_handlers/payment_intents.rb @@ -169,12 +169,18 @@ def succeeded_payment_intent(payment_intent) payment_intent[:status] = 'succeeded' btxn = new_balance_transaction('txn', { source: payment_intent[:id] }) - payment_intent[:charges][:data] << Data.mock_charge( + charge_id = new_id('ch') + + charges[charge_id] = Data.mock_charge( + id: charge_id, balance_transaction: btxn, + payment_intent: payment_intent[:id], amount: payment_intent[:amount], currency: payment_intent[:currency] ) + payment_intent[:charges][:data] << charges[charge_id].clone + payment_intent end end diff --git a/spec/shared_stripe_examples/payment_intent_examples.rb b/spec/shared_stripe_examples/payment_intent_examples.rb index 8f6993eab..4eeb89608 100644 --- a/spec/shared_stripe_examples/payment_intent_examples.rb +++ b/spec/shared_stripe_examples/payment_intent_examples.rb @@ -2,6 +2,11 @@ shared_examples 'PaymentIntent API' do + let(:customer) do + token = Stripe::Token.retrieve(stripe_helper.generate_card_token(number: '4242424242424242')) + Stripe::Customer.create(email: 'alice@bob.com', source: token.id) + end + it "creates a succeeded stripe payment_intent" do payment_intent = Stripe::PaymentIntent.create(amount: 100, currency: "usd") @@ -92,6 +97,25 @@ expect(Stripe::BalanceTransaction.retrieve(balance_txn).id).to eq(balance_txn) end + it 'creates a charge for a stripe payment_intent with confirm flag to true' do + payment_intent = Stripe::PaymentIntent.create amount: 100, + currency: 'usd', + confirm: true, + customer: customer, + payment_method: customer.sources.first + + charge = payment_intent.charges.data.first + expect(charge.amount).to eq(payment_intent.amount) + expect(charge.payment_intent).to eq(payment_intent.id) + expect(charge.description).to be_nil + + charge.description = 'Updated description' + charge.save + + updated = Stripe::Charge.retrieve(charge.id) + expect(updated.description).to eq('Updated description') + end + it "confirms a stripe payment_intent" do payment_intent = Stripe::PaymentIntent.create(amount: 100, currency: "usd") confirmed_payment_intent = payment_intent.confirm() @@ -100,6 +124,25 @@ expect(confirmed_payment_intent.charges.data.first.object).to eq('charge') end + it 'creates a charge for a confirmed stripe payment_intent' do + payment_intent = Stripe::PaymentIntent.create amount: 100, + currency: 'usd', + customer: customer, + payment_method: customer.sources.first + + confirmed_payment_intent = payment_intent.confirm + charge = confirmed_payment_intent.charges.data.first + expect(charge.amount).to eq(confirmed_payment_intent.amount) + expect(charge.payment_intent).to eq(confirmed_payment_intent.id) + expect(charge.description).to be_nil + + charge.description = 'Updated description' + charge.save + + updated = Stripe::Charge.retrieve(charge.id) + expect(updated.description).to eq('Updated description') + end + it "captures a stripe payment_intent" do payment_intent = Stripe::PaymentIntent.create(amount: 100, currency: "usd") confirmed_payment_intent = payment_intent.capture() @@ -108,6 +151,27 @@ expect(confirmed_payment_intent.charges.data.first.object).to eq('charge') end + it 'creates a charge for a captured stripe payment_intent' do + payment_intent = Stripe::PaymentIntent.create amount: 3055, + currency: 'usd', + customer: customer, + payment_method: customer.sources.first, + confirm: true, + capture_method: 'manual' + + captured_payment_intent = payment_intent.capture + charge = captured_payment_intent.charges.data.first + expect(charge.amount).to eq(captured_payment_intent.amount) + expect(charge.payment_intent).to eq(captured_payment_intent.id) + expect(charge.description).to be_nil + + charge.description = 'Updated description' + charge.save + + updated = Stripe::Charge.retrieve(charge.id) + expect(updated.description).to eq('Updated description') + end + it "cancels a stripe payment_intent" do payment_intent = Stripe::PaymentIntent.create(amount: 100, currency: "usd") confirmed_payment_intent = payment_intent.cancel() From f84d9b509a372ef2b35c6830ea283f7f86feeb68 Mon Sep 17 00:00:00 2001 From: Yvonne Ng Date: Mon, 19 Apr 2021 14:07:46 -0400 Subject: [PATCH 02/28] Add balance transaction when creating transfer --- lib/stripe_mock/request_handlers/transfers.rb | 13 ++++++++++++- spec/shared_stripe_examples/transfer_examples.rb | 11 ++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/stripe_mock/request_handlers/transfers.rb b/lib/stripe_mock/request_handlers/transfers.rb index 6e04ea3ce..e7daa567e 100644 --- a/lib/stripe_mock/request_handlers/transfers.rb +++ b/lib/stripe_mock/request_handlers/transfers.rb @@ -45,7 +45,18 @@ def new_transfer(route, method_url, params, headers) raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400) end - transfers[id] = Data.mock_transfer(params.merge :id => id) + bal_trans_params = { amount: params[:amount].to_i, source: id } + + balance_transaction_id = new_balance_transaction('txn', bal_trans_params) + + transfers[id] = Data.mock_transfer(params.merge(id: id, balance_transaction: balance_transaction_id)) + + transfer = transfers[id].clone + if params[:expand] == ['balance_transaction'] + transfer[:balance_transaction] = balance_transactions[balance_transaction_id] + end + + transfer end def get_transfer(route, method_url, params, headers) diff --git a/spec/shared_stripe_examples/transfer_examples.rb b/spec/shared_stripe_examples/transfer_examples.rb index 56a87d088..444d33d51 100644 --- a/spec/shared_stripe_examples/transfer_examples.rb +++ b/spec/shared_stripe_examples/transfer_examples.rb @@ -9,7 +9,7 @@ expect(transfer.id).to match /^test_tr/ expect(transfer.amount).to eq(100) expect(transfer.amount_reversed).to eq(0) - expect(transfer.balance_transaction).to eq('txn_2dyYXXP90MN26R') + expect(transfer.balance_transaction).to eq('test_txn_1') expect(transfer.created).to eq(1304114826) expect(transfer.currency).to eq('usd') expect(transfer.description).to eq('Transfer description') @@ -30,6 +30,15 @@ expect(transfer.transfer_group).to eq("group_ch_164xRv2eZvKYlo2Clu1sIJWB") end + it "creates a balance transaction" do + destination = Stripe::Account.create(type: "custom", email: "#{SecureRandom.uuid}@example.com", id: "acct_12345", requested_capabilities: ['card_payments', 'platform_payments']) + transfer = Stripe::Transfer.create(amount: 100, currency: "usd", destination: destination.id) + + bal_trans = Stripe::BalanceTransaction.retrieve(transfer.balance_transaction) + expect(bal_trans.amount).to eq(100) + expect(bal_trans.source).to eq(transfer.id) + end + describe "listing transfers" do let(:destination) { Stripe::Account.create(type: "custom", email: "#{SecureRandom.uuid}@example.com", business_name: "MyCo") } From c9d6f38dcba877825447d226666d55914fdfdf2f Mon Sep 17 00:00:00 2001 From: Guilherme Goncalves Date: Thu, 27 May 2021 18:28:52 -0300 Subject: [PATCH 03/28] Allows filtering prices by currency and product This commit implements the `currency` and `product` filters in the prices list, since they are supported by the Stripe API. See https://stripe.com/docs/api/prices/list --- lib/stripe_mock/request_handlers/prices.rb | 12 +++++ spec/shared_stripe_examples/price_examples.rb | 44 ++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/lib/stripe_mock/request_handlers/prices.rb b/lib/stripe_mock/request_handlers/prices.rb index c5bd52dba..58ea3c9df 100644 --- a/lib/stripe_mock/request_handlers/prices.rb +++ b/lib/stripe_mock/request_handlers/prices.rb @@ -37,6 +37,18 @@ def list_prices(route, method_url, params, headers) end end + if params.key?(:currency) + price_data.select! do |price| + params[:currency] == price[:currency] + end + end + + if params.key?(:product) + price_data.select! do |price| + params[:product] == price[:product] + end + end + Data.mock_list_object(price_data.first(limit), params.merge!(limit: limit)) end end diff --git a/spec/shared_stripe_examples/price_examples.rb b/spec/shared_stripe_examples/price_examples.rb index 7138aa923..efb17a629 100644 --- a/spec/shared_stripe_examples/price_examples.rb +++ b/spec/shared_stripe_examples/price_examples.rb @@ -1,8 +1,11 @@ require 'spec_helper' shared_examples 'Price API' do - let(:product) { stripe_helper.create_product } - let(:product_id) { product.id } + let(:product_id) { "product_id_1" } + let(:product) { stripe_helper.create_product(id: product_id) } + + let(:other_product_id) { "product_id_2" } + let(:other_product) { stripe_helper.create_product(id: other_product_id) } let(:price_attributes) { { :id => "price_abc123", @@ -26,6 +29,7 @@ before(:each) do product + other_product end it "creates a stripe price" do @@ -125,6 +129,42 @@ expect(two.map &:amount).to include(98765) end + it "retrieves prices filtering by currency" do + 5.times do | i| + stripe_helper.create_price(id: "usd price #{i}", product: product_id, amount: 11, currency: 'usd') + stripe_helper.create_price(id: "brl price #{i}", product: product_id, amount: 11, currency: 'brl') + end + + all = Stripe::Price.list() + expect(all.count).to eq(10) + + usd = Stripe::Price.list({currency: 'usd'}) + expect(usd.count).to eq(5) + expect(usd.all? {|p| p.currency == 'usd' }).to be_truthy + + brl = Stripe::Price.list({currency: 'brl'}) + expect(brl.count).to eq(5) + expect(brl.all? {|p| p.currency == 'brl' }).to be_truthy + end + + it "retrieves prices filtering by product" do + 5.times do | i| + stripe_helper.create_price(id: "product 1 price #{i}", product: product_id) + stripe_helper.create_price(id: "product 2 price #{i}", product: other_product_id) + end + + all = Stripe::Price.list() + expect(all.count).to eq(10) + + product_prices = Stripe::Price.list({product: product.id}) + expect(product_prices.count).to eq(5) + expect(product_prices.all? {|p| p.product == product.id }).to be_truthy + + other_product_prices = Stripe::Price.list({product: other_product.id}) + expect(other_product_prices.count).to eq(5) + expect(other_product_prices.all? {|p| p.product == other_product.id }).to be_truthy + end + describe "Validations", :live => true do include_context "stripe validator" let(:params) { stripe_helper.create_price_params(product: product_id) } From 7eb1cc949ed55e39dd85a8f2f0b07f1fe41b7385 Mon Sep 17 00:00:00 2001 From: Luke Rodgers Date: Thu, 17 Jun 2021 17:00:12 -0400 Subject: [PATCH 04/28] Add support for discount IDs Stripe API returns an ID like "di_abc123" for discount objects. --- lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb | 1 + spec/shared_stripe_examples/subscription_examples.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb b/lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb index 0e5fcb9d2..e8118057c 100644 --- a/lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb +++ b/lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb @@ -7,6 +7,7 @@ def add_coupon_to_object(object, coupon) attrs[:coupon] = coupon attrs[:start] = Time.now.to_i attrs[:end] = (DateTime.now >> coupon[:duration_in_months].to_i).to_time.to_i if coupon[:duration] == 'repeating' + attrs[:id] = new_id("di") end object[:discount] = Stripe::Discount.construct_from(discount_attrs) diff --git a/spec/shared_stripe_examples/subscription_examples.rb b/spec/shared_stripe_examples/subscription_examples.rb index 60db89596..41da74403 100644 --- a/spec/shared_stripe_examples/subscription_examples.rb +++ b/spec/shared_stripe_examples/subscription_examples.rb @@ -179,6 +179,7 @@ expect(subscriptions.data).to be_a(Array) expect(subscriptions.data.count).to eq(1) expect(subscriptions.data.first.discount).not_to be_nil + expect(subscriptions.data.first.discount.id).not_to be_nil expect(subscriptions.data.first.discount).to be_a(Stripe::Discount) expect(subscriptions.data.first.discount.coupon.id).to eq(coupon.id) end From 94395640462ad3cca7160f2ca9b730a6e807cb78 Mon Sep 17 00:00:00 2001 From: Artem Krivonozhko Date: Thu, 6 Jan 2022 20:07:51 +0300 Subject: [PATCH 05/28] Create SetupIntent with another status is payment method is provided --- lib/stripe_mock/request_handlers/setup_intents.rb | 4 +++- spec/shared_stripe_examples/setup_intent_examples.rb | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/stripe_mock/request_handlers/setup_intents.rb b/lib/stripe_mock/request_handlers/setup_intents.rb index 8c6cd4239..ac06c8a3d 100644 --- a/lib/stripe_mock/request_handlers/setup_intents.rb +++ b/lib/stripe_mock/request_handlers/setup_intents.rb @@ -25,10 +25,12 @@ def SetupIntents.included(klass) def new_setup_intent(route, method_url, params, headers) id = new_id('si') + status = params[:payment_method] ? 'requires_action' : 'requires_payment_method' setup_intents[id] = Data.mock_setup_intent( params.merge( - id: id + id: id, + status: status ) ) diff --git a/spec/shared_stripe_examples/setup_intent_examples.rb b/spec/shared_stripe_examples/setup_intent_examples.rb index ecf9b3052..10cbdc608 100644 --- a/spec/shared_stripe_examples/setup_intent_examples.rb +++ b/spec/shared_stripe_examples/setup_intent_examples.rb @@ -10,6 +10,14 @@ expect(setup_intent.status).to eq('requires_payment_method') end + it 'creates a stripe setup intent with payment method' do + setup_intent = Stripe::SetupIntent.create(payment_method: 'random') + + expect(setup_intent.id).to match(/^test_si/) + expect(setup_intent.metadata.to_hash).to eq({}) + expect(setup_intent.status).to eq('requires_action') + end + describe "listing setup_intent" do before do 3.times do From 9c5e87530dae23ee5d63f49b27c318e06a99e852 Mon Sep 17 00:00:00 2001 From: Cameron2920 Date: Thu, 3 Feb 2022 03:32:42 -0500 Subject: [PATCH 06/28] Added delete account support. --- lib/stripe_mock/request_handlers/accounts.rb | 23 ++++++++++++++----- .../account_examples.rb | 10 +++++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/stripe_mock/request_handlers/accounts.rb b/lib/stripe_mock/request_handlers/accounts.rb index bac67965e..ea36619c5 100644 --- a/lib/stripe_mock/request_handlers/accounts.rb +++ b/lib/stripe_mock/request_handlers/accounts.rb @@ -4,12 +4,13 @@ module Accounts VALID_START_YEAR = 2009 def Accounts.included(klass) - klass.add_handler 'post /v1/accounts', :new_account - klass.add_handler 'get /v1/account', :get_account - klass.add_handler 'get /v1/accounts/(.*)', :get_account - klass.add_handler 'post /v1/accounts/(.*)', :update_account - klass.add_handler 'get /v1/accounts', :list_accounts - klass.add_handler 'post /oauth/deauthorize',:deauthorize + klass.add_handler 'post /v1/accounts', :new_account + klass.add_handler 'get /v1/account', :get_account + klass.add_handler 'get /v1/accounts/(.*)', :get_account + klass.add_handler 'post /v1/accounts/(.*)', :update_account + klass.add_handler 'get /v1/accounts', :list_accounts + klass.add_handler 'post /oauth/deauthorize', :deauthorize + klass.add_handler 'delete /v1/accounts/(.*)', :delete_account end def new_account(route, method_url, params, headers) @@ -48,6 +49,16 @@ def deauthorize(route, method_url, params, headers) Stripe::StripeObject.construct_from(:stripe_user_id => params[:stripe_user_id]) end + def delete_account(route, method_url, params, headers) + init_account + route =~ method_url + assert_existence :account, $1, accounts[$1] + accounts[$1] = { + id: accounts[$1][:id], + deleted: true + } + end + private def init_account diff --git a/spec/shared_stripe_examples/account_examples.rb b/spec/shared_stripe_examples/account_examples.rb index b595d84d1..596602d21 100644 --- a/spec/shared_stripe_examples/account_examples.rb +++ b/spec/shared_stripe_examples/account_examples.rb @@ -1,7 +1,7 @@ require 'spec_helper' shared_examples 'Account API' do - describe 'retrive accounts' do + describe 'retrieve accounts' do it 'retrieves a stripe account', live: true do account = Stripe::Account.retrieve @@ -86,6 +86,14 @@ end end + describe 'delete account' do + it 'deletes a stripe account' do + account = Stripe::Account.create(email: 'test@test.com') + account = account.delete + expect(account.deleted).to eq(true) + end + end + it 'deauthorizes the stripe account', live: false do account = Stripe::Account.retrieve result = account.deauthorize('CLIENT_ID') From 824da36c3b5e5a90f61e394884c20ed6407d099d Mon Sep 17 00:00:00 2001 From: Robert Clark Date: Tue, 22 Mar 2022 17:26:06 -0400 Subject: [PATCH 07/28] Allow user to pass in a customer ID on create A few lines down `params[:id]` is used, however since it is not in the allowed params list it is not possible to take the code path to create a subscription with an existing param. This allows the ID param through. --- lib/stripe_mock/request_handlers/subscriptions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stripe_mock/request_handlers/subscriptions.rb b/lib/stripe_mock/request_handlers/subscriptions.rb index d8c31d1d9..d4c1ffef3 100644 --- a/lib/stripe_mock/request_handlers/subscriptions.rb +++ b/lib/stripe_mock/request_handlers/subscriptions.rb @@ -97,7 +97,7 @@ def create_subscription(route, method_url, params, headers) customer[:default_source] = new_card[:id] end - 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 enable_incomplete_payments cancel_at_period_end default_tax_rates payment_behavior pending_invoice_item_interval default_payment_method collection_method off_session trial_from_plan proration_behavior backdate_start_date transfer_data expand automatic_tax) + allowed_params = %w(id 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 enable_incomplete_payments cancel_at_period_end default_tax_rates payment_behavior pending_invoice_item_interval default_payment_method collection_method off_session trial_from_plan proration_behavior backdate_start_date transfer_data expand automatic_tax) unknown_params = params.keys - allowed_params.map(&:to_sym) if unknown_params.length > 0 raise Stripe::InvalidRequestError.new("Received unknown parameter: #{unknown_params.join}", unknown_params.first.to_s, http_status: 400) From b811935a698fc73d26f6dfb799df63358fdbe5ff Mon Sep 17 00:00:00 2001 From: Adam Stegman Date: Mon, 9 May 2022 17:57:31 -0500 Subject: [PATCH 08/28] Implement search API Stripe has added a [search API](https://stripe.com/docs/search) to the latest API version, `2020-08-27`. Supporting this in all its permutations would be very difficult, essentially partially implementing a full search engine. For now, supporting exact-match searches on each of the supported fields is relatively easy. --- lib/stripe_mock.rb | 1 + lib/stripe_mock/request_handlers/charges.rb | 21 +++- lib/stripe_mock/request_handlers/customers.rb | 13 ++- .../helpers/search_helpers.rb | 67 +++++++++++++ lib/stripe_mock/request_handlers/invoices.rb | 11 ++- .../request_handlers/payment_intents.rb | 11 ++- lib/stripe_mock/request_handlers/prices.rb | 17 +++- lib/stripe_mock/request_handlers/products.rb | 19 +++- .../request_handlers/subscriptions.rb | 11 ++- .../shared_stripe_examples/charge_examples.rb | 97 +++++++++++++++++++ .../customer_examples.rb | 56 +++++++++++ .../invoice_examples.rb | 82 ++++++++++++++++ .../payment_intent_examples.rb | 62 ++++++++++++ spec/shared_stripe_examples/price_examples.rb | 69 +++++++++++++ .../product_examples.rb | 68 +++++++++++++ .../subscription_examples.rb | 59 +++++++++++ 16 files changed, 650 insertions(+), 14 deletions(-) create mode 100644 lib/stripe_mock/request_handlers/helpers/search_helpers.rb diff --git a/lib/stripe_mock.rb b/lib/stripe_mock.rb index 543ae1b90..cca36af5f 100644 --- a/lib/stripe_mock.rb +++ b/lib/stripe_mock.rb @@ -42,6 +42,7 @@ require 'stripe_mock/request_handlers/helpers/card_helpers.rb' require 'stripe_mock/request_handlers/helpers/charge_helpers.rb' require 'stripe_mock/request_handlers/helpers/coupon_helpers.rb' +require 'stripe_mock/request_handlers/helpers/search_helpers.rb' require 'stripe_mock/request_handlers/helpers/subscription_helpers.rb' require 'stripe_mock/request_handlers/helpers/token_helpers.rb' diff --git a/lib/stripe_mock/request_handlers/charges.rb b/lib/stripe_mock/request_handlers/charges.rb index 54c3050a8..f26765e40 100644 --- a/lib/stripe_mock/request_handlers/charges.rb +++ b/lib/stripe_mock/request_handlers/charges.rb @@ -5,7 +5,8 @@ module Charges def Charges.included(klass) klass.add_handler 'post /v1/charges', :new_charge klass.add_handler 'get /v1/charges', :get_charges - klass.add_handler 'get /v1/charges/(.*)', :get_charge + klass.add_handler 'get /v1/charges/search', :search_charges + klass.add_handler 'get /v1/charges/((?!search).*)', :get_charge klass.add_handler 'post /v1/charges/(.*)/capture', :capture_charge klass.add_handler 'post /v1/charges/(.*)/refund', :refund_charge klass.add_handler 'post /v1/charges/(.*)/refunds', :refund_charge @@ -90,6 +91,24 @@ def get_charges(route, method_url, params, headers) Data.mock_list_object(clone.values, params) end + SEARCH_FIELDS = [ + "amount", + "currency", + "customer", + "payment_method_details.card.brand", + "payment_method_details.card.exp_month", + "payment_method_details.card.exp_year", + "payment_method_details.card.fingerprint", + "payment_method_details.card.last4", + "status", + ].freeze + def search_charges(route, method_url, params, headers) + require_param(:query) unless params[:query] + + results = search_results(charges.values, params[:query], fields: SEARCH_FIELDS, resource_name: "charges") + Data.mock_list_object(results, params) + end + def get_charge(route, method_url, params, headers) route =~ method_url charge_id = $1 || params[:charge] diff --git a/lib/stripe_mock/request_handlers/customers.rb b/lib/stripe_mock/request_handlers/customers.rb index 32946ab04..ca9ac9bc3 100644 --- a/lib/stripe_mock/request_handlers/customers.rb +++ b/lib/stripe_mock/request_handlers/customers.rb @@ -5,9 +5,10 @@ module Customers def Customers.included(klass) klass.add_handler 'post /v1/customers', :new_customer klass.add_handler 'post /v1/customers/([^/]*)', :update_customer - klass.add_handler 'get /v1/customers/([^/]*)', :get_customer + klass.add_handler 'get /v1/customers/((?!search)[^/]*)', :get_customer klass.add_handler 'delete /v1/customers/([^/]*)', :delete_customer klass.add_handler 'get /v1/customers', :list_customers + klass.add_handler 'get /v1/customers/search', :search_customers klass.add_handler 'delete /v1/customers/([^/]*)/discount', :delete_customer_discount end @@ -140,6 +141,16 @@ def list_customers(route, method_url, params, headers) Data.mock_list_object(customers[stripe_account]&.values, params) end + SEARCH_FIELDS = ["email", "name", "phone"].freeze + def search_customers(route, method_url, params, headers) + require_param(:query) unless params[:query] + + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + all_customers = customers[stripe_account]&.values + results = search_results(all_customers, params[:query], fields: SEARCH_FIELDS, resource_name: "customers") + Data.mock_list_object(results, params) + end + def delete_customer_discount(route, method_url, params, headers) stripe_account = headers && headers[:stripe_account] || Stripe.api_key route =~ method_url diff --git a/lib/stripe_mock/request_handlers/helpers/search_helpers.rb b/lib/stripe_mock/request_handlers/helpers/search_helpers.rb new file mode 100644 index 000000000..76b4cfa20 --- /dev/null +++ b/lib/stripe_mock/request_handlers/helpers/search_helpers.rb @@ -0,0 +1,67 @@ +module StripeMock + module RequestHandlers + module Helpers + # Only supports exact matches on a single field, e.g. + # - 'amount:100' + # - 'email:"name@domain.com"' + # - 'name:"Foo Bar"' + # - 'metadata["foo"]:"bar"' + QUERYSTRING_PATTERN = /\A(?[\w\.]+)(\[['"](?[^'"]*)['"]\])?:['"]?(?[^'"]*)['"]?\z/ + def search_results(all_values, querystring, fields: [], resource_name:) + values = all_values.dup + query_match = QUERYSTRING_PATTERN.match(querystring) + raise Stripe::InvalidRequestError.new( + 'We were unable to parse your search query.' \ + ' Try using the format `metadata["key"]:"value"` to query for metadata or key:"value" to query for other fields.', + nil, + http_status: 400, + ) unless query_match + + case query_match[:field] + when *fields + values = values.select { |resource| + exact_match?(actual: field_value(resource, field: query_match[:field]), expected: query_match[:value]) + } + when "metadata" + values = values.select { |resource| + resource[:metadata] && + exact_match?(actual: resource[:metadata][query_match[:metadata_key].to_sym], expected: query_match[:value]) + } + else + raise Stripe::InvalidRequestError.new( + "Field `#{query_match[:field]}` is an unsupported search field for resource `#{resource_name}`." \ + " See http://stripe.com/docs/search#query-fields-for-#{resource_name.gsub('_', '-')} for a list of supported fields.", + nil, + http_status: 400, + ) + end + + values + end + + def exact_match?(actual:, expected:) + # allow comparisons of integers + if actual.respond_to?(:to_i) && actual.to_i == actual + expected = expected.to_i + end + # allow comparisons of boolean + case expected + when "true" + expected = true + when "false" + expected = false + end + + actual == expected + end + + def field_value(resource, field:) + value = resource + field.split('.').each do |segment| + value = value[segment.to_sym] + end + value + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/invoices.rb b/lib/stripe_mock/request_handlers/invoices.rb index 653096633..94f6b6a9d 100644 --- a/lib/stripe_mock/request_handlers/invoices.rb +++ b/lib/stripe_mock/request_handlers/invoices.rb @@ -6,7 +6,8 @@ def Invoices.included(klass) klass.add_handler 'post /v1/invoices', :new_invoice klass.add_handler 'get /v1/invoices/upcoming', :upcoming_invoice klass.add_handler 'get /v1/invoices/(.*)/lines', :get_invoice_line_items - klass.add_handler 'get /v1/invoices/(.*)', :get_invoice + klass.add_handler 'get /v1/invoices/((?!search).*)', :get_invoice + klass.add_handler 'get /v1/invoices/search', :search_invoices klass.add_handler 'get /v1/invoices', :list_invoices klass.add_handler 'post /v1/invoices/(.*)/pay', :pay_invoice klass.add_handler 'post /v1/invoices/(.*)', :update_invoice @@ -25,6 +26,14 @@ def update_invoice(route, method_url, params, headers) invoices[$1].merge!(params) end + SEARCH_FIELDS = ["currency", "customer", "number", "receipt_number", "subscription", "total"].freeze + def search_invoices(route, method_url, params, headers) + require_param(:query) unless params[:query] + + results = search_results(invoices.values, params[:query], fields: SEARCH_FIELDS, resource_name: "invoices") + Data.mock_list_object(results, params) + end + def list_invoices(route, method_url, params, headers) params[:offset] ||= 0 params[:limit] ||= 10 diff --git a/lib/stripe_mock/request_handlers/payment_intents.rb b/lib/stripe_mock/request_handlers/payment_intents.rb index d01b42aa9..2008cdac2 100644 --- a/lib/stripe_mock/request_handlers/payment_intents.rb +++ b/lib/stripe_mock/request_handlers/payment_intents.rb @@ -6,7 +6,8 @@ module PaymentIntents def PaymentIntents.included(klass) klass.add_handler 'post /v1/payment_intents', :new_payment_intent klass.add_handler 'get /v1/payment_intents', :get_payment_intents - klass.add_handler 'get /v1/payment_intents/(.*)', :get_payment_intent + klass.add_handler 'get /v1/payment_intents/((?!search).*)', :get_payment_intent + klass.add_handler 'get /v1/payment_intents/search', :search_payment_intents klass.add_handler 'post /v1/payment_intents/(.*)/confirm', :confirm_payment_intent klass.add_handler 'post /v1/payment_intents/(.*)/capture', :capture_payment_intent klass.add_handler 'post /v1/payment_intents/(.*)/cancel', :cancel_payment_intent @@ -70,6 +71,14 @@ def get_payment_intent(route, method_url, params, headers) payment_intent end + SEARCH_FIELDS = ["amount", "currency", "customer", "status"].freeze + def search_payment_intents(route, method_url, params, headers) + require_param(:query) unless params[:query] + + results = search_results(payment_intents.values, params[:query], fields: SEARCH_FIELDS, resource_name: "payment_intents") + Data.mock_list_object(results, params) + end + def capture_payment_intent(route, method_url, params, headers) route =~ method_url payment_intent = assert_existence :payment_intent, $1, payment_intents[$1] diff --git a/lib/stripe_mock/request_handlers/prices.rb b/lib/stripe_mock/request_handlers/prices.rb index 46146a4ce..15ceab434 100644 --- a/lib/stripe_mock/request_handlers/prices.rb +++ b/lib/stripe_mock/request_handlers/prices.rb @@ -3,10 +3,11 @@ module RequestHandlers module Prices def Prices.included(klass) - klass.add_handler 'post /v1/prices', :new_price - klass.add_handler 'post /v1/prices/(.*)', :update_price - klass.add_handler 'get /v1/prices/(.*)', :get_price - klass.add_handler 'get /v1/prices', :list_prices + klass.add_handler 'post /v1/prices', :new_price + klass.add_handler 'post /v1/prices/(.*)', :update_price + klass.add_handler 'get /v1/prices/((?!search).*)', :get_price + klass.add_handler 'get /v1/prices/search', :search_prices + klass.add_handler 'get /v1/prices', :list_prices end def new_price(route, method_url, params, headers) @@ -45,6 +46,14 @@ def list_prices(route, method_url, params, headers) Data.mock_list_object(price_data.first(limit), params.merge!(limit: limit)) end + + SEARCH_FIELDS = ["active", "currency", "lookup_key", "product", "type"].freeze + def search_prices(route, method_url, params, headers) + require_param(:query) unless params[:query] + + results = search_results(prices.values, params[:query], fields: SEARCH_FIELDS, resource_name: "prices") + Data.mock_list_object(results, params) + end end end end diff --git a/lib/stripe_mock/request_handlers/products.rb b/lib/stripe_mock/request_handlers/products.rb index a75461c32..a5d81ef44 100644 --- a/lib/stripe_mock/request_handlers/products.rb +++ b/lib/stripe_mock/request_handlers/products.rb @@ -2,11 +2,12 @@ module StripeMock module RequestHandlers module Products def self.included(base) - base.add_handler 'post /v1/products', :create_product - base.add_handler 'get /v1/products/(.*)', :retrieve_product - base.add_handler 'post /v1/products/(.*)', :update_product - base.add_handler 'get /v1/products', :list_products - base.add_handler 'delete /v1/products/(.*)', :destroy_product + base.add_handler 'post /v1/products', :create_product + base.add_handler 'get /v1/products/((?!search).*)', :retrieve_product + base.add_handler 'get /v1/products/search', :search_products + base.add_handler 'post /v1/products/(.*)', :update_product + base.add_handler 'get /v1/products', :list_products + base.add_handler 'delete /v1/products/(.*)', :destroy_product end def create_product(_route, _method_url, params, _headers) @@ -32,6 +33,14 @@ def list_products(_route, _method_url, params, _headers) Data.mock_list_object(products.values.take(limit), params) end + SEARCH_FIELDS = ["active", "description", "name", "shippable", "url"].freeze + def search_products(route, method_url, params, headers) + require_param(:query) unless params[:query] + + results = search_results(products.values, params[:query], fields: SEARCH_FIELDS, resource_name: "products") + Data.mock_list_object(results, params) + end + def destroy_product(route, method_url, _params, _headers) id = method_url.match(route).captures.first assert_existence :product, id, products[id] diff --git a/lib/stripe_mock/request_handlers/subscriptions.rb b/lib/stripe_mock/request_handlers/subscriptions.rb index d8c31d1d9..da64f2ea3 100644 --- a/lib/stripe_mock/request_handlers/subscriptions.rb +++ b/lib/stripe_mock/request_handlers/subscriptions.rb @@ -5,8 +5,9 @@ module Subscriptions def Subscriptions.included(klass) klass.add_handler 'get /v1/subscriptions', :retrieve_subscriptions klass.add_handler 'post /v1/subscriptions', :create_subscription - klass.add_handler 'get /v1/subscriptions/(.*)', :retrieve_subscription + klass.add_handler 'get /v1/subscriptions/((?!search).*)', :retrieve_subscription klass.add_handler 'post /v1/subscriptions/(.*)', :update_subscription + klass.add_handler 'get /v1/subscriptions/search', :search_subscriptions klass.add_handler 'delete /v1/subscriptions/(.*)', :cancel_subscription klass.add_handler 'post /v1/customers/(.*)/subscription(?:s)?', :create_customer_subscription @@ -293,6 +294,14 @@ def cancel_subscription(route, method_url, params, headers) subscription end + SEARCH_FIELDS = ["status"].freeze + def search_subscriptions(route, method_url, params, headers) + require_param(:query) unless params[:query] + + results = search_results(subscriptions.values, params[:query], fields: SEARCH_FIELDS, resource_name: "subscriptions") + Data.mock_list_object(results, params) + end + private def get_subscription_plans_from_params(params) diff --git a/spec/shared_stripe_examples/charge_examples.rb b/spec/shared_stripe_examples/charge_examples.rb index c26b8f602..942325dfd 100644 --- a/spec/shared_stripe_examples/charge_examples.rb +++ b/spec/shared_stripe_examples/charge_examples.rb @@ -494,4 +494,101 @@ end end + context "search" do + # the Search API requires about a minute between writes and reads, so add sleeps accordingly when running live + it "searches charges for exact matches", :aggregate_failures do + response = Stripe::Charge.search({query: 'amount:100'}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(0) + + customer = Stripe::Customer.create(email: 'johnny@appleseed.com') + one = Stripe::Charge.create( + customer: customer.id, + amount: 100, + currency: "usd", + payment_method_details: { + card: StripeMock::Data.mock_charge[:payment_method_details][:card].merge( + brand: "mastercard", + exp_month: 1, + exp_year: 2111, + fingerprint: "un", + last4: "1111", + ), + }, + status: "succeeded", + metadata: {key: 'uno'}, + ) + two = Stripe::Charge.create( + customer: customer.id, + amount: 200, + currency: "gbp", + payment_method_details: { + card: StripeMock::Data.mock_charge[:payment_method_details][:card].merge( + brand: "visa", + exp_month: 2, + exp_year: 2222, + fingerprint: "deux", + last4: "2222", + ), + }, + status: "pending", + metadata: {key: 'dos'}, + ) + + response = Stripe::Charge.search({query: 'amount:100'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Charge.search({query: 'currency:"gbp"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::Charge.search({query: %(customer:"#{customer.id}")}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id, two.id]) + + response = Stripe::Charge.search({query: 'payment_method_details.card.brand:mastercard'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Charge.search({query: 'payment_method_details.card.exp_month:2'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::Charge.search({query: 'payment_method_details.card.exp_year:2111'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Charge.search({query: 'payment_method_details.card.fingerprint:un'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Charge.search({query: 'payment_method_details.card.last4:2222'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::Charge.search({query: 'status:"succeeded"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Charge.search({query: 'metadata["key"]:"uno"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + end + + it "respects limit", :aggregate_failures do + customer = Stripe::Customer.create(email: 'johnny@appleseed.com') + 11.times do + Stripe::Charge.create(customer: customer.id, amount: 100, currency: "usd") + end + + response = Stripe::Charge.search({query: %(customer:"#{customer.id}")}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(10) + response = Stripe::Charge.search({query: %(customer:"#{customer.id}"), limit: 1}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(1) + end + + it "reports search errors", :aggregate_failures do + expect { + Stripe::Charge.search({limit: 1}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Missing required param: query./) + + expect { + Stripe::Charge.search({query: 'asdf'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /We were unable to parse your search query./) + + expect { + Stripe::Charge.search({query: 'foo:"bar"'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Field `foo` is an unsupported search field for resource `charges`./) + end + end end diff --git a/spec/shared_stripe_examples/customer_examples.rb b/spec/shared_stripe_examples/customer_examples.rb index 60d8487df..d564ad22f 100644 --- a/spec/shared_stripe_examples/customer_examples.rb +++ b/spec/shared_stripe_examples/customer_examples.rb @@ -349,6 +349,62 @@ def gen_card_tk expect(all.data.map &:email).to include('one@one.com', 'two@two.com') end + context "search" do + # the Search API requires about a minute between writes and reads, so add sleeps accordingly when running live + it "searches customers for exact matches", :aggregate_failures do + response = Stripe::Customer.search({query: 'email:"one@one.com"'}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(0) + + one = Stripe::Customer.create(email: 'one@one.com', name: 'one', phone: '1111111111', metadata: {key: 'uno'}) + two = Stripe::Customer.create(email: 'two@two.com', name: 'two', phone: '2222222222', metadata: {key: 'dos'}) + + response = Stripe::Customer.search({query: 'email:"one@one.com"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Customer.search({query: 'name:"two"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::Customer.search({query: 'phone:"2222222222"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::Customer.search({query: 'metadata["key"]:"uno"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + end + + it "respects limit", :aggregate_failures do + one = Stripe::Customer.create(email: 'one@one.com', name: 'one') + two = Stripe::Customer.create(email: 'two@two.com', name: 'one') + three = Stripe::Customer.create(email: 'three@three.com', name: 'one') + four = Stripe::Customer.create(email: 'four@four.com', name: 'one') + five = Stripe::Customer.create(email: 'five@five.com', name: 'one') + six = Stripe::Customer.create(email: 'six@six.com', name: 'one') + seven = Stripe::Customer.create(email: 'seven@seven.com', name: 'one') + eight = Stripe::Customer.create(email: 'eight@eight.com', name: 'one') + nine = Stripe::Customer.create(email: 'nine@nine.com', name: 'one') + ten = Stripe::Customer.create(email: 'ten@ten.com', name: 'one') + eleven = Stripe::Customer.create(email: 'eleven@eleven.com', name: 'one') + + response = Stripe::Customer.search({query: 'name:"one"'}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(10) + response = Stripe::Customer.search({query: 'name:"one"', limit: 1}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(1) + end + + it "reports search errors", :aggregate_failures do + expect { + Stripe::Customer.search({limit: 1}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Missing required param: query./) + + expect { + Stripe::Customer.search({query: 'asdf'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /We were unable to parse your search query./) + + expect { + Stripe::Customer.search({query: 'foo:"bar"'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Field `foo` is an unsupported search field for resource `customers`./) + end + end + it "updates a stripe customer" do original = Stripe::Customer.create(id: 'test_customer_update') email = original.email diff --git a/spec/shared_stripe_examples/invoice_examples.rb b/spec/shared_stripe_examples/invoice_examples.rb index 244ae20e8..a6a56f194 100644 --- a/spec/shared_stripe_examples/invoice_examples.rb +++ b/spec/shared_stripe_examples/invoice_examples.rb @@ -74,6 +74,88 @@ end end + context "searching invoices" do + # the Search API requires about a minute between writes and reads, so add sleeps accordingly when running live + it "searches invoices for exact matches", :aggregate_failures do + response = Stripe::Invoice.search({query: 'currency:"usd"'}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(0) + + product = stripe_helper.create_product + stripe_helper.create_plan( + amount: 500, + interval: 'month', + product: product.id, + currency: 'usd', + id: 'Sample5', + ) + customer = Stripe::Customer.create(email: 'johnny@appleseed.com', source: stripe_helper.generate_card_token) + subscription = Stripe::Subscription.create(customer: customer.id, items: [{plan: 'Sample5'}]) + one = Stripe::Invoice.create( + customer: customer.id, + currency: 'usd', + subscription: subscription.id, + metadata: {key: 'uno'}, + number: 'one-1', + receipt_number: '111', + ) + two = Stripe::Invoice.create( + customer: customer.id, + currency: 'gbp', + subscription: subscription.id, + metadata: {key: 'dos'}, + number: 'two-2', + receipt_number: '222', + ) + + response = Stripe::Invoice.search({query: 'currency:"gbp"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::Invoice.search({query: %(customer:"#{customer.id}")}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id, two.id]) + + response = Stripe::Invoice.search({query: 'number:"one-1"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Invoice.search({query: 'receipt_number:"222"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::Invoice.search({query: %(subscription:"#{subscription.id}")}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id, two.id]) + + response = Stripe::Invoice.search({query: 'total:1000'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id, two.id]) + + response = Stripe::Invoice.search({query: 'metadata["key"]:"uno"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + end + + it "respects limit", :aggregate_failures do + customer = Stripe::Customer.create(email: 'one@one.com', name: 'one', phone: '1111111111', metadata: {key: 'uno'}) + 11.times do + Stripe::Invoice.create(customer: customer.id, currency: 'usd') + end + + response = Stripe::Invoice.search({query: %(customer:"#{customer.id}")}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(10) + response = Stripe::Invoice.search({query: %(customer:"#{customer.id}"), limit: 1}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(1) + end + + it "reports search errors", :aggregate_failures do + expect { + Stripe::Invoice.search({limit: 1}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Missing required param: query./) + + expect { + Stripe::Invoice.search({query: 'asdf'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /We were unable to parse your search query./) + + expect { + Stripe::Invoice.search({query: 'foo:"bar"'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Field `foo` is an unsupported search field for resource `invoices`./) + end + end + context "paying an invoice" do before do @invoice = Stripe::Invoice.create diff --git a/spec/shared_stripe_examples/payment_intent_examples.rb b/spec/shared_stripe_examples/payment_intent_examples.rb index 8f6993eab..fcd0fdf72 100644 --- a/spec/shared_stripe_examples/payment_intent_examples.rb +++ b/spec/shared_stripe_examples/payment_intent_examples.rb @@ -144,4 +144,66 @@ expect(e.http_status).to eq(400) } end + + context "search" do + # the Search API requires about a minute between writes and reads, so add sleeps accordingly when running live + it "searches payment intents for exact matches", :aggregate_failures do + response = Stripe::PaymentIntent.search({query: 'currency:"usd"'}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(0) + + customer = Stripe::Customer.create(email: 'johnny@appleseed.com', source: stripe_helper.generate_card_token) + one = Stripe::PaymentIntent.create( + amount: 100, + customer: customer.id, + currency: 'usd', + metadata: {key: 'uno'}, + ) + two = Stripe::PaymentIntent.create( + amount: 3184, # status: requires_action + customer: customer.id, + currency: 'gbp', + metadata: {key: 'dos'}, + ) + + response = Stripe::PaymentIntent.search({query: 'amount:100'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::PaymentIntent.search({query: 'currency:"gbp"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::PaymentIntent.search({query: %(customer:"#{customer.id}")}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id, two.id]) + + response = Stripe::PaymentIntent.search({query: 'status:"requires_action"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::PaymentIntent.search({query: 'metadata["key"]:"uno"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + end + + it "respects limit", :aggregate_failures do + 11.times do + Stripe::PaymentIntent.create(amount: 100, currency: 'usd') + end + + response = Stripe::PaymentIntent.search({query: 'amount:100'}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(10) + response = Stripe::PaymentIntent.search({query: 'amount:100', limit: 1}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(1) + end + + it "reports search errors", :aggregate_failures do + expect { + Stripe::PaymentIntent.search({limit: 1}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Missing required param: query./) + + expect { + Stripe::PaymentIntent.search({query: 'asdf'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /We were unable to parse your search query./) + + expect { + Stripe::PaymentIntent.search({query: 'foo:"bar"'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Field `foo` is an unsupported search field for resource `payment_intents`./) + end + end end diff --git a/spec/shared_stripe_examples/price_examples.rb b/spec/shared_stripe_examples/price_examples.rb index 7138aa923..67e555e50 100644 --- a/spec/shared_stripe_examples/price_examples.rb +++ b/spec/shared_stripe_examples/price_examples.rb @@ -125,6 +125,75 @@ expect(two.map &:amount).to include(98765) end + context "searching prices" do + # the Search API requires about a minute between writes and reads, so add sleeps accordingly when running live + it "searches prices for exact matches", :aggregate_failures do + response = Stripe::Price.search({query: 'currency:"usd"'}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(0) + + one = stripe_helper.create_price( + amount: 100, + currency: 'usd', + lookup_key: 'one', + product: product_id, + metadata: {key: 'uno'}, + type: "one_time", + ) + two = stripe_helper.create_price( + active: false, + amount: 200, + currency: 'gbp', + lookup_key: 'two', + product: product_id, + recurring: {interval: 'month'}, + metadata: {key: 'dos'}, + ) + + response = Stripe::Price.search({query: 'active:"true"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Price.search({query: 'currency:"gbp"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::Price.search({query: 'lookup_key:"one"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Price.search({query: %(product:"#{product.id}")}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id, two.id]) + + response = Stripe::Price.search({query: 'type:"recurring"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::Price.search({query: 'metadata["key"]:"uno"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + end + + it "respects limit", :aggregate_failures do + 11.times do + stripe_helper.create_price(product: product_id) + end + + response = Stripe::Price.search({query: %(product:"#{product.id}")}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(10) + response = Stripe::Price.search({query: %(product:"#{product.id}"), limit: 1}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(1) + end + + it "reports search errors", :aggregate_failures do + expect { + Stripe::Price.search({limit: 1}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Missing required param: query./) + + expect { + Stripe::Price.search({query: 'asdf'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /We were unable to parse your search query./) + + expect { + Stripe::Price.search({query: 'foo:"bar"'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Field `foo` is an unsupported search field for resource `prices`./) + end + end + describe "Validations", :live => true do include_context "stripe validator" let(:params) { stripe_helper.create_price_params(product: product_id) } diff --git a/spec/shared_stripe_examples/product_examples.rb b/spec/shared_stripe_examples/product_examples.rb index 05fe7574e..37a8e91f3 100644 --- a/spec/shared_stripe_examples/product_examples.rb +++ b/spec/shared_stripe_examples/product_examples.rb @@ -85,6 +85,74 @@ expect(all.count).to eq(100) end + context "searching products" do + # the Search API requires about a minute between writes and reads, so add sleeps accordingly when running live + it "searches products for exact matches", :aggregate_failures do + response = Stripe::Product.search({query: 'name:"one"'}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(0) + + one = stripe_helper.create_product( + id: "product_1", + name: "one", + description: "un", + shippable: true, + url: "http://example.com/one", + metadata: {key: "uno"}, + ) + two = stripe_helper.create_product( + id: "product_2", + name: "two", + active: false, + description: "deux", + url: "http://example.com/two", + metadata: {key: "dos"}, + ) + + response = Stripe::Product.search({query: 'active:"true"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Product.search({query: 'description:"deux"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::Product.search({query: 'name:"one"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Product.search({query: 'shippable:"true"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Product.search({query: 'url:"http://example.com/two"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + + response = Stripe::Product.search({query: 'metadata["key"]:"uno"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + end + + it "respects limit", :aggregate_failures do + 11.times do |i| + stripe_helper.create_product(id: "product_#{i}", name: "Product #{i}") + end + + response = Stripe::Product.search({query: 'active:"true"'}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(10) + response = Stripe::Product.search({query: 'active:"true"', limit: 1}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(1) + end + + it "reports search errors", :aggregate_failures do + expect { + Stripe::Product.search({limit: 1}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Missing required param: query./) + + expect { + Stripe::Product.search({query: 'asdf'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /We were unable to parse your search query./) + + expect { + Stripe::Product.search({query: 'foo:"bar"'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Field `foo` is an unsupported search field for resource `products`./) + end + end + describe "Validation", :live => true do include_context "stripe validator" let(:params) { stripe_helper.create_product_params } diff --git a/spec/shared_stripe_examples/subscription_examples.rb b/spec/shared_stripe_examples/subscription_examples.rb index c2da97304..9863902a9 100644 --- a/spec/shared_stripe_examples/subscription_examples.rb +++ b/spec/shared_stripe_examples/subscription_examples.rb @@ -1347,6 +1347,65 @@ expect(subscription.items.data[0].metadata.to_h).to eq(foo: 'bar') end end + + context "search" do + # the Search API requires about a minute between writes and reads, so add sleeps accordingly when running live + it "searches subscriptions for exact matches", :aggregate_failures do + response = Stripe::Subscription.search({query: 'status:"active"'}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(0) + + stripe_helper.create_plan( + amount: 500, + interval: 'month', + product: product.id, + currency: 'usd', + id: 'Sample5' + ) + customer = Stripe::Customer.create(email: 'johnny@appleseed.com', source: gen_card_tk) + one = Stripe::Subscription.create(customer: customer.id, items: [{plan: "Sample5"}], metadata: {key: 'uno'}) + two = Stripe::Subscription.create(customer: customer.id, items: [{plan: "Sample5"}], metadata: {key: 'dos'}) + Stripe::Subscription.delete(two.id) + + response = Stripe::Subscription.search({query: 'status:"active"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([one.id]) + + response = Stripe::Subscription.search({query: 'metadata["key"]:"dos"'}, stripe_version: '2020-08-27') + expect(response.data.map(&:id)).to match_array([two.id]) + end + + it "respects limit", :aggregate_failures do + stripe_helper.create_plan( + amount: 500, + interval: 'month', + product: product.id, + currency: 'usd', + id: 'Sample5' + ) + customer = Stripe::Customer.create(email: 'johnny@appleseed.com', source: gen_card_tk) + 11.times do + Stripe::Subscription.create(customer: customer.id, items: [{plan: "Sample5"}]) + end + + response = Stripe::Subscription.search({query: 'status:"active"'}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(10) + response = Stripe::Subscription.search({query: 'status:"active"', limit: 1}, stripe_version: '2020-08-27') + expect(response.data.size).to eq(1) + end + + it "reports search errors", :aggregate_failures do + expect { + Stripe::Subscription.search({limit: 1}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Missing required param: query./) + + expect { + Stripe::Subscription.search({query: 'asdf'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /We were unable to parse your search query./) + + expect { + Stripe::Subscription.search({query: 'foo:"bar"'}, stripe_version: '2020-08-27') + }.to raise_error(Stripe::InvalidRequestError, /Field `foo` is an unsupported search field for resource `subscriptions`./) + end + end end shared_examples 'Customer Subscriptions with prices' do From 1d76c1008f1fd269d1878dc3bc059437a4b44702 Mon Sep 17 00:00:00 2001 From: Louis-Michel Couture Date: Fri, 26 Aug 2022 20:20:14 +0200 Subject: [PATCH 09/28] Allow subscription filter on current_period values current_period_start and current_period_end --- .../helpers/subscription_helpers.rb | 13 ++++++++ .../request_handlers/subscriptions.rb | 6 ++++ .../subscription_examples.rb | 32 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb b/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb index 33bfd370f..8b8e4864e 100644 --- a/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb +++ b/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb @@ -125,6 +125,19 @@ def total_items_amount(items) end total end + + def filter_by_timestamp(subscriptions, field:, value:) + if value.is_a?(Hash) + operator_mapping = { gt: :>, gte: :>=, lt: :<, lte: :<= } + subscriptions.filter do |sub| + sub[field].public_send(operator_mapping[value.keys[0]], value.values[0]) + end + else + subscriptions.filter do |sub| + sub[field] == value + end + end + end end end end diff --git a/lib/stripe_mock/request_handlers/subscriptions.rb b/lib/stripe_mock/request_handlers/subscriptions.rb index d8c31d1d9..688b16339 100644 --- a/lib/stripe_mock/request_handlers/subscriptions.rb +++ b/lib/stripe_mock/request_handlers/subscriptions.rb @@ -188,6 +188,12 @@ def retrieve_subscriptions(route, method_url, params, headers) else subs = subs.filter {|subscription| subscription[:status] == params[:status]} end + if params[:current_period_end] + subs = filter_by_timestamp(subs, field: :current_period_end, value: params[:current_period_end]) + end + if params[:current_period_start] + subs = filter_by_timestamp(subs, field: :current_period_start, value: params[:current_period_start]) + end Data.mock_list_object(subs, params) end diff --git a/spec/shared_stripe_examples/subscription_examples.rb b/spec/shared_stripe_examples/subscription_examples.rb index d9a934e17..005e9bcce 100644 --- a/spec/shared_stripe_examples/subscription_examples.rb +++ b/spec/shared_stripe_examples/subscription_examples.rb @@ -1299,6 +1299,38 @@ expect(list.data).to be_empty expect(list.data.length).to eq(0) end + + it "filters out subscriptions based on their current_period", live: true do + price = stripe_helper.create_price(recurring: { interval: 'month' }) + price2 = stripe_helper.create_price(recurring: { interval: 'year' }) + + subscription1 = Stripe::Subscription.create( + customer: Stripe::Customer.create(source: gen_card_tk).id, + items: [{ price: price.id, quantity: 1 }] + ) + subscription2 = Stripe::Subscription.create( + customer: Stripe::Customer.create(source: gen_card_tk).id, + items: [{ price: price2.id, quantity: 1 }] + ) + + list = Stripe::Subscription.list({ current_period_end: { gt: subscription1.current_period_end }}) + expect(list.data).to contain_exactly(subscription2) + + list = Stripe::Subscription.list({ current_period_end: { gte: subscription1.current_period_end }}) + expect(list.data).to contain_exactly(subscription1, subscription2) + + list = Stripe::Subscription.list({ current_period_end: { lt: subscription1.current_period_end }}) + expect(list.data).to be_empty + + list = Stripe::Subscription.list({ current_period_end: { lte: subscription1.current_period_end }}) + expect(list.data).to contain_exactly(subscription1) + + list = Stripe::Subscription.list({ current_period_start: subscription1.current_period_start }) + expect(list.data).to contain_exactly(subscription1, subscription2) + + list = Stripe::Subscription.list({ current_period_end: subscription2.current_period_end }) + expect(list.data).to contain_exactly(subscription2) + end end describe "metadata" do From ffbbf96c8c93f2fe5b641e776c02ef45e8b780f0 Mon Sep 17 00:00:00 2001 From: Luke Rodgers Date: Thu, 22 Sep 2022 11:38:05 -0400 Subject: [PATCH 10/28] Support invoice number --- lib/stripe_mock/data.rb | 3 ++- spec/shared_stripe_examples/invoice_examples.rb | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/stripe_mock/data.rb b/lib/stripe_mock/data.rb index 5f958b7c3..2ef202dd6 100644 --- a/lib/stripe_mock/data.rb +++ b/lib/stripe_mock/data.rb @@ -450,7 +450,8 @@ def self.mock_invoice(lines, params={}) next_payment_attempt: 1349825350, charge: nil, discount: nil, - subscription: nil + subscription: nil, + number: "6C41730-0001" }.merge(params) if invoice[:discount] invoice[:total] = [0, invoice[:subtotal] - invoice[:discount][:coupon][:amount_off]].max if invoice[:discount][:coupon][:amount_off] diff --git a/spec/shared_stripe_examples/invoice_examples.rb b/spec/shared_stripe_examples/invoice_examples.rb index 244ae20e8..80d8135e0 100644 --- a/spec/shared_stripe_examples/invoice_examples.rb +++ b/spec/shared_stripe_examples/invoice_examples.rb @@ -14,6 +14,11 @@ expect(data[invoice.id]).to_not be_nil expect(data[invoice.id][:id]).to eq(invoice.id) end + + it "supports invoice number" do + original = Stripe::Invoice.create + expect(original.number).to be + end end context "retrieving an invoice" do From b35555f09c478240dbdb9f4ae408bb56c3f4374e Mon Sep 17 00:00:00 2001 From: Greg Ciampa Date: Tue, 18 Oct 2022 22:47:29 -0500 Subject: [PATCH 11/28] Add PromotionCode resource --- lib/stripe_mock.rb | 1 + lib/stripe_mock/data.rb | 38 +++++++++++ lib/stripe_mock/instance.rb | 7 +- .../request_handlers/promotion_codes.rb | 43 ++++++++++++ .../promotion_code_examples.rb | 68 +++++++++++++++++++ spec/support/stripe_examples.rb | 1 + 6 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 lib/stripe_mock/request_handlers/promotion_codes.rb create mode 100644 spec/shared_stripe_examples/promotion_code_examples.rb diff --git a/lib/stripe_mock.rb b/lib/stripe_mock.rb index 543ae1b90..791a9b474 100644 --- a/lib/stripe_mock.rb +++ b/lib/stripe_mock.rb @@ -65,6 +65,7 @@ require 'stripe_mock/request_handlers/orders.rb' require 'stripe_mock/request_handlers/plans.rb' require 'stripe_mock/request_handlers/prices.rb' +require 'stripe_mock/request_handlers/promotion_codes.rb' require 'stripe_mock/request_handlers/recipients.rb' require 'stripe_mock/request_handlers/refunds.rb' require 'stripe_mock/request_handlers/transfers.rb' diff --git a/lib/stripe_mock/data.rb b/lib/stripe_mock/data.rb index 5f958b7c3..2c94f37e7 100644 --- a/lib/stripe_mock/data.rb +++ b/lib/stripe_mock/data.rb @@ -657,6 +657,44 @@ def self.mock_product(params={}) }.merge(params) end + def self.mock_promotion_code(params={}) + { + id: "mock_promo_abc123", + object: "promotion_code", + active: true, + code: "TESTCODE", + coupon: { + id: "mock_coupon_abc123", + object: "coupon", + amount_off: nil, + created: 1665773498, + currency: "usd", + duration: "repeating", + duration_in_months: 1, + livemode: false, + max_redemptions: nil, + metadata: {}, + name: "Mock Coupon", + percent_off: 10.0, + redeem_by: nil, + times_redeemed: 0, + valid: true + }, + created: 1665773499, + customer: nil, + expires_at: nil, + livemode: false, + max_redemptions: nil, + metadata: {}, + restrictions: { + first_time_transaction: false, + minimum_amount: nil, + minimum_amount_currency: nil + }, + times_redeemed: 0 + }.merge(params) + end + def self.mock_recipient(cards, params={}) rp_id = params[:id] || "test_rp_default" cards.each {|card| card[:recipient] = rp_id} diff --git a/lib/stripe_mock/instance.rb b/lib/stripe_mock/instance.rb index b1ad87d0a..56ab20eef 100644 --- a/lib/stripe_mock/instance.rb +++ b/lib/stripe_mock/instance.rb @@ -44,6 +44,7 @@ def self.handler_for_method_url(method_url) include StripeMock::RequestHandlers::Plans include StripeMock::RequestHandlers::Prices include StripeMock::RequestHandlers::Products + include StripeMock::RequestHandlers::PromotionCodes include StripeMock::RequestHandlers::Refunds include StripeMock::RequestHandlers::Recipients include StripeMock::RequestHandlers::Transfers @@ -57,8 +58,9 @@ def self.handler_for_method_url(method_url) attr_reader :accounts, :balance, :balance_transactions, :bank_tokens, :charges, :coupons, :customers, :disputes, :events, :invoices, :invoice_items, :orders, :payment_intents, :payment_methods, - :setup_intents, :plans, :prices, :recipients, :refunds, :transfers, :payouts, :subscriptions, :country_spec, - :subscriptions_items, :products, :tax_rates, :checkout_sessions, :checkout_session_line_items + :setup_intents, :plans, :prices, :promotion_codes, :recipients, :refunds, :transfers, :payouts, + :subscriptions, :country_spec, :subscriptions_items, :products, :tax_rates, :checkout_sessions, + :checkout_session_line_items attr_accessor :error_queue, :debug, :conversion_rate, :account_balance @@ -83,6 +85,7 @@ def initialize @plans = {} @prices = {} @products = {} + @promotion_codes = {} @recipients = {} @refunds = {} @transfers = {} diff --git a/lib/stripe_mock/request_handlers/promotion_codes.rb b/lib/stripe_mock/request_handlers/promotion_codes.rb new file mode 100644 index 000000000..a8ecf11bc --- /dev/null +++ b/lib/stripe_mock/request_handlers/promotion_codes.rb @@ -0,0 +1,43 @@ +module StripeMock + module RequestHandlers + module PromotionCodes + + def PromotionCodes.included(klass) + klass.add_handler 'post /v1/promotion_codes', :new_promotion_code + klass.add_handler 'post /v1/promotion_codes/([^/]*)', :update_promotion_code + klass.add_handler 'get /v1/promotion_codes/([^/]*)', :get_promotion_code + klass.add_handler 'get /v1/promotion_codes', :list_promotion_code + end + + def new_promotion_code(route, method_url, params, headers) + params[:id] ||= new_id("promo") + raise Stripe::InvalidRequestError.new("Missing required param: coupon", "promotion_code", http_status: 400) unless params[:coupon] + + if params[:restrictions] + if params[:restrictions][:minimum_amount] && !params[:restrictions][:minimum_amount_currency] + raise Stripe::InvalidRequestError.new( + "You must pass minimum_amount_currency when passing minimum_amount", "minimum_amount_currency", http_status: 400 + ) + end + end + + promotion_codes[ params[:id] ] = Data.mock_promotion_code(params) + end + + def update_promotion_code(route, method_url, params, headers) + route =~ method_url + assert_existence :promotion_code, $1, promotion_codes[$1] + promotion_codes[$1].merge!(params) + end + + def get_promotion_code(route, method_url, params, headers) + route =~ method_url + assert_existence :promotion_code, $1, promotion_codes[$1] + end + + def list_promotion_code(route, method_url, params, headers) + Data.mock_list_object(promotion_codes.values, params) + end + end + end +end diff --git a/spec/shared_stripe_examples/promotion_code_examples.rb b/spec/shared_stripe_examples/promotion_code_examples.rb new file mode 100644 index 000000000..a3ae3aa70 --- /dev/null +++ b/spec/shared_stripe_examples/promotion_code_examples.rb @@ -0,0 +1,68 @@ +require "spec_helper" + +shared_examples "PromotionCode API" do + let(:coupon) { stripe_helper.create_coupon } + + it "creates a promotion code" do + promotion_code = Stripe::PromotionCode.create({id: "promo_123", coupon: coupon.id, code: "FREESTUFF"}) + + expect(promotion_code.id).to eq("promo_123") + expect(promotion_code.code).to eq("FREESTUFF") + expect(promotion_code.coupon).to eq(coupon.id) + end + + it "creates a promotion code without specifying code" do + promotion_code = Stripe::PromotionCode.create({id: "promo_123", coupon: coupon.id}) + + expect(promotion_code.id).to eq("promo_123") + expect(promotion_code.code).to eq("TESTCODE") + expect(promotion_code.coupon).to eq(coupon.id) + end + + it "cannot create a promotion code without a coupon" do + expect { + Stripe::PromotionCode.create + }.to raise_error { |e| + expect(e).to be_a(Stripe::InvalidRequestError) + expect(e.message).to eq("Missing required param: coupon") + } + end + + it "requires minimum amount currency when minimum amount is provided" do + expect { + Stripe::PromotionCode.create(coupon: coupon, restrictions: {minimum_amount: 100}) + }.to raise_error { |e| + expect(e).to be_a(Stripe::InvalidRequestError) + expect(e.message).to eq("You must pass minimum_amount_currency when passing minimum_amount") + } + end + + it "updates a promotion code" do + promotion_code = Stripe::PromotionCode.create({coupon: coupon.id}) + expect(promotion_code.active).to eq(true) + + updated = Stripe::PromotionCode.update(promotion_code.id, active: false) + + expect(updated.active).to eq(false) + end + + it "retrieves a promotion code" do + original = Stripe::PromotionCode.create({coupon: coupon.id}) + + promotion_code = Stripe::PromotionCode.retrieve(original.id) + + expect(promotion_code.id).to eq(original.id) + expect(promotion_code.code).to eq(original.code) + expect(promotion_code.coupon).to eq(original.coupon) + end + + it "lists all promotion codes" do + Stripe::PromotionCode.create({coupon: coupon.id, code: "10PERCENT"}) + Stripe::PromotionCode.create({coupon: coupon.id, code: "20PERCENT"}) + + all = Stripe::PromotionCode.list + + expect(all.count).to eq(2) + expect(all.map(&:code)).to include("10PERCENT", "20PERCENT") + end +end diff --git a/spec/support/stripe_examples.rb b/spec/support/stripe_examples.rb index 931ffe146..f41980e24 100644 --- a/spec/support/stripe_examples.rb +++ b/spec/support/stripe_examples.rb @@ -24,6 +24,7 @@ def it_behaves_like_stripe(&block) it_behaves_like 'Plan API', &block it_behaves_like 'Price API', &block it_behaves_like 'Product API', &block + it_behaves_like 'PromotionCode API', &block it_behaves_like 'Recipient API', &block it_behaves_like 'Refund API', &block it_behaves_like 'Transfer API', &block From bd20b2b3b962367ae282c27d28fdecda54557988 Mon Sep 17 00:00:00 2001 From: Greg Ciampa Date: Wed, 19 Oct 2022 11:09:14 -0500 Subject: [PATCH 12/28] Allow promotion code parameter when creating a subscription --- .../request_handlers/subscriptions.rb | 70 ++++++++++++++++++- .../subscription_examples.rb | 39 +++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/lib/stripe_mock/request_handlers/subscriptions.rb b/lib/stripe_mock/request_handlers/subscriptions.rb index d8c31d1d9..cc316c805 100644 --- a/lib/stripe_mock/request_handlers/subscriptions.rb +++ b/lib/stripe_mock/request_handlers/subscriptions.rb @@ -69,6 +69,24 @@ def create_customer_subscription(route, method_url, params, headers) end end + if params[:promotion_code] + promotion_code_id = params[:promotion_code] + + promotion_code = promotion_codes[promotion_code_id] + + if promotion_code + coupon_id = promotion_code[:coupon][:id] + coupon = coupons[coupon_id] + if coupon + add_coupon_to_object(subscription, coupon) + else + raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400) + end + else + raise Stripe::InvalidRequestError.new("No such promotion code: #{promotion_code_id}", 'promotion_code', http_status: 400) + end + end + subscriptions[subscription[:id]] = subscription add_subscription_to_customer(customer, subscription) @@ -97,7 +115,7 @@ def create_subscription(route, method_url, params, headers) customer[:default_source] = new_card[:id] end - 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 enable_incomplete_payments cancel_at_period_end default_tax_rates payment_behavior pending_invoice_item_interval default_payment_method collection_method off_session trial_from_plan proration_behavior backdate_start_date transfer_data expand automatic_tax) + 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 enable_incomplete_payments cancel_at_period_end default_tax_rates payment_behavior pending_invoice_item_interval default_payment_method collection_method off_session trial_from_plan proration_behavior backdate_start_date transfer_data expand automatic_tax promotion_code) unknown_params = params.keys - allowed_params.map(&:to_sym) if unknown_params.length > 0 raise Stripe::InvalidRequestError.new("Received unknown parameter: #{unknown_params.join}", unknown_params.first.to_s, http_status: 400) @@ -113,6 +131,10 @@ def create_subscription(route, method_url, params, headers) # Note: needs updating for subscriptions with multiple plans verify_card_present(customer, subscription_plans.first, subscription, params) + if params[:coupon] && params[:promotion_code] + raise Stripe::InvalidRequestError.new("You may only specify one of these parameters: coupon, promotion_code", "coupon", http_status: 400) + end + if params[:coupon] coupon_id = params[:coupon] @@ -128,6 +150,24 @@ def create_subscription(route, method_url, params, headers) end end + if params[:promotion_code] + promotion_code_id = params[:promotion_code] + + promotion_code = promotion_codes[promotion_code_id] + + if promotion_code + coupon_id = promotion_code[:coupon][:id] + coupon = coupons[coupon_id] + if coupon + add_coupon_to_object(subscription, coupon) + else + raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400) + end + else + raise Stripe::InvalidRequestError.new("No such promotion code: #{promotion_code_id}", 'promotion_code', http_status: 400) + end + end + if params[:trial_period_days] subscription[:status] = 'trialing' end @@ -236,6 +276,34 @@ def update_subscription(route, method_url, params, headers) end end + if params[:promotion_code] + promotion_code_id = params[:promotion_code] + + promotion_code = promotion_codes[promotion_code_id] + + if promotion_code + # You can't apply a promotion code with amount restrictions on the Customer object or on a subscription + # update API call + if promotion_code[:restrictions][:minimum_amount] + raise Stripe::InvalidRequestError.new( + "This promotion code cannot be redeemed on a subcription update because it uses the `minimum_amount` restriction.", + "promotion_code", + http_status: 400 + ) + end + + coupon_id = promotion_code[:coupon][:id] + coupon = coupons[coupon_id] + if coupon + add_coupon_to_object(subscription, coupon) + else + raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400) + end + else + raise Stripe::InvalidRequestError.new("No such promotion code: #{promotion_code_id}", 'promotion_code', http_status: 400) + end + end + if params[:trial_period_days] subscription[:status] = 'trialing' end diff --git a/spec/shared_stripe_examples/subscription_examples.rb b/spec/shared_stripe_examples/subscription_examples.rb index d9a934e17..fec2b2f0c 100644 --- a/spec/shared_stripe_examples/subscription_examples.rb +++ b/spec/shared_stripe_examples/subscription_examples.rb @@ -195,6 +195,28 @@ } end + it "allows promotion code" do + customer = Stripe::Customer.create(source: gen_card_tk) + coupon = stripe_helper.create_coupon + promotion_code = Stripe::PromotionCode.create(coupon: coupon) + + subscription = Stripe::Subscription.create(plan: plan.id, customer: customer.id, promotion_code: promotion_code.id) + + expect(subscription.discount.coupon).to eq(coupon) + end + + it "does not permit both coupon and promotion code" do + customer = Stripe::Customer.create(source: gen_card_tk) + + expect { + Stripe::Subscription.create(plan: plan.id, customer: customer.id, coupon: "test", promotion_code: "test") + }.to raise_error { |e| + expect(e).to be_a Stripe::InvalidRequestError + expect(e.http_status).to eq(400) + expect(e.message).to eq("You may only specify one of these parameters: coupon, promotion_code") + } + end + it "correctly sets quantity, application_fee_percent and tax_percent" do customer = Stripe::Customer.create(id: 'test_customer_sub', source: gen_card_tk) @@ -947,6 +969,23 @@ expect(subscription.discount).to be_nil end + it "throws an error when promotion code has an amount restriction" do + coupon = stripe_helper.create_coupon + promotion_code = Stripe::PromotionCode.create( + coupon: coupon, restrictions: {minimum_amount: 100, minimum_amount_currency: "USD"} + ) + customer = Stripe::Customer.create(source: gen_card_tk, plan: plan.id) + subscription = Stripe::Subscription.retrieve(customer.subscriptions.data.first.id) + + subscription.promotion_code = promotion_code.id + + expect { subscription.save }.to raise_error { |e| + expect(e).to be_a Stripe::InvalidRequestError + expect(e.http_status).to eq(400) + expect(e.message).to_not be_nil + } + end + it "throws an error when plan does not exist" do customer = Stripe::Customer.create(id: 'cardless', plan: free_plan.id) From 598e4f1163f0a8adc902d466c1deb172e0758005 Mon Sep 17 00:00:00 2001 From: Jake Bills Date: Tue, 28 Feb 2023 16:22:14 -0700 Subject: [PATCH 13/28] adds check for refund amount and test demonstrating behavior --- .../request_handlers/helpers/charge_helpers.rb | 6 ++++++ spec/shared_stripe_examples/refund_examples.rb | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/stripe_mock/request_handlers/helpers/charge_helpers.rb b/lib/stripe_mock/request_handlers/helpers/charge_helpers.rb index 63270b0f1..81ecb13f3 100644 --- a/lib/stripe_mock/request_handlers/helpers/charge_helpers.rb +++ b/lib/stripe_mock/request_handlers/helpers/charge_helpers.rb @@ -3,6 +3,12 @@ module RequestHandlers module Helpers def add_refund_to_charge(refund, charge) + if refund[:amount] + charge[:amount_refunded] > charge[:amount] + raise Stripe::InvalidRequestError.new( + "Charge #{charge[:id]} has already been refunded.", + 'amount' + ) + end refunds = charge[:refunds] refunds[:data] << refund refunds[:total_count] = refunds[:data].count diff --git a/spec/shared_stripe_examples/refund_examples.rb b/spec/shared_stripe_examples/refund_examples.rb index bf0645357..c83e1ee63 100644 --- a/spec/shared_stripe_examples/refund_examples.rb +++ b/spec/shared_stripe_examples/refund_examples.rb @@ -338,6 +338,17 @@ expect(half.data.first.id).to eq(all_refunds.data.at(2).id) end + it "returns an InvalidRequestError when attempting to refund more than the original charge amount" do + charge = Stripe::Charge.create( + amount: 1000, + currency: 'usd', + source: stripe_helper.generate_card_token + ) + expect { + Stripe::Refund.create(charge: charge.id, amount: 2000) + }.to raise_error(Stripe::InvalidRequestError) + end + describe "idempotency" do let(:customer) { Stripe::Customer.create(email: 'johnny@appleseed.com') } let(:charge) do From 876a656fc66bd7c097a061fbd5716fd4b0006b3d Mon Sep 17 00:00:00 2001 From: Greg Ciampa Date: Sun, 5 Mar 2023 18:57:37 -0500 Subject: [PATCH 14/28] Promotion code is not available on subscription --- .../request_handlers/subscriptions.rb | 28 ++----------------- .../subscription_examples.rb | 6 ++-- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/lib/stripe_mock/request_handlers/subscriptions.rb b/lib/stripe_mock/request_handlers/subscriptions.rb index cc316c805..a13c1ec24 100644 --- a/lib/stripe_mock/request_handlers/subscriptions.rb +++ b/lib/stripe_mock/request_handlers/subscriptions.rb @@ -74,15 +74,7 @@ def create_customer_subscription(route, method_url, params, headers) promotion_code = promotion_codes[promotion_code_id] - if promotion_code - coupon_id = promotion_code[:coupon][:id] - coupon = coupons[coupon_id] - if coupon - add_coupon_to_object(subscription, coupon) - else - raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400) - end - else + unless promotion_code raise Stripe::InvalidRequestError.new("No such promotion code: #{promotion_code_id}", 'promotion_code', http_status: 400) end end @@ -155,15 +147,7 @@ def create_subscription(route, method_url, params, headers) promotion_code = promotion_codes[promotion_code_id] - if promotion_code - coupon_id = promotion_code[:coupon][:id] - coupon = coupons[coupon_id] - if coupon - add_coupon_to_object(subscription, coupon) - else - raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400) - end - else + unless promotion_code raise Stripe::InvalidRequestError.new("No such promotion code: #{promotion_code_id}", 'promotion_code', http_status: 400) end end @@ -291,14 +275,6 @@ def update_subscription(route, method_url, params, headers) http_status: 400 ) end - - coupon_id = promotion_code[:coupon][:id] - coupon = coupons[coupon_id] - if coupon - add_coupon_to_object(subscription, coupon) - else - raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400) - end else raise Stripe::InvalidRequestError.new("No such promotion code: #{promotion_code_id}", 'promotion_code', http_status: 400) end diff --git a/spec/shared_stripe_examples/subscription_examples.rb b/spec/shared_stripe_examples/subscription_examples.rb index fec2b2f0c..805f2322e 100644 --- a/spec/shared_stripe_examples/subscription_examples.rb +++ b/spec/shared_stripe_examples/subscription_examples.rb @@ -200,9 +200,9 @@ coupon = stripe_helper.create_coupon promotion_code = Stripe::PromotionCode.create(coupon: coupon) - subscription = Stripe::Subscription.create(plan: plan.id, customer: customer.id, promotion_code: promotion_code.id) - - expect(subscription.discount.coupon).to eq(coupon) + expect { + Stripe::Subscription.create(plan: plan.id, customer: customer.id, promotion_code: promotion_code.id) + }.not_to raise_error(Stripe::InvalidRequestError) end it "does not permit both coupon and promotion code" do From 8151b32247c54c51d232db68063c5cb0d9698966 Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Sat, 11 Mar 2023 13:53:56 -0800 Subject: [PATCH 15/28] use progress formatting --- .rspec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.rspec b/.rspec index 660778bdc..89da67b60 100644 --- a/.rspec +++ b/.rspec @@ -1 +1,2 @@ ---colour --format documentation +--colour +--format progress From 3cc801722d679fa00da89e8a0e9d4230bb6cad58 Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Sat, 11 Mar 2023 13:54:03 -0800 Subject: [PATCH 16/28] fix the test suite --- spec/instance_spec.rb | 4 +++- spec/readme_spec.rb | 2 +- spec/shared_stripe_examples/webhook_event_examples.rb | 11 ++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/spec/instance_spec.rb b/spec/instance_spec.rb index 9455cfadc..d7bed5b0e 100644 --- a/spec/instance_spec.rb +++ b/spec/instance_spec.rb @@ -6,7 +6,9 @@ let(:stripe_helper) { StripeMock.create_test_helper } it_behaves_like_stripe do - def test_data_source(type); StripeMock.instance.send(type); end + def test_data_source(type) + StripeMock.instance.send(type) + end end before { StripeMock.start } diff --git a/spec/readme_spec.rb b/spec/readme_spec.rb index 0bf5524b7..6985ed1ca 100644 --- a/spec/readme_spec.rb +++ b/spec/readme_spec.rb @@ -47,7 +47,7 @@ customer_object = event.data.object expect(customer_object.id).to_not be_nil - expect(customer_object.default_card).to_not be_nil + expect(customer_object.default_source).to_not be_nil # etc. end diff --git a/spec/shared_stripe_examples/webhook_event_examples.rb b/spec/shared_stripe_examples/webhook_event_examples.rb index 07a416063..d2d8c1a52 100644 --- a/spec/shared_stripe_examples/webhook_event_examples.rb +++ b/spec/shared_stripe_examples/webhook_event_examples.rb @@ -257,7 +257,7 @@ expect(subscription_created_event).to be_a(Stripe::Event) expect(subscription_created_event.id).to_not be_nil expect(subscription_created_event.data.object.items.data.class).to be Array - expect(subscription_created_event.data.object.items.data.length).to be 2 + expect(subscription_created_event.data.object.items.data.length).to be 1 expect(subscription_created_event.data.object.items.data.first).to respond_to(:plan) expect(subscription_created_event.data.object.items.data.first.id).to eq('si_00000000000000') end @@ -267,7 +267,7 @@ expect(subscription_deleted_event).to be_a(Stripe::Event) expect(subscription_deleted_event.id).to_not be_nil expect(subscription_deleted_event.data.object.items.data.class).to be Array - expect(subscription_deleted_event.data.object.items.data.length).to be 2 + expect(subscription_deleted_event.data.object.items.data.length).to be 1 expect(subscription_deleted_event.data.object.items.data.first).to respond_to(:plan) expect(subscription_deleted_event.data.object.items.data.first.id).to eq('si_00000000000000') end @@ -277,7 +277,7 @@ expect(subscription_updated_event).to be_a(Stripe::Event) expect(subscription_updated_event.id).to_not be_nil expect(subscription_updated_event.data.object.items.data.class).to be Array - expect(subscription_updated_event.data.object.items.data.length).to be 2 + expect(subscription_updated_event.data.object.items.data.length).to be 1 expect(subscription_updated_event.data.object.items.data.first).to respond_to(:plan) expect(subscription_updated_event.data.object.items.data.first.id).to eq('si_00000000000000') end @@ -298,10 +298,11 @@ invoice_payment_succeeded = StripeMock.mock_webhook_event('invoice.payment_succeeded') expect(invoice_payment_succeeded).to be_a(Stripe::Event) expect(invoice_payment_succeeded.id).to_not be_nil + puts "invoice_payment_succeeded.data.object.lines: #{invoice_payment_succeeded.data.object.lines}" expect(invoice_payment_succeeded.data.object.lines.data.class).to be Array - expect(invoice_payment_succeeded.data.object.lines.data.length).to be 2 + expect(invoice_payment_succeeded.data.object.lines.data.length).to be 1 expect(invoice_payment_succeeded.data.object.lines.data.first).to respond_to(:plan) - expect(invoice_payment_succeeded.data.object.lines.data.first.id).to eq('sub_00000000000000') + expect(invoice_payment_succeeded.data.object.lines.data.first.id).to eq('il_000000000000000000000000') end end end From 404df10da6c84485db4b79858f5e34980c42cf51 Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Sat, 11 Mar 2023 13:55:46 -0800 Subject: [PATCH 17/28] add note (maybe this belongs in the wiki?) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2f7fbe654..2630561f7 100644 --- a/README.md +++ b/README.md @@ -406,6 +406,8 @@ Patches are welcome and greatly appreciated! If you're contributing to fix a pro be sure to write tests that illustrate the problem being fixed. This will help ensure that the problem remains fixed in future updates. +Note: You may need to `ulimit -n 4048` before running the test suite to get all tests to pass. + ## Copyright Copyright (c) 2013 Gilbert From ca7b2346f2437bb75a11e91d2319ae188d8ee25d Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Sat, 11 Mar 2023 16:35:44 -0800 Subject: [PATCH 18/28] remove puts --- spec/shared_stripe_examples/webhook_event_examples.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/shared_stripe_examples/webhook_event_examples.rb b/spec/shared_stripe_examples/webhook_event_examples.rb index d2d8c1a52..a3dc12f8b 100644 --- a/spec/shared_stripe_examples/webhook_event_examples.rb +++ b/spec/shared_stripe_examples/webhook_event_examples.rb @@ -298,7 +298,6 @@ invoice_payment_succeeded = StripeMock.mock_webhook_event('invoice.payment_succeeded') expect(invoice_payment_succeeded).to be_a(Stripe::Event) expect(invoice_payment_succeeded.id).to_not be_nil - puts "invoice_payment_succeeded.data.object.lines: #{invoice_payment_succeeded.data.object.lines}" expect(invoice_payment_succeeded.data.object.lines.data.class).to be Array expect(invoice_payment_succeeded.data.object.lines.data.length).to be 1 expect(invoice_payment_succeeded.data.object.lines.data.first).to respond_to(:plan) From 23c7f109ce86e787b3a701c8249117251da14066 Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Sat, 11 Mar 2023 16:34:24 -0800 Subject: [PATCH 19/28] implement webhook: "payment_method.detached" --- lib/stripe_mock/api/webhooks.rb | 1 + .../payment_method.detached.json | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 lib/stripe_mock/webhook_fixtures/payment_method.detached.json diff --git a/lib/stripe_mock/api/webhooks.rb b/lib/stripe_mock/api/webhooks.rb index eaceacc1c..974cb4ab4 100644 --- a/lib/stripe_mock/api/webhooks.rb +++ b/lib/stripe_mock/api/webhooks.rb @@ -94,6 +94,7 @@ def self.event_list 'payment_link.created', 'payment_link.updated', 'payment_method.attached', + 'payment_method.detached', 'payout.created', 'payout.paid', 'payout.updated', diff --git a/lib/stripe_mock/webhook_fixtures/payment_method.detached.json b/lib/stripe_mock/webhook_fixtures/payment_method.detached.json new file mode 100644 index 000000000..d371091d4 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/payment_method.detached.json @@ -0,0 +1,62 @@ +{ + "id": "evt_000000000000000000000000", + "object": "event", + "api_version": "2020-08-27", + "created": 1648320107, + "data": { + "object": { + "id" : "pm_000000000000000000000000", + "object" : "payment_method", + "created" : 1678559798, + "billing_details" : { + "address" : { + "city" : null, + "country" : "US", + "line1" : null, + "line2" : null, + "postal_code" : "42424", + "state" : null + }, + "email" : null, + "name" : null, + "phone" : null + }, + "card" : { + "brand" : "visa", + "checks" : { + "address_line1_check" : null, + "address_postal_code_check" : "pass", + "cvc_check" : "pass" + }, + "country" : "US", + "exp_month" : 4, + "exp_year" : 2024, + "fingerprint" : "ZoVSX2dK5igWt2SB", + "funding" : "credit", + "generated_from" : null, + "last4" : "4242", + "networks" : { + "available" : [ + "visa" + ], + "preferred" : null + }, + "three_d_secure_usage" : { + "supported" : true + }, + "wallet" : null + }, + "customer" : null, + "livemode" : false, + "metadata" : {}, + "type" : "card" + } + }, + "livemode": false, + "pending_webhooks": 2, + "request": { + "id": "req_00000000000000", + "idempotency_key": "381c7773-97ac-4c18-8fbe-e8bff5e6bbad" + }, + "type": "payment_method.detached" +} From 69d23032195ff85e3451cee3d60eb41e80c8802c Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Mon, 10 Apr 2023 15:30:59 -0700 Subject: [PATCH 20/28] change invoice status to paid when paid = true --- lib/stripe_mock/request_handlers/invoices.rb | 7 ++++++- spec/shared_stripe_examples/invoice_examples.rb | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/stripe_mock/request_handlers/invoices.rb b/lib/stripe_mock/request_handlers/invoices.rb index 653096633..7d95038d4 100644 --- a/lib/stripe_mock/request_handlers/invoices.rb +++ b/lib/stripe_mock/request_handlers/invoices.rb @@ -53,7 +53,12 @@ def pay_invoice(route, method_url, params, headers) route =~ method_url assert_existence :invoice, $1, invoices[$1] charge = invoice_charge(invoices[$1]) - invoices[$1].merge!(:paid => true, :attempted => true, :charge => charge[:id]) + invoices[$1].merge!( + :paid => true, + :status => "paid", + :attempted => true, + :charge => charge[:id], + ) end def upcoming_invoice(route, method_url, params, headers = {}) diff --git a/spec/shared_stripe_examples/invoice_examples.rb b/spec/shared_stripe_examples/invoice_examples.rb index 244ae20e8..d1e385d15 100644 --- a/spec/shared_stripe_examples/invoice_examples.rb +++ b/spec/shared_stripe_examples/invoice_examples.rb @@ -83,12 +83,28 @@ @invoice = @invoice.pay expect(@invoice.attempted).to eq(true) expect(@invoice.paid).to eq(true) + expect(@invoice.status).to eq("paid") end it 'creates a new charge object' do expect{ @invoice.pay }.to change { Stripe::Charge.list.data.count }.by 1 end + it 'should work with Stripe::Invoice.pay(invoice_id)' do + expect(@invoice.paid).to_not eq(true) + + expect { + Stripe::Invoice.pay(@invoice.id) + }.to change { Stripe::Charge.list.data.count }.by 1 + + @invoice = Stripe::Invoice.retrieve(id: @invoice.id) + expect(@invoice).to_not be_nil + + expect(@invoice.attempted).to eq(true) + expect(@invoice.paid).to eq(true) + expect(@invoice.status).to eq("paid") + end + it 'sets the charge attribute' do @invoice = @invoice.pay expect(@invoice.charge).to be_a String From 763d028409ec52b775e26e494dff605941ac0a94 Mon Sep 17 00:00:00 2001 From: Tim Broder Date: Thu, 27 Jul 2023 08:42:24 -0400 Subject: [PATCH 21/28] add payment_settings trial_settings to subscription allowed_params Added additional allowed_params to create_subscription for better testing: payment_settings trial_settings --- lib/stripe_mock/request_handlers/subscriptions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stripe_mock/request_handlers/subscriptions.rb b/lib/stripe_mock/request_handlers/subscriptions.rb index d4c1ffef3..2373b6a3b 100644 --- a/lib/stripe_mock/request_handlers/subscriptions.rb +++ b/lib/stripe_mock/request_handlers/subscriptions.rb @@ -97,7 +97,7 @@ def create_subscription(route, method_url, params, headers) customer[:default_source] = new_card[:id] end - allowed_params = %w(id 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 enable_incomplete_payments cancel_at_period_end default_tax_rates payment_behavior pending_invoice_item_interval default_payment_method collection_method off_session trial_from_plan proration_behavior backdate_start_date transfer_data expand automatic_tax) + allowed_params = %w(id 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 enable_incomplete_payments cancel_at_period_end default_tax_rates payment_behavior pending_invoice_item_interval default_payment_method collection_method off_session trial_from_plan proration_behavior backdate_start_date transfer_data expand automatic_tax payment_settings trial_settings) unknown_params = params.keys - allowed_params.map(&:to_sym) if unknown_params.length > 0 raise Stripe::InvalidRequestError.new("Received unknown parameter: #{unknown_params.join}", unknown_params.first.to_s, http_status: 400) From 2337a11abbf35709b363b02b0c2643d81406e558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Roll=C3=A9n?= Date: Tue, 10 Oct 2023 17:44:22 -0400 Subject: [PATCH 22/28] Fix typo in account.updated webhook fixture The `account.updated` webhook fixture contains a field named `charge_enabled`, but the actual field sent by stripe is named `charges_enabled`. See the [account object](https://stripe.com/docs/api/accounts/object) in the API docs --- lib/stripe_mock/webhook_fixtures/account.updated.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stripe_mock/webhook_fixtures/account.updated.json b/lib/stripe_mock/webhook_fixtures/account.updated.json index 255b8e30b..20c8eb208 100644 --- a/lib/stripe_mock/webhook_fixtures/account.updated.json +++ b/lib/stripe_mock/webhook_fixtures/account.updated.json @@ -10,7 +10,7 @@ "email": "test@stripe.com", "statement_descriptor": "TEST", "details_submitted": true, - "charge_enabled": false, + "charges_enabled": false, "payouts_enabled": false, "currencies_supported": [ "USD" From 49812a38ee8dd0b0cff10d0ca1d8511fdf884d8e Mon Sep 17 00:00:00 2001 From: Alexander Mamonchik Date: Sun, 15 Oct 2023 00:42:14 +0600 Subject: [PATCH 23/28] Init Github Actions and remove Travis (#886) init Github Actions flow * Added test status badge to README * use 2.6 as a minimum ruby version in Actions * remove Travis config * Fixed no implicit conversion of Range into Integer for ruby 2.6 * increased minimum ruby Version from 2.4 to 2.6 --- .github/workflows/rspec_tests.yml | 38 +++++++++++++++++++++++++++++++ .travis.yml | 25 -------------------- README.md | 4 ++-- lib/stripe_mock/data.rb | 5 ++-- 4 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/rspec_tests.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/rspec_tests.yml b/.github/workflows/rspec_tests.yml new file mode 100644 index 000000000..52428cc3b --- /dev/null +++ b/.github/workflows/rspec_tests.yml @@ -0,0 +1,38 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake +# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby + +name: Tests + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + test: + + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['2.6', '2.7', '3.0'] + + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, + # change this to (see https://github.com/ruby/setup-ruby#versioning): + # uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run tests + run: bundle exec rspec diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e209a0c64..000000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -sudo: required -language: ruby -rvm: - - 2.4.6 - - 2.5.5 - - 2.6.3 - - 2.7.0 -before_install: - - gem install bundler -v '< 2' -before_script: - - "sudo touch /var/log/stripe-mock-server.log" - - "sudo chown travis /var/log/stripe-mock-server.log" -script: "bundle exec rspec && bundle exec rspec -t live" - -env: - global: - - IS_TRAVIS=true STRIPE_TEST_SECRET_KEY_A=sk_test_BsztzqQjzd7lqkgo1LjEG5DF00KzH7tWKF STRIPE_TEST_SECRET_KEY_B=sk_test_rKCEu0x8jzg6cKPqoey8kUPQ00usQO3KYE STRIPE_TEST_SECRET_KEY_C=sk_test_qeaB7R6Ywp8sC9pzd1ZIABH700YLC7nhmZ STRIPE_TEST_SECRET_KEY_D=sk_test_r1NwHkUW7UyoozyP4aEBD6cs00CI5uDiGq - -notifications: - webhooks: - urls: - - https://webhooks.gitter.im/e/44a1f4718ae2efb67eac - on_success: change # options: [always|never|change] default: always - on_failure: always # options: [always|never|change] default: always - on_start: false # default: false diff --git a/README.md b/README.md index 2630561f7..47af2d718 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# stripe-ruby-mock [![Build Status](https://travis-ci.org/stripe-ruby-mock/stripe-ruby-mock.png?branch=master)](https://travis-ci.org/stripe-ruby-mock/stripe-ruby-mock) [![Gitter chat](https://badges.gitter.im/rebelidealist/stripe-ruby-mock.png)](https://gitter.im/rebelidealist/stripe-ruby-mock) +# stripe-ruby-mock [![Tests](https://github.com/stripe-ruby-mock/stripe-ruby-mock/actions/workflows/rspec_tests.yml/badge.svg)](https://github.com/stripe-ruby-mock/stripe-ruby-mock/actions/workflows/rspec_tests.yml) * Homepage: https://github.com/stripe-ruby-mock/stripe-ruby-mock * Issues: https://github.com/stripe-ruby-mock/stripe-ruby-mock/issues @@ -29,7 +29,7 @@ version `3.0.0` has [breaking changes](https://github.com/stripe-ruby-mock/strip ### Requirements -* ruby >= 2.4.0 +* ruby >= 2.6.0 * stripe >= 5.0.0 ### Specifications diff --git a/lib/stripe_mock/data.rb b/lib/stripe_mock/data.rb index 5f958b7c3..0fd6ee6bc 100644 --- a/lib/stripe_mock/data.rb +++ b/lib/stripe_mock/data.rb @@ -1259,9 +1259,10 @@ def self.mock_payment_method(params = {}) payment_method_id = params[:id] || 'pm_1ExEuFL2DI6wht39WNJgbybl' type = params[:type].to_sym + last4 = params.dig(:card, :number) data = { card: { - brand: case params.dig(:card, :number)&.to_s + brand: case last4&.to_s when /^4/, nil 'visa' when /^5[1-5]/ @@ -1280,7 +1281,7 @@ def self.mock_payment_method(params = {}) fingerprint: 'Hr3Ly5z5IYxsokWA', funding: 'credit', generated_from: nil, - last4: params.dig(:card, :number)&.[](-4..) || '3155', + last4: last4.nil? ? '3155' : last4.to_s[-4..], three_d_secure_usage: { supported: true }, wallet: nil }, From b6c6e0c0bc37b4f430c1862709aa0abe7f8fabf6 Mon Sep 17 00:00:00 2001 From: Alexander Mamonchik Date: Tue, 24 Oct 2023 20:57:56 +0600 Subject: [PATCH 24/28] Revert "Adds error when attempting to refund a charge that has already been refunded / refunding more than the charge amount" --- .../request_handlers/helpers/charge_helpers.rb | 6 ------ spec/shared_stripe_examples/refund_examples.rb | 11 ----------- 2 files changed, 17 deletions(-) diff --git a/lib/stripe_mock/request_handlers/helpers/charge_helpers.rb b/lib/stripe_mock/request_handlers/helpers/charge_helpers.rb index 81ecb13f3..63270b0f1 100644 --- a/lib/stripe_mock/request_handlers/helpers/charge_helpers.rb +++ b/lib/stripe_mock/request_handlers/helpers/charge_helpers.rb @@ -3,12 +3,6 @@ module RequestHandlers module Helpers def add_refund_to_charge(refund, charge) - if refund[:amount] + charge[:amount_refunded] > charge[:amount] - raise Stripe::InvalidRequestError.new( - "Charge #{charge[:id]} has already been refunded.", - 'amount' - ) - end refunds = charge[:refunds] refunds[:data] << refund refunds[:total_count] = refunds[:data].count diff --git a/spec/shared_stripe_examples/refund_examples.rb b/spec/shared_stripe_examples/refund_examples.rb index c83e1ee63..bf0645357 100644 --- a/spec/shared_stripe_examples/refund_examples.rb +++ b/spec/shared_stripe_examples/refund_examples.rb @@ -338,17 +338,6 @@ expect(half.data.first.id).to eq(all_refunds.data.at(2).id) end - it "returns an InvalidRequestError when attempting to refund more than the original charge amount" do - charge = Stripe::Charge.create( - amount: 1000, - currency: 'usd', - source: stripe_helper.generate_card_token - ) - expect { - Stripe::Refund.create(charge: charge.id, amount: 2000) - }.to raise_error(Stripe::InvalidRequestError) - end - describe "idempotency" do let(:customer) { Stripe::Customer.create(email: 'johnny@appleseed.com') } let(:charge) do From 5a5d85490626c1173b5872c7871231ad1ae74f2f Mon Sep 17 00:00:00 2001 From: Leslie Poolman Date: Thu, 23 Nov 2023 10:23:55 -0500 Subject: [PATCH 25/28] Added webhooks for tax_rate.updated and tax_rate.created --- lib/stripe_mock/api/webhooks.rb | 2 + .../webhook_fixtures/tax_rate.created.json | 32 ++++++++++++++++ .../webhook_fixtures/tax_rate.updated.json | 37 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 lib/stripe_mock/webhook_fixtures/tax_rate.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/tax_rate.updated.json diff --git a/lib/stripe_mock/api/webhooks.rb b/lib/stripe_mock/api/webhooks.rb index 974cb4ab4..031076a3a 100644 --- a/lib/stripe_mock/api/webhooks.rb +++ b/lib/stripe_mock/api/webhooks.rb @@ -119,6 +119,8 @@ def self.event_list 'subscription_schedule.created', 'subscription_schedule.released', 'subscription_schedule.updated', + 'tax_rate.created', + 'tax_rate.updated', 'transfer.created', 'transfer.failed', 'transfer.paid', diff --git a/lib/stripe_mock/webhook_fixtures/tax_rate.created.json b/lib/stripe_mock/webhook_fixtures/tax_rate.created.json new file mode 100644 index 000000000..29c600f7b --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/tax_rate.created.json @@ -0,0 +1,32 @@ +{ + "id": "evt_000000000000000000000000", + "object": "event", + "api_version": "2020-08-27", + "created": 1700752531, + "data": { + "object": { + "id": "txr_00000000000000", + "object": "tax_rate", + "active": true, + "country": "DE", + "created": 1700750289, + "description": "VAT Germany", + "display_name": "VAT", + "effective_percentage": null, + "inclusive": false, + "jurisdiction": "DE", + "livemode": false, + "metadata": {}, + "percentage": 16.0, + "state": null, + "tax_type": "vat" + } + }, + "livemode": false, + "pending_webhooks": 2, + "request": { + "id": "req_00000000000000", + "idempotency_key": "cd3e4fc0-9d4c-42fd-a818-1b9789537ce9" + }, + "type": "tax_rate.created" +} \ No newline at end of file diff --git a/lib/stripe_mock/webhook_fixtures/tax_rate.updated.json b/lib/stripe_mock/webhook_fixtures/tax_rate.updated.json new file mode 100644 index 000000000..e90fba2d8 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/tax_rate.updated.json @@ -0,0 +1,37 @@ +{ + "id": "evt_000000000000000000000000", + "object": "event", + "api_version": "2020-08-27", + "created": 1700752371, + "data": { + "object": { + "id": "txr_00000000000000", + "object": "tax_rate", + "active": true, + "country": "DE", + "created": 1700750289, + "description": "VAT Germany", + "display_name": "VAT", + "effective_percentage": null, + "inclusive": false, + "jurisdiction": "DE", + "livemode": false, + "metadata": {}, + "percentage": 16.0, + "state": null, + "tax_type": "vat" + }, + "previous_attributes": { + "metadata": { + "foo": null + } + } + }, + "livemode": false, + "pending_webhooks": 2, + "request": { + "id": "req_00000000000000", + "idempotency_key": "7eb234a6-64bc-4320-bc7f-780c546ab026" + }, + "type": "tax_rate.updated" +} \ No newline at end of file From c1b537eb7ab7e1f6440b0639b4953dbcefbbf680 Mon Sep 17 00:00:00 2001 From: Alex Mamonchik Date: Wed, 3 Jan 2024 01:46:23 +0600 Subject: [PATCH 26/28] 3.1.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ README.md | 2 +- lib/stripe_mock/version.rb | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f76a14f3c..df369325e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ ### Unreleased +- [#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) +- [#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) +- [#687](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/687) Add PaymentIntent Webhooks by [@klaustopher](https://github.com/klaustopher) +- [#708](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/708) Implement Stripe::Checkout::Session.retrieve by [@coorasse](https://github.com/coorasse) +- [#711](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/711) Adding account link mock by [@amenon](https://github.com/amenon) +- [#715](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/715) Added application_fee_amount to mock charge object by [@espen](https://github.com/espen) +- [#694](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/694) Introduce ideal and sepa_debit types for PaymentMethod by [@mnin](https://github.com/mnin) +- [#720](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/720) Add additional parameters by [@rpietraszko](https://github.com/rpietraszko) +- [#695](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/695) Add `has_more` attribute to all ListObject instances by [@gbp](https://github.com/gbp) +- [#727](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/727) Fix List initialize error with deleted records by [@jmulieri](https://github.com/jmulieri) +- [#739](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/739) Add data to account links by [@dudyn5ky1](https://github.com/dudyn5ky1) +- [#756](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/756) Added #715 PR to changelog by [@espen](https://github.com/espen) +- [#759](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/759) Adding express dashboard login link mock by [@rohitbegani](https://github.com/rohitbegani) +- [#747](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/747) Fix ruby 2.7 deprecation warnings by [@coding-chimp](https://github.com/coding-chimp) +- [#762](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/762) Support Stripe Connect by adding stripe_account header namespace for customers by [@csalvato](https://github.com/csalvato) +- [#758](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/758) Create price by [@jamesprior](https://github.com/jamesprior) +- [#730](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/730) support on price api by [@hidenba](https://github.com/hidenba) +- [#764](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/764) Fixes erroneous error message when fetching upcoming invoices. by [@csalvato](https://github.com/csalvato) +- [#765](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/765) Properly set the status of a trialing subscription by [@csalvato](https://github.com/csalvato) +- [#755](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/755) Add allowed params to subscriptions by [@dominikdarnel](https://github.com/dominikdarnel) +- [#709](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/709) Remove unnecessary check on customer's currency by [@coorasse](https://github.com/coorasse) + - [#806](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/806) - Remove `payment_method_types` from required arguments for `Stripe::Checkout::Session` - [#806](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/806) - Raise more helpful exception when Stripe::Price cannot be found within a `Stripe::Checkout::Session` `line_items` argument. diff --git a/README.md b/README.md index 47af2d718..ac05f79b3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This gem has unexpectedly grown in popularity and I've gotten pretty busy, so I' In your gemfile: - gem 'stripe-ruby-mock', '~> 3.0.1', :require => 'stripe_mock' + gem 'stripe-ruby-mock', '~> 3.1.0', :require => 'stripe_mock' ## !!! Important diff --git a/lib/stripe_mock/version.rb b/lib/stripe_mock/version.rb index a1fd0ea92..d824f4dfd 100644 --- a/lib/stripe_mock/version.rb +++ b/lib/stripe_mock/version.rb @@ -1,4 +1,4 @@ module StripeMock # stripe-ruby-mock version - VERSION = "3.1.0.rc3" + VERSION = "3.1.0" end From 844108e2d3822bd694d24a9870b1915fc7579a99 Mon Sep 17 00:00:00 2001 From: Alex Mamonchik Date: Sat, 10 Feb 2024 15:03:08 +0600 Subject: [PATCH 27/28] Updated gems --- Gemfile | 5 ----- README.md | 2 +- stripe-ruby-mock.gemspec | 3 +-- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 8cc610a86..e04240a41 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,5 @@ source 'https://rubygems.org' -platforms :ruby_19 do - gem 'mime-types', '~> 2.6' - gem 'rest-client', '~> 1.8' -end - group :test do gem 'rake' gem 'dotenv' diff --git a/README.md b/README.md index ac05f79b3..c7b122adc 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ version `3.0.0` has [breaking changes](https://github.com/stripe-ruby-mock/strip ### Requirements -* ruby >= 2.6.0 +* ruby >= 2.7.0 * stripe >= 5.0.0 ### Specifications diff --git a/stripe-ruby-mock.gemspec b/stripe-ruby-mock.gemspec index e53963d8c..f8a28d6c4 100644 --- a/stripe-ruby-mock.gemspec +++ b/stripe-ruby-mock.gemspec @@ -26,7 +26,6 @@ Gem::Specification.new do |gem| gem.add_dependency 'multi_json', '~> 1.0' gem.add_dependency 'dante', '>= 0.2.0' - gem.add_development_dependency 'rspec', '~> 3.7.0' - gem.add_development_dependency 'rubygems-tasks', '~> 0.2' + gem.add_development_dependency 'rspec', '~> 3.13.0' gem.add_development_dependency 'thin', '~> 1.8.1' end From 8c4bd51638a633365129e052d045fd5a5aeadbb3 Mon Sep 17 00:00:00 2001 From: Alex Mamonchik Date: Sat, 10 Feb 2024 15:05:22 +0600 Subject: [PATCH 28/28] Added ruby 3.2 to Github Actions --- .github/workflows/rspec_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rspec_tests.yml b/.github/workflows/rspec_tests.yml index 52428cc3b..d54b2475b 100644 --- a/.github/workflows/rspec_tests.yml +++ b/.github/workflows/rspec_tests.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['2.6', '2.7', '3.0'] + ruby-version: ['2.7', '3.0', '3.2'] steps: - uses: actions/checkout@v3