Skip to content

Commit

Permalink
added refund support for card payments
Browse files Browse the repository at this point in the history
  • Loading branch information
OlegPhenomenon committed Sep 20, 2023
1 parent 697fafe commit 7c573e9
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 52 deletions.
12 changes: 12 additions & 0 deletions app/contracts/capture_contract.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions app/contracts/void_contract.rb
Original file line number Diff line number Diff line change
@@ -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
9 changes: 7 additions & 2 deletions app/controllers/api/v1/refund/auction_controller.rb
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
33 changes: 33 additions & 0 deletions app/controllers/api/v1/refund/capture_controller.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions app/models/global_variable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -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
51 changes: 26 additions & 25 deletions app/models/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
3 changes: 2 additions & 1 deletion app/services/auction/base_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/services/auction/deposit_prepayment_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
49 changes: 49 additions & 0 deletions app/services/capture_service.rb
Original file line number Diff line number Diff line change
@@ -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
47 changes: 25 additions & 22 deletions app/services/oneoff.rb
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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,
Expand All @@ -78,4 +81,4 @@ def body
'structured_reference' => reference_number.to_s
}
end
end
end
4 changes: 4 additions & 0 deletions app/services/refund_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
50 changes: 50 additions & 0 deletions app/services/void_service.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 7c573e9

Please sign in to comment.