diff --git a/db/migrate/20240418233828_create_accredited_representative_portal_verified_representatives.rb b/db/migrate/20240418233828_create_accredited_representative_portal_verified_representatives.rb new file mode 100644 index 00000000000..d5e5b1fc7ab --- /dev/null +++ b/db/migrate/20240418233828_create_accredited_representative_portal_verified_representatives.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateAccreditedRepresentativePortalVerifiedRepresentatives < ActiveRecord::Migration[7.1] + def change + create_table :accredited_representative_portal_verified_representatives do |t| + t.string :ogc_registration_number, null: false + t.string :first_name + t.string :last_name + t.string :middle_initial + t.string :email, null: false + + t.timestamps + + t.index 'ogc_registration_number', unique: true, name: 'index_verified_representatives_on_ogc_number' + t.index 'email', unique: true, name: 'index_verified_representatives_on_email' + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 8ef34f9a216..ce5577884fc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_04_17_130647) do +ActiveRecord::Schema[7.1].define(version: 2024_04_18_233828) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "pg_stat_statements" @@ -128,6 +128,16 @@ t.index ["poa_code"], name: "index_accredited_organizations_on_poa_code", unique: true end + create_table "accredited_representative_portal_verified_representatives", force: :cascade do |t| + t.string "ogc_registration_number", null: false + t.string "first_name" + t.string "last_name" + t.string "middle_initial" + t.string "email", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false diff --git a/modules/accredited_representative_portal/app/models/accredited_representative_portal/representative_user.rb b/modules/accredited_representative_portal/app/models/accredited_representative_portal/representative_user.rb index b4e58fcee9a..8ba2c67a83a 100644 --- a/modules/accredited_representative_portal/app/models/accredited_representative_portal/representative_user.rb +++ b/modules/accredited_representative_portal/app/models/accredited_representative_portal/representative_user.rb @@ -17,7 +17,7 @@ class RepresentativeUser < Common::RedisStore attribute :last_signed_in attribute :loa attribute :logingov_uuid - attribute :ogc_number + attribute :ogc_registration_number attribute :poa_codes attribute :sign_in attribute :uuid diff --git a/modules/accredited_representative_portal/app/models/accredited_representative_portal/verified_representative.rb b/modules/accredited_representative_portal/app/models/accredited_representative_portal/verified_representative.rb new file mode 100644 index 00000000000..fe18a961790 --- /dev/null +++ b/modules/accredited_representative_portal/app/models/accredited_representative_portal/verified_representative.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module AccreditedRepresentativePortal + # Represents a verified representative within the Accredited Representative Portal. + # This class is responsible for managing the data associated with individuals who have + # been verified as representatives by the ARF Team. The model includes validations to ensure the presence and + # uniqueness of identifiers such as the OGC registration number and email. + # + # Currently, this model is populated manually by engineers as users are accepted into the pilot program. + # There is potential for a UI to be developed in the future that would facilitate administrative tasks + # related to managing verified representatives. + # + # A more automated process may be possible once OGC and MPI data facilitate such a process. + # + # == Associations + # This model may eventually be associated with AccreditedIndividuals to pull POA codes, + # if they exist, based on the OGC registration number. It currently does so via a helper method. + class VerifiedRepresentative < ApplicationRecord + validates :ogc_registration_number, presence: true, uniqueness: { case_sensitive: false } + validates :first_name, presence: true + validates :last_name, presence: true + validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } + + # NOTE: given there will be RepresentativeUsers who are not VerifiedRepresentatives, + # it's okay for this to return nil + def poa_codes + AccreditedIndividual.find_by(registration_number: ogc_registration_number)&.poa_codes + end + end +end diff --git a/modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_loader.rb b/modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_loader.rb index 1f29f749059..2ad1e707209 100644 --- a/modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_loader.rb +++ b/modules/accredited_representative_portal/app/services/accredited_representative_portal/representative_user_loader.rb @@ -9,6 +9,7 @@ class RepresentativeNotFoundError < StandardError; end def initialize(access_token:, request_ip:) @access_token = access_token @request_ip = request_ip + @verified_representative = VerifiedRepresentative.find_by(email: session&.credential_email) end def perform @@ -56,16 +57,16 @@ def user_verification @user_verification ||= session.user_verification end - def get_poa_codes - rep = Veteran::Service::Representative.find_by(representative_id: ogc_number) - # TODO-ARF 80297: Determine how to get ogc_number into RepresentativeUserLoader - # raise RepresentativeNotFoundError unless rep - - rep&.poa_codes + # NOTE: given there will be RepresentativeUsers who are not VerifiedRepresentatives, + # it's okay for this to return nil + def get_ogc_registration_number + @verified_representative&.ogc_registration_number end - def ogc_number - # TODO-ARF 80297: Determine how to get ogc_number into RepresentativeUserLoader + # NOTE: given there will be RepresentativeUsers who are not VerifiedRepresentatives, + # it's okay for this to return nil + def get_poa_codes + @verified_representative&.poa_codes end def current_user @@ -81,7 +82,7 @@ def current_user user.authn_context = authn_context user.loa = loa user.logingov_uuid = user_verification.logingov_uuid - user.ogc_number = ogc_number # TODO-ARF 80297: Determine how to get ogc_number into RepresentativeUserLoader + user.ogc_registration_number = get_ogc_registration_number user.poa_codes = get_poa_codes user.idme_uuid = user_verification.idme_uuid user.last_signed_in = session.created_at diff --git a/modules/accredited_representative_portal/spec/factories/representative_user.rb b/modules/accredited_representative_portal/spec/factories/representative_user.rb index 89ce83812e8..3dd401ab597 100644 --- a/modules/accredited_representative_portal/spec/factories/representative_user.rb +++ b/modules/accredited_representative_portal/spec/factories/representative_user.rb @@ -13,8 +13,6 @@ last_signed_in { Time.zone.now } authn_context { LOA::IDME_LOA3_VETS } loa { { current: LOA::THREE, highest: LOA::THREE } } - ogc_number { '123456789' } - poa_codes { %w[1234 5678] } sign_in { { service_name: SignIn::Constants::Auth::IDME, client_id: SecureRandom.uuid, auth_broker: SignIn::Constants::Auth::BROKER_CODE } diff --git a/modules/accredited_representative_portal/spec/factories/verified_representative.rb b/modules/accredited_representative_portal/spec/factories/verified_representative.rb new file mode 100644 index 00000000000..aaa10cf8806 --- /dev/null +++ b/modules/accredited_representative_portal/spec/factories/verified_representative.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :verified_representative, class: 'AccreditedRepresentativePortal::VerifiedRepresentative' do + ogc_registration_number { Faker::Number.unique.number(digits: 6).to_s } + first_name { Faker::Name.first_name } + last_name { Faker::Name.last_name } + middle_initial { Faker::Name.middle_name } + email { Faker::Internet.unique.email } + end +end diff --git a/modules/accredited_representative_portal/spec/models/accredited_representatiive_portal/verified_representative_spec.rb b/modules/accredited_representative_portal/spec/models/accredited_representatiive_portal/verified_representative_spec.rb new file mode 100644 index 00000000000..035b7dc0d1d --- /dev/null +++ b/modules/accredited_representative_portal/spec/models/accredited_representatiive_portal/verified_representative_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AccreditedRepresentativePortal::VerifiedRepresentative, type: :model do + describe 'validations' do + subject { build(:verified_representative) } + + it { is_expected.to validate_presence_of(:ogc_registration_number) } + it { is_expected.to validate_uniqueness_of(:ogc_registration_number).case_insensitive } + it { is_expected.to validate_presence_of(:first_name) } + it { is_expected.to validate_presence_of(:last_name) } + it { is_expected.to validate_presence_of(:email) } + it { is_expected.to validate_uniqueness_of(:email) } + + it do + expect(subject).to allow_value('test@example.com').for(:email) + expect(subject).not_to allow_value('invalid_email').for(:email) + end + end + + describe '#poa_codes' do + let(:ogc_registration_number) { '12345' } + + context 'when an AccreditedIndividual with a matching registration number exists' do + let!(:accredited_individual) do + create(:accredited_individual, :with_organizations, registration_number: ogc_registration_number) + end + let(:verified_representative) do + create(:verified_representative, ogc_registration_number:) + end + + it 'returns the correct POA codes' do + expect(verified_representative.poa_codes).to be_present + expect(verified_representative.poa_codes).to match_array(accredited_individual.poa_codes) + end + end + + context 'when no AccreditedIndividual with a matching registration number exists' do + let(:verified_representative) do + create(:verified_representative, ogc_registration_number:) + end + + it 'returns nil' do + expect(verified_representative.poa_codes).to be_nil + end + end + end +end diff --git a/modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_loader_spec.rb b/modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_loader_spec.rb index c7579507b92..ebdcad8e504 100644 --- a/modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_loader_spec.rb +++ b/modules/accredited_representative_portal/spec/services/accredited_representative_portal/representative_user_loader_spec.rb @@ -9,8 +9,6 @@ let(:reloaded_user) { representative_user_loader.perform } let(:access_token) { create(:access_token, user_uuid: user.uuid, session_handle:) } - let(:ogc_number) { '123456' } # TODO-ARF 80297: Determine how to get ogc_number into RepresentativeUserLoader - let(:poa_codes) { %w[A1 B2 C3] } let!(:user) do create(:representative_user, uuid: user_uuid, icn: user_icn, loa: user_loa) end @@ -22,14 +20,6 @@ let(:session) { create(:oauth_session, user_account:, user_verification:) } let(:session_handle) { session.handle } let(:request_ip) { '123.456.78.90' } - let!(:representative) do - FactoryBot.create(:representative, first_name: 'Bob', last_name: 'Smith', representative_id: ogc_number, - poa_codes:) - end - - before do - allow_any_instance_of(described_class).to receive(:ogc_number).and_return(ogc_number) - end shared_examples 'reloaded user' do context 'and associated session cannot be found' do @@ -71,14 +61,56 @@ expect(reloaded_user.icn).to eq(user_icn) expect(reloaded_user.idme_uuid).to eq(idme_uuid) expect(reloaded_user.logingov_uuid).to eq(nil) - expect(reloaded_user.ogc_number).to eq(ogc_number) - expect(reloaded_user.poa_codes).to eq(poa_codes) expect(reloaded_user.fingerprint).to eq(request_ip) expect(reloaded_user.last_signed_in).to eq(session.created_at) expect(reloaded_user.authn_context).to eq(authn_context) expect(reloaded_user.loa).to eq(user_loa) expect(reloaded_user.sign_in).to eq(sign_in) end + + context 'verified_representatives' do + let!(:ogc_registration_number) { '12300' } + let!(:verified_representative) do + create(:verified_representative, email: session.credential_email, + ogc_registration_number:) + end + let!(:accredited_individual) do + create(:accredited_individual, :with_organizations, registration_number: ogc_registration_number) + end + + describe '#ogc_registration_number' do + context 'when a matching verified_representative is found' do + it 'returns the OGC registration number' do + expect(reloaded_user.ogc_registration_number).to eq(verified_representative.ogc_registration_number) + end + end + + context 'when a verified_representative record does not exist for the user' do + let(:verified_representative) { nil } + + it 'returns nil' do + expect(reloaded_user.ogc_registration_number).to be_nil + end + end + end + + describe '#poa_codes' do + context 'when reloading a user' do + it 'sets the poa_codes based on the ogc_registration_number on the accredited_individual' do + expect(reloaded_user.poa_codes).to be_present + expect(reloaded_user.poa_codes).to match_array(accredited_individual.poa_codes) + end + end + + context 'when a verified_representative record does not exist for the user' do + let(:verified_representative) { nil } + + it 'returns nil' do + expect(reloaded_user.poa_codes).to be_nil + end + end + end + end end end @@ -99,31 +131,5 @@ it_behaves_like 'reloaded user' end - - describe '#get_poa_codes' do - before do - user.destroy - end - - context 'when reloading a user' do - it 'sets the poa_codes based on the ogc_number' do - expect(reloaded_user.poa_codes).to match_array(poa_codes) - end - end - - # context 'when no representative is found for the ogc_number' do - # let(:non_existent_ogc_number) { 'non-existent-number' } - - # before do - # allow_any_instance_of(described_class).to receive(:ogc_number).and_return(non_existent_ogc_number) - # end - - # it 'raises a RepresentativeNotFoundError' do - # expect do - # reloaded_user - # end.to raise_error(described_class::RepresentativeNotFoundError) - # end - # end - end end end