Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ namespace Unity.GrantManager.ApplicantProfile;

/// <summary>
/// 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.
/// </summary>
public interface IApplicantProfileContactService
{
Expand All @@ -26,4 +27,13 @@ public interface IApplicantProfileContactService
/// <param name="subject">The OIDC subject identifier (e.g. "user@idir").</param>
/// <returns>A list of <see cref="ContactInfoItemDto"/> with <c>IsEditable</c> set to <c>false</c>.</returns>
Task<List<ContactInfoItemDto>> GetApplicationContactsBySubjectAsync(string subject);

/// <summary>
/// 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 <c>@</c>) and converting to upper case.
/// </summary>
/// <param name="subject">The OIDC subject identifier (e.g. "user@idir").</param>
/// <returns>A list of <see cref="ContactInfoItemDto"/> with <c>IsEditable</c> set to <c>false</c>.</returns>
Task<List<ContactInfoItemDto>> GetApplicantAgentContactsBySubjectAsync(string subject);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@
namespace Unity.GrantManager.ApplicantProfile;

/// <summary>
/// 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 <see cref="Contacts.ContactAppService"/> 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
/// <see cref="Contacts.ContactAppService"/> and queries repositories directly.
/// </summary>
public class ApplicantProfileContactService(
IContactRepository contactRepository,
IContactLinkRepository contactLinkRepository,
IRepository<ApplicationFormSubmission, Guid> applicationFormSubmissionRepository,
IRepository<ApplicationContact, Guid> applicationContactRepository)
IRepository<ApplicationContact, Guid> applicationContactRepository,
IRepository<ApplicantAgent, Guid> applicantAgentRepository,
IRepository<Application, Guid> applicationRepository)
: IApplicantProfileContactService, ITransientDependency
{
private const string ApplicantProfileEntityType = "ApplicantProfile";
Expand Down Expand Up @@ -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();
}

/// <inheritdoc />
public async Task<List<ContactInfoItemDto>> 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,
Expand All @@ -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;
}

/// <inheritdoc />
public async Task<List<ContactInfoItemDto>> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Unity.GrantManager.ApplicantProfile
{
/// <summary>
/// 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.
/// </summary>
[ExposeServices(typeof(IApplicantProfileDataProvider))]
public class ContactInfoDataProvider(
Expand All @@ -33,8 +33,15 @@ public async Task<ApplicantProfileDataDto> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ private static ContactInfoDataProvider CreateContactInfoDataProvider()
.Returns(Task.FromResult(new List<ContactInfoItemDto>()));
applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any<string>())
.Returns(Task.FromResult(new List<ContactInfoItemDto>()));
applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(Arg.Any<string>())
.Returns(Task.FromResult(new List<ContactInfoItemDto>()));
return new ContactInfoDataProvider(currentTenant, applicantProfileContactService);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ public ContactInfoDataProviderTests()
_currentTenant = Substitute.For<ICurrentTenant>();
_currentTenant.Change(Arg.Any<Guid?>()).Returns(Substitute.For<IDisposable>());
_applicantProfileContactService = Substitute.For<IApplicantProfileContactService>();

_applicantProfileContactService.GetProfileContactsAsync(Arg.Any<Guid>())
.Returns(new List<ContactInfoItemDto>());
_applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any<string>())
.Returns(new List<ContactInfoItemDto>());
_applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync(Arg.Any<string>())
.Returns(new List<ContactInfoItemDto>());

_provider = new ContactInfoDataProvider(_currentTenant, _applicantProfileContactService);
}

Expand All @@ -38,10 +46,6 @@ public async Task GetDataAsync_ShouldChangeTenant()
{
// Arrange
var request = CreateRequest();
_applicantProfileContactService.GetProfileContactsAsync(Arg.Any<Guid>())
.Returns(new List<ContactInfoItemDto>());
_applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any<string>())
.Returns(new List<ContactInfoItemDto>());

// Act
await _provider.GetDataAsync(request);
Expand All @@ -55,10 +59,6 @@ public async Task GetDataAsync_ShouldCallGetProfileContactsWithProfileId()
{
// Arrange
var request = CreateRequest();
_applicantProfileContactService.GetProfileContactsAsync(Arg.Any<Guid>())
.Returns(new List<ContactInfoItemDto>());
_applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any<string>())
.Returns(new List<ContactInfoItemDto>());

// Act
await _provider.GetDataAsync(request);
Expand All @@ -72,20 +72,29 @@ public async Task GetDataAsync_ShouldCallGetApplicationContactsWithSubject()
{
// Arrange
var request = CreateRequest();
_applicantProfileContactService.GetProfileContactsAsync(Arg.Any<Guid>())
.Returns(new List<ContactInfoItemDto>());
_applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any<string>())
.Returns(new List<ContactInfoItemDto>());

// 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();
Expand All @@ -98,28 +107,29 @@ public async Task GetDataAsync_ShouldCombineBothContactSets()
{
new() { ContactId = Guid.NewGuid(), Name = "App Contact 1", IsEditable = false }
};
var agentContacts = new List<ContactInfoItemDto>
{
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<ApplicantContactInfoDto>();
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]
public async Task GetDataAsync_WithNoContacts_ShouldReturnEmptyList()
{
// Arrange
var request = CreateRequest();
_applicantProfileContactService.GetProfileContactsAsync(Arg.Any<Guid>())
.Returns(new List<ContactInfoItemDto>());
_applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any<string>())
.Returns(new List<ContactInfoItemDto>());

// Act
var result = await _provider.GetDataAsync(request);
Expand All @@ -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();
Expand All @@ -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<ContactInfoItemDto> { profileContact });
_applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject)
_applicantProfileContactService.GetApplicationContactsBySubjectAsync("TESTUSER")
.Returns(new List<ContactInfoItemDto> { appContact });
_applicantProfileContactService.GetApplicantAgentContactsBySubjectAsync("TESTUSER")
.Returns(new List<ContactInfoItemDto> { agentContact });

// Act
var result = await _provider.GetDataAsync(request);
Expand All @@ -158,17 +177,14 @@ public async Task GetDataAsync_ProfileContactsShouldAppearBeforeApplicationConta
var dto = result.ShouldBeOfType<ApplicantContactInfoDto>();
dto.Contacts[0].Name.ShouldBe("Profile First");
dto.Contacts[1].Name.ShouldBe("App Second");
dto.Contacts[2].Name.ShouldBe("Agent Third");
}

[Fact]
public async Task GetDataAsync_ShouldReturnCorrectDataType()
{
// Arrange
var request = CreateRequest();
_applicantProfileContactService.GetProfileContactsAsync(Arg.Any<Guid>())
.Returns(new List<ContactInfoItemDto>());
_applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any<string>())
.Returns(new List<ContactInfoItemDto>());

// Act
var result = await _provider.GetDataAsync(request);
Expand Down
Loading
Loading