diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f66eb4f..51a08f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: - ruby-version: 3.0 + ruby-version: 2.6 bundler-cache: true - name: Run style checks run: bundle exec rubocop \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 273085e..47376f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - shift4 (2.0.0) + shift4 (2.1.0) httparty (~> 0.20) GEM diff --git a/lib/shift4.rb b/lib/shift4.rb index 18c9e60..e9009c1 100644 --- a/lib/shift4.rb +++ b/lib/shift4.rb @@ -17,6 +17,7 @@ require 'shift4/fraud_warnings' require 'shift4/payment_methods' require 'shift4/plans' +require 'shift4/request_options' require 'shift4/subscriptions' require 'shift4/tokens' diff --git a/lib/shift4/communicator.rb b/lib/shift4/communicator.rb index d646a72..6ad1fc0 100644 --- a/lib/shift4/communicator.rb +++ b/lib/shift4/communicator.rb @@ -34,6 +34,9 @@ def self.request(json: nil, query: nil, body: nil, config: Configuration) "Accept" => "application/json", } headers["Shift4-Merchant"] = config.merchant unless config.merchant.nil? + if config.is_a?(RequestOptions) && !config.idempotency_key.nil? + headers["Idempotency-Key"] = config.idempotency_key + end if json raise ArgumentError("Cannot specify both body and json") if body diff --git a/lib/shift4/request_options.rb b/lib/shift4/request_options.rb new file mode 100644 index 0000000..4b94052 --- /dev/null +++ b/lib/shift4/request_options.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Shift4 + class RequestOptions < Configuration + class << self + attr_reader :idempotency_key + end + + def initialize( + config = Configuration, + idempotency_key: nil + ) + super( + secret_key: config.secret_key, + merchant: config.merchant, + api_url: config.api_url, + uploads_url: config.uploads_url + ) + @idempotency_key = idempotency_key + end + + attr_reader :idempotency_key + end +end diff --git a/lib/shift4/version.rb b/lib/shift4/version.rb index 5004a6c..8f9dc52 100644 --- a/lib/shift4/version.rb +++ b/lib/shift4/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Shift4 - VERSION = '2.0.0' + VERSION = '2.1.0' end diff --git a/spec/integration/blacklist_spec.rb b/spec/integration/blacklist_spec.rb index 2c3472a..0ac2156 100644 --- a/spec/integration/blacklist_spec.rb +++ b/spec/integration/blacklist_spec.rb @@ -16,6 +16,29 @@ expect(retrieved['email']).to eq(email) end + it 'create only one rule on blacklist with idempotency_key' do + # given + email = random_email + request = { ruleType: 'email', email: email, } + + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + created = Shift4::Blacklist.create( + request, + request_options + ) + + # when + not_created_because_idempotency = Shift4::Blacklist.create( + request, + request_options + ) + + # then + expect(created['id']).to eq(not_created_because_idempotency['id']) + expect(not_created_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'delete blacklist' do # given email = random_email diff --git a/spec/integration/cards_spec.rb b/spec/integration/cards_spec.rb index 26649ca..f69a47c 100644 --- a/spec/integration/cards_spec.rb +++ b/spec/integration/cards_spec.rb @@ -27,6 +27,39 @@ expect(retrieved['customerId']).to eq(customer_id) end + it 'create only one card with idempotency_key' do + # given + customer = Shift4::Customers.create(TestData.customer) + customer_id = customer['id'] + cardholder_name = random_string + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + created = Shift4::Cards.create(customer_id, + { + number: '4242424242424242', + expMonth: '12', + expYear: '2055', + cvc: '123', + cardholderName: cardholder_name + }, + request_options) + + # when + not_created_because_idempotency = Shift4::Cards.create(customer_id, + { + number: '4242424242424242', + expMonth: '12', + expYear: '2055', + cvc: '123', + cardholderName: cardholder_name + }, + request_options) + + # then + expect(created['id']).to eq(not_created_because_idempotency['id']) + expect(not_created_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'update card' do # given customer = Shift4::Customers.create(TestData.customer) @@ -56,6 +89,47 @@ expect(updated_card['addressLine2']).to eq('updated addressLine2') end + it 'update card only once with idempotency_key' do + # given + customer = Shift4::Customers.create(TestData.customer) + card = Shift4::Cards.create(customer['id'], TestData.card) + + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + Shift4::Cards.update(customer['id'], + card['id'], + { + expMonth: '05', + expYear: '55', + cardholderName: 'updated cardholderName', + addressCountry: 'updated addressCountry', + addressCity: 'updated addressCity', + addressState: 'updated addressState', + addressZip: 'updated addressZip', + addressLine1: 'updated addressLine1', + addressLine2: 'updated addressLine2' + }, + request_options) + not_updated_because_idempotency = Shift4::Cards.update(customer['id'], + card['id'], + { + expMonth: '05', + expYear: '55', + cardholderName: 'updated cardholderName', + addressCountry: 'updated addressCountry', + addressCity: 'updated addressCity', + addressState: 'updated addressState', + addressZip: 'updated addressZip', + addressLine1: 'updated addressLine1', + addressLine2: 'updated addressLine2' + }, + request_options) + + # then + expect(not_updated_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'delete card' do # given customer = Shift4::Customers.create(TestData.customer) diff --git a/spec/integration/charges_spec.rb b/spec/integration/charges_spec.rb index d564427..a6c24ef 100644 --- a/spec/integration/charges_spec.rb +++ b/spec/integration/charges_spec.rb @@ -39,13 +39,27 @@ expect(retrieved['card']['first4']).to eq(charge_req["card"]["first4"]) end + it 'create charge only once with idempotency_key' do + # given + charge_req = TestData.charge(card: TestData.card) + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + created = Shift4::Charges.create(charge_req, request_options) + not_created_because_idempotency = Shift4::Charges.create(charge_req, request_options) + + # then + expect(created['id']).to eq(not_created_because_idempotency['id']) + expect(not_created_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'update charge' do # given card = TestData.card charge_req = TestData.charge(card: card) + created = Shift4::Charges.create(charge_req) # when - created = Shift4::Charges.create(charge_req) updated = Shift4::Charges.update(created['id'], "description" => "updated description", "metadata" => { "key" => "updated value" }) @@ -62,6 +76,33 @@ expect(updated['card']['first4']).to eq(charge_req["card"]["first4"]) end + it 'update charge only once with idempotency_key' do + # given + card = TestData.card + charge_req = TestData.charge(card: card) + created = Shift4::Charges.create(charge_req) + + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + Shift4::Charges.update(created['id'], + { + "description" => "updated description", + "metadata" => { "key" => "updated value" } + }, + request_options) + + not_updated_because_idempotency = Shift4::Charges.update(created['id'], + { + "description" => "updated description", + "metadata" => { "key" => "updated value" } + }, + request_options) + + # then + expect(not_updated_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'capture charge' do # given charge_req = TestData.charge(card: TestData.card, captured: false) @@ -75,6 +116,22 @@ expect(captured['captured']).to eq(true) end + it 'capture charge only once with idempotency_key' do + # given + charge_req = TestData.charge(card: TestData.card, captured: false) + created = Shift4::Charges.create(charge_req) + + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + captured = Shift4::Charges.capture(created['id'], request_options) + not_captured_because_idempotency = Shift4::Charges.capture(created['id'], request_options) + + # then + expect(captured['id']).to eq(not_captured_because_idempotency['id']) + expect(not_captured_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'refund charge' do # given charge_req = TestData.charge(card: TestData.card, captured: false) @@ -88,6 +145,22 @@ expect(refunded['refunded']).to eq(true) end + it 'refund charge only once with idempotency_key' do + # given + charge_req = TestData.charge(card: TestData.card, captured: false) + created = Shift4::Charges.create(charge_req) + + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + refunded = Shift4::Charges.refund(created['id'], nil, request_options) + not_refunded_because_idempotency = Shift4::Charges.refund(created['id'], nil, request_options) + + # then + expect(refunded['id']).to eq(not_refunded_because_idempotency['id']) + expect(not_refunded_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'list charges' do # given customer = Shift4::Customers.create(TestData.customer) diff --git a/spec/integration/credits_spec.rb b/spec/integration/credits_spec.rb index b7223e7..6eb36c6 100644 --- a/spec/integration/credits_spec.rb +++ b/spec/integration/credits_spec.rb @@ -19,6 +19,20 @@ expect(retrieved['card']['first4']).to eq(credit_req["card"]["first4"]) end + it 'create only once with idempotency_key' do + # given + credit_req = TestData.credit(card: TestData.card) + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + created = Shift4::Credits.create(credit_req, request_options) + not_created_because_idempotency = Shift4::Credits.create(credit_req, request_options) + + # then + expect(created['id']).to eq(not_created_because_idempotency['id']) + expect(not_created_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'update credit' do # given card = TestData.card @@ -42,6 +56,32 @@ expect(updated['card']['first4']).to eq(credit_req["card"]["first4"]) end + it 'update credit only once with idempotency_key' do + # given + card = TestData.card + credit_req = TestData.credit(card: card) + created = Shift4::Credits.create(credit_req) + + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + Shift4::Credits.update(created['id'], + { + "description" => "updated description", + "metadata" => { "key" => "updated value" } + }, + request_options) + not_updated_because_idempotency = Shift4::Credits.update(created['id'], + { + "description" => "updated description", + "metadata" => { "key" => "updated value" } + }, + request_options) + + # then + expect(not_updated_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'list credits' do # given customer = Shift4::Customers.create(TestData.customer) diff --git a/spec/integration/customers_spec.rb b/spec/integration/customers_spec.rb index 8fda74a..802e369 100644 --- a/spec/integration/customers_spec.rb +++ b/spec/integration/customers_spec.rb @@ -17,6 +17,20 @@ expect(retrieved['email']).to eq(customer_req['email']) end + it 'create only once with idempotency_key' do + # given + customer_req = TestData.customer + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + created = Shift4::Customers.create(customer_req, request_options) + not_created_because_idempotency = Shift4::Customers.create(customer_req, request_options) + + # then + expect(created['id']).to eq(not_created_because_idempotency['id']) + expect(not_created_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'update customer default card' do # given customer_req = TestData.customer(card: TestData.card) @@ -32,6 +46,26 @@ expect(updated['defaultCardId']).to eq(new_card['id']) end + it 'update customer only once with idempotency_key' do + # given + customer_req = TestData.customer(card: TestData.card) + customer = Shift4::Customers.create(customer_req) + new_card = Shift4::Cards.create(customer['id'], TestData.card) + + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + Shift4::Customers.update(customer['id'], + { defaultCardId: new_card['id'] }, + request_options) + not_updated_because_idempotency = Shift4::Customers.update(customer['id'], + { defaultCardId: new_card['id'] }, + request_options) + + # then + expect(not_updated_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'delete customer' do # given customer_req = TestData.customer(card: TestData.card) diff --git a/spec/integration/disputes_spec.rb b/spec/integration/disputes_spec.rb index b54ee66..4b91d8c 100644 --- a/spec/integration/disputes_spec.rb +++ b/spec/integration/disputes_spec.rb @@ -40,6 +40,25 @@ expect(retrieved['evidence']['customerName']).to eq(evidence_customer_name) end + it 'update dispute only once with idempotency_key' do + # given + dispute, = create_dispute + evidence_customer_name = 'Test Customer' + + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + Shift4::Disputes.update(dispute['id'], + { evidence: { customerName: evidence_customer_name } }, + request_options) + not_updated_because_idempotency = Shift4::Disputes.update(dispute['id'], + { evidence: { customerName: evidence_customer_name } }, + request_options) + + # then + expect(not_updated_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'close dispute' do # given dispute, = create_dispute @@ -51,6 +70,22 @@ # then expect(retrieved['acceptedAsLost']).to eq(true) end + + it 'close dispute only once with idempotency_key' do + # given + dispute, = create_dispute + + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + Shift4::Disputes.close(dispute['id'], + request_options) + not_closed_because_idempotency = Shift4::Disputes.close(dispute['id'], + request_options) + + # then + expect(not_closed_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end end end diff --git a/spec/integration/payment_methods_spec.rb b/spec/integration/payment_methods_spec.rb index 3a06d75..8e4a392 100644 --- a/spec/integration/payment_methods_spec.rb +++ b/spec/integration/payment_methods_spec.rb @@ -21,6 +21,21 @@ expect(retrieved['status']).to eq('chargeable') end + it 'create only once with idempotency_key' do + # given + payment_method_req = TestData.payment_method + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + created = Shift4::PaymentMethods.create(payment_method_req, request_options) + not_created_because_idempotency = Shift4::PaymentMethods.create(payment_method_req, + request_options) + + # then + expect(created['id']).to eq(not_created_because_idempotency['id']) + expect(not_created_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'delete payment_method' do # given payment_method_req = TestData.payment_method diff --git a/spec/integration/plans_spec.rb b/spec/integration/plans_spec.rb index c2ef054..e9c8cb8 100644 --- a/spec/integration/plans_spec.rb +++ b/spec/integration/plans_spec.rb @@ -20,6 +20,20 @@ expect(retrieved['name']).to eq(plan_req['name']) end + it 'create only once with idempotency_key' do + # given + plan_req = TestData.plan + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + created = Shift4::Plans.create(plan_req, request_options) + not_created_because_idempotency = Shift4::Plans.create(plan_req, request_options) + + # then + expect(created['id']).to eq(not_created_because_idempotency['id']) + expect(not_created_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'update plan' do # given plan_req = TestData.plan @@ -37,6 +51,25 @@ expect(retrieved['name']).to eq('Updated plan') end + it 'update plan once with idempotency_key' do + # given + plan_req = TestData.plan + created = Shift4::Plans.create(plan_req) + + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + Shift4::Plans.update(created['id'], + { amount: 222, currency: 'PLN', name: 'Updated plan' }, + request_options) + not_updated_because_idempotency = Shift4::Plans.update(created['id'], + { amount: 222, currency: 'PLN', name: 'Updated plan' }, + request_options) + + # then + expect(not_updated_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'delete plan' do # given plan_req = TestData.plan diff --git a/spec/integration/subscriptions_spec.rb b/spec/integration/subscriptions_spec.rb index 05c7892..5f1673a 100644 --- a/spec/integration/subscriptions_spec.rb +++ b/spec/integration/subscriptions_spec.rb @@ -19,6 +19,29 @@ expect(retrieved['customerId']).to eq(customer['id']) end + it 'create only once with idempotency_key' do + # given + plan = Shift4::Plans.create(TestData.plan) + customer = Shift4::Customers.create(TestData.customer(card: TestData.card)) + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + subscription = Shift4::Subscriptions.create({ + customerId: customer['id'], + planId: plan['id'] + }, + request_options) + not_subscribed_because_idempotency = Shift4::Subscriptions.create({ + customerId: customer['id'], + planId: plan['id'] + }, + request_options) + + # then + expect(subscription['id']).to eq(not_subscribed_because_idempotency['id']) + expect(not_subscribed_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'update subscription' do # given plan = Shift4::Plans.create(TestData.plan) @@ -54,6 +77,50 @@ expect(shipping['address']['country']).to eq("CH") end + it 'update subscription only once with idempotency_key' do + # given + plan = Shift4::Plans.create(TestData.plan) + customer = Shift4::Customers.create(TestData.customer(card: TestData.card)) + subscription = Shift4::Subscriptions.create(customerId: customer['id'], planId: plan['id']) + + request_options = Shift4::RequestOptions.new(idempotency_key: random_idempotency_key.to_s) + + # when + Shift4::Subscriptions.update(subscription['id'], + { + shipping: { + name: 'Updated shipping', + address: { + "line1" => "Updated line1", + "line2" => "Updated line2", + "zip" => "Updated zip", + "city" => "Updated city", + "state" => "Updated state", + "country" => "CH", + }.compact, + } + }, + request_options) + not_updated_because_idempotency = Shift4::Subscriptions.update(subscription['id'], + { + shipping: { + name: 'Updated shipping', + address: { + "line1" => "Updated line1", + "line2" => "Updated line2", + "zip" => "Updated zip", + "city" => "Updated city", + "state" => "Updated state", + "country" => "CH", + }.compact, + } + }, + request_options) + + # then + expect(not_updated_because_idempotency.headers['Idempotent-Replayed']).to eq("true") + end + it 'cancel subscription' do # given plan = Shift4::Plans.create(TestData.plan) diff --git a/spec/support/random_data.rb b/spec/support/random_data.rb index 3135fb4..4928361 100644 --- a/spec/support/random_data.rb +++ b/spec/support/random_data.rb @@ -7,3 +7,7 @@ def random_string def random_email "#{random_string}@#{random_string}.com" end + +def random_idempotency_key + "#{random_string}#{random_string}" +end