635 lines
19 KiB
Ruby
635 lines
19 KiB
Ruby
# encoding: UTF-8
|
|
# frozen_string_literal: true
|
|
|
|
describe Withdraw do
|
|
context 'aasm_state' do
|
|
subject { create(:usd_withdraw, :with_deposit_liability, sum: 1000) }
|
|
|
|
before do
|
|
subject.stubs(:send_withdraw_confirm_email)
|
|
end
|
|
|
|
it 'initializes with state :prepared' do
|
|
expect(subject.prepared?).to be true
|
|
end
|
|
|
|
it 'transitions to :rejected after calling #reject!' do
|
|
subject.accept!
|
|
subject.reject!
|
|
|
|
expect(subject.rejected?).to be true
|
|
end
|
|
|
|
context :accept do
|
|
it 'transitions to :submitted after calling #accept!' do
|
|
subject.accept!
|
|
expect(subject.accepted?).to be true
|
|
expect(subject.sum).to eq subject.account.locked
|
|
end
|
|
|
|
context :record_submit_operations! do
|
|
it 'creates two liability operations' do
|
|
expect{ subject.accept! }.to change{ Operations::Liability.count }.by(2)
|
|
end
|
|
|
|
it 'doesn\'t create asset operations' do
|
|
expect{ subject.accept! }.to_not change{ Operations::Asset.count }
|
|
end
|
|
|
|
it 'debits main liabilities for member' do
|
|
expect{ subject.accept! }.to change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :main)
|
|
}.by(-subject.sum)
|
|
end
|
|
|
|
it 'credits locked liabilities for member' do
|
|
expect{ subject.accept! }.to change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
|
}.by(subject.sum)
|
|
end
|
|
|
|
it 'updates both legacy and operations based member balance' do
|
|
subject.accept!
|
|
|
|
%i[main locked].each do |kind|
|
|
expect(
|
|
subject.member.balance_for(currency: subject.currency, kind: kind)
|
|
).to eq(
|
|
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context :process do
|
|
before { subject.accept! }
|
|
before { subject.accept! }
|
|
|
|
it 'transitions to :processing after calling #process! when withdrawing fiat currency' do
|
|
subject.currency.stubs(:coin?).returns(false)
|
|
|
|
subject.process!
|
|
|
|
expect(subject.processing?).to be true
|
|
end
|
|
|
|
it 'transitions to :failed after calling #fail! when withdrawing fiat currency' do
|
|
subject.currency.stubs(:coin?).returns(false)
|
|
|
|
subject.process!
|
|
|
|
expect { subject.fail! }.to_not change { subject.account.amount }
|
|
|
|
expect(subject.failed?).to be true
|
|
end
|
|
|
|
it 'transitions to :processing after calling #process!' do
|
|
subject.expects(:send_coins!)
|
|
|
|
subject.process!
|
|
|
|
expect(subject.processing?).to be true
|
|
end
|
|
|
|
it 'transitions to :processing after calling #process from :skipped' do
|
|
subject.process!
|
|
expect(subject.processing?).to be true
|
|
|
|
subject.skip!
|
|
expect(subject.skipped?).to be true
|
|
|
|
subject.process!
|
|
expect(subject.processing?).to be true
|
|
end
|
|
|
|
it 'transitions to :errored after calling #err from :processing' do
|
|
subject.process!
|
|
expect(subject.processing?).to be true
|
|
|
|
expect { subject.err! StandardError.new }.to_not change { subject.account.amount }
|
|
expect(subject.errored?).to be true
|
|
|
|
subject.process!
|
|
expect(subject.processing?).to be true
|
|
end
|
|
end
|
|
|
|
context :cancel do
|
|
it 'transitions to :canceled after calling #cancel!' do
|
|
subject.cancel!
|
|
|
|
expect(subject.canceled?).to be true
|
|
end
|
|
|
|
it 'transitions from :submitted to :canceled after calling #cancel!' do
|
|
subject.accept!
|
|
subject.cancel!
|
|
|
|
expect(subject.canceled?).to be true
|
|
end
|
|
|
|
it 'transitions from :accepted to :canceled after calling #cancel!' do
|
|
subject.accept!
|
|
subject.accept!
|
|
subject.cancel!
|
|
|
|
expect(subject.canceled?).to be true
|
|
end
|
|
|
|
context :record_cancel_operations do
|
|
before do
|
|
subject.accept!
|
|
subject.accept!
|
|
end
|
|
it 'creates two liability operations' do
|
|
expect{ subject.cancel! }.to change{ Operations::Liability.count }.by(2)
|
|
end
|
|
|
|
it 'doesn\'t create asset operations' do
|
|
expect{ subject.cancel! }.to_not change{ Operations::Asset.count }
|
|
end
|
|
|
|
it 'credits main liabilities for member' do
|
|
expect{ subject.cancel! }.to change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :main)
|
|
}.by(subject.sum)
|
|
end
|
|
|
|
it 'debits locked liabilities for member' do
|
|
expect{ subject.cancel! }.to change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
|
}.by(-subject.sum)
|
|
end
|
|
|
|
it 'updates both legacy and operations based member balance' do
|
|
subject.cancel!
|
|
|
|
%i[main locked].each do |kind|
|
|
expect(
|
|
subject.member.balance_for(currency: subject.currency, kind: kind)
|
|
).to eq(
|
|
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context :skip do
|
|
before do
|
|
subject.accept!
|
|
subject.accept!
|
|
subject.process!
|
|
end
|
|
|
|
it 'transitions from :accept to :skipped after calling #process' do
|
|
subject.skip!
|
|
|
|
expect(subject.skipped?).to be true
|
|
end
|
|
end
|
|
|
|
context :reject do
|
|
before do
|
|
subject.accept!
|
|
end
|
|
|
|
it 'transitions from :submitted to :rejected after calling #reject!' do
|
|
subject.reject!
|
|
expect(subject.rejected?).to be true
|
|
end
|
|
|
|
it 'transitions from :accepted to :rejected after calling #reject!' do
|
|
subject.accept!
|
|
subject.reject!
|
|
|
|
expect(subject.rejected?).to be true
|
|
end
|
|
|
|
context 'from to_rejected' do
|
|
before do
|
|
subject.update(aasm_state: :to_reject)
|
|
end
|
|
|
|
it 'transitions from :accepted to :rejected after calling #reject!' do
|
|
subject.reject!
|
|
|
|
expect(subject.rejected?).to be true
|
|
end
|
|
end
|
|
|
|
context :record_cancel_operations do
|
|
|
|
it 'creates two liability operations' do
|
|
expect{ subject.reject! }.to change{ Operations::Liability.count }.by(2)
|
|
end
|
|
|
|
it 'doesn\'t create asset operations' do
|
|
expect{ subject.reject! }.to_not change{ Operations::Asset.count }
|
|
end
|
|
|
|
it 'credits main liabilities for member' do
|
|
expect{ subject.reject! }.to change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :main)
|
|
}.by(subject.sum)
|
|
end
|
|
|
|
it 'debits locked liabilities for member' do
|
|
expect{ subject.reject! }.to change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
|
}.by(-subject.sum)
|
|
end
|
|
|
|
it 'updates both legacy and operations based member balance' do
|
|
subject.reject!
|
|
|
|
%i[main locked].each do |kind|
|
|
expect(
|
|
subject.member.balance_for(currency: subject.currency, kind: kind)
|
|
).to eq(
|
|
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context :success do
|
|
|
|
before do
|
|
subject.accept!
|
|
subject.accept!
|
|
subject.process!
|
|
subject.dispatch!
|
|
end
|
|
|
|
it 'transitions from :confirming to :success after calling #success!' do
|
|
subject.success!
|
|
|
|
expect(subject.succeed?).to be true
|
|
end
|
|
|
|
context :record_complete_operations do
|
|
|
|
it 'creates single liability operation' do
|
|
expect{ subject.success! }.to change{ Operations::Liability.count }.by(1)
|
|
end
|
|
|
|
it 'creates asset operation' do
|
|
expect{ subject.success! }.to change{ Operations::Asset.count }.by(1)
|
|
end
|
|
|
|
it 'doesn\'t change main liability balance for member' do
|
|
expect{ subject.success! }.to_not change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :main)
|
|
}
|
|
end
|
|
|
|
it 'debits locked liabilities for member' do
|
|
expect{ subject.success! }.to change {
|
|
subject.member.balance_for(currency: subject.currency, kind: :locked)
|
|
}.by(-subject.sum)
|
|
end
|
|
|
|
it 'updates both legacy and operations based member balance' do
|
|
subject.success!
|
|
|
|
%i[main locked].each do |kind|
|
|
expect(
|
|
subject.member.balance_for(currency: subject.currency, kind: kind)
|
|
).to eq(
|
|
subject.member.legacy_balance_for(currency: subject.currency, kind: kind)
|
|
)
|
|
end
|
|
end
|
|
|
|
it 'credits revenues' do
|
|
expect{ subject.success! }.to change {
|
|
Operations::Revenue.balance(currency: subject.currency)
|
|
}.by(subject.fee)
|
|
end
|
|
|
|
it 'creates revenue operation from member' do
|
|
expect{ subject.success! }.to change {
|
|
Operations::Revenue.where(member: subject.member).count
|
|
}.by(1)
|
|
end
|
|
end
|
|
end
|
|
context :load do
|
|
let(:txid) { 'a738cb8411e2141f3de43c5f3e7a3aabe71c099bb91d296ded84f0daf29d881c' }
|
|
|
|
subject { create(:btc_withdraw, :with_deposit_liability) }
|
|
|
|
before { subject.accept! }
|
|
|
|
it 'doesn\'t change state after calling #load! when withdrawing coin currency' do
|
|
subject.load!
|
|
expect(subject.accepted?).to be true
|
|
end
|
|
|
|
it 'transitions to :confirming after calling #load! when withdrawing coin currency' do
|
|
BlockchainService.any_instance.expects(:fetch_transaction).once.returns(Peatio::Transaction.new)
|
|
subject.update(txid: txid)
|
|
subject.load!
|
|
expect(subject.confirming?).to be true
|
|
end
|
|
end
|
|
|
|
context :load do
|
|
let(:txid) { 'a738cb8411e2141f3de43c5f3e7a3aabe71c099bb91d296ded84f0daf29d881c' }
|
|
|
|
subject { create(:btc_withdraw, :with_deposit_liability) }
|
|
|
|
before { subject.accept! }
|
|
before { subject.accept! }
|
|
|
|
it 'doesn\'t change state after calling #load! when withdrawing coin currency' do
|
|
subject.load!
|
|
expect(subject.accepted?).to be true
|
|
end
|
|
|
|
it 'transitions to :confirming after calling #load! when withdrawing coin currency' do
|
|
BlockchainService.any_instance.expects(:fetch_transaction).once.returns(Peatio::Transaction.new)
|
|
subject.update(txid: txid)
|
|
subject.load!
|
|
expect(subject.confirming?).to be true
|
|
end
|
|
end
|
|
|
|
context :fail do
|
|
subject { create(:btc_withdraw, :with_deposit_liability) }
|
|
|
|
before { subject.accept! }
|
|
before { subject.accept! }
|
|
|
|
context 'from errored' do
|
|
before do
|
|
subject.update!(aasm_state: :processing)
|
|
subject.err!(Peatio::Wallet::ClientError.new('Something wrong with request'))
|
|
end
|
|
|
|
it do
|
|
subject.fail!
|
|
expect(subject.failed?).to be true
|
|
end
|
|
end
|
|
|
|
context 'from skipped' do
|
|
before do
|
|
subject.update!(aasm_state: :skipped)
|
|
end
|
|
|
|
it do
|
|
subject.fail!
|
|
expect(subject.failed?).to be true
|
|
end
|
|
end
|
|
|
|
context 'with archived beneficiary' do
|
|
let(:member) { create(:member) }
|
|
let(:address) { Faker::Blockchain::Ethereum.address }
|
|
let(:coin) { Currency.find(:btc) }
|
|
|
|
subject { create(:btc_withdraw, :with_deposit_liability, member: member, rid: address, beneficiary: beneficiary) }
|
|
|
|
before { subject.accept! }
|
|
before { subject.accept! }
|
|
|
|
let!(:beneficiary) { create(:beneficiary,
|
|
member: member,
|
|
currency: coin,
|
|
state: :active,
|
|
data: generate(:coin_beneficiary_data).merge(address: address)) }
|
|
|
|
before do
|
|
subject.update!(aasm_state: :processing)
|
|
subject.err!(Peatio::Wallet::ClientError.new('Something wrong with request'))
|
|
beneficiary.update!(state: :archived)
|
|
end
|
|
it do
|
|
subject.fail!
|
|
expect(subject.failed?).to be true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'fee is set to fixed value of 10' do
|
|
let(:withdraw) { create(:usd_withdraw, :with_deposit_liability, sum: 200) }
|
|
before { Currency.any_instance.expects(:withdraw_fee).once.returns(10) }
|
|
it 'computes fee' do
|
|
expect(withdraw.fee).to eql 10.to_d
|
|
expect(withdraw.amount).to eql 190.to_d
|
|
end
|
|
end
|
|
|
|
context 'fee exceeds amount' do
|
|
let(:member) { create(:member) }
|
|
let!(:account) { member.get_account(:usd).tap { |x| x.update!(balance: 200.0.to_d) } }
|
|
let(:withdraw) { build(:usd_withdraw, sum: 200, member: member) }
|
|
before { Currency.any_instance.expects(:withdraw_fee).once.returns(200) }
|
|
it 'fails validation' do
|
|
expect(withdraw.save).to eq false
|
|
expect(withdraw.errors[:amount]).to match(["must be greater than 0.0"])
|
|
end
|
|
end
|
|
|
|
it 'automatically generates TID if it is blank' do
|
|
expect(create(:btc_withdraw, :with_deposit_liability).tid).not_to be_blank
|
|
end
|
|
|
|
it 'doesn\'t generate TID if it is not blank' do
|
|
expect(create(:btc_withdraw, :with_deposit_liability, tid: 'TID1234567890xyz').tid).to eq 'TID1234567890xyz'
|
|
end
|
|
|
|
it 'validates uniqueness of TID' do
|
|
record1 = create(:btc_withdraw, :with_deposit_liability)
|
|
record2 = build(:btc_withdraw, tid: record1.tid, member: record1.member)
|
|
record2.save
|
|
expect(record2.errors[:tid]).to match(["has already been taken"])
|
|
end
|
|
|
|
it 'uppercases TID' do
|
|
record = create(:btc_withdraw, :with_deposit_liability)
|
|
expect(record.tid).to eq record.tid.upcase
|
|
end
|
|
|
|
context 'using beneficiary' do
|
|
context 'fiat' do
|
|
let(:withdraw) do
|
|
create(:usd_withdraw,
|
|
:with_beneficiary,
|
|
:with_deposit_liability,
|
|
sum: 200)
|
|
end
|
|
|
|
it 'automatically sets rid from beneficiary' do
|
|
expect(withdraw.rid).to eq withdraw.beneficiary.rid
|
|
end
|
|
end
|
|
|
|
context 'crypto' do
|
|
let(:withdraw) do
|
|
create(:btc_withdraw,
|
|
:with_beneficiary,
|
|
:with_deposit_liability,
|
|
sum: 2)
|
|
end
|
|
|
|
it 'automatically sets rid from beneficiary' do
|
|
expect(withdraw.rid).to eq withdraw.beneficiary.rid
|
|
end
|
|
end
|
|
|
|
context 'non-active beneficiary' do
|
|
let(:currency) { Currency.all.sample }
|
|
let(:beneficiary) { create(:beneficiary, state: :pending, currency: currency) }
|
|
|
|
# Create deposit before withdraw for valid accounting cause withdraw
|
|
# build callback doesn't trigger deposit creation.
|
|
let!(:deposit) do
|
|
create(:deposit_usd, member: beneficiary.member, amount: 12)
|
|
.accept!
|
|
end
|
|
|
|
let(:withdraw) do
|
|
build(:usd_withdraw,
|
|
:with_deposit_liability,
|
|
beneficiary: beneficiary,
|
|
sum: 10,
|
|
member: beneficiary.member)
|
|
end
|
|
|
|
it 'automatically sets rid from beneficiary' do
|
|
expect(withdraw.valid?).to be_falsey
|
|
expect(withdraw.errors[:beneficiary]).to include('not active')
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'validate min withdrawal sum' do
|
|
let(:member) { create(:member) }
|
|
let!(:account) { member.get_account(:btc).tap { |x| x.update!(balance: 1.0.to_d) } }
|
|
subject { build(:btc_withdraw, sum: 0.1, member: member) }
|
|
|
|
before do
|
|
Currency.find('btc').update(min_withdraw_amount: 0.5.to_d)
|
|
end
|
|
|
|
it { expect(subject).not_to be_valid }
|
|
|
|
it do
|
|
subject.save
|
|
expect(subject.errors[:sum]).to match(["must be greater than or equal to 0.5"])
|
|
end
|
|
|
|
end
|
|
|
|
context 'validate note length' do
|
|
let(:member) { create(:member) }
|
|
let!(:account) { member.get_account(:btc).tap { |x| x.update!(balance: 1.0.to_d) } }
|
|
let(:address) { 'bitcoincash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h' }
|
|
|
|
let :record do
|
|
Withdraws::Coin.new \
|
|
currency: Currency.find(:btc),
|
|
member: member,
|
|
rid: address,
|
|
sum: 1.0.to_d,
|
|
note: note
|
|
end
|
|
|
|
context 'valid note' do
|
|
let(:note) { 'TEST' }
|
|
|
|
it do
|
|
expect(record.save).to eq true
|
|
expect(record.note).to eq 'TEST'
|
|
end
|
|
end
|
|
|
|
context 'invalid note' do
|
|
let(:note) { (0...257).map { (65 + rand(26)).chr }.join }
|
|
|
|
it do
|
|
expect(record.save).to eq false
|
|
expect(record.errors.full_messages).to include 'Note is too long (maximum is 256 characters)'
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'validates sum precision' do
|
|
let(:currency) { Currency.find(:usd) }
|
|
let(:member) { create(:member) }
|
|
|
|
# Create deposit before withdraw for valid accounting cause withdraw
|
|
# build callback doesn't trigger deposit creation.
|
|
let!(:deposit) do
|
|
create(:deposit_usd, member: member, amount: 12)
|
|
.accept!
|
|
end
|
|
|
|
let :record do
|
|
build(:usd_withdraw, :with_deposit_liability, :with_beneficiary, member: member, sum: 0.1234)
|
|
end
|
|
|
|
it do
|
|
expect(record.valid?).to be_falsey
|
|
expect(record.errors[:amount]).to include("precision must be less than or equal to #{currency.precision}")
|
|
expect(record.errors[:sum]).to include("precision must be less than or equal to #{currency.precision}")
|
|
end
|
|
end
|
|
|
|
context 'verify_limits' do
|
|
let!(:member) { create(:member, group: 'vip-1', level: 1) }
|
|
let!(:withdraw_limit) { create(:withdraw_limit, group: 'vip-1', kyc_level: 1, limit_24_hour: 6, limit_1_month: 10) }
|
|
let(:withdraw) { build(:btc_withdraw, :with_deposit_liability, member: member, sum: 0.5.to_d) }
|
|
|
|
before do
|
|
Currency.any_instance.unstub(:price)
|
|
Currency.find('btc').update!(price: 10)
|
|
member.get_account(:btc).update!(balance: 1000)
|
|
end
|
|
|
|
context 'enough limits' do
|
|
it { expect(withdraw.verify_limits).to be_truthy }
|
|
end
|
|
|
|
context 'Withdraw 24 hours limit exceeded' do
|
|
it do
|
|
withdraw.sum = 100
|
|
expect(withdraw.verify_limits).to be_falsey
|
|
end
|
|
|
|
it 'withdraw in different currency' do
|
|
Currency.find('usd').update!(price: 1)
|
|
withdraw.sum = 100
|
|
expect(withdraw.verify_limits).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'Withdraw 1 month limit exceeded' do
|
|
before { withdraw.save }
|
|
it do
|
|
withdraw.update(created_at: 2.day.ago)
|
|
withdraw = build(:btc_withdraw, :with_deposit_liability, member: member, sum: 0.6.to_d)
|
|
expect(withdraw.verify_limits).to be_falsey
|
|
end
|
|
end
|
|
|
|
context 'zero limits' do
|
|
before { WithdrawLimit.last.update!(limit_24_hour: 0, limit_1_month: 0) }
|
|
|
|
it { expect(withdraw.valid?).to be_truthy }
|
|
end
|
|
|
|
context 'there are no WLs in DB' do
|
|
before { WithdrawLimit.delete_all }
|
|
|
|
it { expect(withdraw.valid?).to be_truthy }
|
|
end
|
|
end
|
|
end
|