Merge pull request #1502 from internetee/1422-record-payment-method-and-failed-payments

Record every payment attempt
This commit is contained in:
Timo Võhmar 2020-03-06 13:52:58 +02:00 committed by GitHub
commit 444a07c62a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 798 additions and 342 deletions

View file

@ -60,7 +60,7 @@ module Admin
end end
def bind_invoices def bind_invoices
@bank_statement.bind_invoices @bank_statement.bind_invoices(manual: true)
flash[:notice] = t('invoices_were_fully_binded') if @bank_statement.fully_binded? flash[:notice] = t('invoices_were_fully_binded') if @bank_statement.fully_binded?
flash[:warning] = t('invoices_were_partially_binded') if @bank_statement.partially_binded? flash[:warning] = t('invoices_were_partially_binded') if @bank_statement.partially_binded?

View file

@ -34,7 +34,7 @@ module Admin
end end
def bind def bind
if @bank_transaction.bind_invoice(params[:invoice_no]) if @bank_transaction.bind_invoice(params[:invoice_no], manual: true)
flash[:notice] = I18n.t('record_created') flash[:notice] = I18n.t('record_created')
redirect_to [:admin, @bank_transaction] redirect_to [:admin, @bank_transaction]
else else

View file

@ -5,50 +5,51 @@ class Registrar
skip_authorization_check # actually anyone can pay, no problems at all skip_authorization_check # actually anyone can pay, no problems at all
skip_before_action :authenticate_registrar_user!, :check_ip_restriction, skip_before_action :authenticate_registrar_user!, :check_ip_restriction,
only: [:back, :callback] only: [:back, :callback]
before_action :check_supported_payment_method
before_action :check_supported_payment_method, only: [:pay]
def pay def pay
invoice = Invoice.find(params[:invoice_id]) invoice = Invoice.find(params[:invoice_id])
bank = params[:bank] channel = params[:bank]
opts = {
return_url: registrar_return_payment_with_url( @payment_order = PaymentOrder.new_with_type(type: channel, invoice: invoice)
bank, invoice_id: invoice @payment_order.save
), @payment_order.reload
response_url: registrar_response_payment_with_url(
bank, invoice_id: invoice @payment_order.return_url = registrar_return_payment_with_url(@payment_order)
) @payment_order.response_url = registrar_response_payment_with_url(@payment_order)
}
@payment = ::PaymentOrders.create_with_type(bank, invoice, opts) @payment_order.save
@payment.create_transaction @payment_order.reload
end end
def back def back
invoice = Invoice.find(params[:invoice_id]) @payment_order = PaymentOrder.find_by!(id: params[:payment_order])
opts = { response: params } @payment_order.update!(response: params.to_unsafe_h)
@payment = ::PaymentOrders.create_with_type(params[:bank], invoice, opts)
if @payment.valid_response_from_intermediary? && @payment.settled_payment?
Rails.logger.info("User paid invoice ##{invoice.number} successfully")
@payment.complete_transaction if @payment_order.payment_received?
@payment_order.complete_transaction
if invoice.paid? if @payment_order.invoice.paid?
flash[:notice] = t(:pending_applied) flash[:notice] = t('.payment_successful')
else else
flash[:alert] = t(:something_wrong) flash[:alert] = t('.successful_payment_backend_error')
end end
else else
flash[:alert] = t(:something_wrong) @payment_order.create_failure_report
flash[:alert] = t('.payment_not_received')
end end
redirect_to registrar_invoice_path(invoice) redirect_to registrar_invoice_path(@payment_order.invoice)
end end
def callback def callback
invoice = Invoice.find(params[:invoice_id]) @payment_order = PaymentOrder.find_by!(id: params[:payment_order])
opts = { response: params } @payment_order.update!(response: params.to_unsafe_h)
@payment = ::PaymentOrders.create_with_type(params[:bank], invoice, opts)
if @payment.valid_response_from_intermediary? && @payment.settled_payment? if @payment_order.payment_received?
@payment.complete_transaction @payment_order.complete_transaction
else
@payment_order.create_failure_report
end end
render status: 200, json: { status: 'ok' } render status: 200, json: { status: 'ok' }
@ -57,13 +58,9 @@ class Registrar
private private
def check_supported_payment_method def check_supported_payment_method
return if supported_payment_method? return if PaymentOrder.supported_method?(params[:bank], shortname: true)
raise StandardError.new("Not supported payment method")
end
raise(StandardError, 'Not supported payment method')
def supported_payment_method?
PaymentOrders::PAYMENT_METHODS.include?(params[:bank])
end end
end end
end end

View file

@ -86,7 +86,9 @@ class BankStatement < ApplicationRecord
status == FULLY_BINDED status == FULLY_BINDED
end end
def bind_invoices def bind_invoices(manual: false)
bank_transactions.unbinded.each(&:autobind_invoice) bank_transactions.unbinded.each do |transaction|
transaction.autobind_invoice(manual: manual)
end
end end
end end

View file

@ -13,6 +13,7 @@ class BankTransaction < ApplicationRecord
def binded_invoice def binded_invoice
return unless binded? return unless binded?
account_activity.invoice account_activity.invoice
end end
@ -30,31 +31,54 @@ class BankTransaction < ApplicationRecord
@registrar ||= Invoice.find_by(reference_no: parsed_ref_number)&.buyer @registrar ||= Invoice.find_by(reference_no: parsed_ref_number)&.buyer
end end
# For successful binding, reference number, invoice id and sum must match with the invoice # For successful binding, reference number, invoice id and sum must match with the invoice
def autobind_invoice def autobind_invoice(manual: false)
return if binded? return if binded?
return unless registrar return unless registrar
return unless invoice return unless invoice
return unless invoice.payable? return unless invoice.payable?
create_activity(registrar, invoice) channel = if manual
'admin_payment'
else
'system_payment'
end
create_internal_payment_record(channel: channel, invoice: invoice,
registrar: registrar)
end end
def bind_invoice(invoice_no) def create_internal_payment_record(channel: nil, invoice:, registrar:)
if channel.nil?
create_activity(invoice.buyer, invoice)
return
end
payment_order = PaymentOrder.new_with_type(type: channel, invoice: invoice)
payment_order.save!
if create_activity(registrar, invoice)
payment_order.paid!
else
payment_order.update(notes: 'Failed to create activity', status: 'failed')
end
end
def bind_invoice(invoice_no, manual: false)
if binded? if binded?
errors.add(:base, I18n.t('transaction_is_already_binded')) errors.add(:base, I18n.t('transaction_is_already_binded'))
return return
end end
invoice = Invoice.find_by(number: invoice_no) invoice = Invoice.find_by(number: invoice_no)
@registrar = invoice.buyer errors.add(:base, I18n.t('invoice_was_not_found')) unless invoice
validate_invoice_data(invoice)
return if errors.any?
unless invoice create_internal_payment_record(channel: (manual ? 'admin_payment' : nil), invoice: invoice,
errors.add(:base, I18n.t('invoice_was_not_found')) registrar: invoice.buyer)
return end
end
def validate_invoice_data(invoice)
if invoice.paid? if invoice.paid?
errors.add(:base, I18n.t('invoice_is_already_binded')) errors.add(:base, I18n.t('invoice_is_already_binded'))
return return
@ -65,23 +89,21 @@ class BankTransaction < ApplicationRecord
return return
end end
if invoice.total != sum errors.add(:base, I18n.t('invoice_and_transaction_sums_do_not_match')) if invoice.total != sum
errors.add(:base, I18n.t('invoice_and_transaction_sums_do_not_match'))
return
end
create_activity(invoice.buyer, invoice)
end end
def create_activity(registrar, invoice) def create_activity(registrar, invoice)
ActiveRecord::Base.transaction do activity = AccountActivity.new(
create_account_activity!(account: registrar.cash_account, account: registrar.cash_account, bank_transaction: self,
invoice: invoice, invoice: invoice, sum: invoice.subtotal,
sum: invoice.subtotal, currency: currency, description: description,
currency: currency, activity_type: AccountActivity::ADD_CREDIT
description: description, )
activity_type: AccountActivity::ADD_CREDIT) if activity.save
reset_pending_registrar_balance_reload reset_pending_registrar_balance_reload
true
else
false
end end
end end

View file

@ -7,6 +7,7 @@ class Invoice < ApplicationRecord
has_one :account_activity has_one :account_activity
has_many :items, class_name: 'InvoiceItem', dependent: :destroy has_many :items, class_name: 'InvoiceItem', dependent: :destroy
has_many :directo_records, as: :item, class_name: 'Directo' has_many :directo_records, as: :item, class_name: 'Directo'
has_many :payment_orders
accepts_nested_attributes_for :items accepts_nested_attributes_for :items

102
app/models/payment_order.rb Normal file
View file

@ -0,0 +1,102 @@
class PaymentOrder < ApplicationRecord
include Versions
include ActionView::Helpers::NumberHelper
PAYMENT_INTERMEDIARIES = ENV['payments_intermediaries'].to_s.strip.split(', ').freeze
PAYMENT_BANKLINK_BANKS = ENV['payments_banks'].to_s.strip.split(', ').freeze
INTERNAL_PAYMENT_METHODS = %w[admin_payment system_payment].freeze
PAYMENT_METHODS = [PAYMENT_INTERMEDIARIES, PAYMENT_BANKLINK_BANKS,
INTERNAL_PAYMENT_METHODS].flatten.freeze
CUSTOMER_PAYMENT_METHODS = [PAYMENT_INTERMEDIARIES, PAYMENT_BANKLINK_BANKS].flatten.freeze
belongs_to :invoice, optional: false
validate :invoice_cannot_be_already_paid, on: :create
validate :supported_payment_method
enum status: { issued: 'issued', paid: 'paid', cancelled: 'cancelled',
failed: 'failed' }
attr_accessor :return_url, :response_url
def self.supported_methods
supported = []
PAYMENT_METHODS.each do |method|
class_name = ('PaymentOrders::' + method.camelize).constantize
raise(NoMethodError, class_name) unless class_name < PaymentOrder
supported << class_name
end
supported
end
def self.new_with_type(type:, invoice:)
channel = ('PaymentOrders::' + type.camelize).constantize
PaymentOrder.new(type: channel, invoice: invoice)
end
# Name of configuration namespace
def self.config_namespace_name; end
def supported_payment_method
return if PaymentOrder.supported_method?(type)
errors.add(:type, 'is not supported')
end
def invoice_cannot_be_already_paid
return unless invoice&.paid?
errors.add(:invoice, 'is already paid')
end
def self.supported_method?(name, shortname: false)
some_class = if shortname
('PaymentOrders::' + name.camelize).constantize
else
name.constantize
end
supported_methods.include? some_class
rescue NameError
false
end
def base_transaction(sum:, paid_at:, buyer_name:)
BankTransaction.new(
description: invoice.order,
reference_no: invoice.reference_no,
currency: invoice.currency,
iban: invoice.seller_iban,
sum: sum,
paid_at: paid_at,
buyer_name: buyer_name
)
end
def complete_transaction
return NoMethodError unless payment_received?
paid!
transaction = composed_transaction
transaction.save! && transaction.bind_invoice(invoice.number)
return unless transaction.errors.any?
worded_errors = 'Failed to bind. '
transaction.errors.full_messages.each do |err|
worded_errors << "#{err}, "
end
update!(notes: worded_errors)
end
def channel
type.gsub('PaymentOrders::', '')
end
def form_url
ENV["payments_#{self.class.config_namespace_name}_url"]
end
end

View file

@ -1,15 +0,0 @@
module PaymentOrders
PAYMENT_INTERMEDIARIES = ENV['payments_intermediaries'].to_s.strip.split(', ').freeze
PAYMENT_BANKLINK_BANKS = ENV['payments_banks'].to_s.strip.split(', ').freeze
PAYMENT_METHODS = [PAYMENT_INTERMEDIARIES, PAYMENT_BANKLINK_BANKS].flatten.freeze
def self.create_with_type(type, invoice, opts = {})
raise ArgumentError unless PAYMENT_METHODS.include?(type)
if PAYMENT_BANKLINK_BANKS.include?(type)
BankLink.new(type, invoice, opts)
elsif type == 'every_pay'
EveryPay.new(type, invoice, opts)
end
end
end

View file

@ -0,0 +1,9 @@
module PaymentOrders
class AdminPayment < PaymentOrder
CONFIG_NAMESPACE = 'admin_payment'.freeze
def self.config_namespace_name
CONFIG_NAMESPACE
end
end
end

View file

@ -1,44 +1,44 @@
module PaymentOrders module PaymentOrders
class BankLink < Base class BankLink < PaymentOrder
BANK_LINK_VERSION = '008' BANK_LINK_VERSION = '008'.freeze
NEW_TRANSACTION_SERVICE_NUMBER = '1012' NEW_TRANSACTION_SERVICE_NUMBER = '1012'.freeze
SUCCESSFUL_PAYMENT_SERVICE_NUMBER = '1111' SUCCESSFUL_PAYMENT_SERVICE_NUMBER = '1111'.freeze
CANCELLED_PAYMENT_SERVICE_NUMBER = '1911' CANCELLED_PAYMENT_SERVICE_NUMBER = '1911'.freeze
NEW_MESSAGE_KEYS = %w(VK_SERVICE VK_VERSION VK_SND_ID VK_STAMP VK_AMOUNT NEW_MESSAGE_KEYS = %w[VK_SERVICE VK_VERSION VK_SND_ID VK_STAMP VK_AMOUNT
VK_CURR VK_REF VK_MSG VK_RETURN VK_CANCEL VK_CURR VK_REF VK_MSG VK_RETURN VK_CANCEL
VK_DATETIME).freeze VK_DATETIME].freeze
SUCCESS_MESSAGE_KEYS = %w(VK_SERVICE VK_VERSION VK_SND_ID VK_REC_ID VK_STAMP SUCCESS_MESSAGE_KEYS = %w[VK_SERVICE VK_VERSION VK_SND_ID VK_REC_ID VK_STAMP
VK_T_NO VK_AMOUNT VK_CURR VK_REC_ACC VK_REC_NAME VK_T_NO VK_AMOUNT VK_CURR VK_REC_ACC VK_REC_NAME
VK_SND_ACC VK_SND_NAME VK_REF VK_MSG VK_SND_ACC VK_SND_NAME VK_REF VK_MSG
VK_T_DATETIME).freeze VK_T_DATETIME].freeze
CANCEL_MESSAGE_KEYS = %w(VK_SERVICE VK_VERSION VK_SND_ID VK_REC_ID VK_STAMP CANCEL_MESSAGE_KEYS = %w[VK_SERVICE VK_VERSION VK_SND_ID VK_REC_ID VK_STAMP
VK_REF VK_MSG).freeze VK_REF VK_MSG].freeze
def form_fields def form_fields
hash = {} hash = {}
hash["VK_SERVICE"] = NEW_TRANSACTION_SERVICE_NUMBER hash['VK_SERVICE'] = NEW_TRANSACTION_SERVICE_NUMBER
hash["VK_VERSION"] = BANK_LINK_VERSION hash['VK_VERSION'] = BANK_LINK_VERSION
hash["VK_SND_ID"] = seller_account hash['VK_SND_ID'] = seller_account
hash["VK_STAMP"] = invoice.number hash['VK_STAMP'] = invoice.number
hash["VK_AMOUNT"] = number_with_precision(invoice.total, precision: 2, separator: ".") hash['VK_AMOUNT'] = number_with_precision(invoice.total, precision: 2, separator: ".")
hash["VK_CURR"] = invoice.currency hash['VK_CURR'] = invoice.currency
hash["VK_REF"] = "" hash['VK_REF'] = ''
hash["VK_MSG"] = invoice.order hash['VK_MSG'] = invoice.order
hash["VK_RETURN"] = return_url hash['VK_RETURN'] = return_url
hash["VK_CANCEL"] = return_url hash['VK_CANCEL'] = return_url
hash["VK_DATETIME"] = Time.zone.now.strftime("%Y-%m-%dT%H:%M:%S%z") hash['VK_DATETIME'] = Time.zone.now.strftime('%Y-%m-%dT%H:%M:%S%z')
hash["VK_MAC"] = calc_mac(hash) hash['VK_MAC'] = calc_mac(hash)
hash["VK_ENCODING"] = "UTF-8" hash['VK_ENCODING'] = 'UTF-8'
hash["VK_LANG"] = "ENG" hash['VK_LANG'] = 'ENG'
hash hash
end end
def valid_response_from_intermediary? def valid_response_from_intermediary?
return false unless response return false unless response
case response["VK_SERVICE"] case response['VK_SERVICE']
when SUCCESSFUL_PAYMENT_SERVICE_NUMBER when SUCCESSFUL_PAYMENT_SERVICE_NUMBER
valid_successful_transaction? valid_successful_transaction?
when CANCELLED_PAYMENT_SERVICE_NUMBER when CANCELLED_PAYMENT_SERVICE_NUMBER
@ -48,29 +48,31 @@ module PaymentOrders
end end
end end
def complete_transaction def payment_received?
return unless valid_successful_transaction? valid_response_from_intermediary? && settled_payment?
end
transaction = compose_or_find_transaction def create_failure_report
notes = "User failed to make payment. Bank responded with code #{response['VK_SERVICE']}"
status = 'cancelled'
update!(notes: notes, status: status)
end
transaction.sum = response['VK_AMOUNT'] def composed_transaction
transaction.bank_reference = response['VK_T_NO'] paid_at = Time.parse(response['VK_T_DATETIME'])
transaction.buyer_bank_code = response["VK_SND_ID"] transaction = base_transaction(sum: response['VK_AMOUNT'],
transaction.buyer_iban = response["VK_SND_ACC"] paid_at: paid_at,
transaction.buyer_name = response["VK_SND_NAME"] buyer_name: response['VK_SND_NAME'])
transaction.paid_at = Time.parse(response["VK_T_DATETIME"])
transaction.save! transaction.bank_reference = response['VK_T_NO']
transaction.bind_invoice(invoice.number) transaction.buyer_bank_code = response['VK_SND_ID']
if transaction.errors.empty? transaction.buyer_iban = response['VK_SND_ACC']
Rails.logger.info("Invoice ##{invoice.number} was marked as paid")
else transaction
Rails.logger.error("Failed to bind invoice ##{invoice.number}")
end
end end
def settled_payment? def settled_payment?
response["VK_SERVICE"] == SUCCESSFUL_PAYMENT_SERVICE_NUMBER response['VK_SERVICE'] == SUCCESSFUL_PAYMENT_SERVICE_NUMBER
end end
private private
@ -89,17 +91,15 @@ module PaymentOrders
def valid_amount? def valid_amount?
source = number_with_precision( source = number_with_precision(
BigDecimal(response["VK_AMOUNT"]), precision: 2, separator: "." BigDecimal(response['VK_AMOUNT']), precision: 2, separator: '.'
)
target = number_with_precision(
invoice.total, precision: 2, separator: "."
) )
target = number_with_precision(invoice.total, precision: 2, separator: '.')
source == target source == target
end end
def valid_currency? def valid_currency?
invoice.currency == response["VK_CURR"] invoice.currency == response['VK_CURR']
end end
def sign(data) def sign(data)
@ -117,7 +117,7 @@ module PaymentOrders
def valid_mac?(hash, keys) def valid_mac?(hash, keys)
data = keys.map { |element| prepend_size(hash[element]) }.join data = keys.map { |element| prepend_size(hash[element]) }.join
verify_mac(data, hash["VK_MAC"]) verify_mac(data, hash['VK_MAC'])
end end
def verify_mac(data, mac) def verify_mac(data, mac)
@ -126,22 +126,22 @@ module PaymentOrders
end end
def prepend_size(value) def prepend_size(value)
value = (value || "").to_s.strip value = (value || '').to_s.strip
string = "" string = ''
string << format("%03i", value.size) string << format("%03i", value.size)
string << value string << value
end end
def seller_account def seller_account
ENV["payments_#{type}_seller_account"] ENV["payments_#{self.class.config_namespace_name}_seller_account"]
end end
def seller_certificate def seller_certificate
ENV["payments_#{type}_seller_private"] ENV["payments_#{self.class.config_namespace_name}_seller_private"]
end end
def bank_certificate def bank_certificate
ENV["payments_#{type}_bank_certificate"] ENV["payments_#{self.class.config_namespace_name}_bank_certificate"]
end end
end end
end end

View file

@ -1,59 +0,0 @@
module PaymentOrders
class Base
include ActionView::Helpers::NumberHelper
attr_reader :type,
:invoice,
:return_url,
:response_url,
:response
def initialize(type, invoice, opts = {})
@type = type
@invoice = invoice
@return_url = opts[:return_url]
@response_url = opts[:response_url]
@response = opts[:response]
end
def create_transaction
transaction = BankTransaction.where(description: invoice.order).first_or_initialize(
reference_no: invoice.reference_no,
currency: invoice.currency,
iban: invoice.seller_iban
)
transaction.save!
end
def compose_or_find_transaction
transaction = BankTransaction.find_by(base_transaction_params)
# Transaction already autobinded (possibly) invalid invoice
if transaction.binded?
Rails.logger.info("Transaction #{transaction.id} is already binded")
Rails.logger.info('Creating new BankTransaction record.')
transaction = new_base_transaction
end
transaction
end
def new_base_transaction
BankTransaction.new(base_transaction_params)
end
def base_transaction_params
{
description: invoice.order,
currency: invoice.currency,
iban: invoice.seller_iban,
}
end
def form_url
ENV["payments_#{type}_url"]
end
end
end

View file

@ -1,9 +1,15 @@
module PaymentOrders module PaymentOrders
class EveryPay < Base class EveryPay < PaymentOrder
USER = ENV['payments_every_pay_api_user'].freeze USER = ENV['payments_every_pay_api_user']
KEY = ENV['payments_every_pay_api_key'].freeze KEY = ENV['payments_every_pay_api_key']
ACCOUNT_ID = ENV['payments_every_pay_seller_account'].freeze ACCOUNT_ID = ENV['payments_every_pay_seller_account']
SUCCESSFUL_PAYMENT = %w(settled authorized).freeze SUCCESSFUL_PAYMENT = %w[settled authorized].freeze
CONFIG_NAMESPACE = 'every_pay'.freeze
def self.config_namespace_name
CONFIG_NAMESPACE
end
def form_fields def form_fields
base_json = base_params base_json = base_params
@ -25,25 +31,23 @@ module PaymentOrders
end end
def settled_payment? def settled_payment?
SUCCESSFUL_PAYMENT.include?(response[:payment_state]) SUCCESSFUL_PAYMENT.include?(response['payment_state'])
end end
def complete_transaction def payment_received?
return unless valid_response_from_intermediary? && settled_payment? valid_response_from_intermediary? && settled_payment?
end
transaction = compose_or_find_transaction def composed_transaction
base_transaction(sum: response['amount'],
paid_at: Date.strptime(response['timestamp'], '%s'),
buyer_name: response['cc_holder_name'])
end
transaction.sum = response[:amount] def create_failure_report
transaction.paid_at = Date.strptime(response[:timestamp], '%s') notes = "User failed to make valid payment. Payment state: #{response['payment_state']}"
transaction.buyer_name = response[:cc_holder_name] status = 'cancelled'
update!(notes: notes, status: status)
transaction.save!
transaction.bind_invoice(invoice.number)
if transaction.errors.empty?
Rails.logger.info("Invoice ##{invoice.number} marked as paid")
else
Rails.logger.error("Failed to bind invoice ##{invoice.number}")
end
end end
private private
@ -63,24 +67,27 @@ module PaymentOrders
end end
def valid_hmac? def valid_hmac?
hmac_fields = response[:hmac_fields].split(',') hmac_fields = response['hmac_fields'].split(',')
hmac_hash = {} hmac_hash = {}
hmac_fields.map do |field| hmac_fields.map do |field|
symbol = field.to_sym hmac_hash[field] = response[field]
hmac_hash[symbol] = response[symbol]
end end
hmac_string = hmac_hash.map { |key, _v| "#{key}=#{hmac_hash[key]}" }.join('&') hmac_string = hmac_hash.map { |key, _v| "#{key}=#{hmac_hash[key]}" }.join('&')
expected_hmac = OpenSSL::HMAC.hexdigest('sha1', KEY, hmac_string) expected_hmac = OpenSSL::HMAC.hexdigest('sha1', KEY, hmac_string)
expected_hmac == response[:hmac] expected_hmac == response['hmac']
rescue NoMethodError
false
end end
def valid_amount? def valid_amount?
invoice.total == BigDecimal(response[:amount]) return false unless response.key? 'amount'
invoice.total == BigDecimal(response['amount'])
end end
def valid_account? def valid_account?
response[:account_id] == ACCOUNT_ID response['account_id'] == ACCOUNT_ID
end end
end end
end end

View file

@ -0,0 +1,7 @@
module PaymentOrders
class Lhv < BankLink
def self.config_namespace_name
'lhv'
end
end
end

View file

@ -0,0 +1,7 @@
module PaymentOrders
class Seb < BankLink
def self.config_namespace_name
'seb'
end
end
end

View file

@ -0,0 +1,7 @@
module PaymentOrders
class Swed < BankLink
def self.config_namespace_name
'swed'
end
end
end

View file

@ -0,0 +1,9 @@
module PaymentOrders
class SystemPayment < PaymentOrder
CONFIG_NAMESPACE = 'system_payment'.freeze
def self.config_namespace_name
CONFIG_NAMESPACE
end
end
end

View file

@ -0,0 +1,4 @@
class PaymentOrderVersion < PaperTrail::Version
self.table_name = :log_payment_orders
self.sequence_name = :log_payment_orders_id_seq
end

View file

@ -21,3 +21,5 @@
.col-md-6= render 'registrar/invoices/partials/buyer' .col-md-6= render 'registrar/invoices/partials/buyer'
.row .row
.col-md-12= render 'registrar/invoices/partials/items' .col-md-12= render 'registrar/invoices/partials/items'
.row
.col-md-12= render 'registrar/invoices/partials/payment_orders'

View file

@ -0,0 +1,19 @@
%h4= "Payment Orders"
%hr
.table-responsive
%table.table.table-hover.table-condensed
%thead
%tr
%th{class: 'col-xs-1'}= "#"
%th{class: 'col-xs-1'}= "Channel"
%th{class: 'col-xs-2'}= "Status"
%th{class: 'col-xs-3'}= "Initiated"
%th{class: 'col-xs-4'}= "Notes"
%tbody
- @invoice.payment_orders.each do |payment_order|
%tr
%td= payment_order.id
%td= payment_order.channel
%td= payment_order.status
%td= payment_order.created_at
%td= payment_order.notes

View file

@ -17,4 +17,4 @@
- if @invoice.payable? - if @invoice.payable?
.row.semifooter .row.semifooter
.col-md-6-offset-6.text-right= render 'registrar/invoices/partials/banklinks', locals: { payment_channels: PaymentOrders::PAYMENT_METHODS } .col-md-6-offset-6.text-right= render 'registrar/invoices/partials/banklinks', locals: { payment_channels: PaymentOrder::CUSTOMER_PAYMENT_METHODS }

View file

@ -2,8 +2,8 @@
= t('registrar.invoices.redirected_to_intermediary') = t('registrar.invoices.redirected_to_intermediary')
.payment-form .payment-form
= form_tag @payment.form_url, method: :post do = form_tag @payment_order.form_url, method: :post do
- @payment.form_fields.each do |k, v| - @payment_order.form_fields.each do |k, v|
= hidden_field_tag k, v = hidden_field_tag k, v
= submit_tag t('registrar.invoices.go_to_intermediary') = submit_tag t('registrar.invoices.go_to_intermediary')

View file

@ -0,0 +1,7 @@
en:
registrar:
payments:
back:
payment_successful: 'Thank you! Payment received successfully.'
successful_payment_backend_error: 'We received your payment, but something went wrong on our side. Please contact us via email or phone.'
payment_not_received: 'Payment was unsuccessful. Please make sure you have enough funds on your account and try again.'

View file

@ -128,11 +128,11 @@ Rails.application.routes.draw do
end end
end end
get 'pay/return/:bank' => 'payments#back', as: 'return_payment_with' get 'pay/return/:payment_order' => 'payments#back', as: 'return_payment_with'
post 'pay/return/:bank' => 'payments#back' post 'pay/return/:payment_order' => 'payments#back'
put 'pay/return/:bank' => 'payments#back' put 'pay/return/:payment_order' => 'payments#back'
post 'pay/callback/:bank' => 'payments#callback', as: 'response_payment_with' post 'pay/callback/:payment_order' => 'payments#callback', as: 'response_payment_with'
get 'pay/go/:bank' => 'payments#pay', as: 'payment_with' get 'pay/go/:bank' => 'payments#pay', as: 'payment_with'
namespace :settings do namespace :settings do
resource :balance_auto_reload, controller: :balance_auto_reload, only: %i[edit update destroy] resource :balance_auto_reload, controller: :balance_auto_reload, only: %i[edit update destroy]

View file

@ -0,0 +1,15 @@
class CreatePaymentOrders < ActiveRecord::Migration[5.0]
def change
create_table :payment_orders do |t|
t.string :type, null: false
t.string :status, default: 'issued', null: false
t.belongs_to :invoice, foreign_key: true
t.jsonb :response, null: true
t.string :notes, null: true
t.string :creator_str
t.string :updator_str
t.timestamps
end
end
end

View file

@ -0,0 +1,16 @@
class CreatePaymentOrderVersions < ActiveRecord::Migration[5.0]
def change
create_table :log_payment_orders do |t|
t.string :item_type, null: false
t.integer :item_id, null: false
t.string :event, null: false
t.string :whodunnit
t.jsonb :object
t.jsonb :object_changes
t.datetime :created_at
t.string :session
t.jsonb :children
t.string :uuid
end
end
end

View file

@ -746,7 +746,6 @@ CREATE TABLE public.domains (
locked_by_registrant_at timestamp without time zone, locked_by_registrant_at timestamp without time zone,
force_delete_start timestamp without time zone, force_delete_start timestamp without time zone,
force_delete_data public.hstore force_delete_data public.hstore
); );
@ -1516,6 +1515,44 @@ CREATE SEQUENCE public.log_notifications_id_seq
ALTER SEQUENCE public.log_notifications_id_seq OWNED BY public.log_notifications.id; ALTER SEQUENCE public.log_notifications_id_seq OWNED BY public.log_notifications.id;
--
-- Name: log_payment_orders; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
CREATE TABLE public.log_payment_orders (
id integer NOT NULL,
item_type character varying NOT NULL,
item_id integer NOT NULL,
event character varying NOT NULL,
whodunnit character varying,
object jsonb,
object_changes jsonb,
created_at timestamp without time zone,
session character varying,
children jsonb,
uuid character varying
);
--
-- Name: log_payment_orders_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.log_payment_orders_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: log_payment_orders_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.log_payment_orders_id_seq OWNED BY public.log_payment_orders.id;
-- --
-- Name: log_registrant_verifications; Type: TABLE; Schema: public; Owner: -; Tablespace: -- Name: log_registrant_verifications; Type: TABLE; Schema: public; Owner: -; Tablespace:
-- --
@ -1819,6 +1856,43 @@ CREATE SEQUENCE public.notifications_id_seq
ALTER SEQUENCE public.notifications_id_seq OWNED BY public.notifications.id; ALTER SEQUENCE public.notifications_id_seq OWNED BY public.notifications.id;
--
-- Name: payment_orders; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
CREATE TABLE public.payment_orders (
id integer NOT NULL,
type character varying NOT NULL,
status character varying DEFAULT 'issued'::character varying NOT NULL,
invoice_id integer,
response jsonb,
notes character varying,
creator_str character varying,
updator_str character varying,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
);
--
-- Name: payment_orders_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.payment_orders_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: payment_orders_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.payment_orders_id_seq OWNED BY public.payment_orders.id;
-- --
-- Name: prices; Type: TABLE; Schema: public; Owner: -; Tablespace: -- Name: prices; Type: TABLE; Schema: public; Owner: -; Tablespace:
-- --
@ -2500,6 +2574,13 @@ ALTER TABLE ONLY public.log_nameservers ALTER COLUMN id SET DEFAULT nextval('pub
ALTER TABLE ONLY public.log_notifications ALTER COLUMN id SET DEFAULT nextval('public.log_notifications_id_seq'::regclass); ALTER TABLE ONLY public.log_notifications ALTER COLUMN id SET DEFAULT nextval('public.log_notifications_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.log_payment_orders ALTER COLUMN id SET DEFAULT nextval('public.log_payment_orders_id_seq'::regclass);
-- --
-- Name: id; Type: DEFAULT; Schema: public; Owner: - -- Name: id; Type: DEFAULT; Schema: public; Owner: -
-- --
@ -2556,6 +2637,13 @@ ALTER TABLE ONLY public.nameservers ALTER COLUMN id SET DEFAULT nextval('public.
ALTER TABLE ONLY public.notifications ALTER COLUMN id SET DEFAULT nextval('public.notifications_id_seq'::regclass); ALTER TABLE ONLY public.notifications ALTER COLUMN id SET DEFAULT nextval('public.notifications_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.payment_orders ALTER COLUMN id SET DEFAULT nextval('public.payment_orders_id_seq'::regclass);
-- --
-- Name: id; Type: DEFAULT; Schema: public; Owner: - -- Name: id; Type: DEFAULT; Schema: public; Owner: -
-- --
@ -2905,6 +2993,14 @@ ALTER TABLE ONLY public.log_notifications
ADD CONSTRAINT log_notifications_pkey PRIMARY KEY (id); ADD CONSTRAINT log_notifications_pkey PRIMARY KEY (id);
--
-- Name: log_payment_orders_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
ALTER TABLE ONLY public.log_payment_orders
ADD CONSTRAINT log_payment_orders_pkey PRIMARY KEY (id);
-- --
-- Name: log_registrant_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- Name: log_registrant_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
-- --
@ -2969,6 +3065,14 @@ ALTER TABLE ONLY public.notifications
ADD CONSTRAINT notifications_pkey PRIMARY KEY (id); ADD CONSTRAINT notifications_pkey PRIMARY KEY (id);
--
-- Name: payment_orders_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
ALTER TABLE ONLY public.payment_orders
ADD CONSTRAINT payment_orders_pkey PRIMARY KEY (id);
-- --
-- Name: prices_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- Name: prices_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
-- --
@ -3630,6 +3734,13 @@ CREATE INDEX index_nameservers_on_domain_id ON public.nameservers USING btree (d
CREATE INDEX index_notifications_on_registrar_id ON public.notifications USING btree (registrar_id); CREATE INDEX index_notifications_on_registrar_id ON public.notifications USING btree (registrar_id);
--
-- Name: index_payment_orders_on_invoice_id; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
CREATE INDEX index_payment_orders_on_invoice_id ON public.payment_orders USING btree (invoice_id);
-- --
-- Name: index_prices_on_zone_id; Type: INDEX; Schema: public; Owner: -; Tablespace: -- Name: index_prices_on_zone_id; Type: INDEX; Schema: public; Owner: -; Tablespace:
-- --
@ -3896,6 +4007,14 @@ ALTER TABLE ONLY public.registrant_verifications
ADD CONSTRAINT fk_rails_f41617a0e9 FOREIGN KEY (domain_id) REFERENCES public.domains(id); ADD CONSTRAINT fk_rails_f41617a0e9 FOREIGN KEY (domain_id) REFERENCES public.domains(id);
--
-- Name: fk_rails_f9dc5857c3; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.payment_orders
ADD CONSTRAINT fk_rails_f9dc5857c3 FOREIGN KEY (invoice_id) REFERENCES public.invoices(id);
-- --
-- Name: invoice_items_invoice_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: - -- Name: invoice_items_invoice_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: -
-- --
@ -4341,6 +4460,8 @@ INSERT INTO "schema_migrations" (version) VALUES
('20191227110904'), ('20191227110904'),
('20200113091254'), ('20200113091254'),
('20200115102202'), ('20200115102202'),
('20200130092113'),
('20200203143458'),
('20200204103125'); ('20200204103125');

View file

@ -4,3 +4,10 @@ one:
quantity: 1 quantity: 1
unit: pc unit: pc
invoice: one invoice: one
two:
description: Acme services
price: 5
quantity: 1
unit: pc
invoice: unpaid

View file

@ -24,3 +24,30 @@ one:
reference_no: 13 reference_no: 13
number: 1 number: 1
description: Order nr 1 from registrar 1234567 second number 2345678 description: Order nr 1 from registrar 1234567 second number 2345678
unpaid:
issue_date: <%= Date.parse '2010-07-05' %>
due_date: <%= Date.parse '2010-07-06' %>
currency: EUR
seller_name: Seller Ltd
seller_reg_no: 1234
seller_iban: US75512108001245126199
seller_bank: Main Bank
seller_swift: swift
seller_email: info@seller.test
seller_country_code: US
seller_street: Main Street 1
seller_city: New York
seller_contact_name: John Doe
buyer: bestnames
buyer_name: Buyer Ltd
buyer_reg_no: 12345
buyer_email: info@buyer.test
buyer_country_code: GB
buyer_street: Main Street 2
buyer_city: London
vat_rate: 0.1
total: 16.50
reference_no: 13
number: 2
description: Order nr 2 from registrar 1234567 second number 2345678

27
test/fixtures/payment_orders.yml vendored Normal file
View file

@ -0,0 +1,27 @@
everypay_issued:
type: PaymentOrders::EveryPay
status: issued
invoice: one
response:
notes:
banklink_issued:
type: PaymentOrders::Seb
status: issued
invoice: one
response:
notes:
paid:
type: PaymentOrders::EveryPay
status: paid
invoice: unpaid
response: "{}"
notes:
cancelled:
type: PaymentOrders::Seb
status: cancelled
invoice: unpaid
response: "{}"
notes: User failed to make payment. Bank responded with code 1911

View file

@ -6,34 +6,40 @@ class PaymentCallbackTest < ApplicationIntegrationTest
@user = users(:api_bestnames) @user = users(:api_bestnames)
sign_in @user sign_in @user
@payment_order = payment_orders(:everypay_issued)
@invoice = invoices(:one)
@invoice.update!(account_activity: nil, total: 12)
end end
def test_every_pay_callback_returns_status_200 def test_every_pay_callback_returns_status_200
invoice = payable_invoice request_params = every_pay_request_params
assert_matching_bank_transaction_exists(invoice) post "/registrar/pay/callback/#{@payment_order.id}", params: request_params
request_params = every_pay_request_params.merge(invoice_id: invoice.id)
post "/registrar/pay/callback/every_pay", params: request_params
assert_response :ok assert_response :ok
end end
def test_invoice_is_marked_as_paid
request_params = every_pay_request_params
post "/registrar/pay/callback/#{@payment_order.id}", params: request_params
assert @payment_order.invoice.paid?
end
def failure_log_is_created_if_unsuccessful_payment
request_params = every_pay_request_params.dup
request_params['payment_state'] = 'cancelled'
request_params['transaction_result'] = 'failed'
post "/registrar/pay/callback/#{@payment_order.id}", params: request_params
@payment_order.reload
assert @payment_order.cancelled?
assert_includes @payment_order.notes, 'Payment state: cancelled'
end
private private
def payable_invoice
invoice = invoices(:one)
invoice.update!(account_activity: nil)
invoice
end
def assert_matching_bank_transaction_exists(invoice)
assert BankTransaction.find_by(
description: invoice.description,
currency: invoice.currency,
iban: invoice.seller_iban
), 'Matching bank transaction should exist'
end
def every_pay_request_params def every_pay_request_params
{ {
nonce: "392f2d7748bc8cb0d14f263ebb7b8932", nonce: "392f2d7748bc8cb0d14f263ebb7b8932",

View file

@ -8,6 +8,9 @@ class PaymentReturnTest < ApplicationIntegrationTest
sign_in @user sign_in @user
@invoice = invoices(:one) @invoice = invoices(:one)
@invoice.update!(account_activity: nil, total: 12)
@everypay_order = payment_orders(:everypay_issued)
@banklink_order = payment_orders(:banklink_issued)
end end
def every_pay_request_params def every_pay_request_params
@ -57,33 +60,78 @@ class PaymentReturnTest < ApplicationIntegrationTest
} }
end end
def test_every_pay_return_creates_activity_redirects_to_invoice_path def test_successful_bank_payment_marks_invoice_as_paid
request_params = every_pay_request_params.merge(invoice_id: @invoice.id) @invoice.update!(account_activity: nil)
request_params = bank_link_request_params
post "/registrar/pay/return/every_pay", params: request_params post "/registrar/pay/return/#{@banklink_order.id}", params: request_params
@banklink_order.reload
assert @banklink_order.invoice.paid?
end
def test_every_pay_return_creates_activity_redirects_to_invoice_path
request_params = every_pay_request_params
post "/registrar/pay/return/#{@everypay_order.id}", params: request_params
assert_equal(302, response.status) assert_equal(302, response.status)
assert_redirected_to(registrar_invoice_path(@invoice)) assert_redirected_to(registrar_invoice_path(@invoice))
end end
def test_Every_Pay_return_raises_RecordNotFound def test_every_pay_return_raises_record_not_found
request_params = every_pay_request_params.merge(invoice_id: "178907") request_params = every_pay_request_params
assert_raises(ActiveRecord::RecordNotFound) do assert_raises(ActiveRecord::RecordNotFound) do
post "/registrar/pay/return/every_pay", params: request_params post '/registrar/pay/return/123456', params: request_params
end end
end end
def test_bank_link_return_redirects_to_invoice_paths def test_bank_link_return_redirects_to_invoice_paths
request_params = bank_link_request_params.merge(invoice_id: @invoice.id) request_params = bank_link_request_params
post "/registrar/pay/return/seb", params: request_params post "/registrar/pay/return/#{@banklink_order.id}", params: request_params
assert_equal(302, response.status) assert_equal(302, response.status)
assert_redirected_to(registrar_invoice_path(@invoice)) assert_redirected_to(registrar_invoice_path(@invoice))
end end
def test_bank_link_return def test_bank_link_return
request_params = bank_link_request_params.merge(invoice_id: "178907") request_params = bank_link_request_params
assert_raises(ActiveRecord::RecordNotFound) do assert_raises(ActiveRecord::RecordNotFound) do
post "/registrar/pay/return/seb", params: request_params post '/registrar/pay/return/123456', params: request_params
end end
end end
def test_marks_as_paid_and_adds_notes_if_failed_to_bind
request_params = bank_link_request_params
post "/registrar/pay/return/#{@banklink_order.id}", params: request_params
post "/registrar/pay/return/#{@banklink_order.id}", params: request_params
@banklink_order.reload
assert @banklink_order.notes.present?
assert @banklink_order.paid?
assert_includes @banklink_order.notes, 'Failed to bind'
end
def test_failed_bank_link_payment_creates_brief_error_explanation
request_params = bank_link_request_params.dup
request_params['VK_SERVICE'] = '1911'
post "/registrar/pay/return/#{@banklink_order.id}", params: request_params
@banklink_order.reload
assert_includes @banklink_order.notes, 'Bank responded with code 1911'
end
def test_failed_every_pay_payment_creates_brief_error_explanation
request_params = every_pay_request_params.dup
request_params['payment_state'] = 'cancelled'
request_params['transaction_result'] = 'failed'
post "/registrar/pay/return/#{@everypay_order.id}", params: request_params
@everypay_order.reload
assert_includes @everypay_order.notes, 'Payment state: cancelled'
end
end end

View file

@ -8,7 +8,7 @@ class BankLinkTest < ActiveSupport::TestCase
super super
@invoice = invoices(:one) @invoice = invoices(:one)
@invoice.update!(total: 12) @invoice.update!(account_activity: nil, total: 12)
travel_to '2018-04-01 00:30 +0300' travel_to '2018-04-01 00:30 +0300'
create_new_bank_link create_new_bank_link
@ -36,11 +36,11 @@ class BankLinkTest < ActiveSupport::TestCase
'VK_MAC': 'CZZvcptkxfuOxRR88JmT4N+Lw6Hs4xiQfhBWzVYldAcRTQbcB/lPf9MbJzBE4e1/HuslQgkdCFt5g1xW2lJwrVDBQTtP6DAHfvxU3kkw7dbk0IcwhI4whUl68/QCwlXEQTAVDv1AFnGVxXZ40vbm/aLKafBYgrirB5SUe8+g9FE=', 'VK_MAC': 'CZZvcptkxfuOxRR88JmT4N+Lw6Hs4xiQfhBWzVYldAcRTQbcB/lPf9MbJzBE4e1/HuslQgkdCFt5g1xW2lJwrVDBQTtP6DAHfvxU3kkw7dbk0IcwhI4whUl68/QCwlXEQTAVDv1AFnGVxXZ40vbm/aLKafBYgrirB5SUe8+g9FE=',
'VK_ENCODING': 'UTF-8', 'VK_ENCODING': 'UTF-8',
'VK_LANG': 'ENG' 'VK_LANG': 'ENG'
}.with_indifferent_access }.as_json
@completed_bank_link = PaymentOrders::BankLink.new( @completed_bank_link = PaymentOrder.new(type: 'PaymentOrders::Seb',
'seb', @invoice, { response: params } invoice: @invoice,
) response: params)
end end
def create_cancelled_bank_link def create_cancelled_bank_link
@ -55,16 +55,17 @@ class BankLinkTest < ActiveSupport::TestCase
'VK_MAC': 'PElE2mYXXN50q2UBvTuYU1rN0BmOQcbafPummDnWfNdm9qbaGQkGyOn0XaaFGlrdEcldXaHBbZKUS0HegIgjdDfl2NOk+wkLNNH0Iu38KzZaxHoW9ga7vqiyKHC8dcxkHiO9HsOnz77Sy/KpWCq6cz48bi3fcMgo+MUzBMauWoQ=', 'VK_MAC': 'PElE2mYXXN50q2UBvTuYU1rN0BmOQcbafPummDnWfNdm9qbaGQkGyOn0XaaFGlrdEcldXaHBbZKUS0HegIgjdDfl2NOk+wkLNNH0Iu38KzZaxHoW9ga7vqiyKHC8dcxkHiO9HsOnz77Sy/KpWCq6cz48bi3fcMgo+MUzBMauWoQ=',
'VK_ENCODING': 'UTF-8', 'VK_ENCODING': 'UTF-8',
'VK_LANG': 'ENG' 'VK_LANG': 'ENG'
}.with_indifferent_access }.as_json
@cancelled_bank_link = PaymentOrders::BankLink.new( @cancelled_bank_link = PaymentOrder.new(type: 'PaymentOrders::Seb',
'seb', @invoice, { response: params } invoice: @invoice,
) response: params)
end end
def create_new_bank_link def create_new_bank_link
params = { return_url: 'return.url', response_url: 'response.url' } @new_bank_link = PaymentOrder.new(type: 'PaymentOrders::Seb', invoice: @invoice)
@new_bank_link = PaymentOrders::BankLink.new('seb', @invoice, params) @new_bank_link.return_url = 'return.url'
@new_bank_link.response_url = 'response.url'
end end
def test_response_is_not_valid_when_it_is_missing def test_response_is_not_valid_when_it_is_missing
@ -105,23 +106,14 @@ class BankLinkTest < ActiveSupport::TestCase
refute(@cancelled_bank_link.settled_payment?) refute(@cancelled_bank_link.settled_payment?)
end end
def test_complete_transaction_calls_methods_on_transaction def test_successful_payment_creates_bank_transaction
mock_transaction = MiniTest::Mock.new @completed_bank_link.complete_transaction
mock_transaction.expect(:sum= , '12.00', ['12.00'])
mock_transaction.expect(:bank_reference= , '1', ['1'])
mock_transaction.expect(:buyer_bank_code= , 'testvpos', ['testvpos'])
mock_transaction.expect(:buyer_iban= , '1234', ['1234'])
mock_transaction.expect(:paid_at= , Date.parse('2018-04-01 00:30:00 +0300'), [Time.parse('2018-04-01T00:30:00+0300')])
mock_transaction.expect(:buyer_name=, 'John Doe', ['John Doe'])
mock_transaction.expect(:save!, true)
mock_transaction.expect(:binded?, false)
mock_transaction.expect(:bind_invoice, AccountActivity.new, [1])
mock_transaction.expect(:errors, [])
BankTransaction.stub(:find_by, mock_transaction) do transaction = BankTransaction.find_by(
@completed_bank_link.complete_transaction sum: @completed_bank_link.response['VK_AMOUNT'],
end buyer_name: @completed_bank_link.response['VK_SND_NAME']
)
mock_transaction.verify assert transaction.present?
end end
end end

View file

@ -4,36 +4,38 @@ class EveryPayTest < ActiveSupport::TestCase
def setup def setup
super super
@invoice = invoices(:one) @invoice = invoices(:unpaid)
@invoice.update!(total: 12) @invoice.update!(total: 12)
params = { response = {
response: "utf8": '✓',
{ "_method": 'put',
utf8: '✓', "authenticity_token": 'OnA69vbccQtMt3C9wxEWigs5Gpf/7z+NoxRCMkFPlTvaATs8+OgMKF1I4B2f+vuK37zCgpWZaWWtyuslRRSwkw=="',
_method: 'put', "nonce": '392f2d7748bc8cb0d14f263ebb7b8932',
authenticity_token: 'OnA69vbccQtMt3C9wxEWigs5Gpf/7z+NoxRCMkFPlTvaATs8+OgMKF1I4B2f+vuK37zCgpWZaWWtyuslRRSwkw==', "timestamp": '1524136727',
nonce: '392f2d7748bc8cb0d14f263ebb7b8932', "api_username": 'ca8d6336dd750ddb',
timestamp: '1524136727', "transaction_result": 'completed',
api_username: 'ca8d6336dd750ddb', "payment_reference": 'fd5d27b59a1eb597393cd5ff77386d6cab81ae05067e18d530b10f3802e30b56',
transaction_result: 'completed', "payment_state": 'settled',
payment_reference: 'fd5d27b59a1eb597393cd5ff77386d6cab81ae05067e18d530b10f3802e30b56', "amount": '12.00',
payment_state: 'settled', "order_reference": 'e468a2d59a731ccc546f2165c3b1a6',
amount: '12.00', "account_id": 'EUR3D1',
order_reference: 'e468a2d59a731ccc546f2165c3b1a6', "cc_type": 'master_card',
account_id: 'EUR3D1', "cc_last_four_digits": '0487',
cc_type: 'master_card', "cc_month": '10',
cc_last_four_digits: '0487', "cc_year": '2018',
cc_month: '10', "cc_holder_name": 'John Doe',
cc_year: '2018', "hmac_fields": 'account_id,amount,api_username,cc_holder_name,cc_last_four_digits,cc_month,cc_type,cc_year,hmac_fields,nonce,order_reference,payment_reference,payment_state,timestamp,transaction_result',
cc_holder_name: 'John Doe', "hmac": 'efac1c732835668cd86023a7abc140506c692f0d',
hmac_fields: 'account_id,amount,api_username,cc_holder_name,cc_last_four_digits,cc_month,cc_type,cc_year,hmac_fields,nonce,order_reference,payment_reference,payment_state,timestamp,transaction_result', "invoice_id": '2'
hmac: 'efac1c732835668cd86023a7abc140506c692f0d', }.as_json
invoice_id: '1',
}, @successful_payment = PaymentOrder.new(type: 'PaymentOrders::EveryPay',
} invoice: @invoice,
@every_pay = PaymentOrders::EveryPay.new('every_pay', @invoice, params) response: response)
@other_pay = PaymentOrders::EveryPay.new('every_pay', @invoice, {})
@failed_payment = @successful_payment.dup
@failed_payment.response['payment_state'] = 'cancelled'
travel_to Time.zone.parse('2018-04-01 00:30:00 +0000') travel_to Time.zone.parse('2018-04-01 00:30:00 +0000')
end end
@ -47,39 +49,37 @@ class EveryPayTest < ActiveSupport::TestCase
transaction_type: 'charge', transaction_type: 'charge',
hmac_fields: 'account_id,amount,api_username,callback_url,customer_url,hmac_fields,nonce,order_reference,timestamp,transaction_type' hmac_fields: 'account_id,amount,api_username,callback_url,customer_url,hmac_fields,nonce,order_reference,timestamp,transaction_type'
} }
form_fields = @every_pay.form_fields form_fields = @successful_payment.form_fields
expected_fields.each do |k, v| expected_fields.each do |k, v|
assert_equal(v, form_fields[k]) assert_equal(v, form_fields[k])
end end
end end
def test_valid_response_from_intermediary? def test_valid_response_from_intermediary?
assert(@every_pay.valid_response_from_intermediary?) assert(@successful_payment.valid_response_from_intermediary?)
refute(@other_pay.valid_response_from_intermediary?)
@failed_payment.response = { 'what': 'definitely not valid everypay response' }
refute(@failed_payment.valid_response_from_intermediary?)
end
def test_valid_and_successful_payment_is_determined
assert(@successful_payment.payment_received?)
refute(@failed_payment.payment_received?)
end end
def test_settled_payment? def test_settled_payment?
assert(@every_pay.settled_payment?) assert(@successful_payment.settled_payment?)
other_pay = PaymentOrders::EveryPay.new( refute(@failed_payment.settled_payment?)
'every_pay', @invoice, {response: {payment_state: 'CANCELLED'}}
)
refute(other_pay.settled_payment?)
end end
def test_complete_transaction_calls_methods_on_transaction def test_successful_payment_creates_bank_transaction
mock_transaction = MiniTest::Mock.new @successful_payment.complete_transaction
mock_transaction.expect(:sum= , '12.00', ['12.00'])
mock_transaction.expect(:paid_at= , Date.strptime('1524136727', '%s'), [Date.strptime('1524136727', '%s')])
mock_transaction.expect(:buyer_name=, 'John Doe', ['John Doe'])
mock_transaction.expect(:save!, true)
mock_transaction.expect(:binded?, false)
mock_transaction.expect(:bind_invoice, AccountActivity.new, [1])
mock_transaction.expect(:errors, [])
BankTransaction.stub(:find_by, mock_transaction) do transaction = BankTransaction.find_by(
@every_pay.complete_transaction sum: @successful_payment.response['amount'],
end buyer_name: @successful_payment.response['cc_holder_name']
)
mock_transaction.verify assert transaction.present?
end end
end end

View file

@ -5,23 +5,21 @@ class PaymentOrdersTest < ActiveSupport::TestCase
super super
@original_methods = ENV['payment_methods'] @original_methods = ENV['payment_methods']
@original_seb_URL = ENV['seb_payment_url'] @original_seb_url = ENV['seb_payment_url']
ENV['payment_methods'] = 'seb, swed, credit_card' ENV['payment_methods'] = 'seb, swed, every_pay'
ENV['seb_payment_url'] = nil ENV['seb_payment_url'] = nil
@not_implemented_payment = PaymentOrders::Base.new( @not_implemented_payment = PaymentOrder.new(invoice: Invoice.new)
'not_implemented', Invoice.new
)
end end
def teardown def teardown
super super
ENV['payment_methods'] = @original_methods ENV['payment_methods'] = @original_methods
ENV['seb_payment_url'] = @original_seb_URL ENV['seb_payment_url'] = @original_seb_url
end end
def test_variable_assignment def test_variable_assignment
assert_equal 'not_implemented', @not_implemented_payment.type assert_nil @not_implemented_payment.type
assert_nil @not_implemented_payment.response_url assert_nil @not_implemented_payment.response_url
assert_nil @not_implemented_payment.return_url assert_nil @not_implemented_payment.return_url
assert_nil @not_implemented_payment.form_url assert_nil @not_implemented_payment.form_url
@ -45,14 +43,61 @@ class PaymentOrdersTest < ActiveSupport::TestCase
end end
end end
def test_that_create_with_type_raises_argument_error def test_correct_channel_is_assigned
assert_raise ArgumentError do everypay_channel = PaymentOrder.new_with_type(type: 'every_pay', invoice: @invoice)
PaymentOrders.create_with_type("not_implemented", Invoice.new) assert_equal everypay_channel.channel, 'EveryPay'
assert_equal everypay_channel.class.config_namespace_name, 'every_pay'
swed_channel = PaymentOrder.new_with_type(type: 'swed', invoice: @invoice)
assert_equal swed_channel.channel, 'Swed'
assert_equal swed_channel.class.config_namespace_name, 'swed'
seb_channel = PaymentOrder.new_with_type(type: 'seb', invoice: @invoice)
assert_equal seb_channel.channel, 'Seb'
assert_equal seb_channel.class.config_namespace_name, 'seb'
lhv_channel = PaymentOrder.new_with_type(type: 'lhv', invoice: @invoice)
assert_equal lhv_channel.channel, 'Lhv'
assert_equal lhv_channel.class.config_namespace_name, 'lhv'
admin_channel = PaymentOrder.new_with_type(type: 'admin_payment', invoice: @invoice)
assert_equal admin_channel.channel, 'AdminPayment'
assert_equal admin_channel.class.config_namespace_name, 'admin_payment'
system_channel = PaymentOrder.new_with_type(type: 'system_payment', invoice: @invoice)
assert_equal system_channel.channel, 'SystemPayment'
assert_equal system_channel.class.config_namespace_name, 'system_payment'
end
def test_can_not_create_order_for_paid_invoice
invoice = invoices(:one)
payment_order = PaymentOrder.new_with_type(type: 'every_pay', invoice: invoice)
assert payment_order.invalid?
assert_includes payment_order.errors[:invoice], 'is already paid'
end
def test_order_without_channel_is_invalid
payment_order = PaymentOrder.new
assert payment_order.invalid?
assert_includes payment_order.errors[:type], 'is not supported'
end
def test_can_not_create_order_with_invalid_type
assert_raise NameError do
PaymentOrder.new_with_type(type: 'not_implemented', invoice: Invoice.new)
end end
end end
def test_create_with_correct_subclass def test_supported_method_bool_does_not_fail
payment = PaymentOrders.create_with_type('seb', Invoice.new) assert_not PaymentOrder.supported_method?('not_implemented', shortname: true)
assert_equal PaymentOrders::BankLink, payment.class assert PaymentOrder.supported_method?('every_pay', shortname: true)
assert_not PaymentOrder.supported_method?('PaymentOrders::NonExistant')
assert PaymentOrder.supported_method?('PaymentOrders::EveryPay')
end
def test_can_create_with_correct_subclass
payment = PaymentOrder.new_with_type(type: 'seb', invoice: Invoice.new)
assert_equal PaymentOrders::Seb, payment.class
end end
end end

View file

@ -58,6 +58,30 @@ class ProcessPaymentsTaskTest < ActiveSupport::TestCase
assert @invoice.paid? assert @invoice.paid?
end end
def test_attaches_paid_payment_order_to_invoice
assert @invoice.unpaid?
capture_io { run_task }
@invoice.reload
payment_order = @invoice.payment_orders.last
assert_equal 'PaymentOrders::SystemPayment', payment_order.type
assert payment_order.paid?
end
def test_attaches_failed_payment_order_to_invoice
assert @invoice.unpaid?
account = accounts(:cash)
account.update!(registrar: registrars(:goodnames))
capture_io { run_task }
@invoice.reload
payment_order = @invoice.payment_orders.last
assert_equal 'PaymentOrders::SystemPayment', payment_order.type
assert payment_order.failed?
end
def test_output def test_output
assert_output "Transactions processed: 1\n" do assert_output "Transactions processed: 1\n" do
run_task run_task
@ -75,4 +99,4 @@ class ProcessPaymentsTaskTest < ActiveSupport::TestCase
invoice.update!({ account_activity: nil, cancelled_at: nil }.merge(attributes)) invoice.update!({ account_activity: nil, cancelled_at: nil }.merge(attributes))
invoice invoice
end end
end end