Skip to content

Commit

Permalink
Merge pull request #162 from railslove/foreign-currency-credits
Browse files Browse the repository at this point in the history
[AZV] enable foreign currency transfers
  • Loading branch information
Uepsilon authored May 3, 2018
2 parents e8eabbe + aeabd55 commit d6200fa
Show file tree
Hide file tree
Showing 16 changed files with 486 additions and 34 deletions.
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ gem 'grape-swagger-entity'
gem 'faraday'
gem 'nokogiri'
gem 'sepa_king'
gem 'king_dtaus', git: 'https://github.com/salesking/king_dtaus.git'
gem 'sequel'
gem 'puma'
gem 'jwt'
Expand All @@ -27,7 +28,6 @@ else
gem 'epics', '~> 1.5.0'
end


group :development, :test do
gem 'database_cleaner'
gem 'dotenv'
Expand Down
11 changes: 8 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ GIT
camt_parser (1.0.2)
nokogiri

GIT
remote: https://github.com/salesking/king_dtaus.git
revision: 84a72f2f0476b6fae5253aa7a139cfc639eace08
specs:
king_dtaus (2.0.4)
i18n

GEM
remote: https://rubygems.org/
specs:
Expand Down Expand Up @@ -191,6 +198,7 @@ DEPENDENCIES
grape-swagger
grape-swagger-entity
jwt
king_dtaus!
nokogiri
pg
pry
Expand All @@ -202,6 +210,3 @@ DEPENDENCIES
sequel
timecop
webmock

BUNDLED WITH
1.16.0
8 changes: 8 additions & 0 deletions box/adapters/fake.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def HAC(from = nil, to = nil)
::File.open( ::File.expand_path("~/hac_empty.xml"))
end

def HTD
::File.open( ::File.expand_path("~/htd.xml"))
end

def CD1(pain)
["TRX#{SecureRandom.hex(6)}", "N#{SecureRandom.hex(6)}"]
end
Expand Down Expand Up @@ -80,6 +84,10 @@ def CCT(pain)
Event.statement_created(statement)
["TRX#{SecureRandom.hex(6)}", "N#{SecureRandom.hex(6)}"]
end

def AZV(dtazv)
["TRX#{SecureRandom.hex(6)}", "N#{SecureRandom.hex(6)}"]
end
end
end
end
1 change: 1 addition & 0 deletions box/apis/v1/content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# Business processes
require_relative '../../business_processes/credit'
require_relative '../../business_processes/foreign_credit'
require_relative '../../business_processes/direct_debit'
require_relative '../../jobs/fetch_statements'

Expand Down
31 changes: 23 additions & 8 deletions box/apis/v2/credit_transfers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
require_relative '../../validations/length'
require_relative '../../errors/business_process_failure'


module Box
module Apis
module V2
Expand Down Expand Up @@ -44,7 +43,6 @@ class CreditTransfers < Grape::API
present query.paginate(declared(params)).all, with: Entities::V2::CreditTransfer
end


###
### POST /credit_transfers
###
Expand All @@ -66,22 +64,39 @@ class CreditTransfers < Grape::API
params do
requires :account, type: String, desc: "the account to use", documentation: { param_type: 'body' }
requires :name, type: String, desc: "the customers name"
optional :bic , type: String, desc: "the customers bic", allow_blank: false
requires :iban, type: String, desc: "the customers iban"

optional :currency, type: String, desc: "currency of the transfer", length: 3, regexp: /[A-Z]{3}/, default: 'EUR'
requires :iban, type: String, desc: "the customers account"

given currency: ->(val) { val != 'EUR' } do
optional :fee_handling, type: Symbol, values: %i[split sender receiver], default: :split
requires :bic, type: String, desc: "the customers bic", allow_blank: false
requires :country_code, type: String, desc: "the customers country", allow_blank: false
end

given currency: ->(val) { val == 'EUR' } do
optional :urgent, type: Boolean, desc: "requested execution date", default: false
end

requires :end_to_end_reference, type: String, desc: "unique end to end reference", unique_transaction_eref: true, length_transaction_eref: true

requires :amount_in_cents, type: Integer, desc: "amount to credit (charged in cents)", values: 1..1200000000
requires :end_to_end_reference, type: String, desc: "unique end to end reference", unique_transaction_eref: true
optional :reference, type: String, length: 140, desc: "description of the transaction (max. 140 char)"
optional :execution_date, type: Date, desc: "requested execution date", default: -> { Date.today }
optional :urgent, type: Boolean, desc: "requested execution date", default: false
end

post do
account = current_organization.find_account!(params[:account])
Credit.v2_create!(current_user, account, declared(params))

if params[:currency] == "EUR"
Credit.v2_create!(current_user, account, declared(params))
else
ForeignCredit.v2_create!(current_user, account, declared(params))
end

{ message: 'Credit transfer has been initiated successfully!' }
end


###
### GET /credit_transfers/:id
###
Expand Down
6 changes: 3 additions & 3 deletions box/business_processes/credit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ def self.create!(account, params, user)
user_id: user.id,
payload: Base64.strict_encode64(sct.to_xml),
eref: params[:eref],
amount: params[:amount]
currency: 'EUR',
amount: params[:amount],
metadata: params.slice(:name, :iban, :bic, :execution_date, :reference)
)
else
fail(Box::BusinessProcessFailure.new(sct.errors))
Expand All @@ -51,9 +53,7 @@ def self.v2_create!(user, account, params)
# Set urgent flag or fall back to SEPA
params[:service_level] = params[:urgent] ? 'URGP' : 'SEPA'

# Execute v1 method
create!(account, params, user)
end

end
end
85 changes: 85 additions & 0 deletions box/business_processes/foreign_credit.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require 'king_dtaus'
require_relative '../errors/business_process_failure'

module Box
class ForeignCredit
class Payload < OpenStruct
def sender
KingDta::Account.new({
owner_name: account.name,
bank_number: account.bank_number,
owner_country_code: account.bank_country_code,
bank_account_number: account.bank_account_number
})
end

def receiver
KingDta::Account.new({
bank_bic: params[:bic],
owner_name: params[:name],
owner_country_code: params[:country_code],
** account_number
})
end

def account_number
if params[:iban] =~ /[A-Z]{2}/
{ bank_iban: params[:iban] }
else
{ bank_account_number: params[:iban] }
end
end

def amount
params[:amount] / 100.0
end

def fee_handling
{
split: '00',
sender: '01',
receiver: '02',
}[params[:fee_handling]]
end

def booking
KingDta::Booking.new(receiver, amount, params[:eref], nil, params[:currency]).tap do |booking|
booking.payment_type = '00'
booking.charge_bearer_code = fee_handling
end
end

def create
azv = KingDta::Dtazv.new(params[:execution_date])
azv.account = sender
azv.add(booking)

azv.create
end
end

def self.v2_create!(user, account, params)
params[:requested_date] = params[:execution_date].to_time.to_i

# Transform a few params
params[:amount] = params[:amount_in_cents]
params[:eref] = params[:end_to_end_reference]
params[:remittance_information] = params[:reference]

payload = Payload.new(account: account, params: params)

Queue.execute_credit(
account_id: account.id,
user_id: user.id,
payload: Base64.strict_encode64(payload.create),
eref: params[:eref],
currency: params[:currency],
amount: params[:amount],
metadata: params.slice(:name, :iban, :bic, :execution_date, :reference, :country_code, :fee_handling)
)
rescue ArgumentError => e
# TODO: Will be fixed upstream in the sepa_king gem by us
fail Box::BusinessProcessFailure.new({ base: e.message }, 'Invalid data')
end
end
end
8 changes: 6 additions & 2 deletions box/business_processes/import_statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ def self.create_statement(account, bank_transaction, bank_statement_id, unique_i
end

def self.link_statement_to_transaction(account, statement)
if transaction = account.transactions_dataset.where(eref: statement.eref).first
transaction.add_statement(statement)
# find transactions via EREF
transaction = account.transactions_dataset.where(eref: statement.eref).first
# fallback to finding via statement information
transaction ||= account.transactions_dataset.exclude(currency: 'EUR', status: ['credit_received', 'debit_received']).where{ created_at > 14.days.ago}.detect{|t| statement.information =~ /#{t.eref}/i }

if transaction
transaction.add_statement(statement)
if statement.credit?
transaction.update_status("credit_received")
elsif statement.debit?
Expand Down
40 changes: 35 additions & 5 deletions box/entities/v2/credit_transfer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ module V2
class CreditTransfer < Grape::Entity
expose(:public_id, as: "id")
expose(:account) { |transaction| transaction.account.iban }
expose(:name) { |trx| trx.parsed_payload[:payments].first[:transactions].first[:name] }
expose(:iban) { |trx| trx.parsed_payload[:payments].first[:transactions].first[:iban] }
expose(:bic) { |trx| trx.parsed_payload[:payments].first[:transactions].first[:bic] }
expose(:name)
expose(:iban)
expose(:bic)
expose(:amount, as: "amount_in_cents")
expose(:eref, as: 'end_to_end_reference')
expose(:reference) { |trx| trx.parsed_payload[:payments].first[:transactions].first[:remittance_information] }
expose(:executed_on) { |trx| trx.parsed_payload[:payments].first[:execution_date] }
expose(:reference)
expose(:executed_on)
expose(:status)
expose(:_links) do |transaction|
iban = transaction.account.iban
Expand All @@ -22,6 +22,36 @@ class CreditTransfer < Grape::Entity
account: Box.configuration.app_url + "/accounts/#{iban}/",
}
end

def name
object.metadata.fetch('name') do
object.parsed_payload[:payments].first[:transactions].first[:name]
end
end

def iban
object.metadata.fetch('iban') do
object.parsed_payload[:payments].first[:transactions].first[:iban]
end
end

def bic
object.metadata.fetch('bic') do
object.parsed_payload[:payments].first[:transactions].first[:bic]
end
end

def reference
object.metadata.fetch('reference') do
object.parsed_payload[:payments].first[:transactions].first[:remittance_information]
end
end

def executed_on
object.metadata.fetch('execution_date') do
object.parsed_payload[:payments].first[:execution_date]
end
end
end
end
end
Expand Down
8 changes: 7 additions & 1 deletion box/jobs/credit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
module Box
module Jobs
class Credit
INSTRUMENT_MAPPING = Hash.new('AZV').update({
"EUR" => :CCT,
})

def self.process!(message)
transaction = Transaction.create(
account_id: message[:account_id],
Expand All @@ -15,8 +19,10 @@ def self.process!(message)
type: "credit",
payload: Base64.strict_decode64(message[:payload]),
eref: message[:eref],
currency: message[:currency],
status: "created",
order_type: :CCT
order_type: INSTRUMENT_MAPPING[message[:currency]],
metadata: message[:metadata]
)

transaction.execute!
Expand Down
19 changes: 19 additions & 0 deletions box/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,25 @@ def credit_pain_attributes_hash
values.slice(:name, :bic, :iban)
end

def bank_account_number
@bank_account_number ||= (bank_account_metadata; @bank_account_number)
end

def bank_number
@bank_number ||= (bank_account_metadata; @bank_number)
end

def bank_country_code
iban[0...2]
end

def bank_account_metadata
Nokogiri::XML(transport_client.HTD).tap do |htd|
@bank_account_number ||= htd.at_xpath("//xmlns:AccountNumber[@international='false']", xmlns: "urn:org:ebics:H004").text
@bank_number ||= htd.at_xpath("//xmlns:BankCode[@international='false']", xmlns: "urn:org:ebics:H004").text
end
end

def last_imported_at
DB[:imports].where(account_id: id).order(:date).last.try(:[], :date)
end
Expand Down
15 changes: 15 additions & 0 deletions box/validations/unique_transaction_eref.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,19 @@ def validate(request)
eref_unused or fail(Grape::Exceptions::Validation, params: [@scope.full_name(:end_to_end_reference)], message: "must be unique")
end
end

class LengthTransactionEref < Grape::Validations::Base

def length(currency)
Hash.new(27).update(
'EUR' => 64
)[currency]
end

def validate(request)
return if request.params[:end_to_end_reference].to_s.size <= length(request.params[:currency])

fail(Grape::Exceptions::Validation, params: [@scope.full_name(:end_to_end_reference)], message: "must be at the most #{length(request.params[:currency])} characters long")
end
end
end
Loading

0 comments on commit d6200fa

Please sign in to comment.