diff --git a/README.md b/README.md index c571350..123076f 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,13 @@ Install the gem and add to the application's Gemfile by executing: bundle add "monopay-ruby" ``` -If bundler is not being used to manage dependencies, install the gem by executing: +If bundler is not being used to manage dependencies, install the gem by executing the: ```ruby gem install "monopay-ruby" ``` -## Usage +## Configuration Add API token. There are two ways: First - add to the initializer file: @@ -46,10 +46,32 @@ Development: [https://api.monobank.ua/](https://api.monobank.ua/) Production: [https://fop.monobank.ua/](https://fop.monobank.ua/) -Just get the token and go to earn moneys! 🚀 +Just get the token and go to earn money! 🚀 + + +_______________________________________________________________ + +Optional + +You may add a minimum value to your payment: + +```ruby +# config/initializers/monopay-ruby.rb +MonopayRuby.configure do |config| + config.min_price = 1 +end +``` +* 0.01 UAH - it is a minimal valid value for Monobank: + - if you use 1 as an Integer it is equal to 0.01 UAH + - if you use BigDecimal(1) it's equal to 1 UAH + +The default value is 1 (0.01 UAH) + ### Generate payment request +Simple: + ```ruby # app/controllers/payments_controller.rb class PaymentsController < ApplicationController @@ -59,8 +81,8 @@ class PaymentsController < ApplicationController "https://example.com/payments/webhook" ) - if payment.create(amount: 100, destination: "Payment description",) - # your success code processing + if payment.create(amount: 100, destination: "Payment description") + # your successful code processing else # your error code processing # flash[:error] = payment.error_messages @@ -69,6 +91,34 @@ class PaymentsController < ApplicationController end ``` +With discount: + +```ruby +# app/controllers/payments_controller.rb +class PaymentsController < ApplicationController + def create + payment = MonopayRuby::Invoices::SimpleInvoice.new( + "https://example.com", + "https://example.com/payments/webhook" + ) + + if payment.create(amount: 100, discount: 20, discount_is_fixed: true, destination: "Payment description") + # your successful code processing + else + # your error code processing + # flash[:error] = payment.error_messages + end + end +end +``` + +Options: +- discount - is an number, which represents a % of discount if discount_is_fixed: false and an amount of discount if discount_is_fixed: true +- discount_is_fixed - a Boolean which sets the type of discount: + - ```true``` fixed amount of discount will be applied, for example a coupon (use Integer or BigDecimal in ```discount``` ) + - ```false``` discount amount will be preceded by % (use Integer in ```discount``` for correct counting of 0-100 %) + + ### Verify transaction ```ruby @@ -80,7 +130,7 @@ class PaymentsController < ApplicationController webhook_validator = MonopayRuby::Webhooks::Validator.new(request) if webhook_validator.valid? - # your success code processing + # your successful code processing else # your error code processing # flash[:error] = webhook_validator.error_messages diff --git a/lib/monopay-ruby/configuration.rb b/lib/monopay-ruby/configuration.rb index ada7feb..326b850 100644 --- a/lib/monopay-ruby/configuration.rb +++ b/lib/monopay-ruby/configuration.rb @@ -1,7 +1,8 @@ class MonopayRuby::Configuration - attr_accessor :api_token + attr_accessor :api_token, :min_price def initialize - @api_token = ENV["MONOBANK_API_TOKEN"] # note ability to use ENV variable in docs + @api_token = ENV["MONOBANK_API_TOKEN"] + @min_price = ENV["MIN_PRICE"] || 1 end end diff --git a/lib/monopay-ruby/invoices/simple_invoice.rb b/lib/monopay-ruby/invoices/simple_invoice.rb index 9d6cf8a..f0c06fe 100644 --- a/lib/monopay-ruby/invoices/simple_invoice.rb +++ b/lib/monopay-ruby/invoices/simple_invoice.rb @@ -30,11 +30,20 @@ def initialize(redirect_url: nil, webhook_url: nil) # @param [String] destination - additional info about payment # @param [String] reference - bill number or other reference # @return [Boolean] true if invoice was created successfully, false otherwise - def create(amount, destination: nil, reference: nil) + def create(amount:, options: {}) + @amount = MonopayRuby::Services::ValidateValue.call(amount, DEFAULT_CURRENCY) + discount = options[:discount] + discount_is_fixed = options[:discount_is_fixed] + @destination = options[:destination] + @reference = options[:reference] + + @min_amount = MonopayRuby::Services::ValidateValue.call(MonopayRuby.configuration.min_price, DEFAULT_CURRENCY, "Minimal amount") begin - @amount = convert_to_cents(amount) - @destination = destination - @reference = reference + if discount + discount = MonopayRuby::Services::ConvertAmount.call(discount, DEFAULT_CURRENCY) + + @amount = MonopayRuby::Services::Discount.call(@amount, discount, discount_is_fixed, @min_amount) + end response = RestClient.post(API_CREATE_INVOICE_URL, request_body, headers) response_body = JSON.parse(response.body) @@ -71,14 +80,6 @@ def request_body } }.to_json end - - def convert_to_cents(amount) - if amount.is_a?(BigDecimal) - Money.from_amount(amount, DEFAULT_CURRENCY).cents - else - amount - end - end end end end diff --git a/lib/monopay-ruby/services/base_service.rb b/lib/monopay-ruby/services/base_service.rb new file mode 100644 index 0000000..129b371 --- /dev/null +++ b/lib/monopay-ruby/services/base_service.rb @@ -0,0 +1,9 @@ +module MonopayRuby + module Services + class BaseService + def self.call(*args) + new(*args).call + end + end + end +end diff --git a/lib/monopay-ruby/services/convert_amount.rb b/lib/monopay-ruby/services/convert_amount.rb new file mode 100644 index 0000000..3d3b80e --- /dev/null +++ b/lib/monopay-ruby/services/convert_amount.rb @@ -0,0 +1,25 @@ +require "money" +require "bigdecimal" + +module MonopayRuby + module Services + class ConvertAmount < BaseService + attr_reader :amount, :currency + + def initialize(amount, currency) + @amount = amount + @currency = currency + end + + def call + if amount.is_a?(BigDecimal) + Money.from_amount(amount, currency).cents + elsif amount.is_a?(Integer) + amount + else + raise TypeError, "allowed to use only a BigDecimal or Integer type" + end + end + end + end +end diff --git a/lib/monopay-ruby/services/discount.rb b/lib/monopay-ruby/services/discount.rb new file mode 100644 index 0000000..cffb194 --- /dev/null +++ b/lib/monopay-ruby/services/discount.rb @@ -0,0 +1,24 @@ +module MonopayRuby + module Services + class Discount < BaseService + attr_reader :amount, :discount, :discount_is_fixed, :min_amount + + def initialize(amount, discount, discount_is_fixed, min_amount) + @amount = amount + @discount = discount + @discount_is_fixed = discount_is_fixed + @min_amount = min_amount + end + + def call + if discount_is_fixed + sum = amount - discount + else + sum = amount * (1 - (discount.to_f / 100)) + end + + [sum.to_i, min_amount].max + end + end + end +end diff --git a/lib/monopay-ruby/services/validate_value.rb b/lib/monopay-ruby/services/validate_value.rb new file mode 100644 index 0000000..8873d37 --- /dev/null +++ b/lib/monopay-ruby/services/validate_value.rb @@ -0,0 +1,37 @@ +require "bigdecimal" + +module MonopayRuby + module Services + class ValidateValue < BaseService + attr_reader :amount, :currency, :type + + def initialize(amount, currency, type='Amount') + @amount = amount + @currency = currency + @type = type + end + + def call + if amount.is_a?(Integer) || amount.is_a?(BigDecimal) + if amount > 0 + MonopayRuby::Services::ConvertAmount.call(amount, currency) + else + raise ArgumentError, "#{type} must be greater than 0" + end + else + raise TypeError, "#{type} is allowed to be Integer or BigDecimal, got #{amount.class}" unless amount.is_a?(Integer) || amount.is_a?(BigDecimal) + end + end + + private + + def name(var) + binding.local_variables.each do |name| + value = binding.local_variable_get(name) + return name.to_s if value == var + end + nil + end + end + end +end diff --git a/lib/monopay-ruby/webhooks/public_key.rb b/lib/monopay-ruby/webhooks/public_key.rb index 199d3d8..cc9bc36 100644 --- a/lib/monopay-ruby/webhooks/public_key.rb +++ b/lib/monopay-ruby/webhooks/public_key.rb @@ -2,7 +2,6 @@ require "base64" require "json" -require "pry" module MonopayRuby module Webhooks class PublicKey < MonopayRuby::Base diff --git a/spec/lib/invoices/simple_invoice_spec.rb b/spec/lib/invoices/simple_invoice_spec.rb index 676c5e4..0163f54 100644 --- a/spec/lib/invoices/simple_invoice_spec.rb +++ b/spec/lib/invoices/simple_invoice_spec.rb @@ -43,26 +43,64 @@ allow(RestClient).to receive(:post).and_return(double(body: response_example.to_json)) end - it "returns true" do - expect(simple_invoice_instance.create(2000)).to be_truthy + context "only with amount" do + it "returns true" do + expect(simple_invoice_instance.create(amount: 2000)).to be_truthy + end + + it "sets invoice_id" do + expect do + simple_invoice_instance.create(amount: 2000) + end.to change(simple_invoice_instance, :invoice_id).from(nil).to(invoice_id) + end + + it "sets page_url" do + expect do + simple_invoice_instance.create(amount: 2000) + end.to change(simple_invoice_instance, :page_url).from(nil).to(page_url) + end end - it "sets invoice_id" do - expect do - simple_invoice_instance.create(2000) - end.to change(simple_invoice_instance, :invoice_id).from(nil).to(invoice_id) + context "with amount and discount (fixed)" do + it "returns true" do + expect(simple_invoice_instance.create(amount: 2000, options: { discount: 20.to_d, discount_is_fixed: true }) ).to be_truthy + end + + it "sets invoice_id" do + expect do + simple_invoice_instance.create(amount: 2000, options: { discount: 20.to_d, discount_is_fixed: true }) + end.to change(simple_invoice_instance, :invoice_id).from(nil).to(invoice_id) + end + + it "sets page_url" do + expect do + simple_invoice_instance.create(amount: 2000, options: { discount: 20.to_d, discount_is_fixed: true }) + end.to change(simple_invoice_instance, :page_url).from(nil).to(page_url) + end end - it "sets page_url" do - expect do - simple_invoice_instance.create(2000) - end.to change(simple_invoice_instance, :page_url).from(nil).to(page_url) + context "with amount and discount (not fixed)" do + it "returns true" do + expect(simple_invoice_instance.create(amount: 2000, options: { discount: 20, discount_is_fixed: false }) ).to be_truthy + end + + it "sets invoice_id" do + expect do + simple_invoice_instance.create(amount: 2000, options: { discount: 20, discount_is_fixed: false }) + end.to change(simple_invoice_instance, :invoice_id).from(nil).to(invoice_id) + end + + it "sets page_url" do + expect do + simple_invoice_instance.create(amount: 2000, options: { discount: 20, discount_is_fixed: false }) + end.to change(simple_invoice_instance, :page_url).from(nil).to(page_url) + end end context "when amount is BigDecimal" do it "sets amount" do expect do - simple_invoice_instance.create(BigDecimal("20")) + simple_invoice_instance.create(amount: BigDecimal("20")) end.to change(simple_invoice_instance, :amount).from(nil).to(2000) end end @@ -87,11 +125,11 @@ end it "returns false" do - expect(subject.create(2000)).to be_falsey + expect(subject.create(amount: 2000)).to be_falsey end it "has error message" do - subject.create(2000) + subject.create(amount: 2000) expect(subject.error_messages).to include(missing_x_token_header_error_message) end @@ -114,40 +152,30 @@ end it "returns false" do - expect(subject.create(2000)).to be_falsey + expect(subject.create(amount: 2000)).to be_falsey end it "has error message" do - subject.create(2000) + subject.create(amount: 2000) expect(subject.error_messages).to include(invalid_token_error_message) end end - context "with invalid params" do - let(:invalid_amount_server_error_message) { { "errCode" => "BAD_REQUEST", "errText" => "json unmarshal: : json: cannot unmarshal string into Go struct field InvoiceCreateRequest.amount of type int64" } } - let(:error_code) { "400 Bad Request" } - let(:invalid_amount_error_message) do - [error_code, invalid_amount_server_error_message].join(", ") - end - let(:exception_instance) do - RestClient::ExceptionWithResponse.new(double(body: invalid_amount_server_error_message.to_json)) - end - - before do - exception_instance.message = error_code - - allow(RestClient).to receive(:post).and_raise(exception_instance) + context 'when amount is a invalid' do + it 'raises a TypeError if amount type is a string' do + expect { subject.create(amount: '13') } + .to raise_error(TypeError, 'Amount is allowed to be Integer or BigDecimal, got String') end - it "returns false" do - expect(subject.create("")).to be_falsey + it 'raises a TypeError if amount type is a float' do + expect { subject.create(amount: 666.0) } + .to raise_error(TypeError, 'Amount is allowed to be Integer or BigDecimal, got Float') end - it "has error message" do - subject.create("") - - expect(subject.error_messages).to include(invalid_amount_error_message) + it 'raises a ArgumentError if amount negative' do + expect { subject.create(amount: -1) } + .to raise_error(ArgumentError, 'Amount must be greater than 0') end end end