# encoding: UTF-8 # frozen_string_literal: true module API module V2 module Account class Withdraws < Grape::API before { withdraws_must_be_permitted! } helpers API::V2::Account::Utils desc 'List your withdraws as paginated collection.', is_array: true, success: API::V2::Entities::Withdraw params do optional :currency, type: String, values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist'}, desc: 'Currency code.' optional :limit, type: { value: Integer, message: 'account.withdraw.non_integer_limit' }, values: { value: 1..100, message: 'account.withdraw.invalid_limit' }, default: 100, desc: "Number of withdraws per page (defaults to 100, maximum is 100)." optional :state, values: { value: ->(v) { (Array.wrap(v) - Withdraw::STATES.map(&:to_s)).blank? }, message: 'account.withdraw.invalid_state' }, desc: 'Filter withdrawals by states.' optional :rid, type: String, allow_blank: false, desc: 'Wallet address on the Blockchain.' optional :time_from, allow_blank: { value: false, message: 'account.withdraw.empty_time_from' }, type: { value: Integer, message: 'account.withdraw.non_integer_time_from' }, desc: 'An integer represents the seconds elapsed since Unix epoch.' optional :time_to, type: { value: Integer, message: 'account.withdraw.non_integer_time_to' }, allow_blank: { value: false, message: 'account.withdraw.empty_time_to' }, desc: 'An integer represents the seconds elapsed since Unix epoch.' optional :page, type: { value: Integer, message: 'account.withdraw.non_integer_page' }, values: { value: -> (p){ p.try(:positive?) }, message: 'account.withdraw.non_positive_page'}, default: 1, desc: 'Page number (defaults to 1).' end get '/withdraws' do user_authorize! :read, ::Withdraw currency = Currency.find(params[:currency]) if params[:currency].present? current_user.withdraws.order(id: :desc) .tap { |q| q.where!(currency: currency) if currency } .tap { |q| q.where!(aasm_state: params[:state]) if params[:state] } .tap { |q| q.where!(rid: params[:rid]) if params[:rid] } .tap { |q| q.where!('updated_at >= ?', Time.at(params[:time_from])) if params[:time_from].present? } .tap { |q| q.where!('updated_at <= ?', Time.at(params[:time_to])) if params[:time_to].present? } .tap { |q| present paginate(q), with: API::V2::Entities::Withdraw } end desc 'Returns withdrawal sums for last 4 hours and 1 month' get '/withdraws/sums' do user_authorize! :read, ::Withdraw sum_24_hours, sum_1_month = Withdraw.sanitize_execute_sum_queries(current_user.id) present({ last_24_hours: sum_24_hours, last_1_month: sum_1_month }) end desc 'Creates new withdrawal to active beneficiary.' params do requires :otp, type: { value: Integer, message: 'account.withdraw.non_integer_otp' }, allow_blank: false, desc: 'OTP to perform action' requires :beneficiary_id, type: { value: Integer, message: 'account.withdraw.non_integer_beneficiary_id' }, allow_blank: false, desc: 'ID of Active Beneficiary belonging to user.' requires :currency, type: String, values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist'}, desc: 'The currency code.' requires :amount, type: { value: BigDecimal, message: 'account.withdraw.non_decimal_amount' }, values: { value: ->(v) { v.try(:positive?) }, message: 'account.withdraw.non_positive_amount' }, desc: 'The amount to withdraw.' optional :note, type: String, values: { value: ->(v) { v.size <= 256 }, message: 'account.withdraw.too_long_note' }, desc: 'Optional user metadata to be applied to the transaction. Used to tag transactions with memorable comments.' end post '/withdraws' do user_authorize! :create, ::Withdraw withdraw_api_must_be_enabled! if current_user.otp.present? && current_user.read_cache('withdraw_2fa').blank? error!({ errors: ['account.withdraw.need.2fa'] }, 403) end totp = Vault::TOTPAction.new('withdraw-coin') error!({ errors: ['withdraw.totp.code'] }, 422) unless totp.validate?(current_user.uid, params[:otp]) beneficiary = current_user .beneficiaries .available_to_member .find_by(id: params[:beneficiary_id]) if beneficiary.blank? error!({ errors: ['account.beneficiary.doesnt_exist'] }, 422) elsif !beneficiary.active? error!({ errors: ['account.beneficiary.invalid_state_for_withdrawal'] }, 422) end currency = Currency.find(params[:currency]) error!({ errors: ['account.currency.withdrawal_disabled'] }, 422) unless currency.withdrawal_enabled? # TODO: Delete subclasses from Deposit and Withdraw withdraw = "withdraws/#{currency.type}".camelize.constantize.new \ beneficiary: beneficiary, sum: params[:amount], member: current_user, currency: currency, note: params[:note] withdraw.save! withdraw.with_lock { withdraw.accept! } current_user.delete_cache('withdraw_2fa') present withdraw, with: API::V2::Entities::Withdraw rescue ::Account::AccountError => e report_api_error(e, request) error!({ errors: ['account.withdraw.insufficient_balance'] }, 422) rescue ActiveRecord::RecordInvalid => e report_api_error(e, request) # TODO: Check if there are other errors possible here. # For now single error which is not handled by params validations is # sum precision validation error (PrecisionValidator). error!({ errors: ['account.withdraw.invalid_amount'] }, 422) rescue => e report_exception(e) error!({ errors: ['account.withdraw.create_error'] }, 422) end desc 'Creates new fiat withdrawal' params do requires :amount, type: { value: BigDecimal, message: 'account.withdraw.non_decimal_amount' }, values: { value: ->(v) { v.try(:positive?) }, message: 'account.withdraw.non_positive_amount' }, desc: 'The amount to withdraw.' requires :iban, type: String, desc: 'iban bank number' requires :currency, type: String, default: 'irt', desc: -> { 'currency' } optional :note, type: String, values: { value: ->(v) { v.size <= 256 }, message: 'account.withdraw.too_long_note' }, desc: 'Optional user metadata to be applied to the transaction. Used to tag transactions with memorable comments.' end post '/withdraws/fiat' do user_authorize! :create, ::Withdraw withdraw_api_must_be_enabled! if current_user.otp.present? && current_user.read_cache('withdraw_2fa').blank? error!({ errors: ['account.withdraw.need.2fa'] }, 403) end error!({ errors: ['account.withdraw.use_valid_iban'] }, 403) if current_user.ibans.exclude?(params[:iban]) currency = Currency.find(params[:currency]) withdraw = "withdraws/#{currency.type}".camelize.constantize.new \ rid: params[:iban], sum: params[:amount], member: current_user, currency: currency, transfer_type: :fiat, note: params[:note] withdraw.save error!({ errors: withdraw.errors.full_messages }, 422) unless withdraw.valid? # delete 2fa cache current_user.delete_cache('withdraw_2fa') publish_confirmation_code(current_user, 'withdraw-fiat') present withdraw, with: API::V2::Entities::Withdraw end # # desc 'confirm fiat withdrawal' params do requires :id, type: Integer, desc: 'withdraw id' requires :otp, type: Integer, desc: 'auth otp in sms or email' end post '/withdraws/confirm' do user_authorize! :create, ::Withdraw withdraw = current_user.withdraws.find_by(id: params[:id]) error!({ errors: ['account.withdraw.not.found'] }, 403) unless withdraw.present? error!({ errors: ['account.withdraw.process_before'] }, 403) unless withdraw.aasm_state == 'prepared' totp = Vault::TOTPAction.new('withdraw-fiat') error!({ errors: ['withdraw.totp.code'] }, 422) unless totp.validate?(current_user.uid, params[:otp]) # here must lock found withdraw.with_lock { withdraw.accept! } response = ::VandarService.new.create_withdraw(amount: withdraw.sum, iban: withdraw.rid) if response.dig('status').to_i.positive? withdraw.update(txid: response['data'].dig('settlement', 0, 'transaction_id'), aasm_state: 'confirming') else withdraw.with_lock { withdraw.reject! } end present(response: response) end # # desc 'Get fiat withdrawal' params do requires :id, type: Integer, desc: 'withdraw id' optional :with_otp, type: String, desc: -> { 'send otp or not' } end get '/withdraw' do user_authorize! :read, ::Withdraw withdraw = current_user.withdraws.find_by(id: params[:id]) if withdraw.present? && withdraw.accepted? && params['with_otp'].present? publish_confirmation_code(current_user, 'withdraw') end present withdraw, with: API::V2::Entities::Withdraw end end end end end