diff --git a/app/contracts/capture_contract.rb b/app/contracts/capture_contract.rb new file mode 100644 index 0000000..571bd5b --- /dev/null +++ b/app/contracts/capture_contract.rb @@ -0,0 +1,12 @@ +class CaptureContract < Dry::Validation::Contract + # include BaseContract + + params do + required(:amount).filled(:decimal) + required(:payment_reference).filled(:string) + end + + rule(:payment_reference) do + key.failure(I18n.t('api_errors.invalid_payment_reference')) unless Invoice.exists?(payment_reference: value) + end +end diff --git a/app/contracts/void_contract.rb b/app/contracts/void_contract.rb new file mode 100644 index 0000000..f5ad9bf --- /dev/null +++ b/app/contracts/void_contract.rb @@ -0,0 +1,11 @@ +class VoidContract < Dry::Validation::Contract + # include BaseContract + + params do + required(:payment_reference).filled(:string) + end + + rule(:payment_reference) do + key.failure(I18n.t('api_errors.invalid_payment_reference')) unless Invoice.exists?(payment_reference: value) + end +end diff --git a/app/controllers/api/v1/refund/auction_controller.rb b/app/controllers/api/v1/refund/auction_controller.rb index 1d4e137..c365866 100644 --- a/app/controllers/api/v1/refund/auction_controller.rb +++ b/app/controllers/api/v1/refund/auction_controller.rb @@ -1,12 +1,17 @@ module Api module V1 module Refund + # rubocop:disable Metrics class AuctionController < ApplicationController before_action :load_invoice def create - response = RefundService.call(amount: @invoice.transaction_amount, - payment_reference: @invoice.payment_reference) + response = if @invoice.payment_method == 'card' + VoidService.call(payment_reference: @invoice.payment_reference) + else + RefundService.call(amount: @invoice.transaction_amount, + payment_reference: @invoice.payment_reference) + end if response.result? @invoice.update(status: 'refunded') diff --git a/app/controllers/api/v1/refund/capture_controller.rb b/app/controllers/api/v1/refund/capture_controller.rb new file mode 100644 index 0000000..d652195 --- /dev/null +++ b/app/controllers/api/v1/refund/capture_controller.rb @@ -0,0 +1,33 @@ +module Api + module V1 + module Refund + class CaptureController < ApplicationController + before_action :load_invoice + + def create + unless @invoice.payment_method == 'card' + render json: { message: 'Invoice was not paid by card' }, status: :ok and return + end + + response = CaptureService.call(amount: @invoice.transaction_amount, payment_reference: @invoice.payment_reference) + + if response.result? + @invoice.update(status: 'captured') + render json: { message: 'Invoice was captured' }, status: :ok + else + render json: { error: response.errors }, status: :unprocessable_entity + end + end + + private + + def load_invoice + @invoice = ::Invoice.find_by(invoice_number: params[:params][:invoice_number]) + return if @invoice.present? + + render json: { error: 'Invoice not found' }, status: :not_found + end + end + end + end +end diff --git a/app/models/global_variable.rb b/app/models/global_variable.rb index af9132e..3a79ea8 100644 --- a/app/models/global_variable.rb +++ b/app/models/global_variable.rb @@ -8,6 +8,7 @@ class GlobalVariable BASE_ENDPOINT = ENV['everypay_base'] || 'https://igw-demo.every-pay.com/api/v4' ONEOFF_ENDPOINT = '/payments/oneoff'.freeze ACCOUNT_NAME = ENV['account_name'] + DEPOSIT_ACCOUNT_NAME = ENV['deposit_account_name'] INITIATOR = 'billing'.freeze BILLING_SECRET = ENV['billing_secret'] @@ -25,4 +26,6 @@ class GlobalVariable ALLOWED_DEV_BASE_URLS = ENV['allowed_base_urls'] REFUND_ENDPOINT = '/payments/refund'.freeze + VOID_ENDPOINT = '/payments/void'.freeze + CAPTURE_ENDPOINT = '/payments/capture'.freeze end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 292055c..e7759e8 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -6,34 +6,35 @@ class Invoice < ApplicationRecord AUCTION = 'auction'.freeze EEID = 'eeid'.freeze + store_accessor :everypay_response, :payment_method + pg_search_scope :search_by_number, against: [:invoice_number], - using: { - tsearch: { - prefix: true - } - } + using: { + tsearch: { + prefix: true + } + } - enum affiliation: %i[regular auction_deposit] - enum status: %i[unpaid paid cancelled failed refunded] + enum affiliation: { regular: 0, auction_deposit: 1 } + enum status: { unpaid: 0, paid: 1, cancelled: 2, failed: 3, refunded: 4, captured: 5 } - scope :with_status, ->(status) { - where(status: status) if status.present? + scope :with_status, lambda { |status| + where(status:) if status.present? } - scope :with_number, ->(invoice_number) { + scope :with_number, lambda { |invoice_number| search_by_number(invoice_number) if invoice_number.present? } - scope :with_amount_between, ->(low, high) { + scope :with_amount_between, lambda { |low, high| where(transaction_amount: low.to_f..high.to_f) if low.present? && high.present? } - def self.search(params={}) - sort_column = params[:sort].presence_in(%w{ invoice_number status affiliation }) || "id" - sort_direction = params[:direction].presence_in(%w{ asc desc }) || "desc" + def self.search(params = {}) + sort_column = params[:sort].presence_in(%w[invoice_number status affiliation]) || 'id' + sort_direction = params[:direction].presence_in(%w[asc desc]) || 'desc' - self - .with_number(params[:invoice_number].to_s.downcase) + with_number(params[:invoice_number].to_s.downcase) .with_status(params[:status]) .with_amount_between(params[:min_amount], params[:max_amount]) .order(sort_column => sort_direction) @@ -59,15 +60,15 @@ def auction? def to_h { - invoice_number: invoice_number, - initiator: initiator, - payment_reference: payment_reference, - transaction_amount: transaction_amount, - status: status, - in_directo: in_directo, - everypay_response: everypay_response, - transaction_time: transaction_time, - sent_at_omniva: sent_at_omniva + invoice_number:, + initiator:, + payment_reference:, + transaction_amount:, + status:, + in_directo:, + everypay_response:, + transaction_time:, + sent_at_omniva: } end end diff --git a/app/services/auction/base_service.rb b/app/services/auction/base_service.rb index 02b6103..ff4a9f3 100644 --- a/app/services/auction/base_service.rb +++ b/app/services/auction/base_service.rb @@ -4,7 +4,7 @@ module BaseService AUCTION_DEPOSIT = 'auction_deposit'.freeze - def oneoff_link(bulk: false) + def oneoff_link(bulk: false, deposit_banklink: false) invoice_number = InvoiceNumberService.call(invoice_auction_deposit: true) invoice_params = invoice_params(params, invoice_number) @@ -13,6 +13,7 @@ def oneoff_link(bulk: false) customer_url: params[:customer_url], reference_number: params[:reference_number], bulk: bulk, + deposit_banklink: deposit_banklink, bulk_invoices: params[:description].to_s.split(' ')) response.result? ? struct_response(response.instance) : parse_validation_errors(response) end diff --git a/app/services/auction/deposit_prepayment_service.rb b/app/services/auction/deposit_prepayment_service.rb index 8bfbc6c..1e6f95e 100644 --- a/app/services/auction/deposit_prepayment_service.rb +++ b/app/services/auction/deposit_prepayment_service.rb @@ -18,7 +18,7 @@ def call contract = DepositPrepaymentContract.new result = contract.call(params) - result.success? ? oneoff_link : parse_validation_errors(result) + result.success? ? oneoff_link(deposit_banklink: true) : parse_validation_errors(result) end end end diff --git a/app/services/capture_service.rb b/app/services/capture_service.rb new file mode 100644 index 0000000..08815ac --- /dev/null +++ b/app/services/capture_service.rb @@ -0,0 +1,49 @@ +class CaptureService + include Request + include ApplicationService + include ActionView::Helpers::NumberHelper + + attr_reader :amount, :payment_reference + + def initialize(amount:, payment_reference:) + @amount = amount + @payment_reference = payment_reference + end + + def self.call(amount:, payment_reference:) + new(amount:, payment_reference:).call + end + + def call + contract = CaptureContract.new + result = contract.call(amount:, payment_reference:) + + if result.success? + response = base_request + struct_response(response) + else + parse_validation_errors(result) + end + end + + private + + def base_request + uri = URI("#{GlobalVariable::BASE_ENDPOINT}#{GlobalVariable::CAPTURE_ENDPOINT}") + post(direction: 'everypay', path: uri, params: body) + end + + def body + { + 'api_username' => GlobalVariable::API_USERNAME, + 'amount' => number_with_precision(amount, precision: 2), + 'payment_reference' => payment_reference, + 'nonce' => nonce, + 'timestamp' => "#{Time.zone.now.to_formatted_s(:iso8601)}" + } + end + + def nonce + rand(10**30).to_s.rjust(30, '0') + end +end diff --git a/app/services/oneoff.rb b/app/services/oneoff.rb index 0af9423..b6708f7 100644 --- a/app/services/oneoff.rb +++ b/app/services/oneoff.rb @@ -1,44 +1,47 @@ class InvalidParams < StandardError; end +# rubocop:disable Metrics class Oneoff include Request include ApplicationService - attr_reader :invoice_number, :customer_url, :reference_number, :bulk, :bulk_invoices + attr_reader :invoice_number, :customer_url, :reference_number, :bulk, :deposit_banklink, :bulk_invoices - def initialize(invoice_number:, customer_url:, reference_number:, bulk: false, bulk_invoices: []) - @invoice = Invoice.find_by(invoice_number: invoice_number) + def initialize(invoice_number:, customer_url:, reference_number:, bulk: false, deposit_banklink: false, bulk_invoices: []) + @invoice = Invoice.find_by(invoice_number:) @invoice_number = invoice_number @customer_url = customer_url @reference_number = reference_number @bulk = bulk @bulk_invoices = bulk_invoices + @deposit_banklink = deposit_banklink end - def self.call(invoice_number:, customer_url:, reference_number:, bulk: false, bulk_invoices: []) - new(invoice_number: invoice_number, - customer_url: customer_url, - reference_number: reference_number, - bulk: bulk, - bulk_invoices: bulk_invoices).call + def self.call(invoice_number:, customer_url:, reference_number:, bulk: false, deposit_banklink: false, bulk_invoices: []) + new(invoice_number:, + customer_url:, + reference_number:, + bulk:, + deposit_banklink:, + bulk_invoices:).call end def call if @invoice.nil? - if invoice_number.nil? - errors = 'Internal error: called invoice withour number. Please contact to administrator' - else - errors = "Invoice with #{invoice_number} not found in internal system" - end + errors = if invoice_number.nil? + 'Internal error: called invoice withour number. Please contact to administrator' + else + "Invoice with #{invoice_number} not found in internal system" + end - return wrap(result: false, instance: nil, errors: errors) + return wrap(result: false, instance: nil, errors:) end contract = OneoffParamsContract.new - result = contract.call(invoice_number: invoice_number, - customer_url: customer_url, - reference_number: reference_number) + result = contract.call(invoice_number:, + customer_url:, + reference_number:) if result.success? response = base_request struct_response(response) @@ -59,11 +62,11 @@ def body { 'api_username' => GlobalVariable::API_USERNAME, - 'account_name' => GlobalVariable::ACCOUNT_NAME, + 'account_name' => deposit_banklink ? GlobalVariable::DEPOSIT_ACCOUNT_NAME : GlobalVariable::ACCOUNT_NAME, 'amount' => @invoice.transaction_amount.to_f, - 'order_reference' => "#{ bulk ? bulk_description : @invoice.invoice_number }", + 'order_reference' => "#{bulk ? bulk_description : @invoice.invoice_number}", 'token_agreement' => 'unscheduled', - 'nonce' => "#{rand(10 ** 30).to_s.rjust(30,'0')}", + 'nonce' => "#{rand(10**30).to_s.rjust(30, '0')}", 'timestamp' => "#{Time.zone.now.to_formatted_s(:iso8601)}", # 'email' => Setting.registry_email, 'customer_url' => customer_url, @@ -78,4 +81,4 @@ def body 'structured_reference' => reference_number.to_s } end -end \ No newline at end of file +end diff --git a/app/services/refund_service.rb b/app/services/refund_service.rb index 364982e..e68579f 100644 --- a/app/services/refund_service.rb +++ b/app/services/refund_service.rb @@ -18,6 +18,10 @@ def call contract = RefundContract.new result = contract.call(amount: amount, payment_reference: payment_reference) + puts '------ REFUND SERVICE ------' + puts result.inspect + puts '------ END REFUND SERVICE ------' + if result.success? response = base_request struct_response(response) diff --git a/app/services/void_service.rb b/app/services/void_service.rb new file mode 100644 index 0000000..05b29ec --- /dev/null +++ b/app/services/void_service.rb @@ -0,0 +1,50 @@ +class VoidService + include Request + include ApplicationService + + attr_reader :payment_reference + + def initialize(payment_reference:) + @payment_reference = payment_reference + end + + def self.call(payment_reference:) + new(payment_reference: payment_reference).call + end + + def call + contract = VoidContract.new + result = contract.call(payment_reference: payment_reference) + + puts '-------- VOID SERVICE --------' + puts result.inspect + puts '-------- END VOID SERVICE --------' + + if result.success? + response = base_request + struct_response(response) + else + parse_validation_errors(result) + end + end + + private + + def base_request + uri = URI("#{GlobalVariable::BASE_ENDPOINT}#{GlobalVariable::VOID_ENDPOINT}") + post(direction: 'everypay', path: uri, params: body) + end + + def body + { + 'api_username' => GlobalVariable::API_USERNAME, + 'payment_reference' => payment_reference, + 'nonce' => nonce, + 'timestamp' => "#{Time.zone.now.to_formatted_s(:iso8601)}" + } + end + + def nonce + rand(10**30).to_s.rjust(30, '0') + end +end diff --git a/config/application.yml.sample b/config/application.yml.sample index d1d7940..f0d29b3 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -49,7 +49,8 @@ linkpay_prefix: 'https://igw-demo.every-pay.com/lp' linkpay_token: '' api_username: '' everypay_base: https://igw-demo.every-pay.com/api/v4 -account_name: '' +account_name: 'EUR3D1' +deposit_account_name: 'EUR3D4' timeout_is_sec: 3 diff --git a/config/routes.rb b/config/routes.rb index 751e466..73daf71 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,6 +75,7 @@ namespace :refund do resources :auction, only: [:create] + resources :capture, only: [:create] end namespace :callback_handler do