# encoding: UTF-8 # frozen_string_literal: true class Withdraw < ApplicationRecord STATES = %i[ prepared rejected accepted skipped processing succeed canceled failed errored confirming].freeze COMPLETED_STATES = %i[succeed rejected canceled failed].freeze # TODO: skipped errored deleted ??? SUCCEED_PROCESSING_STATES = %i[prepared accepted skipped processing errored confirming succeed].freeze include AASM include AASM::Locking include TIDIdentifiable include FeeChargeable extend Enumerize serialize :error, JSON unless Rails.configuration.database_support_json serialize :metadata, JSON unless Rails.configuration.database_support_json TRANSFER_TYPES = { fiat: 100, crypto: 200 } belongs_to :currency, required: true belongs_to :member, required: true # Optional beneficiary association gives ability to support both in-peatio # beneficiaries and managed by third party application. belongs_to :beneficiary, optional: true acts_as_eventable prefix: 'withdraw', on: %i[create update] after_initialize :initialize_defaults, if: :new_record? before_validation(on: :create) { self.rid ||= beneficiary.rid if beneficiary.present? } before_validation { self.completed_at ||= Time.current if completed? } before_validation { self.transfer_type ||= currency.coin? ? 'crypto' : 'fiat' } validates :rid, :aasm_state, presence: true validates :txid, uniqueness: { scope: :currency_id }, if: :txid? validates :block_number, allow_blank: true, numericality: { greater_than_or_equal_to: 0, only_integer: true } validates :sum, presence: true, numericality: { greater_than_or_equal_to: ->(withdraw) { withdraw.currency.min_withdraw_amount } } validate do errors.add(:beneficiary, 'not active') if beneficiary.present? && !beneficiary.active? && !aasm_state.to_sym.in?(COMPLETED_STATES) end validate :verify_limits, on: :create scope :completed, -> { where(aasm_state: COMPLETED_STATES) } scope :succeed_processing, -> { where(aasm_state: SUCCEED_PROCESSING_STATES) } scope :last_24_hours, -> { where('created_at > ?', 24.hour.ago) } scope :last_1_month, -> { where('created_at > ?', 1.month.ago) } aasm whiny_transitions: false do state :prepared, initial: true state :canceled state :accepted state :skipped state :to_reject state :rejected state :processing state :succeed state :failed state :errored state :confirming event :accept do transitions from: :prepared, to: :accepted after do lock_funds record_submit_operations! end after_commit do # auto process withdrawal if sum less than limits and WITHDRAW_ADMIN_APPROVE env set to false (not set) process! if ENV.false?('WITHDRAW_ADMIN_APPROVE') && currency.coin? end end event :cancel do transitions from: %i[prepared accepted], to: :canceled after do unless aasm.from_state == :prepared unlock_funds record_cancel_operations! end end end event :reject do transitions from: %i[to_reject accepted confirming], to: :rejected after do unlock_funds record_cancel_operations! end end event :process do transitions from: %i[accepted skipped errored], to: :processing after :send_coins! end event :load do transitions from: :accepted, to: :confirming do # Load event is available only for coin withdrawals. guard do currency.coin? && txid? end end after_commit do tx = currency.blockchain_api.fetch_transaction(self) if tx.present? success! if tx.status.success? end end end event :dispatch do transitions from: :processing, to: :confirming do # Validate txid presence on coin withdrawal dispatch. guard do currency.fiat? || txid? end end end event :success do transitions from: %i[confirming errored], to: :succeed do guard do currency.fiat? || txid? end after do unlock_and_sub_funds record_complete_operations! end end end event :skip do transitions from: :processing, to: :skipped end event :fail do transitions from: %i[processing confirming skipped errored], to: :failed after do unlock_funds record_cancel_operations! end end event :err do transitions from: :processing, to: :errored, after: :add_error end end class << self def sum_query 'SELECT sum(w.sum * c.price) as sum FROM withdraws as w ' \ 'INNER JOIN currencies as c ON c.id=w.currency_id ' \ 'where w.member_id = ? AND w.aasm_state IN (?) AND w.created_at > ?;' end def sanitize_execute_sum_queries(member_id) squery_24h = ActiveRecord::Base.sanitize_sql_for_conditions([sum_query, member_id, SUCCEED_PROCESSING_STATES, 24.hours.ago]) squery_1m = ActiveRecord::Base.sanitize_sql_for_conditions([sum_query, member_id, SUCCEED_PROCESSING_STATES, 1.month.ago]) sum_withdraws_24_hours = ActiveRecord::Base.connection.exec_query(squery_24h).to_hash.first['sum'].to_d sum_withdraws_1_month = ActiveRecord::Base.connection.exec_query(squery_1m).to_hash.first['sum'].to_d [sum_withdraws_24_hours, sum_withdraws_1_month] end end def initialize_defaults self.metadata = {} if metadata.blank? end def account member&.get_account(currency) end def add_error(e) if error.blank? update!(error: [{ class: e.class.to_s, message: e.message }]) else update!(error: error << { class: e.class.to_s, message: e.message }) end end def verify_limits if member.last_change_pass.present? && member.last_change_pass + 24.hours > Time.zone.now && type == 'coin' return errors.add(:member_id, 'member password changed recently') end limits = WithdrawLimit.for(kyc_level: member.level, group: member.group, kind: transfer_type) # If there are no limits in DB or current user withdraw limit # has 0.0 for 24 hour and 1 mounth it will skip this checks return true if limits.limit_24_hour.zero? && limits.limit_1_month.zero? # Withdraw limits in USD and withdraw sum in currency. # Convert withdraw sums with price from the currency model. sum_24_hours, sum_1_month = Withdraw.sanitize_execute_sum_queries(member_id) errors.add(:member_id, 'reached 24 hours limitation') if sum_24_hours + sum * currency.get_price >= limits.limit_24_hour errors.add(:member_id, 'reached 1 month limitation') if sum_1_month + sum * currency.get_price >= limits.limit_1_month end def blockchain_api currency.blockchain_api end def confirmations return 0 if block_number.blank? return blockchain.processed_height - block_number if (blockchain.processed_height - block_number) >= 0 'N/A' rescue StandardError => e report_exception(e) 'N/A' end def completed? aasm_state.in?(COMPLETED_STATES.map(&:to_s)) end def as_json_for_event_api { tid: tid, user: { uid: member.uid, email: member.email }, uid: member.uid, rid: rid, currency: currency_id, amount: amount.to_s('F'), fee: fee.to_s('F'), state: aasm_state, created_at: created_at.iso8601, updated_at: updated_at.iso8601, completed_at: completed_at&.iso8601, blockchain_txid: txid } end private # @deprecated def lock_funds account.lock_funds(sum) end # @deprecated def unlock_funds account.unlock_funds(sum) end # @deprecated def unlock_and_sub_funds account.unlock_and_sub_funds(sum) end def record_submit_operations! transaction do # Debit main fiat/crypto Liability account. # Credit locked fiat/crypto Liability account. Operations::Liability.transfer!( amount: sum, currency: currency, reference: self, from_kind: :main, to_kind: :locked, member_id: member_id ) end end def record_cancel_operations! transaction do # Debit locked fiat/crypto Liability account. # Credit main fiat/crypto Liability account. Operations::Liability.transfer!( amount: sum, currency: currency, reference: self, from_kind: :locked, to_kind: :main, member_id: member_id ) end end def record_complete_operations! transaction do # Debit locked fiat/crypto Liability account. Operations::Liability.debit!( amount: sum, currency: currency, reference: self, kind: :locked, member_id: member_id ) # Credit main fiat/crypto Revenue account. # NOTE: Credit amount = fee. Operations::Revenue.credit!( amount: fee, currency: currency, reference: self, member_id: member_id ) # Debit main fiat/crypto Asset account. # NOTE: Debit amount = sum - fee. Operations::Asset.debit!( amount: amount, currency: currency, reference: self ) end end def send_coins! AMQP::Queue.enqueue(:withdraw_coin, id: id) if currency.coin? end end # == Schema Information # Schema version: 20201125134745 # # Table name: withdraws # # id :integer not null, primary key # member_id :integer not null # beneficiary_id :bigint # currency_id :string(10) not null # amount :decimal(32, 16) not null # fee :decimal(32, 16) not null # txid :string(128) # aasm_state :string(30) not null # block_number :integer # sum :decimal(32, 16) not null # type :string(30) not null # transfer_type :integer # tid :string(64) not null # rid :string(256) not null # note :string(256) # metadata :json # error :json # created_at :datetime not null # updated_at :datetime not null # completed_at :datetime # # Indexes # # index_withdraws_on_aasm_state (aasm_state) # index_withdraws_on_currency_id (currency_id) # index_withdraws_on_currency_id_and_txid (currency_id,txid) UNIQUE # index_withdraws_on_member_id (member_id) # index_withdraws_on_tid (tid) # index_withdraws_on_type (type) #