diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs
index 671c49736..3db1d7dcd 100644
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/IApplicantProfileContactService.cs
@@ -7,8 +7,9 @@ namespace Unity.GrantManager.ApplicantProfile;
///
/// Provides applicant-profile-specific contact retrieval operations.
-/// This service aggregates contacts from two sources: profile-linked contacts
-/// and application-level contacts matched by OIDC subject.
+/// This service aggregates contacts from three sources: profile-linked contacts,
+/// application-level contacts matched by OIDC subject, and applicant agent
+/// contacts derived from the submission login token.
///
public interface IApplicantProfileContactService
{
@@ -26,4 +27,13 @@ public interface IApplicantProfileContactService
/// The OIDC subject identifier (e.g. "user@idir").
/// A list of with IsEditable set to false.
Task> GetApplicationContactsBySubjectAsync(string subject);
+
+ ///
+ /// Retrieves contacts derived from applicant agents on applications whose form submissions
+ /// match the given OIDC subject. The join path is Submission → Application → ApplicantAgent.
+ /// The subject is normalized by stripping the domain portion (after @) and converting to upper case.
+ ///
+ /// The OIDC subject identifier (e.g. "user@idir").
+ /// A list of with IsEditable set to false.
+ Task> GetApplicantAgentContactsBySubjectAsync(string subject);
}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs
index 2be1b4ed0..112eed817 100644
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/ApplicantProfile/ProfileData/ContactInfoItemDto.cs
@@ -17,5 +17,6 @@ public class ContactInfoItemDto
public bool IsPrimary { get; set; }
public bool IsEditable { get; set; }
public Guid? ApplicationId { get; set; }
+ public string? ReferenceNo { get; set; }
}
}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs
index 46ef6e66f..eba51fa13 100644
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ApplicantProfileContactService.cs
@@ -13,15 +13,18 @@
namespace Unity.GrantManager.ApplicantProfile;
///
-/// Applicant-profile-specific contact service. Retrieves contacts linked to applicant profiles
-/// and application-level contacts matched by OIDC subject. This service operates independently
-/// from the generic and queries repositories directly.
+/// Applicant-profile-specific contact service. Retrieves contacts linked to applicant profiles,
+/// application-level contacts matched by OIDC subject, and applicant agent contacts derived from
+/// the submission login token. This service operates independently from the generic
+/// and queries repositories directly.
///
public class ApplicantProfileContactService(
IContactRepository contactRepository,
IContactLinkRepository contactLinkRepository,
IRepository applicationFormSubmissionRepository,
- IRepository applicationContactRepository)
+ IRepository applicationContactRepository,
+ IRepository applicantAgentRepository,
+ IRepository applicationRepository)
: IApplicantProfileContactService, ITransientDependency
{
private const string ApplicantProfileEntityType = "ApplicantProfile";
@@ -52,24 +55,23 @@ join contact in contactsQuery on link.ContactId equals contact.Id
Role = link.Role,
IsPrimary = link.IsPrimary,
IsEditable = true,
- ApplicationId = null
+ ApplicationId = null,
+ ReferenceNo = null
}).ToListAsync();
}
///
public async Task> GetApplicationContactsBySubjectAsync(string subject)
{
- var normalizedSubject = subject.Contains('@')
- ? subject[..subject.IndexOf('@')].ToUpperInvariant()
- : subject.ToUpperInvariant();
-
var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync();
var applicationContactsQuery = await applicationContactRepository.GetQueryableAsync();
+ var applicationsQuery = await applicationRepository.GetQueryableAsync();
var applicationContacts = await (
from submission in submissionsQuery
join appContact in applicationContactsQuery on submission.ApplicationId equals appContact.ApplicationId
- where submission.OidcSub == normalizedSubject
+ join application in applicationsQuery on submission.ApplicationId equals application.Id
+ where submission.OidcSub == subject
select new ContactInfoItemDto
{
ContactId = appContact.Id,
@@ -82,12 +84,45 @@ join appContact in applicationContactsQuery on submission.ApplicationId equals a
ContactType = "Application",
IsPrimary = false,
IsEditable = false,
- ApplicationId = appContact.ApplicationId
+ ApplicationId = appContact.ApplicationId,
+ ReferenceNo = application.ReferenceNo
}).ToListAsync();
return applicationContacts;
}
+ ///
+ public async Task> GetApplicantAgentContactsBySubjectAsync(string subject)
+ {
+ var submissionsQuery = await applicationFormSubmissionRepository.GetQueryableAsync();
+ var agentsQuery = await applicantAgentRepository.GetQueryableAsync();
+ var applicationsQuery = await applicationRepository.GetQueryableAsync();
+
+ var agentContacts = await (
+ from submission in submissionsQuery
+ join agent in agentsQuery on submission.ApplicationId equals agent.ApplicationId
+ join application in applicationsQuery on submission.ApplicationId equals application.Id
+ where submission.OidcSub == subject
+ select new ContactInfoItemDto
+ {
+ ContactId = agent.Id,
+ Name = agent.Name,
+ Title = agent.Title,
+ Email = agent.Email,
+ WorkPhoneNumber = agent.Phone,
+ WorkPhoneExtension = agent.PhoneExtension,
+ MobilePhoneNumber = agent.Phone2,
+ Role = agent.RoleForApplicant,
+ ContactType = "ApplicantAgent",
+ IsPrimary = false,
+ IsEditable = false,
+ ApplicationId = agent.ApplicationId,
+ ReferenceNo = application.ReferenceNo
+ }).ToListAsync();
+
+ return agentContacts;
+ }
+
private static string GetMatchingRole(string contactType)
{
return ApplicationContactOptionList.ContactTypeList.TryGetValue(contactType, out string? value)
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs
index 13bd414ee..e028bb1b2 100644
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/ApplicantProfile/ContactInfoDataProvider.cs
@@ -7,7 +7,7 @@ namespace Unity.GrantManager.ApplicantProfile
{
///
/// Provides contact information for the applicant profile by aggregating
- /// profile-linked contacts and application-level contacts.
+ /// profile-linked contacts, application-level contacts, and applicant agent contacts.
///
[ExposeServices(typeof(IApplicantProfileDataProvider))]
public class ContactInfoDataProvider(
@@ -33,8 +33,15 @@ public async Task GetDataAsync(ApplicantProfileInfoRequ
var profileContacts = await applicantProfileContactService.GetProfileContactsAsync(request.ProfileId);
dto.Contacts.AddRange(profileContacts);
- var applicationContacts = await applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject);
+ var normalizedSubject = request.Subject.Contains('@')
+ ? request.Subject[..request.Subject.IndexOf('@')].ToUpperInvariant()
+ : request.Subject.ToUpperInvariant();
+
+ var applicationContacts = await applicantProfileContactService.GetApplicationContactsBySubjectAsync(normalizedSubject);
dto.Contacts.AddRange(applicationContacts);
+
+ var agentContacts = await applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(normalizedSubject);
+ dto.Contacts.AddRange(agentContacts);
}
return dto;
diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs
index cc8d41a68..7d7c20fc7 100644
--- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs
+++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileDataProviderTests.cs
@@ -34,6 +34,8 @@ private static ContactInfoDataProvider CreateContactInfoDataProvider()
.Returns(Task.FromResult(new List()));
applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any())
.Returns(Task.FromResult(new List()));
+ applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(Arg.Any())
+ .Returns(Task.FromResult(new List()));
return new ContactInfoDataProvider(currentTenant, applicantProfileContactService);
}
diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs
index 976ad574c..dde051f27 100644
--- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs
+++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs
@@ -22,6 +22,14 @@ public ContactInfoDataProviderTests()
_currentTenant = Substitute.For();
_currentTenant.Change(Arg.Any()).Returns(Substitute.For());
_applicantProfileContactService = Substitute.For();
+
+ _applicantProfileContactService.GetProfileContactsAsync(Arg.Any())
+ .Returns(new List());
+ _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any())
+ .Returns(new List());
+ _applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(Arg.Any())
+ .Returns(new List());
+
_provider = new ContactInfoDataProvider(_currentTenant, _applicantProfileContactService);
}
@@ -38,10 +46,6 @@ public async Task GetDataAsync_ShouldChangeTenant()
{
// Arrange
var request = CreateRequest();
- _applicantProfileContactService.GetProfileContactsAsync(Arg.Any())
- .Returns(new List());
- _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any())
- .Returns(new List());
// Act
await _provider.GetDataAsync(request);
@@ -55,10 +59,6 @@ public async Task GetDataAsync_ShouldCallGetProfileContactsWithProfileId()
{
// Arrange
var request = CreateRequest();
- _applicantProfileContactService.GetProfileContactsAsync(Arg.Any())
- .Returns(new List());
- _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any())
- .Returns(new List());
// Act
await _provider.GetDataAsync(request);
@@ -72,20 +72,29 @@ public async Task GetDataAsync_ShouldCallGetApplicationContactsWithSubject()
{
// Arrange
var request = CreateRequest();
- _applicantProfileContactService.GetProfileContactsAsync(Arg.Any())
- .Returns(new List());
- _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any())
- .Returns(new List());
// Act
await _provider.GetDataAsync(request);
// Assert
- await _applicantProfileContactService.Received(1).GetApplicationContactsBySubjectAsync(request.Subject);
+ await _applicantProfileContactService.Received(1).GetApplicationContactsBySubjectAsync("TESTUSER");
}
[Fact]
- public async Task GetDataAsync_ShouldCombineBothContactSets()
+ public async Task GetDataAsync_ShouldCallGetApplicantAgentContactsWithSubject()
+ {
+ // Arrange
+ var request = CreateRequest();
+
+ // Act
+ await _provider.GetDataAsync(request);
+
+ // Assert
+ await _applicantProfileContactService.Received(1).GetApplicantAgentContactsBySubjectAsync("TESTUSER");
+ }
+
+ [Fact]
+ public async Task GetDataAsync_ShouldCombineAllContactSets()
{
// Arrange
var request = CreateRequest();
@@ -98,17 +107,22 @@ public async Task GetDataAsync_ShouldCombineBothContactSets()
{
new() { ContactId = Guid.NewGuid(), Name = "App Contact 1", IsEditable = false }
};
+ var agentContacts = new List
+ {
+ new() { ContactId = Guid.NewGuid(), Name = "Agent Contact 1", IsEditable = false, ContactType = "ApplicantAgent" }
+ };
_applicantProfileContactService.GetProfileContactsAsync(request.ProfileId).Returns(profileContacts);
- _applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject).Returns(appContacts);
+ _applicantProfileContactService.GetApplicationContactsBySubjectAsync("TESTUSER").Returns(appContacts);
+ _applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync("TESTUSER").Returns(agentContacts);
// Act
var result = await _provider.GetDataAsync(request);
// Assert
var dto = result.ShouldBeOfType();
- dto.Contacts.Count.ShouldBe(3);
+ dto.Contacts.Count.ShouldBe(4);
dto.Contacts.Count(c => c.IsEditable).ShouldBe(2);
- dto.Contacts.Count(c => !c.IsEditable).ShouldBe(1);
+ dto.Contacts.Count(c => !c.IsEditable).ShouldBe(2);
}
[Fact]
@@ -116,10 +130,6 @@ public async Task GetDataAsync_WithNoContacts_ShouldReturnEmptyList()
{
// Arrange
var request = CreateRequest();
- _applicantProfileContactService.GetProfileContactsAsync(Arg.Any())
- .Returns(new List());
- _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any())
- .Returns(new List());
// Act
var result = await _provider.GetDataAsync(request);
@@ -130,7 +140,7 @@ public async Task GetDataAsync_WithNoContacts_ShouldReturnEmptyList()
}
[Fact]
- public async Task GetDataAsync_ProfileContactsShouldAppearBeforeApplicationContacts()
+ public async Task GetDataAsync_ContactsShouldAppearInExpectedOrder()
{
// Arrange
var request = CreateRequest();
@@ -146,10 +156,19 @@ public async Task GetDataAsync_ProfileContactsShouldAppearBeforeApplicationConta
Name = "App Second",
IsEditable = false
};
+ var agentContact = new ContactInfoItemDto
+ {
+ ContactId = Guid.NewGuid(),
+ Name = "Agent Third",
+ IsEditable = false,
+ ContactType = "ApplicantAgent"
+ };
_applicantProfileContactService.GetProfileContactsAsync(request.ProfileId)
.Returns(new List { profileContact });
- _applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject)
+ _applicantProfileContactService.GetApplicationContactsBySubjectAsync("TESTUSER")
.Returns(new List { appContact });
+ _applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync("TESTUSER")
+ .Returns(new List { agentContact });
// Act
var result = await _provider.GetDataAsync(request);
@@ -158,6 +177,7 @@ public async Task GetDataAsync_ProfileContactsShouldAppearBeforeApplicationConta
var dto = result.ShouldBeOfType();
dto.Contacts[0].Name.ShouldBe("Profile First");
dto.Contacts[1].Name.ShouldBe("App Second");
+ dto.Contacts[2].Name.ShouldBe("Agent Third");
}
[Fact]
@@ -165,10 +185,6 @@ public async Task GetDataAsync_ShouldReturnCorrectDataType()
{
// Arrange
var request = CreateRequest();
- _applicantProfileContactService.GetProfileContactsAsync(Arg.Any())
- .Returns(new List());
- _applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any())
- .Returns(new List());
// Act
var result = await _provider.GetDataAsync(request);
diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs
index 48fadd7b9..874f16fd3 100644
--- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs
+++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs
@@ -18,6 +18,8 @@ public class ApplicantProfileContactServiceTests
private readonly IContactLinkRepository _contactLinkRepository;
private readonly IRepository _submissionRepository;
private readonly IRepository _applicationContactRepository;
+ private readonly IRepository _applicantAgentRepository;
+ private readonly IRepository _applicationRepository;
private readonly ApplicantProfileContactService _service;
public ApplicantProfileContactServiceTests()
@@ -26,12 +28,16 @@ public ApplicantProfileContactServiceTests()
_contactLinkRepository = Substitute.For();
_submissionRepository = Substitute.For>();
_applicationContactRepository = Substitute.For>();
+ _applicantAgentRepository = Substitute.For>();
+ _applicationRepository = Substitute.For>();
_service = new ApplicantProfileContactService(
_contactRepository,
_contactLinkRepository,
_submissionRepository,
- _applicationContactRepository);
+ _applicationContactRepository,
+ _applicantAgentRepository,
+ _applicationRepository);
}
private static T WithId(T entity, Guid id) where T : Entity
@@ -143,11 +149,20 @@ public async Task GetApplicationContactsBySubjectAsync_WithMatchingSubmission_Sh
}, appContactId)
}.AsAsyncQueryable();
+ var applications = new[]
+ {
+ WithId(new Application
+ {
+ ReferenceNo = "REF-001"
+ }, applicationId)
+ }.AsAsyncQueryable();
+
_submissionRepository.GetQueryableAsync().Returns(submissions);
_applicationContactRepository.GetQueryableAsync().Returns(applicationContacts);
+ _applicationRepository.GetQueryableAsync().Returns(applications);
// Act
- var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir");
+ var result = await _service.GetApplicationContactsBySubjectAsync("TESTUSER");
// Assert
result.Count.ShouldBe(1);
@@ -163,10 +178,11 @@ public async Task GetApplicationContactsBySubjectAsync_WithMatchingSubmission_Sh
contact.IsPrimary.ShouldBeFalse();
contact.IsEditable.ShouldBeFalse();
contact.ApplicationId.ShouldBe(applicationId);
+ contact.ReferenceNo.ShouldBe("REF-001");
}
[Fact]
- public async Task GetApplicationContactsBySubjectAsync_ShouldMatchCaseInsensitively()
+ public async Task GetApplicationContactsBySubjectAsync_WithNonMatchingSubject_ShouldReturnEmpty()
{
// Arrange
var applicationId = Guid.NewGuid();
@@ -175,7 +191,7 @@ public async Task GetApplicationContactsBySubjectAsync_ShouldMatchCaseInsensitiv
{
new ApplicationFormSubmission
{
- OidcSub = "TESTUSER",
+ OidcSub = "OTHERUSER",
ApplicationId = applicationId,
ApplicantId = Guid.NewGuid(),
ApplicationFormId = Guid.NewGuid()
@@ -187,71 +203,60 @@ public async Task GetApplicationContactsBySubjectAsync_ShouldMatchCaseInsensitiv
WithId(new ApplicationContact
{
ApplicationId = applicationId,
- ContactFullName = "Case Test",
- ContactType = "ADDITIONAL_CONTACT"
+ ContactFullName = "Should Not Match"
}, Guid.NewGuid())
}.AsAsyncQueryable();
_submissionRepository.GetQueryableAsync().Returns(submissions);
_applicationContactRepository.GetQueryableAsync().Returns(applicationContacts);
+ _applicationRepository.GetQueryableAsync().Returns(
+ new[] { WithId(new Application(), applicationId) }.AsAsyncQueryable());
// Act
- var result = await _service.GetApplicationContactsBySubjectAsync("testuser@IDIR");
+ var result = await _service.GetApplicationContactsBySubjectAsync("DIFFERENTUSER");
// Assert
- result.Count.ShouldBe(1);
+ result.ShouldBeEmpty();
}
[Fact]
- public async Task GetApplicationContactsBySubjectAsync_ShouldStripDomainFromSubject()
+ public async Task GetApplicationContactsBySubjectAsync_WithNoSubmissions_ShouldReturnEmpty()
{
// Arrange
- var applicationId = Guid.NewGuid();
-
- var submissions = new[]
- {
- new ApplicationFormSubmission
- {
- OidcSub = "MYUSER",
- ApplicationId = applicationId,
- ApplicantId = Guid.NewGuid(),
- ApplicationFormId = Guid.NewGuid()
- }
- }.AsAsyncQueryable();
-
- var applicationContacts = new[]
- {
- WithId(new ApplicationContact
- {
- ApplicationId = applicationId,
- ContactFullName = "Domain Strip Test",
- ContactType = "ADDITIONAL_CONTACT"
- }, Guid.NewGuid())
- }.AsAsyncQueryable();
-
- _submissionRepository.GetQueryableAsync().Returns(submissions);
- _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts);
+ _submissionRepository.GetQueryableAsync()
+ .Returns(Array.Empty().AsAsyncQueryable());
+ _applicationContactRepository.GetQueryableAsync()
+ .Returns(Array.Empty().AsAsyncQueryable());
+ _applicationRepository.GetQueryableAsync()
+ .Returns(Array.Empty().AsAsyncQueryable());
// Act
- var result = await _service.GetApplicationContactsBySubjectAsync("myuser@differentdomain");
+ var result = await _service.GetApplicationContactsBySubjectAsync("TESTUSER");
// Assert
- result.Count.ShouldBe(1);
- result[0].Name.ShouldBe("Domain Strip Test");
+ result.ShouldBeEmpty();
}
[Fact]
- public async Task GetApplicationContactsBySubjectAsync_WithSubjectWithoutAtSign_ShouldStillMatch()
+ public async Task GetApplicationContactsBySubjectAsync_WithMultipleSubmissions_ShouldReturnAllContacts()
{
// Arrange
- var applicationId = Guid.NewGuid();
+ var appId1 = Guid.NewGuid();
+ var appId2 = Guid.NewGuid();
var submissions = new[]
{
new ApplicationFormSubmission
{
- OidcSub = "PLAINUSER",
- ApplicationId = applicationId,
+ OidcSub = "TESTUSER",
+ ApplicationId = appId1,
+ ApplicantId = Guid.NewGuid(),
+ ApplicationFormId = Guid.NewGuid()
+ },
+ new ApplicationFormSubmission
+ {
+ OidcSub = "TESTUSER",
+ ApplicationId = appId2,
ApplicantId = Guid.NewGuid(),
ApplicationFormId = Guid.NewGuid()
}
@@ -261,125 +266,112 @@ public async Task GetApplicationContactsBySubjectAsync_WithSubjectWithoutAtSign_
{
WithId(new ApplicationContact
{
- ApplicationId = applicationId,
- ContactFullName = "Plain User Contact",
+ ApplicationId = appId1,
+ ContactFullName = "Contact App 1",
ContactType = "ADDITIONAL_CONTACT"
+ }, Guid.NewGuid()),
+ WithId(new ApplicationContact
+ {
+ ApplicationId = appId2,
+ ContactFullName = "Contact App 2",
+ ContactType = "ADDITIONAL_SIGNING_AUTHORITY"
}, Guid.NewGuid())
}.AsAsyncQueryable();
_submissionRepository.GetQueryableAsync().Returns(submissions);
_applicationContactRepository.GetQueryableAsync().Returns(applicationContacts);
+ _applicationRepository.GetQueryableAsync().Returns(
+ new[]
+ {
+ WithId(new Application(), appId1),
+ WithId(new Application(), appId2)
+ }.AsAsyncQueryable());
// Act
- var result = await _service.GetApplicationContactsBySubjectAsync("plainuser");
+ var result = await _service.GetApplicationContactsBySubjectAsync("TESTUSER");
// Assert
- result.Count.ShouldBe(1);
+ result.Count.ShouldBe(2);
+ result.ShouldAllBe(c => !c.IsEditable);
+ result.ShouldAllBe(c => !c.IsPrimary);
}
[Fact]
- public async Task GetApplicationContactsBySubjectAsync_WithNonMatchingSubject_ShouldReturnEmpty()
+ public async Task GetApplicantAgentContactsBySubjectAsync_WithMatchingSubmission_ShouldReturnAgentContacts()
{
// Arrange
var applicationId = Guid.NewGuid();
+ var agentId = Guid.NewGuid();
var submissions = new[]
{
new ApplicationFormSubmission
{
- OidcSub = "OTHERUSER",
+ OidcSub = "TESTUSER",
ApplicationId = applicationId,
ApplicantId = Guid.NewGuid(),
ApplicationFormId = Guid.NewGuid()
}
}.AsAsyncQueryable();
- var applicationContacts = new[]
+ var agents = new[]
{
- WithId(new ApplicationContact
+ WithId(new ApplicantAgent
{
ApplicationId = applicationId,
- ContactFullName = "Should Not Match"
- }, Guid.NewGuid())
+ ApplicantId = Guid.NewGuid(),
+ Name = "Agent Smith",
+ Title = "Signing Authority",
+ Email = "agent@example.com",
+ Phone = "777-7777",
+ PhoneExtension = "201",
+ Phone2 = "888-8888",
+ RoleForApplicant = "Primary Contact"
+ }, agentId)
}.AsAsyncQueryable();
_submissionRepository.GetQueryableAsync().Returns(submissions);
- _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts);
+ _applicantAgentRepository.GetQueryableAsync().Returns(agents);
+ _applicationRepository.GetQueryableAsync().Returns(
+ new[] { WithId(new Application { ReferenceNo = "REF-AGENT-001" }, applicationId) }.AsAsyncQueryable());
// Act
- var result = await _service.GetApplicationContactsBySubjectAsync("differentuser@idir");
+ var result = await _service.GetApplicantAgentContactsBySubjectAsync("TESTUSER");
// Assert
- result.ShouldBeEmpty();
+ result.Count.ShouldBe(1);
+ var contact = result[0];
+ contact.ContactId.ShouldBe(agentId);
+ contact.Name.ShouldBe("Agent Smith");
+ contact.Title.ShouldBe("Signing Authority");
+ contact.Email.ShouldBe("agent@example.com");
+ contact.WorkPhoneNumber.ShouldBe("777-7777");
+ contact.WorkPhoneExtension.ShouldBe("201");
+ contact.MobilePhoneNumber.ShouldBe("888-8888");
+ contact.Role.ShouldBe("Primary Contact");
+ contact.ContactType.ShouldBe("ApplicantAgent");
+ contact.IsPrimary.ShouldBeFalse();
+ contact.IsEditable.ShouldBeFalse();
+ contact.ApplicationId.ShouldBe(applicationId);
+ contact.ReferenceNo.ShouldBe("REF-AGENT-001");
}
[Fact]
- public async Task GetApplicationContactsBySubjectAsync_WithNoSubmissions_ShouldReturnEmpty()
+ public async Task GetApplicantAgentContactsBySubjectAsync_WithNoMatchingSubmissions_ShouldReturnEmpty()
{
// Arrange
_submissionRepository.GetQueryableAsync()
.Returns(Array.Empty().AsAsyncQueryable());
- _applicationContactRepository.GetQueryableAsync()
- .Returns(Array.Empty().AsAsyncQueryable());
+ _applicantAgentRepository.GetQueryableAsync()
+ .Returns(Array.Empty().AsAsyncQueryable());
+ _applicationRepository.GetQueryableAsync()
+ .Returns(Array.Empty().AsAsyncQueryable());
// Act
- var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir");
+ var result = await _service.GetApplicantAgentContactsBySubjectAsync("TESTUSER");
// Assert
result.ShouldBeEmpty();
}
-
- [Fact]
- public async Task GetApplicationContactsBySubjectAsync_WithMultipleSubmissions_ShouldReturnAllContacts()
- {
- // Arrange
- var appId1 = Guid.NewGuid();
- var appId2 = Guid.NewGuid();
-
- var submissions = new[]
- {
- new ApplicationFormSubmission
- {
- OidcSub = "TESTUSER",
- ApplicationId = appId1,
- ApplicantId = Guid.NewGuid(),
- ApplicationFormId = Guid.NewGuid()
- },
- new ApplicationFormSubmission
- {
- OidcSub = "TESTUSER",
- ApplicationId = appId2,
- ApplicantId = Guid.NewGuid(),
- ApplicationFormId = Guid.NewGuid()
- }
- }.AsAsyncQueryable();
-
- var applicationContacts = new[]
- {
- WithId(new ApplicationContact
- {
- ApplicationId = appId1,
- ContactFullName = "Contact App 1",
- ContactType = "ADDITIONAL_CONTACT"
- }, Guid.NewGuid()),
- WithId(new ApplicationContact
- {
- ApplicationId = appId2,
- ContactFullName = "Contact App 2",
- ContactType = "ADDITIONAL_SIGNING_AUTHORITY"
- }, Guid.NewGuid())
- }.AsAsyncQueryable();
-
- _submissionRepository.GetQueryableAsync().Returns(submissions);
- _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts);
-
- // Act
- var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir");
-
- // Assert
- result.Count.ShouldBe(2);
- result.ShouldAllBe(c => !c.IsEditable);
- result.ShouldAllBe(c => !c.IsPrimary);
- }
}
}
diff --git a/documentation/applicant-portal/applicant-profile-data-providers.md b/documentation/applicant-portal/applicant-profile-data-providers.md
index 93c919dbf..438f7a1bb 100644
--- a/documentation/applicant-portal/applicant-profile-data-providers.md
+++ b/documentation/applicant-portal/applicant-profile-data-providers.md
@@ -101,7 +101,7 @@ sequenceDiagram
### 1. ContactInfoDataProvider (`CONTACTINFO`)
-**Purpose:** Aggregates contact information from two sources — profile-linked contacts and application-level contacts.
+**Purpose:** Aggregates contact information from three sources — profile-linked contacts, application-level contacts, and applicant agent contacts derived from the submission login token.
**Dependencies:**
- `ICurrentTenant` — for multi-tenant scoping
@@ -112,7 +112,8 @@ sequenceDiagram
1. Switches to the requested tenant context.
2. Retrieves **profile contacts** — contacts linked to the applicant profile via `ContactLink` records where `RelatedEntityType == "ApplicantProfile"` and `RelatedEntityId == profileId`. These are **editable** (`IsEditable = true`).
3. Retrieves **application contacts** — contacts on applications whose form submissions match the normalized OIDC subject. These are **read-only** (`IsEditable = false`).
-4. Merges both lists into a single `ApplicantContactInfoDto.Contacts` collection.
+4. Retrieves **applicant agent contacts** — contact information derived from `ApplicantAgent` records on applications whose form submissions match the normalized OIDC subject. The join path is `Submission → Application → ApplicantAgent`. These are **read-only** (`IsEditable = false`).
+5. Merges all three lists into a single `ApplicantContactInfoDto.Contacts` collection.
**Subject Normalization:** The OIDC subject (e.g. `user@idir`) is normalized by stripping everything after `@` and converting to uppercase.
@@ -132,15 +133,27 @@ flowchart TD
AC1["Normalize Subject
strip domain, uppercase"]
AC2["Query ApplicationFormSubmission
WHERE OidcSub = normalizedSubject"]
AC3["JOIN ApplicationContact
ON ApplicationId"]
+ AC3b["JOIN Application
ON ApplicationId
for ReferenceNo"]
AC4["Map to ContactInfoItemDto
IsEditable = false"]
- AC1 --> AC2 --> AC3 --> AC4
+ AC1 --> AC2 --> AC3 --> AC3b --> AC4
+ end
+
+ subgraph AgentContacts["Applicant Agent Contacts - Read-Only"]
+ AG1["Normalize Subject
strip domain, uppercase"]
+ AG2["Query ApplicationFormSubmission
WHERE OidcSub = normalizedSubject"]
+ AG3["JOIN ApplicantAgent
ON ApplicationId"]
+ AG3b["JOIN Application
ON ApplicationId
for ReferenceNo"]
+ AG4["Map to ContactInfoItemDto
ContactType = 'ApplicantAgent'
IsEditable = false"]
+ AG1 --> AG2 --> AG3 --> AG3b --> AG4
end
Start --> Tenant
Tenant --> PC1
Tenant --> AC1
+ Tenant --> AG1
PC3 --> Merge["Merge into Contacts list"]
AC4 --> Merge
+ AG4 --> Merge
Merge --> Return([Return ApplicantContactInfoDto])
```
@@ -149,7 +162,26 @@ flowchart TD
| Source | Entity | Join Path | Editable |
|--------|--------|-----------|----------|
| Profile Contacts | `ContactLink` → `Contact` | `ContactLink.RelatedEntityId = profileId` | ✅ Yes |
-| Application Contacts | `ApplicationFormSubmission` → `ApplicationContact` | `Submission.OidcSub = normalizedSubject` | ❌ No |
+| Application Contacts | `ApplicationFormSubmission` → `ApplicationContact` → `Application` | `Submission.OidcSub = normalizedSubject`, `Application.Id` for `ReferenceNo` | ❌ No |
+| Applicant Agent Contacts | `ApplicationFormSubmission` → `ApplicantAgent` → `Application` | `Submission.ApplicationId = Agent.ApplicationId`, `Application.Id` for `ReferenceNo` | ❌ No |
+
+**Applicant Agent Field Mapping:**
+
+The `ApplicantAgent` entity is populated from the CHEFS submission login token during intake import. Its fields are mapped to `ContactInfoItemDto` as follows:
+
+| ApplicantAgent Field | ContactInfoItemDto Field |
+|---------------------|-------------------------|
+| `Id` | `ContactId` |
+| `Name` | `Name` |
+| `Title` | `Title` |
+| `Email` | `Email` |
+| `Phone` | `WorkPhoneNumber` |
+| `PhoneExtension` | `WorkPhoneExtension` |
+| `Phone2` | `MobilePhoneNumber` |
+| `RoleForApplicant` | `Role` |
+| `ApplicationId` | `ApplicationId` |
+| `Application.ReferenceNo` | `ReferenceNo` |
+| _(literal)_ `"ApplicantAgent"` | `ContactType` |
---
@@ -356,7 +388,7 @@ Providers distinguish between **editable** and **read-only** data:
| Provider | Editable Source | Read-Only Source |
|----------|----------------|-----------------|
-| ContactInfo | Profile-linked contacts | Application-level contacts |
+| ContactInfo | Profile-linked contacts | Application-level contacts, Applicant agent contacts |
| AddressInfo | Addresses linked via ApplicantId | Addresses linked via ApplicationId |
---