mas-rad_core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +3 -0
  4. data/Rakefile +14 -0
  5. data/app/models/accreditation.rb +5 -0
  6. data/app/models/adviser.rb +61 -0
  7. data/app/models/allowed_payment_method.rb +7 -0
  8. data/app/models/authentication.rb +19 -0
  9. data/app/models/firm.rb +143 -0
  10. data/app/models/in_person_advice_method.rb +7 -0
  11. data/app/models/initial_advice_fee_structure.rb +7 -0
  12. data/app/models/initial_meeting_duration.rb +7 -0
  13. data/app/models/investment_size.rb +7 -0
  14. data/app/models/lookup/adviser.rb +10 -0
  15. data/app/models/lookup/firm.rb +17 -0
  16. data/app/models/lookup/subsidiary.rb +11 -0
  17. data/app/models/ongoing_advice_fee_structure.rb +7 -0
  18. data/app/models/other_advice_method.rb +7 -0
  19. data/app/models/principal.rb +94 -0
  20. data/app/models/professional_body.rb +3 -0
  21. data/app/models/professional_standing.rb +3 -0
  22. data/app/models/qualification.rb +5 -0
  23. data/app/models/travel_distance.rb +15 -0
  24. data/config/routes.rb +2 -0
  25. data/db/migrate/20141211110031_create_lookup_firms.rb +11 -0
  26. data/db/migrate/20141221140208_create_principals.rb +23 -0
  27. data/db/migrate/20141222150416_add_last_sign_in_at_to_principal.rb +5 -0
  28. data/db/migrate/20141230112136_create_lookup_subsidiaries.rb +10 -0
  29. data/db/migrate/20150106115732_remove_last_sign_in_at_from_principals.rb +5 -0
  30. data/db/migrate/20150114144343_create_lookup_advisers.rb +10 -0
  31. data/db/migrate/20150114151447_create_firms.rb +13 -0
  32. data/db/migrate/20150115130949_add_address_to_firm.rb +9 -0
  33. data/db/migrate/20150119102735_create_in_person_advice_methods.rb +15 -0
  34. data/db/migrate/20150119111754_create_other_advice_methods.rb +15 -0
  35. data/db/migrate/20150119114218_add_free_initial_meeting_to_firm.rb +5 -0
  36. data/db/migrate/20150119125208_create_initial_meeting_durations.rb +11 -0
  37. data/db/migrate/20150119152325_create_initial_advice_fee_structures.rb +15 -0
  38. data/db/migrate/20150119152336_create_ongoing_advice_fee_structures.rb +15 -0
  39. data/db/migrate/20150120133717_create_allowed_payment_methods.rb +15 -0
  40. data/db/migrate/20150120141928_add_minimum_fixed_fee_to_firm.rb +5 -0
  41. data/db/migrate/20150120150738_create_investment_sizes.rb +15 -0
  42. data/db/migrate/20150121110757_create_advisers.rb +11 -0
  43. data/db/migrate/20150121123437_add_business_income_breakdown_fields_to_firm.rb +11 -0
  44. data/db/migrate/20150121134845_create_qualifications.rb +15 -0
  45. data/db/migrate/20150121154458_create_accreditations.rb +15 -0
  46. data/db/migrate/20150121173015_create_professional_standings.rb +15 -0
  47. data/db/migrate/20150121181341_create_professional_bodies.rb +15 -0
  48. data/db/migrate/20150121183728_add_confirmed_disclaimer_to_advisers.rb +5 -0
  49. data/db/migrate/20150124124350_add_geographical_fields_to_adviser.rb +7 -0
  50. data/db/migrate/20150125145457_add_parent_id_to_firms.rb +5 -0
  51. data/db/migrate/20150125164156_remove_unique_constraint_from_firms_fca_number.rb +5 -0
  52. data/db/migrate/20150127084858_add_ordering_on_reference_data.rb +31 -0
  53. data/db/migrate/20150209144836_remove_covers_whole_of_uk_from_advisers.rb +17 -0
  54. data/lib/mas/rad_core.rb +4 -0
  55. data/lib/mas/rad_core/engine.rb +21 -0
  56. data/lib/mas/rad_core/version.rb +5 -0
  57. data/lib/mas/stats.rb +19 -0
  58. data/lib/mas/tasks/rad_core_tasks.rake +4 -0
  59. data/spec/dummy/Rakefile +6 -0
  60. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  61. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  62. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  63. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  64. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  65. data/spec/dummy/bin/bundle +3 -0
  66. data/spec/dummy/bin/rails +4 -0
  67. data/spec/dummy/bin/rake +4 -0
  68. data/spec/dummy/bin/setup +29 -0
  69. data/spec/dummy/config.ru +4 -0
  70. data/spec/dummy/config/application.rb +26 -0
  71. data/spec/dummy/config/boot.rb +5 -0
  72. data/spec/dummy/config/database.example.yml +14 -0
  73. data/spec/dummy/config/database.yml +13 -0
  74. data/spec/dummy/config/environment.rb +5 -0
  75. data/spec/dummy/config/environments/development.rb +41 -0
  76. data/spec/dummy/config/environments/production.rb +79 -0
  77. data/spec/dummy/config/environments/test.rb +42 -0
  78. data/spec/dummy/config/initializers/assets.rb +11 -0
  79. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  80. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  81. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  82. data/spec/dummy/config/initializers/inflections.rb +16 -0
  83. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  84. data/spec/dummy/config/initializers/session_store.rb +3 -0
  85. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  86. data/spec/dummy/config/locales/en.yml +23 -0
  87. data/spec/dummy/config/routes.rb +3 -0
  88. data/spec/dummy/config/secrets.yml +22 -0
  89. data/spec/dummy/db/schema.rb +246 -0
  90. data/spec/dummy/log/test.log +13097 -0
  91. data/spec/dummy/public/404.html +67 -0
  92. data/spec/dummy/public/422.html +67 -0
  93. data/spec/dummy/public/500.html +66 -0
  94. data/spec/dummy/public/favicon.ico +0 -0
  95. data/spec/factories/accreditation.rb +5 -0
  96. data/spec/factories/adviser.rb +21 -0
  97. data/spec/factories/allowed_payment_method.rb +5 -0
  98. data/spec/factories/firm.rb +34 -0
  99. data/spec/factories/in_person_advice_method.rb +5 -0
  100. data/spec/factories/initial_advice_fee_structure.rb +5 -0
  101. data/spec/factories/initial_meeting_duration.rb +5 -0
  102. data/spec/factories/investment_size.rb +5 -0
  103. data/spec/factories/lookup_firm.rb +6 -0
  104. data/spec/factories/ongoing_advice_fee_structure.rb +5 -0
  105. data/spec/factories/other_advice_method.rb +5 -0
  106. data/spec/factories/principal.rb +15 -0
  107. data/spec/factories/professional_body.rb +5 -0
  108. data/spec/factories/professional_standing.rb +5 -0
  109. data/spec/factories/qualification.rb +5 -0
  110. data/spec/models/accreditation_spec.rb +3 -0
  111. data/spec/models/adviser_spec.rb +93 -0
  112. data/spec/models/allowed_payment_method_spec.rb +3 -0
  113. data/spec/models/firm_spec.rb +188 -0
  114. data/spec/models/in_person_advice_method_spec.rb +3 -0
  115. data/spec/models/initial_advice_fee_structure_spec.rb +3 -0
  116. data/spec/models/initial_meeting_duration_spec.rb +3 -0
  117. data/spec/models/investment_size_spec.rb +3 -0
  118. data/spec/models/lookup/adviser_spec.rb +21 -0
  119. data/spec/models/lookup/firm_spec.rb +19 -0
  120. data/spec/models/ongoing_advice_fee_structure_spec.rb +3 -0
  121. data/spec/models/other_advice_method_spec.rb +3 -0
  122. data/spec/models/principal_spec.rb +185 -0
  123. data/spec/models/qualification_spec.rb +3 -0
  124. data/spec/spec_helper.rb +24 -0
  125. data/spec/support/shared_examples/reference_data.rb +14 -0
  126. metadata +361 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8ddedc793fa1147bec67ccc827f16325a581689c
4
+ data.tar.gz: 82db2d2dc83430c01d804c0402c399f72cd0a82a
5
+ SHA512:
6
+ metadata.gz: 7968241320d7a619acc7c7f8022a29bba5529f2a9132d5f3ca10cb1efb0c3f2288b40d1a8396af7b31a5864ccfc72d13df06058c0add6e0e347012ba91194663
7
+ data.tar.gz: dd469eea433328259512990f84119ce6d8e9c248e9c4d65b49800c19a83969f0190321f95dfcdc572c08313c3d1f6f6270ef9b28a421d18ff48fefc1d00f1c2e
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Ben Lovell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # RadCore
2
+
3
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ require 'bundler/gem_tasks'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
9
+
10
+ load 'rails/tasks/engine.rake'
11
+
12
+ Bundler::GemHelper.install_tasks
13
+
14
+ task default: :spec
@@ -0,0 +1,5 @@
1
+ class Accreditation < ActiveRecord::Base
2
+ validates_presence_of :name
3
+
4
+ default_scope { order(:order) }
5
+ end
@@ -0,0 +1,61 @@
1
+ class Adviser < ActiveRecord::Base
2
+ belongs_to :firm
3
+
4
+ has_and_belongs_to_many :qualifications
5
+ has_and_belongs_to_many :accreditations
6
+ has_and_belongs_to_many :professional_standings
7
+ has_and_belongs_to_many :professional_bodies
8
+
9
+ before_validation :assign_name, if: :reference_number?
10
+
11
+ before_validation :upcase_postcode
12
+
13
+ validates_acceptance_of :confirmed_disclaimer, accept: true
14
+
15
+ validates :travel_distance,
16
+ presence: true,
17
+ inclusion: { in: TravelDistance.all.values }
18
+
19
+ validates :postcode,
20
+ presence: true,
21
+ format: { with: /\A[A-Z\d]{1,4} [A-Z\d]{1,3}\z/ }
22
+
23
+ validates :reference_number,
24
+ presence: true,
25
+ uniqueness: true,
26
+ format: {
27
+ with: /\A[A-Z]{3}[0-9]{5}\z/
28
+ }
29
+
30
+ validate :match_reference_number
31
+
32
+ def field_order
33
+ [
34
+ :reference_number,
35
+ :postcode,
36
+ :travel_distance,
37
+ :confirmed_disclaimer
38
+ ]
39
+ end
40
+
41
+ private
42
+
43
+ def upcase_postcode
44
+ postcode.upcase! if postcode.present?
45
+ end
46
+
47
+ def assign_name
48
+ self.name = Lookup::Adviser.find_by(
49
+ reference_number: reference_number
50
+ ).try(:name)
51
+ end
52
+
53
+ def match_reference_number
54
+ unless Lookup::Adviser.exists?(reference_number: reference_number)
55
+ errors.add(
56
+ :reference_number,
57
+ I18n.t('questionnaire.adviser.reference_number_un_matched')
58
+ )
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ class AllowedPaymentMethod < ActiveRecord::Base
2
+ has_and_belongs_to_many :firms
3
+
4
+ validates_presence_of :name
5
+
6
+ default_scope { order(:order) }
7
+ end
@@ -0,0 +1,19 @@
1
+ module Authentication
2
+ class << self
3
+ def required?
4
+ Rails.env.production?
5
+ end
6
+
7
+ def username
8
+ ENV['AUTH_USERNAME']
9
+ end
10
+
11
+ def password
12
+ ENV['AUTH_PASSWORD']
13
+ end
14
+
15
+ def authenticate(username, password)
16
+ self.username == username && self.password == password
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,143 @@
1
+ class Firm < ActiveRecord::Base
2
+ scope :registered, -> { where.not(email_address: nil) }
3
+
4
+ has_and_belongs_to_many :in_person_advice_methods
5
+ has_and_belongs_to_many :other_advice_methods
6
+ has_and_belongs_to_many :initial_advice_fee_structures
7
+ has_and_belongs_to_many :ongoing_advice_fee_structures
8
+ has_and_belongs_to_many :allowed_payment_methods
9
+ has_and_belongs_to_many :investment_sizes
10
+
11
+ belongs_to :initial_meeting_duration
12
+
13
+ belongs_to :principal, primary_key: :fca_number, foreign_key: :fca_number
14
+
15
+ has_many :advisers
16
+
17
+ has_many :subsidiaries, class_name: 'Firm', foreign_key: :parent_id
18
+
19
+ belongs_to :parent, class_name: 'Firm'
20
+
21
+ attr_accessor :percent_total
22
+
23
+ before_validation :upcase_postcode
24
+
25
+ validates :email_address,
26
+ presence: true,
27
+ length: { maximum: 50 },
28
+ format: { with: /.+@.+\..+/ }
29
+
30
+ validates :telephone_number,
31
+ presence: true,
32
+ length: { maximum: 30 },
33
+ format: { with: /\A[0-9 ]+\z/ }
34
+
35
+ validates :address_line_one,
36
+ presence: true,
37
+ length: { maximum: 100 }
38
+
39
+ validates :address_line_two,
40
+ length: { maximum: 100 }
41
+
42
+ validates :address_postcode,
43
+ presence: true,
44
+ format: { with: /\A[A-Z\d]{1,4} [A-Z\d]{1,3}\z/ }
45
+
46
+ validates :address_town,
47
+ :address_county,
48
+ presence: true
49
+
50
+ validates :free_initial_meeting,
51
+ inclusion: { in: [true, false] }
52
+
53
+ validates :initial_meeting_duration,
54
+ presence: true,
55
+ if: ->{ free_initial_meeting? }
56
+
57
+ validates :initial_advice_fee_structures,
58
+ length: { minimum: 1 }
59
+
60
+ validates :ongoing_advice_fee_structures,
61
+ length: { minimum: 1 }
62
+
63
+ validates :allowed_payment_methods,
64
+ length: { minimum: 1 }
65
+
66
+ validates :minimum_fixed_fee,
67
+ allow_blank: true,
68
+ numericality: { only_integer: true }
69
+
70
+ validate :sum_of_percentages_equals_one_hundred
71
+
72
+ validates :retirement_income_products_percent,
73
+ :pension_transfer_percent,
74
+ :long_term_care_percent,
75
+ :equity_release_percent,
76
+ :inheritance_tax_and_estate_planning_percent,
77
+ :wills_and_probate_percent,
78
+ :other_percent,
79
+ presence: true,
80
+ numericality: { only_integer: true }
81
+
82
+ validates :investment_sizes,
83
+ length: { minimum: 1 }
84
+
85
+ def in_person_advice?
86
+ in_person_advice_methods.present?
87
+ end
88
+
89
+ def subsidiary?
90
+ parent.present?
91
+ end
92
+
93
+ def field_order
94
+ [
95
+ :email_address,
96
+ :telephone_number,
97
+ :address_line_one,
98
+ :address_line_two,
99
+ :address_town,
100
+ :address_county,
101
+ :address_postcode,
102
+ :in_person_advice_methods,
103
+ :free_initial_meeting,
104
+ :initial_meeting_duration,
105
+ :initial_advice_fee_structures,
106
+ :ongoing_advice_fee_structures,
107
+ :allowed_payment_methods,
108
+ :minimum_fixed_fee,
109
+ :percent_total,
110
+ :retirement_income_products_percent,
111
+ :pension_transfer_percent,
112
+ :long_term_care_percent,
113
+ :equity_release_percent,
114
+ :inheritance_tax_and_estate_planning_percent,
115
+ :wills_and_probate_percent,
116
+ :other_percent,
117
+ :investment_sizes
118
+ ]
119
+ end
120
+
121
+ private
122
+
123
+ def upcase_postcode
124
+ address_postcode.upcase! if address_postcode.present?
125
+ end
126
+
127
+ def sum_of_percentages_equals_one_hundred
128
+ total = retirement_income_products_percent.to_i \
129
+ + pension_transfer_percent.to_i \
130
+ + long_term_care_percent.to_i \
131
+ + equity_release_percent.to_i \
132
+ + inheritance_tax_and_estate_planning_percent.to_i \
133
+ + wills_and_probate_percent.to_i \
134
+ + other_percent.to_i
135
+
136
+ unless total == 100
137
+ errors.add(
138
+ :percent_total,
139
+ I18n.t('questionnaire.retirement_advice.percent_total.not_one_hundred')
140
+ )
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,7 @@
1
+ class InPersonAdviceMethod < ActiveRecord::Base
2
+ has_and_belongs_to_many :firms
3
+
4
+ validates_presence_of :name
5
+
6
+ default_scope { order(:order) }
7
+ end
@@ -0,0 +1,7 @@
1
+ class InitialAdviceFeeStructure < ActiveRecord::Base
2
+ has_and_belongs_to_many :firms
3
+
4
+ validates_presence_of :name
5
+
6
+ default_scope { order(:order) }
7
+ end
@@ -0,0 +1,7 @@
1
+ class InitialMeetingDuration < ActiveRecord::Base
2
+ has_many :firms
3
+
4
+ validates_presence_of :name
5
+
6
+ default_scope { order(:order) }
7
+ end
@@ -0,0 +1,7 @@
1
+ class InvestmentSize < ActiveRecord::Base
2
+ has_and_belongs_to_many :firms
3
+
4
+ validates_presence_of :name
5
+
6
+ default_scope { order(:order) }
7
+ end
@@ -0,0 +1,10 @@
1
+ module Lookup
2
+ class Adviser < ActiveRecord::Base
3
+ validates_length_of :reference_number, is: 8
4
+ validates_presence_of :name
5
+
6
+ def self.table_name
7
+ "lookup_#{super}"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ module Lookup
2
+ class Firm < ActiveRecord::Base
3
+ has_many :subsidiaries, primary_key: :fca_number, foreign_key: :fca_number
4
+
5
+ validates :fca_number,
6
+ length: { is: 6 },
7
+ numericality: { only_integer: true }
8
+
9
+ def subsidiaries?
10
+ subsidiaries.present?
11
+ end
12
+
13
+ def self.table_name
14
+ "lookup_#{super}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module Lookup
2
+ class Subsidiary < ActiveRecord::Base
3
+ validates :fca_number,
4
+ length: { is: 6 },
5
+ numericality: { only_integer: true }
6
+
7
+ def self.table_name
8
+ "lookup_#{super}"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ class OngoingAdviceFeeStructure < ActiveRecord::Base
2
+ has_and_belongs_to_many :firms
3
+
4
+ validates_presence_of :name
5
+
6
+ default_scope { order(:order) }
7
+ end
@@ -0,0 +1,7 @@
1
+ class OtherAdviceMethod < ActiveRecord::Base
2
+ has_and_belongs_to_many :firms
3
+
4
+ validates_presence_of :name
5
+
6
+ default_scope { order(:order) }
7
+ end
@@ -0,0 +1,94 @@
1
+ class Principal < ActiveRecord::Base
2
+ self.primary_key = 'token'
3
+
4
+ before_create :generate_token
5
+ after_create :associate_firm
6
+
7
+ has_one :firm, primary_key: :fca_number, foreign_key: :fca_number
8
+
9
+ validates :fca_number,
10
+ presence: true,
11
+ uniqueness: true,
12
+ length: { is: 6 },
13
+ numericality: { only_integer: true }
14
+
15
+ validates :email_address,
16
+ presence: true,
17
+ uniqueness: true,
18
+ length: { maximum: 50 },
19
+ format: { with: /.+@.+\..+/ }
20
+
21
+ validates :first_name, :last_name, :job_title, presence: true, length: 2..80
22
+
23
+ validates :telephone_number,
24
+ presence: true,
25
+ length: { maximum: 50 },
26
+ format: { with: /\A[0-9 ]+\z/ }
27
+
28
+ validates :website_address,
29
+ allow_blank: true,
30
+ length: { maximum: 100 }
31
+
32
+ validates_acceptance_of :confirmed_disclaimer, accept: true
33
+
34
+ validate :match_fca_number, if: :fca_number?
35
+
36
+ def to_param
37
+ token.parameterize
38
+ end
39
+
40
+ def lookup_firm
41
+ @lookup_firm ||= Lookup::Firm.find_by(fca_number: fca_number)
42
+ end
43
+
44
+ delegate :subsidiaries?, to: :lookup_firm
45
+
46
+ def field_order
47
+ [
48
+ :fca_number,
49
+ :website_address,
50
+ :first_name,
51
+ :last_name,
52
+ :job_title,
53
+ :email_address,
54
+ :telephone_number,
55
+ :confirmed_disclaimer
56
+ ]
57
+ end
58
+
59
+ def find_or_create_subsidiary(id)
60
+ subsidiary = lookup_firm.subsidiaries.find(id)
61
+
62
+ find_subsidiary(subsidiary).tap do |firm|
63
+ firm.save(validate: false) unless firm.persisted?
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def find_subsidiary(subsidiary)
70
+ firm.subsidiaries.find_or_initialize_by(
71
+ registered_name: subsidiary.name,
72
+ fca_number: subsidiary.fca_number
73
+ )
74
+ end
75
+
76
+ def associate_firm
77
+ Firm.new(fca_number: lookup_firm.fca_number, registered_name: lookup_firm.registered_name).tap do |f|
78
+ f.save!(validate: false)
79
+ end
80
+ end
81
+
82
+ def match_fca_number
83
+ unless Lookup::Firm.exists?(fca_number: self.fca_number)
84
+ errors.add(
85
+ :fca_number,
86
+ I18n.t('registration.principal.fca_number_un_matched')
87
+ )
88
+ end
89
+ end
90
+
91
+ def generate_token
92
+ self.token = SecureRandom.hex(4)
93
+ end
94
+ end