Skip to content
Open
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 @@ -5,7 +5,6 @@
using Unity.GrantManager.ApplicantProfile.ProfileData;
using Unity.GrantManager.Applications;
using Unity.GrantManager.GrantApplications;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
Expand Down Expand Up @@ -102,23 +101,30 @@ from application in apps.DefaultIfEmpty()
var addressDtos = deduplicated.Select(r => new AddressInfoItemDto
{
Id = r.address.Id,
AddressType = GetAddressTypeName(r.address.AddressType),
AddressType = AddressTypeMapper.ToDisplayName(r.address.AddressType),
Street = r.address.Street ?? string.Empty,
Street2 = r.address.Street2 ?? string.Empty,
Unit = r.address.Unit ?? string.Empty,
City = r.address.City ?? string.Empty,
Province = r.address.Province ?? string.Empty,
PostalCode = r.address.Postal ?? string.Empty,
Country = r.address.Country ?? string.Empty,
IsPrimary = r.address.HasProperty(AddressExtraPropertyNames.IsPrimary) && r.address.GetProperty<bool>(AddressExtraPropertyNames.IsPrimary),
IsPrimary = r.address.IsFlaggedPrimary(),
IsEditable = r.IsFromApplicantPath && !distinctApplicants,
ReferenceNo = r.ReferenceNo
}).ToList();

// If no address is marked as primary, mark the most recent one as primary
if (addressDtos.Count > 0 && !addressDtos.Any(a => a.IsPrimary))
// Primary is scoped to the address type WITHIN an applicant: this provider can return
// addresses for more than one applicant, so grouping by type alone would let one
// applicant's flagged primary suppress the fallback for another.
foreach (var typeGroup in deduplicated.GroupBy(r => new { r.address.ApplicantId, r.address.AddressType }))
{
var mostRecent = deduplicated.OrderByDescending(r => r.CreationTime).First();
if (typeGroup.Any(r => r.address.IsFlaggedPrimary()))
{
continue;
}

var mostRecent = typeGroup.OrderByDescending(r => r.CreationTime).First();
var mostRecentDto = addressDtos.First(a => a.Id == mostRecent.address.Id);
mostRecentDto.IsPrimary = true;
}
Expand All @@ -128,19 +134,5 @@ from application in apps.DefaultIfEmpty()

return dto;
}

/// <summary>
/// Maps an <see cref="AddressType"/> enum value to a human-readable display name.
/// </summary>
private static string GetAddressTypeName(AddressType addressType)
{
return addressType switch
{
AddressType.PhysicalAddress => "Physical",
AddressType.MailingAddress => "Mailing",
AddressType.BusinessAddress => "Business",
_ => addressType.ToString()
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace Unity.GrantManager.GrantsPortal.Handlers;

public class AddressCreateHandler(
IApplicantAddressRepository applicantAddressRepository,
IApplicantAddressManager applicantAddressManager,
ILogger<AddressCreateHandler> logger) : IPortalCommandHandler, ITransientDependency
{
public string DataType => "ADDRESS_CREATE_COMMAND";
Expand Down Expand Up @@ -51,44 +52,26 @@ public virtual async Task<string> HandleAsync(PluginDataPayload payload)
Province = innerData.Province,
Postal = innerData.PostalCode,
Country = innerData.Country,
AddressType = MapAddressType(innerData.AddressType)
AddressType = AddressTypeMapper.FromPortalValue(innerData.AddressType)
};

EntityHelper.TrySetId(address, () => addressId);

address.SetProperty(AddressExtraPropertyNames.ProfileId, profileId.ToString());
address.SetProperty(AddressExtraPropertyNames.IsPrimary, innerData.IsPrimary);
address.SetPrimaryFlag(innerData.IsPrimary);

// Demote existing primary addresses for the same applicant
if (innerData.IsPrimary)
{
var siblingAddresses = await applicantAddressRepository.FindByApplicantIdAsync(innerData.ApplicantId);

foreach (var sibling in siblingAddresses)
{
if (!sibling.HasProperty(AddressExtraPropertyNames.IsPrimary)) continue;
if (!sibling.GetProperty<bool>(AddressExtraPropertyNames.IsPrimary)) continue;

var trackedSibling = await applicantAddressRepository.GetAsync(sibling.Id);
trackedSibling.SetProperty(AddressExtraPropertyNames.IsPrimary, false);
await applicantAddressRepository.UpdateAsync(trackedSibling);
}
// Primary is scoped to the address type, so only same-type siblings are demoted.
await applicantAddressManager.DemotePrimarySiblingsAsync(
innerData.ApplicantId,
address.AddressType,
addressId);
}

await applicantAddressRepository.InsertAsync(address);

logger.LogInformation("Address {AddressId} created successfully", addressId);
return "Address created successfully";
}

private static AddressType MapAddressType(string? portalAddressType)
{
return portalAddressType?.ToUpperInvariant() switch
{
"MAILING" => AddressType.MailingAddress,
"PHYSICAL" => AddressType.PhysicalAddress,
"BUSINESS" => AddressType.BusinessAddress,
_ => AddressType.PhysicalAddress
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Unity.GrantManager.GrantsPortal.Handlers;

public class AddressDeleteHandler(
IApplicantAddressRepository applicantAddressRepository,
IApplicantAddressManager applicantAddressManager,
ILogger<AddressDeleteHandler> logger) : IPortalCommandHandler, ITransientDependency
{
public string DataType => "ADDRESS_DELETE_COMMAND";
Expand All @@ -24,7 +25,17 @@ public virtual async Task<string> HandleAsync(PluginDataPayload payload)
var address = await applicantAddressRepository.FindAsync(addressId);
if (address != null)
{
var wasPrimary = address.IsFlaggedPrimary();
var addressType = address.AddressType;
var applicantId = address.ApplicantId;

await applicantAddressRepository.DeleteAsync(address);

if (wasPrimary && applicantId.HasValue)
{
// The address type group just lost its primary, so promote the most recent survivor.
await applicantAddressManager.ElectPrimaryAsync(applicantId.Value, addressType, addressId);
}
}

logger.LogInformation("Address {AddressId} deleted successfully", addressId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
using Unity.GrantManager.GrantApplications;
using Unity.GrantManager.GrantsPortal.Messages;
using Unity.GrantManager.GrantsPortal.Messages.Commands;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;

namespace Unity.GrantManager.GrantsPortal.Handlers;

public class AddressEditHandler(
IApplicantAddressRepository applicantAddressRepository,
IApplicantAddressManager applicantAddressManager,
ILogger<AddressEditHandler> logger) : IPortalCommandHandler, ITransientDependency
{
public string DataType => "ADDRESS_EDIT_COMMAND";
Expand All @@ -28,52 +28,56 @@ public virtual async Task<string> HandleAsync(PluginDataPayload payload)

var address = await applicantAddressRepository.GetAsync(addressId);

var previousAddressType = address.AddressType;
var wasPrimary = address.IsFlaggedPrimary();

address.Street = innerData.Street;
address.Street2 = innerData.Street2;
address.Unit = innerData.Unit;
address.City = innerData.City;
address.Province = innerData.Province;
address.Postal = innerData.PostalCode;
address.Country = innerData.Country;
address.AddressType = MapAddressType(innerData.AddressType);
address.AddressType = AddressTypeMapper.FromPortalValue(innerData.AddressType);
address.SetPrimaryFlag(innerData.IsPrimary);

if (innerData.IsPrimary && address.ApplicantId.HasValue)
if (address.ApplicantId.HasValue)
{
await DemoteSiblingPrimaryAddressesAsync(address.ApplicantId.Value, addressId);
await ApplyPrimaryScopeAsync(
address.ApplicantId.Value,
addressId,
previousAddressType,
address.AddressType,
wasPrimary,
innerData.IsPrimary);
}

address.SetProperty(AddressExtraPropertyNames.IsPrimary, innerData.IsPrimary);

await applicantAddressRepository.UpdateAsync(address);

logger.LogInformation("Address {AddressId} updated successfully", addressId);
return "Address updated successfully";
}

private async Task DemoteSiblingPrimaryAddressesAsync(Guid applicantId, Guid excludeAddressId)
/// <summary>
/// Keeps the "at most one primary per address type" invariant intact after an edit.
/// An address that moves to another type contests its new group and vacates the old one.
/// </summary>
private async Task ApplyPrimaryScopeAsync(
Guid applicantId,
Guid addressId,
AddressType previousAddressType,
AddressType currentAddressType,
bool wasPrimary,
bool isPrimary)
{
var siblingAddresses = await applicantAddressRepository.FindByApplicantIdAsync(applicantId);

foreach (var sibling in siblingAddresses)
if (isPrimary)
{
if (sibling.Id == excludeAddressId) continue;
if (!sibling.HasProperty(AddressExtraPropertyNames.IsPrimary)) continue;
if (!sibling.GetProperty<bool>(AddressExtraPropertyNames.IsPrimary)) continue;

var trackedSibling = await applicantAddressRepository.GetAsync(sibling.Id);
trackedSibling.SetProperty(AddressExtraPropertyNames.IsPrimary, false);
await applicantAddressRepository.UpdateAsync(trackedSibling);
await applicantAddressManager.DemotePrimarySiblingsAsync(applicantId, currentAddressType, addressId);
}
}

private static AddressType MapAddressType(string? portalAddressType)
{
return portalAddressType?.ToUpperInvariant() switch
if (wasPrimary && previousAddressType != currentAddressType)
{
"MAILING" => AddressType.MailingAddress,
"PHYSICAL" => AddressType.PhysicalAddress,
"BUSINESS" => AddressType.BusinessAddress,
_ => AddressType.PhysicalAddress
};
await applicantAddressManager.ElectPrimaryAsync(applicantId, previousAddressType, addressId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ namespace Unity.GrantManager.GrantsPortal.Handlers;

public class AddressSetPrimaryHandler(
IApplicantAddressRepository applicantAddressRepository,
ILogger<AddressSetPrimaryHandler> logger)
IApplicantAddressManager applicantAddressManager,
ILogger<AddressSetPrimaryHandler> logger)
: IPortalCommandHandler, ITransientDependency
{
public string DataType => "ADDRESS_SET_PRIMARY_COMMAND";
Expand All @@ -28,27 +29,24 @@ public virtual async Task<string> HandleAsync(PluginDataPayload payload)
var address = await applicantAddressRepository.GetAsync(addressId);

address.SetProperty(AddressExtraPropertyNames.ProfileId, profileId.ToString());
address.SetProperty(AddressExtraPropertyNames.IsPrimary, true);
address.SetPrimaryFlag(true);

if (address.ApplicantId.HasValue)
{
var siblingAddresses = await applicantAddressRepository.FindByApplicantIdAsync(address.ApplicantId.Value);

foreach (var sibling in siblingAddresses)
{
if (sibling.Id == addressId) continue;
if (!sibling.HasProperty(AddressExtraPropertyNames.IsPrimary)) continue;
if (!sibling.GetProperty<bool>(AddressExtraPropertyNames.IsPrimary)) continue;

var trackedSibling = await applicantAddressRepository.GetAsync(sibling.Id);
trackedSibling.SetProperty(AddressExtraPropertyNames.IsPrimary, false);
await applicantAddressRepository.UpdateAsync(trackedSibling);
}
// Primary is scoped to the address type, so only same-type siblings are demoted.
await applicantAddressManager.DemotePrimarySiblingsAsync(
address.ApplicantId.Value,
address.AddressType,
addressId);
}

await applicantAddressRepository.UpdateAsync(address);

logger.LogInformation("Address {AddressId} set as primary", addressId);
logger.LogInformation(
"Address {AddressId} set as primary for address type {AddressType}",
addressId,
address.AddressType);

return "Address set as primary";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Linq;

namespace Unity.GrantManager.GrantApplications;

/// <summary>
/// Single place that translates between the <see cref="AddressType"/> enum and the
/// short names exchanged with the applicant portal (for example "Mailing").
/// The translation is derived from the enum members themselves, so adding a new
/// <see cref="AddressType"/> requires no change here.
/// </summary>
public static class AddressTypeMapper
{
/// <summary>
/// Suffix carried by every <see cref="AddressType"/> member name; stripped to form the short name.
/// </summary>
private const string MemberNameSuffix = "Address";

/// <summary>
/// Value used when the portal supplies no address type, or one that is not recognised.
/// </summary>
public const AddressType DefaultAddressType = AddressType.PhysicalAddress;

/// <summary>
/// Maps a short address type name supplied by the portal (case-insensitive, for example
/// "MAILING") to its <see cref="AddressType"/> member, falling back to
/// <see cref="DefaultAddressType"/> when the value is missing or unknown.
/// </summary>
public static AddressType FromPortalValue(string? portalAddressType)
{
if (string.IsNullOrWhiteSpace(portalAddressType))
{
return DefaultAddressType;
}

var candidate = portalAddressType.Trim();

// DefaultIfEmpty carries the fallback: AddressType has no zero member, so
// FirstOrDefault would yield an undefined value rather than the intended default.
return Enum.GetValues<AddressType>()
.Where(addressType => string.Equals(ToDisplayName(addressType), candidate, StringComparison.OrdinalIgnoreCase))
.DefaultIfEmpty(DefaultAddressType)
.First();
}

/// <summary>
/// Maps an <see cref="AddressType"/> member to its human-readable short name
/// (for example <see cref="AddressType.MailingAddress"/> becomes "Mailing").
/// </summary>
public static string ToDisplayName(AddressType addressType)
{
var memberName = addressType.ToString();

if (memberName.Length > MemberNameSuffix.Length
&& memberName.EndsWith(MemberNameSuffix, StringComparison.Ordinal))
{
return memberName[..^MemberNameSuffix.Length];
}

return memberName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using System.Text.Json.Serialization;
using Unity.GrantManager.GrantApplications;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;

Expand Down Expand Up @@ -41,6 +42,26 @@ public virtual Application Application
public AddressType AddressType { get; set; } = AddressType.PhysicalAddress;
public Guid? TenantId { get; set; }

/// <summary>
/// Indicates whether this address carries an explicit primary flag in its extra properties.
/// A missing flag is treated as "not primary".
/// </summary>
public bool IsFlaggedPrimary()
{
return this.HasProperty(AddressExtraPropertyNames.IsPrimary)
&& this.GetProperty<bool>(AddressExtraPropertyNames.IsPrimary);
}

/// <summary>
/// Sets the explicit primary flag on this address.
/// Primary is scoped to <see cref="AddressType"/>: an applicant may hold at most one
/// primary address within each address type group.
/// </summary>
public void SetPrimaryFlag(bool isPrimary)
{
this.SetProperty(AddressExtraPropertyNames.IsPrimary, isPrimary);
}

/// <summary>
/// Returns a search-friendly address string (Street, Street2, City) for geocoding lookups.
/// </summary>
Expand Down
Loading
Loading