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
49 changes: 49 additions & 0 deletions src/SEBT.Portal.Core/Models/Household/CoLoadedCohortClassifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace SEBT.Portal.Core.Models.Household;

/// <summary>
/// Derives <see cref="CoLoadedCohort"/> from pre-filter household case and application state.
/// Shared by household reads and ID proofing off-boarding decisions.
/// </summary>
public static class CoLoadedCohortClassifier
{
/// <summary>
/// Classifies the household based on its case list and applications.
/// See <see cref="CoLoadedCohort"/> for the rule.
/// </summary>
public static CoLoadedCohort Classify(HouseholdData? household)
{
if (household == null)
{
return CoLoadedCohort.NonCoLoaded;
}

var hasCoLoaded = household.SummerEbtCases.Any(c => c.IsCoLoaded);
if (!hasCoLoaded)
{
return CoLoadedCohort.NonCoLoaded;
}

var hasNonCoLoaded = household.SummerEbtCases.Any(c => !c.IsCoLoaded);
var hasInFlightHouseholdApplication = household.Applications.Any(IsInFlightHouseholdApplication);
var hasPendingCase = household.SummerEbtCases.Any(IsPendingApplicant);

return hasNonCoLoaded || hasInFlightHouseholdApplication || hasPendingCase
? CoLoadedCohort.MixedOrApplicantExcluded
: CoLoadedCohort.CoLoadedOnly;
}

/// <summary>
/// Maps a default off-boarding reason to the co-loaded-only screen when the household
/// cohort warrants it.
/// </summary>
public static string ResolveOffboardingReason(string defaultReason, HouseholdData? household) =>
Classify(household) == CoLoadedCohort.CoLoadedOnly
? "coLoadedOnly"
: defaultReason;

private static bool IsPendingApplicant(SummerEbtCase summerEbtCase) =>
summerEbtCase.ApplicationStatus is ApplicationStatus.Pending or ApplicationStatus.UnderReview;

private static bool IsInFlightHouseholdApplication(Application application) =>
application.ApplicationStatus is ApplicationStatus.Pending or ApplicationStatus.UnderReview;
}
32 changes: 26 additions & 6 deletions src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
/// <returns>An array of User instances configured for testing.</returns>
private User[] CreateTestUsers(DateTime now)
{
return new[]
var users = new List<User>
{
UserFactory.CreateCoLoadedUser(u =>
{
Expand All @@ -80,13 +80,33 @@
u.TanfId = "TANF-CO-001";
u.Ssn = "123456789";
}),
};

if (IsDc)
{
users.Add(UserFactory.CreateNonCoLoadedUser(u =>
{
u.Email = _settings.BuildEmail(SeedScenarios.CoLoadedPendingIdProofing.Name);
u.IdProofingStatus = IdProofingStatus.NotStarted;
u.IalLevel = UserIalLevel.None;
u.IdProofingCompletedAt = null;
u.IdProofingExpiresAt = null;

Check warning on line 93 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 93 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 93 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Pa11y Accessibility Tests

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 93 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / co - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 93 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / dc - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'
u.Phone = "8185558438";
u.SnapId = "SNAP-CO-001";
u.TanfId = "TANF-CO-001";
u.Ssn = "123456789";
}));
}

users.AddRange(
[
UserFactory.CreateNonCoLoadedUser(u =>
{
u.Email = _settings.BuildEmail(SeedScenarios.NonCoLoaded.Name);
u.IdProofingStatus = IdProofingStatus.InProgress;
u.IalLevel = UserIalLevel.None;
u.IdProofingCompletedAt = null;
u.IdProofingExpiresAt = null;

Check warning on line 109 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 109 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 109 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Pa11y Accessibility Tests

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 109 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / co - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 109 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / dc - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'
u.Phone = "5555551234";
u.SnapId = "SNAP-NCO-001";
}),
Expand All @@ -96,9 +116,11 @@
u.IdProofingStatus = IdProofingStatus.NotStarted;
u.IalLevel = UserIalLevel.None;
u.IdProofingCompletedAt = null;
u.IdProofingExpiresAt = null;

Check warning on line 119 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 119 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 119 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Pa11y Accessibility Tests

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 119 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / co - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 119 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / dc - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'
})
};
]);

return users.ToArray();
}

/// <summary>
Expand Down Expand Up @@ -170,14 +192,13 @@
}
else if (normalizedEmail == coLoadedPendingIdProofingEmail)
{
user = UserFactory.CreateCoLoadedUser(u =>
user = UserFactory.CreateNonCoLoadedUser(u =>
{
u.Email = normalizedEmail;
u.IdProofingStatus = IdProofingStatus.NotStarted;
u.IalLevel = UserIalLevel.None;
u.IdProofingCompletedAt = null;
u.IdProofingExpiresAt = null;

Check warning on line 201 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 201 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 201 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Pa11y Accessibility Tests

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 201 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / co - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 201 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / dc - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'
u.CoLoadedLastUpdated = now.AddDays(DaysSinceCoLoadedUpdate);
u.Phone = "8185558438";
u.SnapId = "SNAP-CO-001";
u.TanfId = "TANF-CO-001";
Expand Down Expand Up @@ -222,7 +243,7 @@
u.IdProofingStatus = IdProofingStatus.InProgress;
u.IalLevel = UserIalLevel.None;
u.IdProofingCompletedAt = null;
u.IdProofingExpiresAt = null;

Check warning on line 246 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 246 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 246 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Pa11y Accessibility Tests

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 246 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / co - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 246 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / dc - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'
u.IsCoLoaded = false;
u.CoLoadedLastUpdated = null;
u.Phone = "5552223344";
Expand All @@ -248,7 +269,7 @@
// Bogus may pre-set timestamps when the random draw is IAL1+; clear
// so IAL None/1 never retain IdProofingCompletedAt from the generator.
u.IdProofingCompletedAt = null;
u.IdProofingExpiresAt = null;

Check warning on line 272 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 272 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 272 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Pa11y Accessibility Tests

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 272 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / co - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 272 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / dc - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'
}
u.IsCoLoaded = false;
u.CoLoadedLastUpdated = null;
Expand Down Expand Up @@ -370,14 +391,13 @@
}
else if (normalizedEmail == coLoadedPendingIdProofingEmail)
{
user = UserFactory.CreateCoLoadedUser(u =>
user = UserFactory.CreateNonCoLoadedUser(u =>
{
u.Email = normalizedEmail;
u.IdProofingStatus = IdProofingStatus.NotStarted;
u.IalLevel = UserIalLevel.None;
u.IdProofingCompletedAt = null;
u.IdProofingExpiresAt = null;

Check warning on line 400 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 400 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 400 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Pa11y Accessibility Tests

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 400 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / co - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 400 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / dc - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'
u.CoLoadedLastUpdated = now.AddDays(DaysSinceCoLoadedUpdate);
u.Phone = "8185558438";
u.SnapId = "SNAP-CO-001";
u.TanfId = "TANF-CO-001";
Expand Down Expand Up @@ -422,7 +442,7 @@
u.IdProofingStatus = IdProofingStatus.InProgress;
u.IalLevel = UserIalLevel.None;
u.IdProofingCompletedAt = null;
u.IdProofingExpiresAt = null;

Check warning on line 445 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 445 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 445 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Pa11y Accessibility Tests

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 445 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / co - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 445 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / dc - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'
u.IsCoLoaded = false;
u.CoLoadedLastUpdated = null;
u.Phone = "5552223344";
Expand All @@ -446,7 +466,7 @@
else
{
u.IdProofingCompletedAt = null;
u.IdProofingExpiresAt = null;

Check warning on line 469 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 469 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Build DC IIS bundle

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 469 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / Pa11y Accessibility Tests

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 469 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / co - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'

Check warning on line 469 in src/SEBT.Portal.Infrastructure.Seeding/Services/DatabaseSeeder.cs

View workflow job for this annotation

GitHub Actions / dc - Build & Test

'User.IdProofingExpiresAt' is obsolete: 'Expiration is computed from IdProofingCompletedAt + IdProofingValiditySettings.ValidityDays. Do not read or write this field for new code.'
}
u.IsCoLoaded = false;
u.CoLoadedLastUpdated = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public async Task<Result<HouseholdData>> Handle(GetHouseholdDataQuery query, Can
// Classify the household on the PRE-filter state so analytics can
// distinguish cohorts even after co-loaded cases are suppressed. Then
// apply the suppression for the excluded cohort.
householdData.CoLoadedCohort = ClassifyCoLoadedCohort(householdData);
householdData.CoLoadedCohort = CoLoadedCohortClassifier.Classify(householdData);

var nonCoLoaded = householdData.SummerEbtCases.Where(c => !c.IsCoLoaded).ToList();
if (coLoadedCohortFilter.SuppressCoLoadedCasesForExcludedCohort
Expand Down Expand Up @@ -164,44 +164,4 @@ public async Task<Result<HouseholdData>> Handle(GetHouseholdDataQuery query, Can
return Result<HouseholdData>.Success(householdData);
}

/// <summary>
/// Classifies the household based on its pre-filter case list and applications.
/// See <see cref="CoLoadedCohort"/> for the rule.
/// The rule is intentionally derived at runtime from case and application state; changing
/// who falls into each cohort still requires a code change. Whether co-loaded cases are
/// suppressed for the excluded cohort is configured via
/// <see cref="CoLoadedCohortFilterSettings.SuppressCoLoadedCasesForExcludedCohort"/>.
/// </summary>
private static CoLoadedCohort ClassifyCoLoadedCohort(HouseholdData household)
{
var hasCoLoaded = household.SummerEbtCases.Any(c => c.IsCoLoaded);
if (!hasCoLoaded)
{
return CoLoadedCohort.NonCoLoaded;
}

var hasNonCoLoaded = household.SummerEbtCases.Any(c => !c.IsCoLoaded);
var hasInFlightHouseholdApplication = household.Applications.Any(IsInFlightHouseholdApplication);
var hasPendingCase = household.SummerEbtCases.Any(IsPendingApplicant);

return hasNonCoLoaded || hasInFlightHouseholdApplication || hasPendingCase
? CoLoadedCohort.MixedOrApplicantExcluded
: CoLoadedCohort.CoLoadedOnly;
}

/// <summary>
/// A case whose application hasn't been adjudicated yet represents an
/// in-flight applicant experience, which places the household in the
/// applicant-excluded cohort even when the case itself is co-loaded.
/// </summary>
private static bool IsPendingApplicant(SummerEbtCase summerEbtCase) =>
summerEbtCase.ApplicationStatus is ApplicationStatus.Pending or ApplicationStatus.UnderReview;

/// <summary>
/// Household-level <see cref="HouseholdData.Applications"/> often retains historical rows
/// (approved/denied/cancelled). Only pending or under-review applications indicate an active
/// applicant journey alongside co-loaded cases.
/// </summary>
private static bool IsInFlightHouseholdApplication(Application application) =>
application.ApplicationStatus is ApplicationStatus.Pending or ApplicationStatus.UnderReview;
}
41 changes: 41 additions & 0 deletions src/SEBT.Portal.UseCases/IdProofing/IdProofingHouseholdLookup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.Extensions.Logging;
using SEBT.Portal.Core.Models;
using SEBT.Portal.Core.Models.Auth;
using SEBT.Portal.Core.Models.Household;
using SEBT.Portal.Core.Repositories;

namespace SEBT.Portal.UseCases.IdProofing;

/// <summary>
/// Shared warehouse household reads for ID proofing off-boarding cohort checks.
/// </summary>
internal static class IdProofingHouseholdLookup
{
internal static async Task<HouseholdData?> TryGetByEmailForCohortCheckAsync(
IHouseholdRepository householdRepository,
ILogger logger,
User user,
UserIalLevel warehouseIalForEmailReads,
Guid portalUserId,
CancellationToken cancellationToken)
{
try
{
return await householdRepository.GetHouseholdByEmailAsync(
user.Email!,
new PiiVisibility(IncludeAddress: false, IncludeEmail: false, IncludePhone: false),
warehouseIalForEmailReads,
portalUserId,
cancellationToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(
ex,
"Household lookup failed ({ExceptionType}) for user {UserId} during off-boarding cohort check",
ex.GetType().Name,
portalUserId);
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SEBT.Portal.Core.AppSettings;
using SEBT.Portal.Core.Exceptions;
using SEBT.Portal.Core.Models;
using SEBT.Portal.Core.Models.Auth;
using SEBT.Portal.Core.Models.DocVerification;
using SEBT.Portal.Core.Models.Household;
using SEBT.Portal.Core.Repositories;
using SEBT.Portal.Core.Utilities;
using SEBT.Portal.Kernel;
using SEBT.Portal.Kernel.Results;

Expand All @@ -21,7 +25,9 @@ namespace SEBT.Portal.UseCases.IdProofing;
public class ProcessWebhookCommandHandler(
IDocVerificationChallengeRepository challengeRepository,
IUserRepository userRepository,
IHouseholdRepository householdRepository,
SocureSettings socureSettings,
IOptions<IdProofingEligibilitySettings> idProofingEligibilitySettings,
IValidator<ProcessWebhookCommand> validator,
ILogger<ProcessWebhookCommandHandler> logger)
: ICommandHandler<ProcessWebhookCommand>
Expand Down Expand Up @@ -125,7 +131,9 @@ public async Task<Result> Handle(

if (newStatus == DocVerificationStatus.Rejected)
{
challenge.OffboardingReason = "docVerificationFailed";
challenge.OffboardingReason = await ResolveRejectionOffboardingReasonAsync(
challenge.UserId,
cancellationToken);
}

try
Expand Down Expand Up @@ -223,6 +231,33 @@ private bool ValidateWebhookSignature(string? bearerToken)
};
}

private async Task<string> ResolveRejectionOffboardingReasonAsync(
Guid userId,
CancellationToken cancellationToken)
{
const string defaultReason = "docVerificationFailed";

var user = await userRepository.GetUserByIdAsync(userId, cancellationToken);
if (user == null || string.IsNullOrWhiteSpace(user.Email))
{
return defaultReason;
}

var warehouseIal = PreSocureHouseholdWarehouseIal.ForEmailLinkedHouseholdRead(
user.IalLevel,
idProofingEligibilitySettings.Value.RequireQualifyingHouseholdForSocure);

var household = await IdProofingHouseholdLookup.TryGetByEmailForCohortCheckAsync(
householdRepository,
logger,
user,
warehouseIal,
userId,
cancellationToken);

return CoLoadedCohortClassifier.ResolveOffboardingReason(defaultReason, household);
}

private async Task UpdateUserProofingStatus(Guid userId, CancellationToken cancellationToken)
{
var user = await userRepository.GetUserByIdAsync(userId, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,32 +95,58 @@ public async Task<Result<SubmitIdProofingResponse>> Handle(
PreconditionFailedReason.Conflict, "Email is required for ID proofing.");
}

var warehouseIalForEmailReads = PreSocureHouseholdWarehouseIal.ForEmailLinkedHouseholdRead(
user.IalLevel,
idProofingEligibilitySettings.Value.RequireQualifyingHouseholdForSocure);

// Max attempts reached → off-board (3-attempt cap)
const int maxAttempts = 3;
if (user.IdProofingAttemptCount >= maxAttempts)
{
logger.LogInformation(
"User {UserId} has reached the maximum ID proofing attempts ({MaxAttempts})",
command.UserId, maxAttempts);
var householdForMaxAttempts = await IdProofingHouseholdLookup.TryGetByEmailForCohortCheckAsync(
householdRepository,
logger,
user,
warehouseIalForEmailReads,
command.UserId,
cancellationToken);
return Result<SubmitIdProofingResponse>.Success(
new SubmitIdProofingResponse("failed",
AllowIdRetry: false,
OffboardingReason: "maxAttemptsReached"));
OffboardingReason: CoLoadedCohortClassifier.ResolveOffboardingReason(
"maxAttemptsReached",
householdForMaxAttempts)));
}

// Co-loaded users still need a SNAP/TANF identifier so we can household them; off-board
// when no ID is provided. Non-co-loaded users fall through to Socure DocV — Socure's
// Co-loaded-only households still need a SNAP/TANF identifier so we can household them;
// off-board when no ID is provided. Other users fall through to Socure DocV — Socure's
// consumer_onboarding workflow short-circuits to document verification when KYC can't
// resolve the consumer, so national_id is optional for that path.
if (string.IsNullOrWhiteSpace(command.IdType))
{
if (user.IsCoLoaded)
var householdForNoId = await IdProofingHouseholdLookup.TryGetByEmailForCohortCheckAsync(
householdRepository,
logger,
user,
warehouseIalForEmailReads,
command.UserId,
cancellationToken);
var coLoadedOnlyCohort = CoLoadedCohortClassifier.Classify(householdForNoId) == CoLoadedCohort.CoLoadedOnly;

if (user.IsCoLoaded || coLoadedOnlyCohort)
{
logger.LogInformation(
"Co-loaded user {UserId} submitted ID proofing without an ID type; off-boarding (householding requires a benefit ID)",
"User {UserId} submitted ID proofing without an ID type; off-boarding (householding requires a benefit ID)",
command.UserId);
return Result<SubmitIdProofingResponse>.Success(
new SubmitIdProofingResponse("failed", OffboardingReason: "noIdProvided"));
new SubmitIdProofingResponse(
"failed",
OffboardingReason: CoLoadedCohortClassifier.ResolveOffboardingReason(
"noIdProvided",
householdForNoId)));
}

logger.LogInformation(
Expand All @@ -146,10 +172,6 @@ public async Task<Result<SubmitIdProofingResponse>> Handle(
// Persist the parsed DOB on the user; all downstream save paths will carry it through.
user.DateOfBirth = submittedDob;

var warehouseIalForEmailReads = PreSocureHouseholdWarehouseIal.ForEmailLinkedHouseholdRead(
user.IalLevel,
idProofingEligibilitySettings.Value.RequireQualifyingHouseholdForSocure);

// Co-loaded discovery: SNAP/TANF ids are an in-portal lookup (never Socure as national_id).
// User-level IsCoLoaded isn't presumed from a pre-populated flag — the match itself is the
// determination, and on success we persist it so downstream UI flows can rely on the claim.
Expand Down Expand Up @@ -228,7 +250,9 @@ public async Task<Result<SubmitIdProofingResponse>> Handle(
new SubmitIdProofingResponse(
"failed",
AllowIdRetry: allowBenefitRetry,
OffboardingReason: "idProofingFailed"));
OffboardingReason: CoLoadedCohortClassifier.ResolveOffboardingReason(
"idProofingFailed",
benefitHousehold)));
}

// Fetch household data for Socure: state/CMS may supply name, address, and phone when available.
Expand Down Expand Up @@ -353,7 +377,9 @@ public async Task<Result<SubmitIdProofingResponse>> Handle(
new SubmitIdProofingResponse(
"failed",
AllowIdRetry: allowIdRetry,
OffboardingReason: "idProofingFailed"));
OffboardingReason: CoLoadedCohortClassifier.ResolveOffboardingReason(
"idProofingFailed",
householdForSocure)));

case IdProofingOutcome.DocumentVerificationRequired:
await userRepository.UpdateUserAsync(user, cancellationToken);
Expand Down Expand Up @@ -479,4 +505,5 @@ private static bool IsExactlyNineDigitsAfterStripping(string? idValue)
}
return digitCount == 9;
}

}
Loading
Loading