Initial commit
This commit is contained in:
485
spec/api/v2/management/transfers_spec.rb
Normal file
485
spec/api/v2/management/transfers_spec.rb
Normal file
@@ -0,0 +1,485 @@
|
||||
# encoding: UTF-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe API::V2::Management::Transfers, type: :request do
|
||||
before do
|
||||
defaults_for_management_api_v1_security_configuration!
|
||||
management_api_v1_security_configuration.merge! \
|
||||
scopes: {
|
||||
read_transfers: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
|
||||
write_transfers: { permitted_signers: %i[alex jeff james], mandatory_signers: %i[alex jeff] }
|
||||
}
|
||||
end
|
||||
|
||||
describe 'create operation' do
|
||||
def request
|
||||
post_json '/api/v2/management/transfers/new', multisig_jwt_management_api_v1({ data: data }, *signers)
|
||||
end
|
||||
|
||||
let(:currency) { Currency.coins.sample }
|
||||
let(:signers) { %i[alex jeff] }
|
||||
let(:data) do
|
||||
{ key: generate(:transfer_key),
|
||||
category: Transfer::CATEGORIES.sample,
|
||||
description: "Referral program payoffs (#{Time.now.to_date})",
|
||||
operations: operations }
|
||||
end
|
||||
|
||||
let(:valid_operation) do
|
||||
{ currency: :btc,
|
||||
amount: '0.0001',
|
||||
account_src: {
|
||||
code: 102
|
||||
},
|
||||
account_dst: {
|
||||
code: 102
|
||||
} }
|
||||
end
|
||||
|
||||
context 'automatically creates an account, if transfer will be send' do
|
||||
let!(:sender_member) { create(:member, :level_3) }
|
||||
let!(:receiver_member) { create(:member, :level_3) }
|
||||
let!(:deposit) { create(:deposit_btc, member: sender_member, amount: 1) }
|
||||
|
||||
let(:operation) do
|
||||
{
|
||||
currency: :btc,
|
||||
amount: '0.5',
|
||||
account_src: {
|
||||
code: 202,
|
||||
uid: sender_member.uid
|
||||
},
|
||||
account_dst: {
|
||||
code: 202,
|
||||
uid: receiver_member.uid
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:operations) { [operation] }
|
||||
|
||||
before do
|
||||
deposit.accept!
|
||||
deposit.process!
|
||||
deposit.dispatch!
|
||||
end
|
||||
|
||||
it do
|
||||
expect(receiver_member.accounts.count).to eq(0)
|
||||
|
||||
request
|
||||
expect(response).to have_http_status(201)
|
||||
expect(receiver_member.accounts.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'empty key' do
|
||||
let(:operations) { [valid_operation] }
|
||||
|
||||
before do
|
||||
data.delete(:key)
|
||||
request
|
||||
end
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/key is missing/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'empty category' do
|
||||
let(:operations) {[valid_operation]}
|
||||
|
||||
before do
|
||||
data.delete(:category)
|
||||
request
|
||||
end
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/category is missing/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'empty description' do
|
||||
let(:operations) {[valid_operation]}
|
||||
|
||||
before do
|
||||
data.delete(:description)
|
||||
request
|
||||
end
|
||||
|
||||
it { expect(response).to have_http_status(201) }
|
||||
end
|
||||
|
||||
context 'empty operations' do
|
||||
let(:operations) {[]}
|
||||
|
||||
before { request }
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/operations is empty/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid account code' do
|
||||
let(:operations) do
|
||||
valid_operation[:account_src][:code] = 999
|
||||
[valid_operation]
|
||||
end
|
||||
before { request }
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/does not have a valid value/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid currency' do
|
||||
let(:operations) do
|
||||
valid_operation[:currency] = :neo
|
||||
[valid_operation]
|
||||
end
|
||||
before { request }
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/does not have a valid value/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid amount' do
|
||||
let(:operations) do
|
||||
valid_operation[:amount] = -1
|
||||
[valid_operation]
|
||||
end
|
||||
before { request }
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response.body).to match(/does not have a valid value/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'existing transfer key' do
|
||||
let(:operations) {[valid_operation]}
|
||||
before do
|
||||
t = create(:transfer)
|
||||
data[:key] = t.key
|
||||
request
|
||||
end
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/key has already been taken/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'debit liability on account with insufficient balance' do
|
||||
let!(:sender_member) { create(:member, :level_3) }
|
||||
let!(:receiver_member) { create(:member, :level_3) }
|
||||
|
||||
let!(:deposit) { create(:deposit_btc, member: sender_member, amount: 1) }
|
||||
let(:operation) do
|
||||
{
|
||||
currency: :btc,
|
||||
amount: '1.1',
|
||||
account_src: {
|
||||
code: 202,
|
||||
uid: sender_member.uid
|
||||
},
|
||||
account_dst: {
|
||||
code: 202,
|
||||
uid: receiver_member.uid
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:operations) {[operation]}
|
||||
|
||||
before { request }
|
||||
|
||||
it do
|
||||
expect(response).to have_http_status 422
|
||||
expect(response.body).to match(/account balance is insufficient/i)
|
||||
end
|
||||
end
|
||||
|
||||
context 'referral program story' do
|
||||
# In case of referral program some fees received by platform
|
||||
# during trading are returned to the referrer once per 4h for example.
|
||||
# We debit Revenue account balance and credit member Liabilities.
|
||||
|
||||
before do
|
||||
# Credit Revenue accounts.
|
||||
create(:revenue, currency_id: base_unit,
|
||||
code: coin_revenues_code, credit: 10)
|
||||
create(:revenue, currency_id: quote_unit,
|
||||
code: fiat_revenues_code, credit: 100)
|
||||
end
|
||||
|
||||
let(:referrer1) { create(:member, :barong) }
|
||||
let(:referrer2) { create(:member, :barong) }
|
||||
let(:referrer3) { create(:member, :barong) }
|
||||
|
||||
let(:base_unit) { Currency.coins.ids.sample }
|
||||
let(:quote_unit) { Currency.fiats.ids.sample }
|
||||
|
||||
let(:coin_liabilities_code) { 202 }
|
||||
let(:fiat_liabilities_code) { 201 }
|
||||
|
||||
let(:coin_revenues_code) { 302 }
|
||||
let(:fiat_revenues_code) { 301 }
|
||||
|
||||
# Consider we have BTC/USD market
|
||||
# Return all BTC/USD fees for 4h in single batch.
|
||||
# Balance changes:
|
||||
# Liability:
|
||||
# referrer1:
|
||||
# base_unit: 0.0001 + 0.0003
|
||||
# quote_unit: 0
|
||||
# referrer2:
|
||||
# base_unit: 0.00015
|
||||
# quote_unit: 0.05
|
||||
# referrer3:
|
||||
# base_unit: 0
|
||||
# quote_unit: 0.075
|
||||
# Revenue:
|
||||
# base_unit: -(0.0001 + 0.0003 + 0.00015)
|
||||
# quote_unit -(0.05 + 0.075)
|
||||
let(:operations) do
|
||||
[
|
||||
{
|
||||
currency: base_unit,
|
||||
amount: '0.0001',
|
||||
account_src: {
|
||||
code: coin_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: referrer1.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: base_unit,
|
||||
amount: '0.00015',
|
||||
account_src: {
|
||||
code: coin_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: referrer2.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: base_unit,
|
||||
amount: '0.0003',
|
||||
account_src: {
|
||||
code: coin_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: referrer1.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: quote_unit,
|
||||
amount: '0.075',
|
||||
account_src: {
|
||||
code: fiat_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: fiat_liabilities_code,
|
||||
uid: referrer3.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: quote_unit,
|
||||
amount: '0.05',
|
||||
account_src: {
|
||||
code: fiat_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: fiat_liabilities_code,
|
||||
uid: referrer2.uid
|
||||
}
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
it do
|
||||
request
|
||||
expect(response).to have_http_status 201
|
||||
end
|
||||
|
||||
it 'returns transfer with operations' do
|
||||
request
|
||||
expect(JSON.parse(response.body)['key']).to eq data[:key]
|
||||
expect(JSON.parse(response.body)['category']).to eq data[:category]
|
||||
expect(JSON.parse(response.body)['description']).to eq data[:description]
|
||||
expect(JSON.parse(response.body)['liabilities'].size).to eq operations.size
|
||||
expect(JSON.parse(response.body)['revenues'].size).to eq operations.size
|
||||
end
|
||||
|
||||
it 'saves liabilities' do
|
||||
expect { request }.to change(::Operations::Liability, :count).by(operations.size)
|
||||
end
|
||||
|
||||
it 'saves revenues' do
|
||||
expect { request }.to change(::Operations::Revenue, :count).by(operations.size)
|
||||
end
|
||||
|
||||
it 'updates legacy balances' do
|
||||
expect { request }.to change{ referrer1.get_account(base_unit).balance }.by(0.0001 + 0.0003).and \
|
||||
change{ referrer2.get_account(base_unit).balance }.by(0.00015).and \
|
||||
change{ referrer2.get_account(quote_unit).balance }.by(0.05).and \
|
||||
change{ referrer3.get_account(quote_unit).balance }.by(0.075)
|
||||
end
|
||||
|
||||
context 'wrong account code' do
|
||||
let(:operations) do
|
||||
[
|
||||
{
|
||||
currency: base_unit,
|
||||
amount: '0.0001',
|
||||
account_src: {
|
||||
code: fiat_revenues_code # Wrong code because base_unit is coin.
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: referrer2.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: quote_unit,
|
||||
amount: '0.05',
|
||||
account_src: {
|
||||
code: fiat_revenues_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code, # Wrong code because quote_unit is fiat.
|
||||
uid: referrer2.uid
|
||||
}
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
it do
|
||||
request
|
||||
expect(response).to have_http_status 422
|
||||
end
|
||||
|
||||
it 'doesn\'t save transfer' do
|
||||
expect { request }.to_not change(Transfer, :count)
|
||||
end
|
||||
|
||||
it 'doesn\'t save liabilities' do
|
||||
expect { request }.to_not change(::Operations::Liability, :count)
|
||||
end
|
||||
|
||||
it 'doesn\'t save revenues' do
|
||||
expect { request }.to_not change(::Operations::Revenue, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'token distribution story' do
|
||||
# In token distribution story we credit member token balance
|
||||
# once member is signed in for the first time.
|
||||
before do
|
||||
# Add token-distribution Liabilities account.
|
||||
Rails.configuration.x.chart_of_accounts << coin_distribution_account
|
||||
create(:operations_account, coin_distribution_account)
|
||||
end
|
||||
|
||||
before do
|
||||
# 1. Credit main Assets account.
|
||||
# 2. Credit token-distribution Liabilities account.
|
||||
# So we keep Balance Sheet equal Income Statement.
|
||||
create(:asset, currency_id: coin,
|
||||
code: coin_assets_code, credit: 1000)
|
||||
create(:liability, currency_id: coin,
|
||||
code: coin_distribution_account_code, credit: 1000, member_id: nil)
|
||||
end
|
||||
|
||||
let(:member1) { create(:member, :barong) }
|
||||
let(:member2) { create(:member, :barong) }
|
||||
|
||||
let(:coin) { :trst }
|
||||
|
||||
let(:coin_assets_code) { 102 }
|
||||
|
||||
let(:coin_liabilities_code) { 202 }
|
||||
let(:coin_distribution_account_code) do
|
||||
coin_distribution_account[:code]
|
||||
end
|
||||
let(:coin_distribution_account) do
|
||||
{ code: 292,
|
||||
type: :liability,
|
||||
kind: 'token-distribution',
|
||||
currency_type: :coin,
|
||||
description: 'Token Distributions Liabilities Account',
|
||||
scope: :platform
|
||||
}
|
||||
end
|
||||
|
||||
# Balance changes:
|
||||
# Liability-main:
|
||||
# member1:
|
||||
# coin: 10
|
||||
# member2:
|
||||
# coin: 5
|
||||
# Liability-token-distribution:
|
||||
# coin: -15
|
||||
let(:operations) do
|
||||
[
|
||||
{
|
||||
currency: coin,
|
||||
amount: 10,
|
||||
account_src: {
|
||||
code: coin_distribution_account_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: member1.uid
|
||||
}
|
||||
},
|
||||
{
|
||||
currency: coin,
|
||||
amount: 5,
|
||||
account_src: {
|
||||
code: coin_distribution_account_code
|
||||
},
|
||||
account_dst: {
|
||||
code: coin_liabilities_code,
|
||||
uid: member2.uid
|
||||
}
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
it do
|
||||
request
|
||||
expect(response).to have_http_status 201
|
||||
end
|
||||
|
||||
it 'returns transfer with liabilities' do
|
||||
request
|
||||
expect(JSON.parse(response.body)['key']).to eq data[:key]
|
||||
expect(JSON.parse(response.body)['category']).to eq data[:category]
|
||||
expect(JSON.parse(response.body)['description']).to eq data[:description]
|
||||
# Two liability operation for each token-distribution operation.
|
||||
expect(JSON.parse(response.body)['liabilities'].size).to eq operations.size * 2
|
||||
end
|
||||
|
||||
it 'saves liabilities' do
|
||||
expect { request }.to change(::Operations::Liability, :count).by(operations.size * 2)
|
||||
end
|
||||
|
||||
it 'updates legacy balance' do
|
||||
expect { request }.to change{ member1.get_account(coin).balance }.by(10).and \
|
||||
change{ member2.get_account(coin).balance }.by(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user