# encoding: UTF-8 # frozen_string_literal: true class Beneficiary < ApplicationRecord # == Constants ============================================================ extend Enumerize include Vault::EncryptedModel vault_lazy_decrypt! acts_as_eventable prefix: 'beneficiary', on: %i[create update] include AASM include AASM::Locking STATES_MAPPING = { pending: 0, active: 1, archived: 2, aml_processing: 3, aml_suspicious: 4 }.freeze STATES = %i[pending aml_processing aml_suspicious active archived].freeze STATES_AVAILABLE_FOR_MEMBER = %i[pending active] PIN_LENGTH = 6 PIN_RANGE = 10**5..10**Beneficiary::PIN_LENGTH INVALID_ADDRESS_SYMBOLS = /[\<\>\'\,\[\]\}\{\"\)\(\*\&\^\%\$\#\`\~\{\}\@]/.freeze # == Attributes =========================================================== vault_attribute :data, serialize: :json, default: {} # == Extensions =========================================================== enumerize :state, in: STATES_MAPPING, scope: true aasm column: :state, enum: :states_mapping, whiny_transitions: false do state :pending, initial: true state :active state :aml_processing state :aml_suspicious state :archived event :activate do if Peatio::AML.adapter.present? transitions from: :pending, to: :aml_processing, guard: :valid_pin? after do enable! if aml_check! end else transitions from: :pending, to: :active, guard: :valid_pin? end end event :enable do transitions from: :aml_processing, to: :active end if Peatio::AML.adapter.present? event :aml_suspicious do transitions from: :aml_processing, to: :aml_suspicious end if Peatio::AML.adapter.present? event :archive do transitions from: %i[pending aml_processing aml_suspicious active], to: :archived end end # == Relationships ======================================================== belongs_to :currency, required: true belongs_to :member, required: true # == Validations ========================================================== validates :pin, presence: true, numericality: { only_integer: true } validates :data, presence: true # Validates that data contains address field which is required for coin. validate if: ->(b) { b.currency.present? && b.currency.coin? } do errors.add(:data, 'address can\'t be blank') if data.blank? || data.symbolize_keys[:address].blank? end # Validates address field which is required for coin. validate if: ->(b) { b.currency.present? && b.currency.coin? } do errors.add(:data, 'invlalid address') if data.present? && data.symbolize_keys[:address].present? && data.symbolize_keys[:address].match?(INVALID_ADDRESS_SYMBOLS) end # Validates that data contains full_name field which is required for fiat. validate if: ->(b) { b.currency.present? && b.currency.fiat? } do errors.add(:data, 'full_name can\'t be blank') if data.blank? || data.symbolize_keys[:full_name].blank? end # == Scopes =============================================================== scope :available_to_member, -> { with_state(:pending, :active) } scope :active_to_member, -> { with_state(:active) } # == Callbacks ============================================================ before_validation(on: :create) do # Truncate spaces data['address'] = data['address'].gsub(/\s+/, '') if data.present? && data['address'].present? # Generate Beneficiary Pin self.pin ||= self.class.generate_pin # Record time when we send event to Event API self.sent_at = Time.now end # == Class Methods ======================================================== class << self def generate_pin SecureRandom.rand(Beneficiary::PIN_RANGE) end # Method used for passing states mapping to AASM. def states_mapping STATES_MAPPING end end # == Instance Methods ===================================================== def as_json_for_event_api { user: { uid: member.uid, email: member.email }, currency: currency_id, name: name, description: description, data: data, pin: pin, state: state, sent_at: sent_at.iso8601, created_at: created_at.iso8601, updated_at: updated_at.iso8601 } end def aml_check! result = Peatio::AML.check!(rid, currency_id, member.uid) if result.risk_detected b.aml_suspicious! return nil end return nil if result.pending true end def valid_pin?(user_pin) case user_pin when Integer return pin == user_pin when String return pin == user_pin.to_i else return false end end def rid currency.coin? ? coin_rid : fiat_rid end def regenerate_pin! update(pin: self.class.generate_pin, sent_at: Time.now) end def masked_account_number account_number = data.symbolize_keys[:account_number] if data.present? && account_number.present? account_number.sub(/(?<=\A.{2})(.*)(?=.{4}\z)/) { |match| '*' * match.length } end end def masked_data data.merge(account_number: masked_account_number).compact if data.present? end private def coin_rid return unless currency.coin? data.symbolize_keys[:address] end def fiat_rid return unless currency.fiat? "%s-%s-%08d" % [data.symbolize_keys[:full_name].downcase.split.join('-'), currency_id.downcase, id] end end # == Schema Information # Schema version: 20201125134745 # # Table name: beneficiaries # # id :bigint not null, primary key # member_id :bigint not null # currency_id :string(10) not null # name :string(64) not null # description :string(255) default("") # data_encrypted :string(1024) # pin :integer unsigned, not null # sent_at :datetime # state :integer default("pending"), unsigned, not null # created_at :datetime not null # updated_at :datetime not null # # Indexes # # index_beneficiaries_on_currency_id (currency_id) # index_beneficiaries_on_member_id (member_id) #