dscf-core 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/dscf/core/authenticatable.rb +81 -0
  3. data/app/controllers/concerns/dscf/core/common.rb +200 -0
  4. data/app/controllers/concerns/dscf/core/filterable.rb +12 -0
  5. data/app/controllers/concerns/dscf/core/json_response.rb +66 -0
  6. data/app/controllers/concerns/dscf/core/pagination.rb +71 -0
  7. data/app/controllers/concerns/dscf/core/token_authenticatable.rb +53 -0
  8. data/app/controllers/dscf/core/application_controller.rb +19 -0
  9. data/app/controllers/dscf/core/auth_controller.rb +120 -0
  10. data/app/errors/dscf/core/authentication_error.rb +19 -0
  11. data/app/models/concerns/dscf/core/user_authenticatable.rb +58 -0
  12. data/app/models/dscf/core/address.rb +11 -0
  13. data/app/models/dscf/core/business.rb +2 -0
  14. data/app/models/dscf/core/document.rb +19 -0
  15. data/app/models/dscf/core/refresh_token.rb +36 -0
  16. data/app/models/dscf/core/user.rb +14 -4
  17. data/app/models/dscf/core/user_profile.rb +31 -0
  18. data/app/services/dscf/core/auth_service.rb +75 -0
  19. data/app/services/dscf/core/token_service.rb +55 -0
  20. data/config/initializers/jwt.rb +23 -0
  21. data/config/initializers/ransack.rb +4 -0
  22. data/config/locales/en.yml +23 -0
  23. data/config/routes.rb +5 -0
  24. data/db/migrate/20250822072222_create_dscf_core_businesses.rb +1 -1
  25. data/db/migrate/20250822075206_create_dscf_core_addresses.rb +19 -0
  26. data/db/migrate/20250822081522_create_dscf_core_documents.rb +14 -0
  27. data/db/migrate/20250822083000_change_document_type_in_dscf_core_documents.rb +11 -0
  28. data/db/migrate/20250822092031_create_dscf_core_user_profiles.rb +26 -0
  29. data/db/migrate/20250824114725_create_dscf_core_refresh_tokens.rb +15 -0
  30. data/db/migrate/20250824191957_change_pep_status_to_integer_in_user_profiles.rb +5 -0
  31. data/db/migrate/20250824200927_make_email_and_phone_optional_for_users.rb +6 -0
  32. data/db/migrate/20250825192113_add_defaults_to_user_profiles.rb +6 -0
  33. data/lib/dscf/core/version.rb +1 -1
  34. data/lib/dscf/core.rb +2 -0
  35. data/spec/factories/dscf/core/addresses.rb +15 -0
  36. data/spec/factories/dscf/core/documents.rb +25 -0
  37. data/spec/factories/dscf/core/refresh_tokens.rb +25 -0
  38. data/spec/factories/dscf/core/user_profiles.rb +22 -0
  39. metadata +173 -3
@@ -0,0 +1,58 @@
1
+ module Dscf
2
+ module Core
3
+ module UserAuthenticatable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ has_secure_password
8
+
9
+ validates :email, uniqueness: true, allow_blank: true
10
+ validates :phone, uniqueness: true, allow_blank: true
11
+ validate :email_or_phone_present
12
+
13
+ has_many :refresh_tokens, dependent: :destroy, class_name: "Dscf::Core::RefreshToken"
14
+ end
15
+
16
+ def authenticate_with_password(password)
17
+ authenticate(password)
18
+ end
19
+
20
+ def generate_auth_tokens(request)
21
+ AuthService.generate_auth_tokens(self, request)
22
+ end
23
+
24
+ def valid_for_authentication?
25
+ active? && !locked?
26
+ end
27
+
28
+ def active?
29
+ true # Override in specific models if needed
30
+ end
31
+
32
+ def locked?
33
+ false # Override in specific models if needed
34
+ end
35
+
36
+ def track_device(request)
37
+ # Track device information for security
38
+ {
39
+ user_agent: request.user_agent,
40
+ ip_address: request.remote_ip,
41
+ device_id: request.params[:device_id]
42
+ }
43
+ end
44
+
45
+ def revoke_all_tokens
46
+ AuthService.revoke_all_user_tokens(self)
47
+ end
48
+
49
+ private
50
+
51
+ def email_or_phone_present
52
+ return unless email.blank? && phone.blank?
53
+
54
+ errors.add(:base, "Either email or phone must be provided")
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,11 @@
1
+ module Dscf
2
+ module Core
3
+ class Address < ApplicationRecord
4
+ belongs_to :user, class_name: "Dscf::Core::User"
5
+
6
+ enum :address_type, {shipping: 0, residence: 1, current: 2, branch: 3}
7
+
8
+ validates :country, presence: true
9
+ end
10
+ end
11
+ end
@@ -6,6 +6,8 @@ module Dscf
6
6
 
7
7
  has_one_attached :logo
8
8
 
9
+ has_many :documents, as: :documentable, dependent: :destroy, class_name: "Dscf::Core::Document"
10
+
9
11
  validates :name, presence: true
10
12
 
11
13
  def logo_url
@@ -0,0 +1,19 @@
1
+ module Dscf
2
+ module Core
3
+ class Document < ApplicationRecord
4
+ belongs_to :documentable, polymorphic: true
5
+ belongs_to :verified_by, polymorphic: true, optional: true
6
+ validates :document_type, presence: true
7
+
8
+ has_one_attached :file
9
+
10
+ enum :document_type, {business_license: 0, drivers_license: 1, claim: 2}
11
+
12
+ def file_url
13
+ return nil unless file.attached?
14
+
15
+ Rails.application.routes.url_helpers.rails_blob_url(file, only_path: true)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,36 @@
1
+ module Dscf
2
+ module Core
3
+ class RefreshToken < ApplicationRecord
4
+ belongs_to :user, class_name: "Dscf::Core::User"
5
+
6
+ validates :refresh_token, presence: true, uniqueness: true
7
+ validates :expires_at, presence: true
8
+ validates :user_id, presence: true
9
+
10
+ # scopes for querying tokens
11
+ scope :active, -> { where("expires_at > ?", Time.current) }
12
+ scope :for_device, ->(device) { where(device: device) }
13
+
14
+ # generate token and set expiry on creation
15
+ before_validation :set_token_and_expiry, on: :create
16
+
17
+ def expired?
18
+ expires_at < Time.current
19
+ end
20
+
21
+ def set_token_and_expiry
22
+ self.refresh_token ||= SecureRandom.hex(32)
23
+ self.expires_at ||= 30.days.from_now
24
+ end
25
+
26
+ def self.generate(user, request)
27
+ create(
28
+ user: user,
29
+ device: request.params[:device_id] || "unknown",
30
+ ip_address: request.remote_ip,
31
+ user_agent: request.user_agent
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,14 +1,24 @@
1
1
  module Dscf
2
2
  module Core
3
3
  class User < ApplicationRecord
4
- has_secure_password
5
-
6
- validates :email, presence: true, uniqueness: true
7
- validates :phone, presence: true, uniqueness: true
4
+ include UserAuthenticatable
8
5
 
9
6
  has_many :user_roles, class_name: "Dscf::Core::UserRole"
10
7
  has_many :roles, through: :user_roles, class_name: "Dscf::Core::Role"
11
8
  has_many :businesses, dependent: :destroy, class_name: "Dscf::Core::Business"
9
+ has_many :addresses, dependent: :destroy, class_name: "Dscf::Core::Address"
10
+ has_many :documents, as: :documentable, dependent: :destroy, class_name: "Dscf::Core::Document"
11
+ has_one :user_profile, dependent: :destroy, class_name: "Dscf::Core::UserProfile"
12
+
13
+ accepts_nested_attributes_for :user_profile
14
+
15
+ before_create :set_default_temp_password
16
+
17
+ private
18
+
19
+ def set_default_temp_password
20
+ self.temp_password = false if temp_password.nil?
21
+ end
12
22
  end
13
23
  end
14
24
  end
@@ -0,0 +1,31 @@
1
+ module Dscf
2
+ module Core
3
+ class UserProfile < ApplicationRecord
4
+ belongs_to :user, class_name: "Dscf::Core::User"
5
+
6
+ validates :first_name, presence: true
7
+ validates :last_name, presence: true
8
+ validates :watchlist_hit, inclusion: {in: [true, false]}
9
+ validates :verification_status, presence: true
10
+
11
+ enum :gender, {
12
+ male: 0,
13
+ female: 1
14
+ }
15
+
16
+ enum :pep_status, {
17
+ no_exposure: 0,
18
+ low: 1,
19
+ medium: 2,
20
+ high: 3
21
+ }
22
+
23
+ enum :verification_status, {
24
+ pending: 0,
25
+ verified: 1,
26
+ rejected: 2,
27
+ under_review: 3
28
+ }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,75 @@
1
+ require "ostruct"
2
+
3
+ module Dscf
4
+ module Core
5
+ class AuthService
6
+ class << self
7
+ def authenticate_user(email_or_phone, password)
8
+ user = find_user_by_email_or_phone(email_or_phone)
9
+ return nil unless user&.authenticate(password)
10
+
11
+ user
12
+ end
13
+
14
+ def generate_auth_tokens(user, request)
15
+ access_token = TokenService.issue(TokenService.access_token_payload(user))
16
+ refresh_token = create_refresh_token(user, request)
17
+
18
+ {
19
+ access_token: access_token,
20
+ refresh_token: refresh_token,
21
+ user: user
22
+ }
23
+ end
24
+
25
+ def refresh_access_token(refresh_token_value, request)
26
+ refresh_token = RefreshToken.active.find_by(refresh_token: refresh_token_value)
27
+ return nil unless refresh_token
28
+
29
+ # Validate device and IP for security
30
+ if refresh_token.ip_address != request.remote_ip
31
+ refresh_token.destroy
32
+ raise AuthenticationError, "Token compromised - IP address changed"
33
+ end
34
+
35
+ user = refresh_token.user
36
+ access_token = TokenService.issue(TokenService.access_token_payload(user))
37
+
38
+ {
39
+ access_token: access_token,
40
+ user: user
41
+ }
42
+ end
43
+
44
+ def revoke_refresh_token(token_value)
45
+ RefreshToken.find_by(refresh_token: token_value)&.destroy
46
+ end
47
+
48
+ def revoke_all_user_tokens(user)
49
+ user.refresh_tokens.destroy_all
50
+ end
51
+
52
+ private
53
+
54
+ def find_user_by_email_or_phone(email_or_phone)
55
+ if email_or_phone.include?("@")
56
+ User.find_by(email: email_or_phone)
57
+ else
58
+ User.find_by(phone: email_or_phone)
59
+ end
60
+ end
61
+
62
+ def create_refresh_token(user, request)
63
+ # Create a simple struct-like object for the refresh token generation
64
+ request_data = OpenStruct.new(
65
+ params: {device_id: request.params[:device_id]},
66
+ remote_ip: request.remote_ip,
67
+ user_agent: request.user_agent
68
+ )
69
+
70
+ RefreshToken.generate(user, request_data)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,55 @@
1
+ require "jwt"
2
+
3
+ module Dscf
4
+ module Core
5
+ class TokenService
6
+ class << self
7
+ def issue(payload, expires_in: 15.minutes)
8
+ payload = payload.merge(exp: Time.current.to_i + expires_in.to_i)
9
+ ::JWT.encode(payload, key, "HS256")
10
+ end
11
+
12
+ def decode(token)
13
+ return nil if token.blank?
14
+
15
+ begin
16
+ decoded = ::JWT.decode(token, key, true, algorithm: "HS256")
17
+ decoded.first
18
+ rescue ::JWT::ExpiredSignature
19
+ raise AuthenticationError, "Token has expired"
20
+ rescue ::JWT::DecodeError
21
+ raise AuthenticationError, "Invalid token"
22
+ end
23
+ end
24
+
25
+ def refresh_token_payload(user, device: nil, ip_address: nil)
26
+ {
27
+ user_id: user.id,
28
+ identifier: user.email || user.phone,
29
+ device: device,
30
+ ip_address: ip_address,
31
+ type: "refresh"
32
+ }
33
+ end
34
+
35
+ def access_token_payload(user)
36
+ {
37
+ user_id: user.id,
38
+ identifier: user.email || user.phone,
39
+ type: "access"
40
+ }
41
+ end
42
+
43
+ def key
44
+ @key ||= ENV.fetch("JWT_SECRET_KEY", nil) ||
45
+ if Rails.env.test?
46
+ "test_jwt_secret_key_for_testing_purposes_only_12345678901234567890"
47
+ else
48
+ Rails.application.credentials.secret_key_base
49
+ end ||
50
+ "fallback_secret_key_for_development_only_12345678901234567890"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,23 @@
1
+ # JWT Configuration
2
+ JWT_CONFIG = {
3
+ algorithm: "HS256",
4
+ access_token_expiry: 15.minutes,
5
+ refresh_token_expiry: 30.days,
6
+ issuer: "dscf-core",
7
+ audience: "dscf-api"
8
+ }.freeze
9
+
10
+ # JWT Secret Key Configuration
11
+ # Priority: ENV["JWT_SECRET_KEY"] > Rails credentials > Rails secret_key_base
12
+ JWT_SECRET_KEY = ENV.fetch("JWT_SECRET_KEY") do
13
+ if Rails.env.test?
14
+ "test_jwt_secret_key_for_testing_purposes_only_12345678901234567890"
15
+ else
16
+ Rails.application.credentials.secret_key_base
17
+ end
18
+ end
19
+
20
+ # # Validate JWT configuration on startup
21
+ # Rails.application.config.after_initialize do
22
+ # raise "JWT_SECRET_KEY must be set and at least 32 characters long" unless JWT_SECRET_KEY.present? && JWT_SECRET_KEY.length >= 32
23
+ # end
@@ -0,0 +1,4 @@
1
+ Ransack.configure do |config|
2
+ config.ignore_unknown_conditions = false
3
+
4
+ end
@@ -0,0 +1,23 @@
1
+ en:
2
+ auth:
3
+ success:
4
+ login: "Login successful."
5
+ signup: "Account created successfully. Please log in to continue."
6
+ logout: "Logged out successfully."
7
+ me: "Profile fetched successfully."
8
+ refresh: "Token refreshed successfully."
9
+ errors:
10
+ invalid_credentials: "Invalid credentials."
11
+ missing_email_or_phone: "Either email or phone must be provided."
12
+ signup_failed: "Failed to create account."
13
+ invalid_token: "Invalid refresh token."
14
+
15
+ role:
16
+ success:
17
+ show: "Role details retrieved successfully"
18
+ create: "Role created successfully"
19
+ update: "Role updated successfully"
20
+ errors:
21
+ create: "Failed to create role"
22
+ update: "Failed to update role"
23
+ show: "Role not found"
data/config/routes.rb CHANGED
@@ -1,2 +1,7 @@
1
1
  Dscf::Core::Engine.routes.draw do
2
+ post "auth/login", to: "auth#login"
3
+ post "auth/logout", to: "auth#logout"
4
+ post "auth/signup", to: "auth#signup"
5
+ post "auth/refresh", to: "auth#refresh"
6
+ get "auth/me", to: "auth#me"
2
7
  end
@@ -1,4 +1,4 @@
1
- class CreateDscfCoreBusinesses < ActiveRecord::Migration[7.1]
1
+ class CreateDscfCoreBusinesses < ActiveRecord::Migration[8.0]
2
2
  def change
3
3
  create_table :dscf_core_businesses do |t|
4
4
  t.string :name, null: false
@@ -0,0 +1,19 @@
1
+ class CreateDscfCoreAddresses < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :dscf_core_addresses do |t|
4
+ t.references :user, null: false, index: {name: "user_on_dc_add_indx"}, foreign_key: {to_table: :dscf_core_users}
5
+ t.integer :address_type
6
+ t.string :country, null: false, default: "Ethiopia"
7
+ t.string :city
8
+ t.string :sub_city
9
+ t.string :woreda
10
+ t.string :kebele
11
+ t.string :house_numbers
12
+ t.string :po_box
13
+ t.decimal :latitude, precision: 10, scale: 6
14
+ t.decimal :longitude, precision: 10, scale: 6
15
+
16
+ t.timestamps
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ class CreateDscfCoreDocuments < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :dscf_core_documents do |t|
4
+ t.references :documentable, polymorphic: true, null: false
5
+ t.string :document_type, null: false
6
+ t.boolean :is_verified, default: false
7
+ t.references :verified_by, polymorphic: true, null: true
8
+ t.datetime :verified_at
9
+ t.jsonb :metadata, default: {}
10
+
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ class ChangeDocumentTypeInDscfCoreDocuments < ActiveRecord::Migration[8.0]
2
+ def up
3
+ remove_column :dscf_core_documents, :document_type
4
+ add_column :dscf_core_documents, :document_type, :integer, null: false
5
+ end
6
+
7
+ def down
8
+ remove_column :dscf_core_documents, :document_type
9
+ add_column :dscf_core_documents, :document_type, :string, null: false
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ class CreateDscfCoreUserProfiles < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :dscf_core_user_profiles do |t|
4
+ t.references :user, null: false, index: {name: "user_on_dc_up_indx"}, foreign_key: {to_table: :dscf_core_users}
5
+ t.string :first_name, null: false
6
+ t.string :middle_name
7
+ t.string :last_name, null: false
8
+ t.integer :gender
9
+ t.string :nationality
10
+ t.date :date_of_birth
11
+ t.string :place_of_birth
12
+ t.string :fayda_number
13
+ t.string :occupation
14
+ t.string :position_title
15
+ t.string :employer_name
16
+ t.decimal :avg_monthly_income, precision: 18, scale: 2
17
+ t.decimal :avg_annual_income, precision: 18, scale: 2
18
+ t.integer :pep_status
19
+ t.decimal :risk_score, precision: 5, scale: 2
20
+ t.boolean :watchlist_hit, null: false
21
+ t.integer :verification_status, null: false, default: 0
22
+
23
+ t.timestamps
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ class CreateDscfCoreRefreshTokens < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :dscf_core_refresh_tokens do |t|
4
+ t.references :user, null: false, foreign_key: {to_table: :dscf_core_users}
5
+ t.string :refresh_token, null: false
6
+ t.datetime :expires_at, null: false
7
+ t.string :device
8
+ t.string :ip_address
9
+ t.string :user_agent
10
+
11
+ t.timestamps
12
+ end
13
+ add_index :dscf_core_refresh_tokens, :refresh_token, unique: true
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ class ChangePepStatusToIntegerInUserProfiles < ActiveRecord::Migration[8.0]
2
+ def change
3
+ change_column :dscf_core_user_profiles, :pep_status, :integer
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class MakeEmailAndPhoneOptionalForUsers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ change_column_null :dscf_core_users, :email, true
4
+ change_column_null :dscf_core_users, :phone, true
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class AddDefaultsToUserProfiles < ActiveRecord::Migration[8.0]
2
+ def change
3
+ change_column_default :dscf_core_user_profiles, :watchlist_hit, false
4
+ change_column_default :dscf_core_user_profiles, :verification_status, 0 # pending
5
+ end
6
+ end
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Core
3
- VERSION = "0.1.1".freeze
3
+ VERSION = "0.1.3".freeze
4
4
  end
5
5
  end
data/lib/dscf/core.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require "dscf/core/version"
2
2
  require "dscf/core/engine"
3
+ require "ransack"
4
+ require "active_model_serializers"
3
5
 
4
6
  module Dscf
5
7
  module Core
@@ -0,0 +1,15 @@
1
+ FactoryBot.define do
2
+ factory :address, class: "Dscf::Core::Address" do
3
+ user
4
+ address_type { Dscf::Core::Address.address_types.keys.sample }
5
+ country { Faker::Address.country }
6
+ city { Faker::Address.city }
7
+ sub_city { Faker::Address.community }
8
+ woreda { Faker::Address.community }
9
+ kebele { Faker::Address.community }
10
+ house_numbers { Faker::Address.building_number }
11
+ po_box { Faker::Address.postcode }
12
+ latitude { Faker::Address.latitude }
13
+ longitude { Faker::Address.longitude }
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ FactoryBot.define do
2
+ factory :document, class: "Dscf::Core::Document" do
3
+ association :documentable, factory: :user # Default documentable
4
+ document_type { Dscf::Core::Document.document_types.keys.sample }
5
+ is_verified { false }
6
+ verified_at { nil }
7
+ metadata { {"source" => "test"} }
8
+
9
+ trait :for_user do
10
+ association :documentable, factory: :user
11
+ end
12
+
13
+ trait :for_business do
14
+ association :documentable, factory: :business
15
+ end
16
+
17
+ after(:build) do |document|
18
+ document.file.attach(
19
+ io: File.open(Rails.root.join("app", "fixtures", "images", "logo.jpg")),
20
+ filename: "logo.jpg",
21
+ content_type: "image/jpeg"
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ FactoryBot.define do
2
+ factory :refresh_token, class: "Dscf::Core::RefreshToken" do
3
+ association :user, factory: :user
4
+ refresh_token { SecureRandom.hex(32) }
5
+ expires_at { 30.days.from_now }
6
+ device { Faker::Device.model_name }
7
+ ip_address { Faker::Internet.ip_v4_address }
8
+ user_agent { Faker::Internet.user_agent }
9
+ end
10
+
11
+ # Factory for expired tokens
12
+ factory :expired_refresh_token, parent: :refresh_token do
13
+ expires_at { 1.day.ago }
14
+ end
15
+
16
+ # Factory for tokens without automatic generation
17
+ factory :manual_refresh_token, parent: :refresh_token do
18
+ refresh_token { nil }
19
+ expires_at { nil }
20
+
21
+ after(:build) do |token|
22
+ token.instance_variable_set(:@skip_before_validation, true)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ FactoryBot.define do
2
+ factory :user_profile, class: "Dscf::Core::UserProfile" do
3
+ user
4
+ first_name { Faker::Name.first_name }
5
+ middle_name { Faker::Name.middle_name }
6
+ last_name { Faker::Name.last_name }
7
+ gender { %w[male female].sample }
8
+ nationality { Faker::Address.country }
9
+ date_of_birth { Faker::Date.birthday(min_age: 18, max_age: 65) }
10
+ place_of_birth { Faker::Address.city }
11
+ fayda_number { Faker::Alphanumeric.alpha(number: 10) }
12
+ occupation { Faker::Job.title }
13
+ position_title { Faker::Job.position }
14
+ employer_name { Faker::Company.name }
15
+ avg_monthly_income { Faker::Number.decimal(l_digits: 5, r_digits: 2) }
16
+ avg_annual_income { Faker::Number.decimal(l_digits: 6, r_digits: 2) }
17
+ pep_status { [0, 1, 2, 3].sample }
18
+ risk_score { Faker::Number.decimal(l_digits: 1, r_digits: 2) }
19
+ watchlist_hit { false }
20
+ verification_status { 0 }
21
+ end
22
+ end