diff --git a/README.markdown b/README.markdown index 4f0e595..898868a 100644 --- a/README.markdown +++ b/README.markdown @@ -208,6 +208,51 @@ You can check the trasaction [status](https://api-reference.datatrans.ch/#tag/v1 end ``` +Merchant Initiated Payments +--------- + +It's possible to authorize transactions without user interaction, via [merchant initiated payments](https://docs.datatrans.ch/docs/merchant-initiated-payments). + +To perform a so-called "dedicated registration" (so we can later charge the card via its `alias`), you should follow the same steps as described above, but not provide an amount: + +```ruby +transaction = datatrans.json_transaction( + refno: 'ABCDEF', + amount: 0, # omit amount for dedicated registrations + currency: "CHF", + payment_methods: ["ECA", "VIS"], + success_url: , + cancel_url: , + error_url: +) + +init = transaction.authorize + +# successful authorization call returns in response a transaction id +if init + transaction_id = transaction.response.params["transactionId"] +end +``` + +Then, at a later point in time, and without needing any user interaction, you can create a payment via `merchant_authorize`: + +```ruby +dedicated_registration = datatrans.json_transaction(transaction_id: transaction_id) +dedicated_registration.status # this will contain the card information + +card_alias = dedicated_registration.response.params["card"]["alias"] +card_expiry_month = dedicated_registration.response.params["card"]["expiryMonth"] +card_expiry_year = dedicated_registration.response.params["card"]["expiryYear"] + +transaction = datatrans.json_transaction( + refno: "ABCDEF", + amount: 1000, + currency: "CHF", + card: {alias: card_alias, expiryMonth: card_expiry_month, expiryYear: card_expiry_year} +) + +transaction.merchant_authorize # this will charge the card without user interaction +``` XML Transactions ================ diff --git a/lib/datatrans/config.rb b/lib/datatrans/config.rb index b225aa8..101a66e 100644 --- a/lib/datatrans/config.rb +++ b/lib/datatrans/config.rb @@ -56,6 +56,9 @@ def url(what, options = {}) when :init_transaction subdomain = SUBDOMAINS[:server_to_server_api] path = "/v1/transactions" + when :authorize_transaction + subdomain = SUBDOMAINS[:server_to_server_api] + path = "/v1/transactions/authorize" when :start_json_transaction subdomain = SUBDOMAINS[:payment_page] path = "/v1/start/#{options[:transaction_id]}" diff --git a/lib/datatrans/json/transaction.rb b/lib/datatrans/json/transaction.rb index cd0b3f7..d021032 100644 --- a/lib/datatrans/json/transaction.rb +++ b/lib/datatrans/json/transaction.rb @@ -16,6 +16,12 @@ def authorize @response.successful? end + def merchant_authorize + self.request = MerchantAuthorize.new(self.datatrans, params) + @response = MerchantAuthorizeResponse.new(self.datatrans, request.process) + @response.successful? + end + def status self.request = Status.new(self.datatrans, params) @response = StatusResponse.new(self.datatrans, request.process) @@ -35,5 +41,6 @@ def transaction_path end require 'datatrans/json/transaction/authorize' +require 'datatrans/json/transaction/merchant_authorize' require 'datatrans/json/transaction/status' require 'datatrans/json/transaction/settle' diff --git a/lib/datatrans/json/transaction/merchant_authorize.rb b/lib/datatrans/json/transaction/merchant_authorize.rb new file mode 100644 index 0000000..b28bc9d --- /dev/null +++ b/lib/datatrans/json/transaction/merchant_authorize.rb @@ -0,0 +1,45 @@ +require 'httparty' +require 'datatrans/json/transaction/response' + +class Datatrans::JSON::Transaction + class MerchantAuthorize + # class to authorize a transaction without user interaction https://api-reference.datatrans.ch/#tag/v1transactions/operation/authorize + attr_accessor :params, :datatrans + + def initialize(datatrans, params) + @datatrans = datatrans + @params = params + end + + def post(url, options = {}) + options = options + .merge(self.datatrans.proxy) + .merge(:basic_auth => { :username => self.datatrans.merchant_id, :password => self.datatrans.password }) + HTTParty.post(url, **options) + end + + def process + post(self.datatrans.url(:authorize_transaction), + :headers => { 'Content-Type' => 'application/json' }, + :body => request_body.to_json).parsed_response + end + + def request_body + auto_settle = params[:auto_settle].nil? ? true : params[:auto_settle] + + { + "currency": params[:currency], + "refno": params[:refno], + "amount": params[:amount], + "card": params[:card], + "autoSettle": auto_settle, + } + end + end + + class MerchantAuthorizeResponse < Response + def successful? + params["error"].blank? && params["transactionId"].present? + end + end +end diff --git a/spec/json/merchant_authorize_spec.rb b/spec/json/merchant_authorize_spec.rb new file mode 100644 index 0000000..19a2a1e --- /dev/null +++ b/spec/json/merchant_authorize_spec.rb @@ -0,0 +1,97 @@ +require "spec_helper" + +describe Datatrans::JSON::Transaction::MerchantAuthorize do + before do + @successful_response = { + "transactionId" => "230223022302230223", + "acquirerAuthorizationCode" => "123456", + "card" => { + "masked" => "411111xxxxxx1111" + } + } + + @failed_response = { + "error" => { + "code" => "INVALID_PROPERTY", + "message" => "authorize.card.alias or number is mandatory" + } + } + + @valid_params = { + currency: "CHF", + refno: "B4B4B4B4B", + amount: 1000, + card: { + alias: "AAABcH0Bq92s3kgAESIAAbGj5NIsAHWC", + expiryMonth: "01", + expiryYear: "23" + }, + auto_settle: true + } + + @expected_request_body = { + "currency": "CHF", + "refno": "B4B4B4B4B", + "amount": 1000, + "card": { + "alias": "AAABcH0Bq92s3kgAESIAAbGj5NIsAHWC", + "expiryMonth": "01", + "expiryYear": "23" + }, + "autoSettle": true + } + + @invalid_params = { + currency: "CHF", + refno: "B4B4B4B4B", + amount: 1000, + card: { + expiryMonth: "01", + expiryYear: "23" + } + } + end + + context "successful response" do + before do + allow_any_instance_of(Datatrans::JSON::Transaction::MerchantAuthorize).to receive(:process).and_return(@successful_response) + end + + it "generates correct request_body" do + request = Datatrans::JSON::Transaction::MerchantAuthorize.new(@datatrans, @valid_params) + expect(request.request_body).to eq(@expected_request_body) + end + + it "#process handles a valid datatrans merchant authorize response" do + transaction = Datatrans::JSON::Transaction.new(@datatrans, @valid_params) + expect(transaction.merchant_authorize).to be true + end + end + + context "with autoSettle specified" do + it "handles autoSettle correctly in request_body" do + params_with_auto_settle = @valid_params.merge(auto_settle: false) + request = Datatrans::JSON::Transaction::MerchantAuthorize.new(@datatrans, params_with_auto_settle) + + expected_request_body_without_auto_settle = @expected_request_body.merge(autoSettle: false) + expect(request.request_body).to eq(expected_request_body_without_auto_settle) + end + end + + context "failed response" do + before do + allow_any_instance_of(Datatrans::JSON::Transaction::MerchantAuthorize).to receive(:process).and_return(@failed_response) + @transaction = Datatrans::JSON::Transaction.new(@datatrans, @invalid_params) + end + + it "#process handles a failed datatrans merchant authorize response" do + expect(@transaction.merchant_authorize).to be false + end + + it "returns error details" do + @transaction.merchant_authorize + expect(@transaction.response.error_code).to eq "INVALID_PROPERTY" + expect(@transaction.response.error_message).to eq "authorize.card.alias or number is mandatory" + end + end +end