-
-
@ApplicationContactsWidgetViewModel.ContactTypeValue(contact.ContactType)
-
@contact.ContactFullName, @contact.ContactTitle
- @if (!contact.ContactEmail.IsNullOrEmpty())
- {
-
-
-
@contact.ContactEmail
-
- }
- @if (!contact.ContactMobilePhone.IsNullOrEmpty())
- {
-
-
-
@contact.ContactMobilePhone
-
- }
- @if (!contact.ContactWorkPhone.IsNullOrEmpty())
- {
-
-
-
@contact.ContactWorkPhone
-
- }
-
- @if(!(Model.IsReadOnly)) {
-
-
+@{
+ bool IsAdditionalContactAddable = await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Create);
+ bool IsAdditionalContactEditable = await PermissionChecker.IsGrantedAsync(UnitySelector.Applicant.AdditionalContact.Update);
+}
+
+
@L["Summary:ContactsTitle"].Value
+
+@if (Model.ApplicationContacts.Count > 0) {
+
Info
+
+}
+
+@foreach (var contact in Model.ApplicationContacts)
+{
+
+
+
+
@ApplicationContactsWidgetViewModel.ContactTypeValue(contact.ContactType)
+
@contact.ContactFullName, @contact.ContactTitle
+ @if (!contact.ContactEmail.IsNullOrEmpty())
+ {
+
+
+
@contact.ContactEmail
+
+ }
+ @if (!contact.ContactMobilePhone.IsNullOrEmpty())
+ {
+
+
+
@contact.ContactMobilePhone
+
+ }
+ @if (!contact.ContactWorkPhone.IsNullOrEmpty())
+ {
+
+
+
@contact.ContactWorkPhone
}
-
+ @if (IsAdditionalContactEditable)
+ {
+
+ }
- }
-
+
+
+}
+
+@if (IsAdditionalContactAddable)
+{
+
+}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js
index 5e48b25eb..f1858be26 100644
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationContactsWidget/Default.js
@@ -1,74 +1,102 @@
$(function () {
-
- let contactModal = new abp.ModalManager({
+ let applicantContactsWidgetToken = null;
+ let _createContactModal = new abp.ModalManager(abp.appPath + 'ApplicationContact/CreateContactModal');
+ let _editContactModal = new abp.ModalManager({
viewUrl: abp.appPath + 'ApplicationContact/EditContactModal',
- modalClass: "editContactModal"
+ scriptUrl: abp.appPath + 'Pages/ApplicationContact/EditContactModal.js',
+ modalClass: "editOrDeleteContactModal"
});
- abp.modals.editContactModal = function () {
- let initModal = function (publicApi, args) {
- setupContactModal(args);
- };
- return { initModal: initModal };
- }
-
- $('body').on('click','.contact-edit-btn',function(e){
- e.preventDefault();
- let itemId = $(this).data('id');
- contactModal.open({
- id: itemId
- });
+ // Handle modal result - refresh the widget after successful contact creation
+ _createContactModal.onResult(function () {
+ PubSub.publish("refresh_application_contacts");
+ abp.notify.success(
+ 'The application contact has been successfully added.',
+ 'Application Contacts'
+ );
});
- contactModal.onResult(function () {
+ _editContactModal.onResult(function () {
+ PubSub.publish("refresh_application_contacts");
abp.notify.success(
- 'The application contact have been successfully updated.',
+ 'The application contact has been successfully updated.',
'Application Contacts'
);
- PubSub.publish("refresh_application_contacts");
});
- let setupContactModal = function (args) {
- $('#DeleteContactButton').click(handleDeleteContact);
+ abp.widgets.ApplicationContactsWidget = function ($wrapper) {
- function handleDeleteContact(e) {
- e.preventDefault();
- showDeleteConfirmation();
- }
-
- function showDeleteConfirmation() {
- abp.message.confirm('Are you sure to delete this contact?')
- .then(processDeleteConfirmation);
- }
-
- function processDeleteConfirmation(confirmed) {
- if (confirmed) {
- deleteContact();
- }
- }
-
- function deleteContact() {
- try {
- unity.grantManager.grantApplications.applicationContact
- .delete(args.id)
- .done(onContactDeleted)
- .fail(onDeleteFailure);
- } catch (error) {
- onDeleteFailure(error);
- }
- }
-
- function onContactDeleted() {
- PubSub.publish("refresh_application_contacts");
- contactModal.close();
- abp.notify.success('The contact has been deleted.');
- }
-
- function onDeleteFailure(error) {
- abp.notify.error('Contact deletion failed.');
- if (error) {
- console.log(error);
+ let _widgetManager = $wrapper.data('abp-widget-manager');
+
+ let widgetApi = {
+ applicationId: null, // Cache the applicationId to prevent reading from stale DOM
+
+ getFilters: function () {
+ const appId = this.applicationId || $wrapper.find('#ApplicationContactsWidget_ApplicationId').val();
+
+ return {
+ applicationId: appId
+ };
+ },
+
+ init: function (filters) {
+ this.applicationId = $wrapper.find('#ApplicationContactsWidget_ApplicationId').val();
+ this.setupEventHandlers();
+ },
+
+ refresh: function () {
+ const currentFilters = this.getFilters();
+ _widgetManager.refresh($wrapper, currentFilters);
+ },
+
+ setupEventHandlers: function() {
+ const self = this;
+
+ // Unsubscribe from previous subscription if it exists
+ // This prevents duplicate event handlers after widget refresh
+ if (applicantContactsWidgetToken) {
+ PubSub.unsubscribe(applicantContactsWidgetToken);
+ applicantContactsWidgetToken = null;
+ }
+
+ applicantContactsWidgetToken = PubSub.subscribe(
+ 'refresh_application_contacts',
+ () => {
+ self.refresh();
+ }
+ );
+
+ // Prevent duplicate delegated click handlers on re-init by removing any
+ // existing handlers in this widget's namespace before re-binding.
+ $wrapper.off('click.ApplicationContactsWidget', '#CreateContactButton');
+ $wrapper.off('click.ApplicationContactsWidget', '.contact-edit-btn');
+
+ // Handle Add Contact button click
+ $wrapper.on('click.ApplicationContactsWidget', '#CreateContactButton', function (e) {
+ e.preventDefault();
+ _createContactModal.open({
+ applicationId: self.applicationId || $wrapper.find('#ApplicationContactsWidget_ApplicationId').val()
+ });
+ });
+
+ $wrapper.on('click.ApplicationContactsWidget', '.contact-edit-btn', function (e) {
+ e.preventDefault();
+ let itemId = $(this).data('id');
+ _editContactModal.open({
+ id: itemId
+ });
+ });
}
}
- }
+
+ return widgetApi;
+ };
+
+ // Initialize the ApplicationContactsWidget manager with filter callback
+ let applicationContactsWidgetManager = new abp.WidgetManager({
+ wrapper: '.abp-widget-wrapper[data-widget-name="ApplicationContactsWidget"]'
+ });
+
+ // Initialize the widget
+ applicationContactsWidgetManager.init();
});
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js
index 4e29d4aea..dcaeb4b1a 100644
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js
@@ -136,6 +136,7 @@ $(function () {
})
.catch(function (error) {
abp.notify.error('Error deleting application link.');
+ dataTable.ajax.reload();
});
}
}
@@ -163,6 +164,9 @@ $(function () {
'The application links have been successfully updated.',
'Application Links'
);
+ });
+
+ applicationLinksModal.onClose(function () {
dataTable.ajax.reload();
});
@@ -921,17 +925,28 @@ $(function () {
data: formData,
processData: false,
contentType: false,
- success: () => {
+ success: (response) => {
applicationLinksModal.close();
- abp.notify.success(
- 'The application links have been successfully updated.',
- 'Application Links'
- );
+ if (response.success)
+ {
+ abp.notify.success('The application links have been successfully updated.','Application Links');
+ }
+ else
+ { // Display the error message from the server
+ abp.notify.error(response.message || 'Failed to update application links.','Application Links');
+ }
dataTable.ajax.reload();
},
error: (xhr, status, error) => {
console.error('Error updating application links:', status, error);
- abp.notify.error('Error updating application links: ' + error);
+ let errorMessage = 'Error updating application links.';
+
+ // Try to extract error message from response
+ if (xhr.responseJSON?.message) {
+ errorMessage = xhr.responseJSON.message;
+ }
+
+ abp.notify.error(errorMessage);
}
});
}
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/SummaryWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/SummaryWidget/Default.js
index 84d64f5e4..72d6b2e03 100644
--- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/SummaryWidget/Default.js
+++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/SummaryWidget/Default.js
@@ -1,38 +1,5 @@
$(function () {
- let applicationId = document.getElementById('SummaryWidgetApplicationId').value;
- let isReadOnly = document.getElementById('SummaryWidgetIsReadOnly').value;
- let contactModal = new abp.ModalManager(abp.appPath + 'ApplicationContact/CreateContactModal');
-
- let applicationContactsWidgetManager = new abp.WidgetManager({
- wrapper: '#applicationContactsWidget',
- filterCallback: function () {
- return {
- 'applicationId': applicationId,
- 'isReadOnly': isReadOnly
- };
- }
- });
-
- $('#AddContactButton').click(function (e) {
- e.preventDefault();
- contactModal.open({
- applicationId: applicationId
- });
- });
-
- contactModal.onResult(function () {
- abp.notify.success(
- 'The application contact have been successfully added.',
- 'Application Contacts'
- );
- applicationContactsWidgetManager.refresh();
- });
-
- PubSub.subscribe(
- 'refresh_application_contacts',
- (msg, data) => {
- applicationContactsWidgetManager.refresh();
- }
- );
+ // SummaryWidget initialization
+ // Contact modal and widget management moved to ApplicationContactsWidget component
});
diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileAppServiceTests.cs
index 29cde4344..81f518089 100644
--- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileAppServiceTests.cs
+++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Applicants/ApplicantProfileAppServiceTests.cs
@@ -1,8 +1,8 @@
using Shouldly;
using System;
using System.Threading.Tasks;
-using Unity.GrantManager.Applicants.ApplicantProfile;
-using Unity.GrantManager.Applicants.ProfileData;
+using Unity.GrantManager.ApplicantProfile;
+using Unity.GrantManager.ApplicantProfile.ProfileData;
using Xunit;
using Xunit.Abstractions;
@@ -21,7 +21,7 @@ public ApplicantProfileAppServiceTests(ITestOutputHelper outputHelper) : base(ou
{
ProfileId = Guid.NewGuid(),
Subject = "testuser@idir",
- TenantId = Guid.NewGuid(),
+ TenantId = Guid.Empty,
Key = key
};
@@ -37,7 +37,7 @@ public async Task GetApplicantProfileAsync_WithValidKey_ShouldReturnData(string
var request = CreateRequest(key);
// Act
- var result = await _service.GetApplicantProfileAsync(request);
+ var result = await WithUnitOfWorkAsync(() => _service.GetApplicantProfileAsync(request));
// Assert
result.ShouldNotBeNull();
@@ -60,7 +60,7 @@ public async Task GetApplicantProfileAsync_WithValidKey_ShouldReturnCorrectDataT
var request = CreateRequest(key);
// Act
- var result = await _service.GetApplicantProfileAsync(request);
+ var result = await WithUnitOfWorkAsync(() => _service.GetApplicantProfileAsync(request));
// Assert
result.Data.ShouldNotBeNull();
@@ -89,7 +89,7 @@ public async Task GetApplicantProfileAsync_KeyLookupIsCaseInsensitive()
var request = CreateRequest("contactinfo");
// Act
- var result = await _service.GetApplicantProfileAsync(request);
+ var result = await WithUnitOfWorkAsync(() => _service.GetApplicantProfileAsync(request));
// Assert
result.Data.ShouldNotBeNull();
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 c9fa64a0f..74ccc3f9a 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
@@ -1,9 +1,12 @@
+using NSubstitute;
using Shouldly;
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using Unity.GrantManager.Applicants.ApplicantProfile;
-using Unity.GrantManager.Applicants.ProfileData;
+using Unity.GrantManager.ApplicantProfile;
+using Unity.GrantManager.ApplicantProfile.ProfileData;
+using Volo.Abp.MultiTenancy;
using Xunit;
namespace Unity.GrantManager.Applicants
@@ -18,17 +21,29 @@ public class ApplicantProfileDataProviderTests
Key = key
};
+ private static ContactInfoDataProvider CreateContactInfoDataProvider()
+ {
+ var currentTenant = Substitute.For
();
+ currentTenant.Change(Arg.Any()).Returns(Substitute.For());
+ var applicantProfileContactService = Substitute.For();
+ applicantProfileContactService.GetProfileContactsAsync(Arg.Any())
+ .Returns(Task.FromResult(new List()));
+ applicantProfileContactService.GetApplicationContactsBySubjectAsync(Arg.Any())
+ .Returns(Task.FromResult(new List()));
+ return new ContactInfoDataProvider(currentTenant, applicantProfileContactService);
+ }
+
[Fact]
public void ContactInfoDataProvider_Key_ShouldMatchExpected()
{
- var provider = new ContactInfoDataProvider();
+ var provider = CreateContactInfoDataProvider();
provider.Key.ShouldBe(ApplicantProfileKeys.ContactInfo);
}
[Fact]
public async Task ContactInfoDataProvider_GetDataAsync_ShouldReturnContactInfoDto()
{
- var provider = new ContactInfoDataProvider();
+ var provider = CreateContactInfoDataProvider();
var result = await provider.GetDataAsync(CreateRequest(ApplicantProfileKeys.ContactInfo));
result.ShouldNotBeNull();
result.ShouldBeOfType();
@@ -103,7 +118,7 @@ public void AllProviders_ShouldHaveUniqueKeys()
{
IApplicantProfileDataProvider[] providers =
[
- new ContactInfoDataProvider(),
+ CreateContactInfoDataProvider(),
new OrgInfoDataProvider(),
new AddressInfoDataProvider(),
new SubmissionInfoDataProvider(),
diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs
new file mode 100644
index 000000000..e44be9fb4
--- /dev/null
+++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactAppServiceTests.cs
@@ -0,0 +1,604 @@
+using NSubstitute;
+using Shouldly;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Unity.GrantManager.TestHelpers;
+using Volo.Abp;
+using Volo.Abp.Domain.Entities;
+using Xunit;
+
+namespace Unity.GrantManager.Contacts
+{
+ public class ContactAppServiceTests
+ {
+ private readonly IContactRepository _contactRepository;
+ private readonly IContactLinkRepository _contactLinkRepository;
+ private readonly ContactAppService _service;
+
+ public ContactAppServiceTests()
+ {
+ _contactRepository = Substitute.For();
+ _contactLinkRepository = Substitute.For();
+
+ _service = new ContactAppService(
+ _contactRepository,
+ _contactLinkRepository);
+ }
+
+ private static T WithId(T entity, Guid id) where T : Entity
+ {
+ EntityHelper.TrySetId(entity, () => id);
+ return entity;
+ }
+
+ #region GetContactsByEntityAsync
+
+ [Fact]
+ public async Task GetContactsByEntityAsync_WithMatchingLinks_ShouldReturnAllFields()
+ {
+ // Arrange
+ var entityId = Guid.NewGuid();
+ var contactId = Guid.NewGuid();
+
+ var contacts = new[]
+ {
+ WithId(new Contact
+ {
+ Name = "John Doe",
+ Title = "Manager",
+ Email = "john@example.com",
+ HomePhoneNumber = "111-1111",
+ MobilePhoneNumber = "222-2222",
+ WorkPhoneNumber = "333-3333",
+ WorkPhoneExtension = "101"
+ }, contactId)
+ }.AsAsyncQueryable();
+
+ var contactLinks = new[]
+ {
+ new ContactLink
+ {
+ ContactId = contactId,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ Role = "Primary Contact",
+ IsPrimary = true,
+ IsActive = true
+ }
+ }.AsAsyncQueryable();
+
+ _contactRepository.GetQueryableAsync().Returns(contacts);
+ _contactLinkRepository.GetQueryableAsync().Returns(contactLinks);
+
+ // Act
+ var result = await _service.GetContactsByEntityAsync("TestEntity", entityId);
+
+ // Assert
+ result.Count.ShouldBe(1);
+ var contact = result[0];
+ contact.ContactId.ShouldBe(contactId);
+ contact.Name.ShouldBe("John Doe");
+ contact.Title.ShouldBe("Manager");
+ contact.Email.ShouldBe("john@example.com");
+ contact.HomePhoneNumber.ShouldBe("111-1111");
+ contact.MobilePhoneNumber.ShouldBe("222-2222");
+ contact.WorkPhoneNumber.ShouldBe("333-3333");
+ contact.WorkPhoneExtension.ShouldBe("101");
+ contact.Role.ShouldBe("Primary Contact");
+ contact.IsPrimary.ShouldBeTrue();
+ }
+
+ [Fact]
+ public async Task GetContactsByEntityAsync_WithMultipleContacts_ShouldReturnAll()
+ {
+ // Arrange
+ var entityId = Guid.NewGuid();
+ var contactId1 = Guid.NewGuid();
+ var contactId2 = Guid.NewGuid();
+
+ var contacts = new[]
+ {
+ WithId(new Contact { Name = "Contact One" }, contactId1),
+ WithId(new Contact { Name = "Contact Two" }, contactId2)
+ }.AsAsyncQueryable();
+
+ var contactLinks = new[]
+ {
+ new ContactLink
+ {
+ ContactId = contactId1,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ IsPrimary = true,
+ IsActive = true
+ },
+ new ContactLink
+ {
+ ContactId = contactId2,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ IsPrimary = false,
+ IsActive = true
+ }
+ }.AsAsyncQueryable();
+
+ _contactRepository.GetQueryableAsync().Returns(contacts);
+ _contactLinkRepository.GetQueryableAsync().Returns(contactLinks);
+
+ // Act
+ var result = await _service.GetContactsByEntityAsync("TestEntity", entityId);
+
+ // Assert
+ result.Count.ShouldBe(2);
+ result.ShouldContain(c => c.Name == "Contact One" && c.IsPrimary);
+ result.ShouldContain(c => c.Name == "Contact Two" && !c.IsPrimary);
+ }
+
+ [Fact]
+ public async Task GetContactsByEntityAsync_ShouldExcludeInactiveLinks()
+ {
+ // Arrange
+ var entityId = Guid.NewGuid();
+ var contactId = Guid.NewGuid();
+
+ var contacts = new[]
+ {
+ WithId(new Contact { Name = "Inactive Contact" }, contactId)
+ }.AsAsyncQueryable();
+
+ var contactLinks = new[]
+ {
+ new ContactLink
+ {
+ ContactId = contactId,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ IsActive = false
+ }
+ }.AsAsyncQueryable();
+
+ _contactRepository.GetQueryableAsync().Returns(contacts);
+ _contactLinkRepository.GetQueryableAsync().Returns(contactLinks);
+
+ // Act
+ var result = await _service.GetContactsByEntityAsync("TestEntity", entityId);
+
+ // Assert
+ result.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public async Task GetContactsByEntityAsync_ShouldExcludeDifferentEntityType()
+ {
+ // Arrange
+ var entityId = Guid.NewGuid();
+ var contactId = Guid.NewGuid();
+
+ var contacts = new[]
+ {
+ WithId(new Contact { Name = "Wrong Type" }, contactId)
+ }.AsAsyncQueryable();
+
+ var contactLinks = new[]
+ {
+ new ContactLink
+ {
+ ContactId = contactId,
+ RelatedEntityType = "OtherType",
+ RelatedEntityId = entityId,
+ IsActive = true
+ }
+ }.AsAsyncQueryable();
+
+ _contactRepository.GetQueryableAsync().Returns(contacts);
+ _contactLinkRepository.GetQueryableAsync().Returns(contactLinks);
+
+ // Act
+ var result = await _service.GetContactsByEntityAsync("TestEntity", entityId);
+
+ // Assert
+ result.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public async Task GetContactsByEntityAsync_ShouldExcludeDifferentEntityId()
+ {
+ // Arrange
+ var entityId = Guid.NewGuid();
+ var otherEntityId = Guid.NewGuid();
+ var contactId = Guid.NewGuid();
+
+ var contacts = new[]
+ {
+ WithId(new Contact { Name = "Other Entity" }, contactId)
+ }.AsAsyncQueryable();
+
+ var contactLinks = new[]
+ {
+ new ContactLink
+ {
+ ContactId = contactId,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = otherEntityId,
+ IsActive = true
+ }
+ }.AsAsyncQueryable();
+
+ _contactRepository.GetQueryableAsync().Returns(contacts);
+ _contactLinkRepository.GetQueryableAsync().Returns(contactLinks);
+
+ // Act
+ var result = await _service.GetContactsByEntityAsync("TestEntity", entityId);
+
+ // Assert
+ result.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public async Task GetContactsByEntityAsync_WithNoLinks_ShouldReturnEmpty()
+ {
+ // Arrange
+ _contactRepository.GetQueryableAsync().Returns(Array.Empty().AsAsyncQueryable());
+ _contactLinkRepository.GetQueryableAsync().Returns(Array.Empty().AsAsyncQueryable());
+
+ // Act
+ var result = await _service.GetContactsByEntityAsync("TestEntity", Guid.NewGuid());
+
+ // Assert
+ result.ShouldBeEmpty();
+ }
+
+ #endregion
+
+ #region CreateContactAsync
+
+ [Fact]
+ public async Task CreateContactAsync_ShouldCreateContactAndLink()
+ {
+ // Arrange
+ var contactId = Guid.NewGuid();
+ var entityId = Guid.NewGuid();
+
+ _contactRepository.InsertAsync(Arg.Any(), true, Arg.Any())
+ .Returns(ci =>
+ {
+ var c = ci.Arg();
+ EntityHelper.TrySetId(c, () => contactId);
+ return c;
+ });
+
+ _contactLinkRepository.GetQueryableAsync()
+ .Returns(Array.Empty().AsAsyncQueryable());
+
+ var input = new CreateContactLinkDto
+ {
+ Name = "New Contact",
+ Title = "Analyst",
+ Email = "new@example.com",
+ HomePhoneNumber = "111-1111",
+ MobilePhoneNumber = "222-2222",
+ WorkPhoneNumber = "333-3333",
+ WorkPhoneExtension = "101",
+ Role = "Reviewer",
+ IsPrimary = false,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId
+ };
+
+ // Act
+ var result = await _service.CreateContactAsync(input);
+
+ // Assert
+ result.ContactId.ShouldBe(contactId);
+ result.Name.ShouldBe("New Contact");
+ result.Title.ShouldBe("Analyst");
+ result.Email.ShouldBe("new@example.com");
+ result.HomePhoneNumber.ShouldBe("111-1111");
+ result.MobilePhoneNumber.ShouldBe("222-2222");
+ result.WorkPhoneNumber.ShouldBe("333-3333");
+ result.WorkPhoneExtension.ShouldBe("101");
+ result.Role.ShouldBe("Reviewer");
+ result.IsPrimary.ShouldBeFalse();
+
+ await _contactRepository.Received(1).InsertAsync(
+ Arg.Is(c =>
+ c.Name == "New Contact"
+ && c.Title == "Analyst"
+ && c.Email == "new@example.com"
+ && c.HomePhoneNumber == "111-1111"
+ && c.MobilePhoneNumber == "222-2222"
+ && c.WorkPhoneNumber == "333-3333"
+ && c.WorkPhoneExtension == "101"),
+ true,
+ Arg.Any());
+
+ await _contactLinkRepository.Received(1).InsertAsync(
+ Arg.Is(l =>
+ l.ContactId == contactId
+ && l.RelatedEntityType == "TestEntity"
+ && l.RelatedEntityId == entityId
+ && l.Role == "Reviewer"
+ && !l.IsPrimary
+ && l.IsActive),
+ true,
+ Arg.Any());
+ }
+
+ [Fact]
+ public async Task CreateContactAsync_NonPrimary_ShouldNotClearExistingPrimary()
+ {
+ // Arrange
+ var contactId = Guid.NewGuid();
+ var entityId = Guid.NewGuid();
+
+ _contactRepository.InsertAsync(Arg.Any(), true, Arg.Any())
+ .Returns(ci =>
+ {
+ var c = ci.Arg();
+ EntityHelper.TrySetId(c, () => contactId);
+ return c;
+ });
+
+ var input = new CreateContactLinkDto
+ {
+ Name = "Non-Primary Contact",
+ IsPrimary = false,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId
+ };
+
+ // Act
+ await _service.CreateContactAsync(input);
+
+ // Assert GetQueryableAsync should not be called (ClearPrimaryAsync not invoked)
+ await _contactLinkRepository.DidNotReceive().GetQueryableAsync();
+ }
+
+ [Fact]
+ public async Task CreateContactAsync_WhenPrimary_ShouldClearExistingPrimary()
+ {
+ // Arrange
+ var contactId = Guid.NewGuid();
+ var entityId = Guid.NewGuid();
+ var existingLinkId = Guid.NewGuid();
+
+ var existingLink = new ContactLink
+ {
+ ContactId = Guid.NewGuid(),
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ IsPrimary = true,
+ IsActive = true
+ };
+ EntityHelper.TrySetId(existingLink, () => existingLinkId);
+
+ _contactLinkRepository.GetQueryableAsync()
+ .Returns(
+ new[] { existingLink }.AsAsyncQueryable(),
+ Array.Empty().AsAsyncQueryable());
+
+ _contactRepository.InsertAsync(Arg.Any(), true, Arg.Any())
+ .Returns(ci =>
+ {
+ var c = ci.Arg();
+ EntityHelper.TrySetId(c, () => contactId);
+ return c;
+ });
+
+ var input = new CreateContactLinkDto
+ {
+ Name = "Primary Contact",
+ IsPrimary = true,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId
+ };
+
+ // Act
+ var result = await _service.CreateContactAsync(input);
+
+ // Assert
+ result.IsPrimary.ShouldBeTrue();
+ await _contactLinkRepository.Received(1).UpdateAsync(
+ Arg.Is(l => l.Id == existingLinkId && !l.IsPrimary),
+ true,
+ Arg.Any());
+ }
+
+ #endregion
+
+ #region SetPrimaryContactAsync
+
+ [Fact]
+ public async Task SetPrimaryContactAsync_ShouldClearExistingAndSetNew()
+ {
+ // Arrange
+ var entityId = Guid.NewGuid();
+ var contactId = Guid.NewGuid();
+ var existingPrimaryLinkId = Guid.NewGuid();
+ var targetLinkId = Guid.NewGuid();
+
+ var existingPrimaryLink = new ContactLink
+ {
+ ContactId = Guid.NewGuid(),
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ IsPrimary = true,
+ IsActive = true
+ };
+ EntityHelper.TrySetId(existingPrimaryLink, () => existingPrimaryLinkId);
+
+ var targetLink = new ContactLink
+ {
+ ContactId = contactId,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ IsPrimary = false,
+ IsActive = true
+ };
+ EntityHelper.TrySetId(targetLink, () => targetLinkId);
+
+ _contactLinkRepository.GetQueryableAsync()
+ .Returns(
+ new[] { existingPrimaryLink }.AsAsyncQueryable(),
+ new[] { targetLink }.AsAsyncQueryable());
+
+ // Act
+ await _service.SetPrimaryContactAsync("TestEntity", entityId, contactId);
+
+ // Assert
+ await _contactLinkRepository.Received(1).UpdateAsync(
+ Arg.Is(l => l.Id == existingPrimaryLinkId && !l.IsPrimary),
+ true,
+ Arg.Any());
+ await _contactLinkRepository.Received(1).UpdateAsync(
+ Arg.Is(l => l.Id == targetLinkId && l.IsPrimary),
+ true,
+ Arg.Any());
+ }
+
+ [Fact]
+ public async Task SetPrimaryContactAsync_WithNoExistingPrimary_ShouldSetNew()
+ {
+ // Arrange
+ var entityId = Guid.NewGuid();
+ var contactId = Guid.NewGuid();
+ var targetLinkId = Guid.NewGuid();
+
+ var targetLink = new ContactLink
+ {
+ ContactId = contactId,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ IsPrimary = false,
+ IsActive = true
+ };
+ EntityHelper.TrySetId(targetLink, () => targetLinkId);
+
+ _contactLinkRepository.GetQueryableAsync()
+ .Returns(
+ Array.Empty().AsAsyncQueryable(),
+ new[] { targetLink }.AsAsyncQueryable());
+
+ // Act
+ await _service.SetPrimaryContactAsync("TestEntity", entityId, contactId);
+
+ // Assert only the target link should be updated (set to primary)
+ await _contactLinkRepository.Received(1).UpdateAsync(
+ Arg.Is(l => l.Id == targetLinkId && l.IsPrimary),
+ true,
+ Arg.Any());
+ }
+
+ [Fact]
+ public async Task SetPrimaryContactAsync_WithMultipleExistingPrimaries_ShouldClearAll()
+ {
+ // Arrange
+ var entityId = Guid.NewGuid();
+ var contactId = Guid.NewGuid();
+ var primaryLinkId1 = Guid.NewGuid();
+ var primaryLinkId2 = Guid.NewGuid();
+ var targetLinkId = Guid.NewGuid();
+
+ var primaryLink1 = new ContactLink
+ {
+ ContactId = Guid.NewGuid(),
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ IsPrimary = true,
+ IsActive = true
+ };
+ EntityHelper.TrySetId(primaryLink1, () => primaryLinkId1);
+
+ var primaryLink2 = new ContactLink
+ {
+ ContactId = Guid.NewGuid(),
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ IsPrimary = true,
+ IsActive = true
+ };
+ EntityHelper.TrySetId(primaryLink2, () => primaryLinkId2);
+
+ var targetLink = new ContactLink
+ {
+ ContactId = contactId,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ IsPrimary = false,
+ IsActive = true
+ };
+ EntityHelper.TrySetId(targetLink, () => targetLinkId);
+
+ _contactLinkRepository.GetQueryableAsync()
+ .Returns(
+ new[] { primaryLink1, primaryLink2 }.AsAsyncQueryable(),
+ new[] { targetLink }.AsAsyncQueryable());
+
+ // Act
+ await _service.SetPrimaryContactAsync("TestEntity", entityId, contactId);
+
+ // Assert both existing primaries cleared
+ await _contactLinkRepository.Received(1).UpdateAsync(
+ Arg.Is(l => l.Id == primaryLinkId1 && !l.IsPrimary),
+ true,
+ Arg.Any());
+ await _contactLinkRepository.Received(1).UpdateAsync(
+ Arg.Is(l => l.Id == primaryLinkId2 && !l.IsPrimary),
+ true,
+ Arg.Any());
+ // Target set as primary
+ await _contactLinkRepository.Received(1).UpdateAsync(
+ Arg.Is(l => l.Id == targetLinkId && l.IsPrimary),
+ true,
+ Arg.Any());
+ }
+
+ [Fact]
+ public async Task SetPrimaryContactAsync_ShouldNotMatchInactiveLink()
+ {
+ // Arrange
+ var entityId = Guid.NewGuid();
+ var contactId = Guid.NewGuid();
+
+ var inactiveLink = new ContactLink
+ {
+ ContactId = contactId,
+ RelatedEntityType = "TestEntity",
+ RelatedEntityId = entityId,
+ IsPrimary = false,
+ IsActive = false
+ };
+
+ _contactLinkRepository.GetQueryableAsync()
+ .Returns(
+ Array.Empty().AsAsyncQueryable(),
+ new[] { inactiveLink }.AsAsyncQueryable());
+
+ // Act & Assert
+ await Should.ThrowAsync(
+ () => _service.SetPrimaryContactAsync("TestEntity", entityId, contactId));
+ }
+
+ [Fact]
+ public async Task SetPrimaryContactAsync_WhenContactLinkNotFound_ShouldThrow()
+ {
+ // Arrange
+ var entityId = Guid.NewGuid();
+ var contactId = Guid.NewGuid();
+
+ _contactLinkRepository.GetQueryableAsync()
+ .Returns(
+ Array.Empty().AsAsyncQueryable(),
+ Array.Empty().AsAsyncQueryable());
+
+ // Act & Assert
+ var ex = await Should.ThrowAsync(
+ () => _service.SetPrimaryContactAsync("TestEntity", entityId, contactId));
+ ex.Code.ShouldBe("Contacts:ContactLinkNotFound");
+ }
+
+ #endregion
+ }
+}
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
new file mode 100644
index 000000000..976ad574c
--- /dev/null
+++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoDataProviderTests.cs
@@ -0,0 +1,180 @@
+using NSubstitute;
+using Shouldly;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Unity.GrantManager.ApplicantProfile;
+using Unity.GrantManager.ApplicantProfile.ProfileData;
+using Volo.Abp.MultiTenancy;
+using Xunit;
+
+namespace Unity.GrantManager.Contacts
+{
+ public class ContactInfoDataProviderTests
+ {
+ private readonly ICurrentTenant _currentTenant;
+ private readonly IApplicantProfileContactService _applicantProfileContactService;
+ private readonly ContactInfoDataProvider _provider;
+
+ public ContactInfoDataProviderTests()
+ {
+ _currentTenant = Substitute.For();
+ _currentTenant.Change(Arg.Any()).Returns(Substitute.For());
+ _applicantProfileContactService = Substitute.For();
+ _provider = new ContactInfoDataProvider(_currentTenant, _applicantProfileContactService);
+ }
+
+ private static ApplicantProfileInfoRequest CreateRequest() => new()
+ {
+ ProfileId = Guid.NewGuid(),
+ Subject = "testuser@idir",
+ TenantId = Guid.NewGuid(),
+ Key = ApplicantProfileKeys.ContactInfo
+ };
+
+ [Fact]
+ 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);
+
+ // Assert
+ _currentTenant.Received(1).Change(request.TenantId);
+ }
+
+ [Fact]
+ 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);
+
+ // Assert
+ await _applicantProfileContactService.Received(1).GetProfileContactsAsync(request.ProfileId);
+ }
+
+ [Fact]
+ 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);
+ }
+
+ [Fact]
+ public async Task GetDataAsync_ShouldCombineBothContactSets()
+ {
+ // Arrange
+ var request = CreateRequest();
+ var profileContacts = new List
+ {
+ new() { ContactId = Guid.NewGuid(), Name = "Profile Contact 1", IsEditable = true },
+ new() { ContactId = Guid.NewGuid(), Name = "Profile Contact 2", IsEditable = true }
+ };
+ var appContacts = new List
+ {
+ new() { ContactId = Guid.NewGuid(), Name = "App Contact 1", IsEditable = false }
+ };
+ _applicantProfileContactService.GetProfileContactsAsync(request.ProfileId).Returns(profileContacts);
+ _applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject).Returns(appContacts);
+
+ // Act
+ var result = await _provider.GetDataAsync(request);
+
+ // Assert
+ var dto = result.ShouldBeOfType();
+ dto.Contacts.Count.ShouldBe(3);
+ dto.Contacts.Count(c => c.IsEditable).ShouldBe(2);
+ dto.Contacts.Count(c => !c.IsEditable).ShouldBe(1);
+ }
+
+ [Fact]
+ 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);
+
+ // Assert
+ var dto = result.ShouldBeOfType();
+ dto.Contacts.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public async Task GetDataAsync_ProfileContactsShouldAppearBeforeApplicationContacts()
+ {
+ // Arrange
+ var request = CreateRequest();
+ var profileContact = new ContactInfoItemDto
+ {
+ ContactId = Guid.NewGuid(),
+ Name = "Profile First",
+ IsEditable = true
+ };
+ var appContact = new ContactInfoItemDto
+ {
+ ContactId = Guid.NewGuid(),
+ Name = "App Second",
+ IsEditable = false
+ };
+ _applicantProfileContactService.GetProfileContactsAsync(request.ProfileId)
+ .Returns(new List { profileContact });
+ _applicantProfileContactService.GetApplicationContactsBySubjectAsync(request.Subject)
+ .Returns(new List { appContact });
+
+ // Act
+ var result = await _provider.GetDataAsync(request);
+
+ // Assert
+ var dto = result.ShouldBeOfType();
+ dto.Contacts[0].Name.ShouldBe("Profile First");
+ dto.Contacts[1].Name.ShouldBe("App Second");
+ }
+
+ [Fact]
+ 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);
+
+ // Assert
+ result.DataType.ShouldBe("CONTACTINFO");
+ }
+ }
+}
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
new file mode 100644
index 000000000..48fadd7b9
--- /dev/null
+++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Contacts/ContactInfoServiceTests.cs
@@ -0,0 +1,385 @@
+using NSubstitute;
+using Shouldly;
+using System;
+using System.Threading.Tasks;
+using Unity.GrantManager.ApplicantProfile;
+using Unity.GrantManager.ApplicantProfile.ProfileData;
+using Unity.GrantManager.Applications;
+using Unity.GrantManager.TestHelpers;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.Domain.Repositories;
+using Xunit;
+
+namespace Unity.GrantManager.Contacts
+{
+ public class ApplicantProfileContactServiceTests
+ {
+ private readonly IContactRepository _contactRepository;
+ private readonly IContactLinkRepository _contactLinkRepository;
+ private readonly IRepository _submissionRepository;
+ private readonly IRepository _applicationContactRepository;
+ private readonly ApplicantProfileContactService _service;
+
+ public ApplicantProfileContactServiceTests()
+ {
+ _contactRepository = Substitute.For();
+ _contactLinkRepository = Substitute.For();
+ _submissionRepository = Substitute.For>();
+ _applicationContactRepository = Substitute.For>();
+
+ _service = new ApplicantProfileContactService(
+ _contactRepository,
+ _contactLinkRepository,
+ _submissionRepository,
+ _applicationContactRepository);
+ }
+
+ private static T WithId(T entity, Guid id) where T : Entity
+ {
+ EntityHelper.TrySetId(entity, () => id);
+ return entity;
+ }
+
+ [Fact]
+ public async Task GetProfileContactsAsync_WithMatchingLinks_ShouldReturnContacts()
+ {
+ // Arrange
+ var profileId = Guid.NewGuid();
+ var contactId = Guid.NewGuid();
+
+ var contacts = new[]
+ {
+ WithId(new Contact
+ {
+ Name = "John Doe",
+ Title = "Manager",
+ Email = "john@example.com",
+ HomePhoneNumber = "111-1111",
+ MobilePhoneNumber = "222-2222",
+ WorkPhoneNumber = "333-3333",
+ WorkPhoneExtension = "101"
+ }, contactId)
+ }.AsAsyncQueryable();
+
+ var contactLinks = new[]
+ {
+ new ContactLink
+ {
+ ContactId = contactId,
+ RelatedEntityType = "ApplicantProfile",
+ RelatedEntityId = profileId,
+ Role = "Primary Contact",
+ IsPrimary = true,
+ IsActive = true
+ }
+ }.AsAsyncQueryable();
+
+ _contactRepository.GetQueryableAsync().Returns(contacts);
+ _contactLinkRepository.GetQueryableAsync().Returns(contactLinks);
+
+ // Act
+ var result = await _service.GetProfileContactsAsync(profileId);
+
+ // Assert
+ result.Count.ShouldBe(1);
+ var contact = result[0];
+ contact.ContactId.ShouldBe(contactId);
+ contact.Name.ShouldBe("John Doe");
+ contact.Title.ShouldBe("Manager");
+ contact.Email.ShouldBe("john@example.com");
+ contact.HomePhoneNumber.ShouldBe("111-1111");
+ contact.MobilePhoneNumber.ShouldBe("222-2222");
+ contact.WorkPhoneNumber.ShouldBe("333-3333");
+ contact.WorkPhoneExtension.ShouldBe("101");
+ contact.Role.ShouldBe("Primary Contact");
+ contact.IsPrimary.ShouldBeTrue();
+ contact.IsEditable.ShouldBeTrue();
+ contact.ApplicationId.ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task GetProfileContactsAsync_WithNoLinks_ShouldReturnEmpty()
+ {
+ // Arrange
+ _contactRepository.GetQueryableAsync().Returns(Array.Empty().AsAsyncQueryable());
+ _contactLinkRepository.GetQueryableAsync().Returns(Array.Empty().AsAsyncQueryable());
+
+ // Act
+ var result = await _service.GetProfileContactsAsync(Guid.NewGuid());
+
+ // Assert
+ result.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public async Task GetApplicationContactsBySubjectAsync_WithMatchingSubmission_ShouldReturnContacts()
+ {
+ // Arrange
+ var applicationId = Guid.NewGuid();
+ var appContactId = Guid.NewGuid();
+
+ var submissions = new[]
+ {
+ new ApplicationFormSubmission
+ {
+ OidcSub = "TESTUSER",
+ ApplicationId = applicationId,
+ ApplicantId = Guid.NewGuid(),
+ ApplicationFormId = Guid.NewGuid()
+ }
+ }.AsAsyncQueryable();
+
+ var applicationContacts = new[]
+ {
+ WithId(new ApplicationContact
+ {
+ ApplicationId = applicationId,
+ ContactFullName = "Jane Smith",
+ ContactTitle = "Director",
+ ContactEmail = "jane@example.com",
+ ContactMobilePhone = "444-4444",
+ ContactWorkPhone = "555-5555",
+ ContactType = "ADDITIONAL_SIGNING_AUTHORITY"
+ }, appContactId)
+ }.AsAsyncQueryable();
+
+ _submissionRepository.GetQueryableAsync().Returns(submissions);
+ _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts);
+
+ // Act
+ var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir");
+
+ // Assert
+ result.Count.ShouldBe(1);
+ var contact = result[0];
+ contact.ContactId.ShouldBe(appContactId);
+ contact.Name.ShouldBe("Jane Smith");
+ contact.Title.ShouldBe("Director");
+ contact.Email.ShouldBe("jane@example.com");
+ contact.MobilePhoneNumber.ShouldBe("444-4444");
+ contact.WorkPhoneNumber.ShouldBe("555-5555");
+ contact.ContactType.ShouldBe("Application");
+ contact.Role.ShouldBe("Additional Signing Authority");
+ contact.IsPrimary.ShouldBeFalse();
+ contact.IsEditable.ShouldBeFalse();
+ contact.ApplicationId.ShouldBe(applicationId);
+ }
+
+ [Fact]
+ public async Task GetApplicationContactsBySubjectAsync_ShouldMatchCaseInsensitively()
+ {
+ // Arrange
+ var applicationId = Guid.NewGuid();
+
+ var submissions = new[]
+ {
+ new ApplicationFormSubmission
+ {
+ OidcSub = "TESTUSER",
+ ApplicationId = applicationId,
+ ApplicantId = Guid.NewGuid(),
+ ApplicationFormId = Guid.NewGuid()
+ }
+ }.AsAsyncQueryable();
+
+ var applicationContacts = new[]
+ {
+ WithId(new ApplicationContact
+ {
+ ApplicationId = applicationId,
+ ContactFullName = "Case Test",
+ ContactType = "ADDITIONAL_CONTACT"
+ }, Guid.NewGuid())
+ }.AsAsyncQueryable();
+
+ _submissionRepository.GetQueryableAsync().Returns(submissions);
+ _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts);
+
+ // Act
+ var result = await _service.GetApplicationContactsBySubjectAsync("testuser@IDIR");
+
+ // Assert
+ result.Count.ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task GetApplicationContactsBySubjectAsync_ShouldStripDomainFromSubject()
+ {
+ // 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);
+
+ // Act
+ var result = await _service.GetApplicationContactsBySubjectAsync("myuser@differentdomain");
+
+ // Assert
+ result.Count.ShouldBe(1);
+ result[0].Name.ShouldBe("Domain Strip Test");
+ }
+
+ [Fact]
+ public async Task GetApplicationContactsBySubjectAsync_WithSubjectWithoutAtSign_ShouldStillMatch()
+ {
+ // Arrange
+ var applicationId = Guid.NewGuid();
+
+ var submissions = new[]
+ {
+ new ApplicationFormSubmission
+ {
+ OidcSub = "PLAINUSER",
+ ApplicationId = applicationId,
+ ApplicantId = Guid.NewGuid(),
+ ApplicationFormId = Guid.NewGuid()
+ }
+ }.AsAsyncQueryable();
+
+ var applicationContacts = new[]
+ {
+ WithId(new ApplicationContact
+ {
+ ApplicationId = applicationId,
+ ContactFullName = "Plain User Contact",
+ ContactType = "ADDITIONAL_CONTACT"
+ }, Guid.NewGuid())
+ }.AsAsyncQueryable();
+
+ _submissionRepository.GetQueryableAsync().Returns(submissions);
+ _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts);
+
+ // Act
+ var result = await _service.GetApplicationContactsBySubjectAsync("plainuser");
+
+ // Assert
+ result.Count.ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task GetApplicationContactsBySubjectAsync_WithNonMatchingSubject_ShouldReturnEmpty()
+ {
+ // Arrange
+ var applicationId = Guid.NewGuid();
+
+ var submissions = new[]
+ {
+ new ApplicationFormSubmission
+ {
+ OidcSub = "OTHERUSER",
+ ApplicationId = applicationId,
+ ApplicantId = Guid.NewGuid(),
+ ApplicationFormId = Guid.NewGuid()
+ }
+ }.AsAsyncQueryable();
+
+ var applicationContacts = new[]
+ {
+ WithId(new ApplicationContact
+ {
+ ApplicationId = applicationId,
+ ContactFullName = "Should Not Match"
+ }, Guid.NewGuid())
+ }.AsAsyncQueryable();
+
+ _submissionRepository.GetQueryableAsync().Returns(submissions);
+ _applicationContactRepository.GetQueryableAsync().Returns(applicationContacts);
+
+ // Act
+ var result = await _service.GetApplicationContactsBySubjectAsync("differentuser@idir");
+
+ // Assert
+ result.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public async Task GetApplicationContactsBySubjectAsync_WithNoSubmissions_ShouldReturnEmpty()
+ {
+ // Arrange
+ _submissionRepository.GetQueryableAsync()
+ .Returns(Array.Empty().AsAsyncQueryable());
+ _applicationContactRepository.GetQueryableAsync()
+ .Returns(Array.Empty().AsAsyncQueryable());
+
+ // Act
+ var result = await _service.GetApplicationContactsBySubjectAsync("testuser@idir");
+
+ // 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/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/TestHelpers/TestAsyncEnumerableQueryable.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/TestHelpers/TestAsyncEnumerableQueryable.cs
new file mode 100644
index 000000000..26b6bcd97
--- /dev/null
+++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/TestHelpers/TestAsyncEnumerableQueryable.cs
@@ -0,0 +1,74 @@
+using Microsoft.EntityFrameworkCore.Query;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Unity.GrantManager.TestHelpers
+{
+ internal class TestAsyncEnumerableQueryable : EnumerableQuery, IAsyncEnumerable, IQueryable
+ {
+ public TestAsyncEnumerableQueryable(IEnumerable enumerable) : base(enumerable) { }
+ public TestAsyncEnumerableQueryable(Expression expression) : base(expression) { }
+
+ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
+ => new TestAsyncEnumerator(this.AsEnumerable().GetEnumerator());
+
+ IQueryProvider IQueryable.Provider => new TestAsyncQueryProvider(this);
+ }
+
+ internal class TestAsyncEnumerator(IEnumerator inner) : IAsyncEnumerator
+ {
+ public T Current => inner.Current;
+ public ValueTask MoveNextAsync() => new(inner.MoveNext());
+ public ValueTask DisposeAsync()
+ {
+ inner.Dispose();
+ return default;
+ }
+ }
+
+ internal class TestAsyncQueryProvider(IQueryProvider inner) : IQueryProvider, IAsyncQueryProvider
+ {
+ public IQueryable CreateQuery(Expression expression)
+ => new TestAsyncEnumerableQueryable(expression);
+
+ public IQueryable CreateQuery(Expression expression)
+ => new TestAsyncEnumerableQueryable(expression);
+
+ public object? Execute(Expression expression)
+ => inner.Execute(expression);
+
+ public TResult Execute(Expression expression)
+ => inner.Execute(expression);
+
+ public TResult ExecuteAsync(Expression expression, CancellationToken cancellationToken = default)
+ {
+ // TResult is typically Task for async operations
+ var resultType = typeof(TResult);
+
+ // Get the actual result synchronously
+ var syncResult = inner.Execute(expression);
+
+ // If TResult is Task, extract T and wrap the result
+ if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Task<>))
+ {
+ var taskResultType = resultType.GetGenericArguments()[0];
+ var taskFromResult = typeof(Task)
+ .GetMethod(nameof(Task.FromResult))!
+ .MakeGenericMethod(taskResultType);
+ return (TResult)taskFromResult.Invoke(null, new[] { syncResult })!;
+ }
+
+ // For non-generic Task or other types, just return as-is
+ return (TResult)(object)Task.CompletedTask;
+ }
+ }
+
+ internal static class TestQueryableExtensions
+ {
+ public static IQueryable AsAsyncQueryable(this IEnumerable source)
+ => new TestAsyncEnumerableQueryable(source);
+ }
+}
diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs
index abab7db3d..9b59458b0 100644
--- a/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs
+++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Components/ApplicationContactWidgetTests.cs
@@ -64,7 +64,7 @@ public async Task ApplicationContactWidgetReturnsStatus()
};
//Act
- var result = await viewComponent.InvokeAsync(applicationId, true) as ViewViewComponentResult;
+ var result = await viewComponent.InvokeAsync(applicationId) as ViewViewComponentResult;
ApplicationContactsWidgetViewModel? resultModel;
resultModel = result!.ViewData!.Model! as ApplicationContactsWidgetViewModel;