backend phase 7: search & matching (nurse_search_index)
Add the discovery layer: the denormalized nurse_search_index read model (one row per bookable variant x covered service area), maintained inline inside each source write's transaction, plus the single public search query behind the INurseSearch seam. - Entity + EF config + migration (search schema): covering search index, filtered-unique (variant_id, city_id, district_id) pair with NULL district participating, nurse_id index, soft-delete. - ISearchIndexMaintainer (write seam) + SearchIndexMaintainer: reindex variant / nurse / fan-out / remove-area / full rebuild, staged in the owning source write's unit of work; wired into the b3/b4/b5/b6 handlers. - INurseSearch (read seam) + SqlNurseSearch (real MVP backend): reads only is_searchable=1, category/city/district(NULL-aware)/gender/price filters, rating sort, pagination. Elasticsearch deferred (config Search:Backend). - SearchNursesQuery (+ validator) and RebuildSearchIndexCommand; public SearchController (GET search/nurses) + admin AdminSearchController (POST admin_search/rebuild_index). - Tests: 9 DB-backed maintainer/search + 4 WebApplicationFactory; updated affected b3/b4/b5/b6 handler tests. Build clean, 167 tests green. - Docs: server CLAUDE.md project map, contract search.md, swagger refresh, handoff, report, mocks-registry rows, STATUS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+6
-4
@@ -1,6 +1,7 @@
|
||||
using Baya.Application.Contracts.Audit;
|
||||
using Baya.Application.Contracts.Common;
|
||||
using Baya.Application.Contracts.Persistence;
|
||||
using Baya.Application.Contracts.Search;
|
||||
using Baya.Application.Contracts.SupportAlerts;
|
||||
using Baya.Application.Features.Verification.Commands.ReviewStep;
|
||||
using Baya.Application.Features.Verification.Commands.ScanExpiringCredentials;
|
||||
@@ -25,6 +26,7 @@ public class AdminVerificationHandlersTests
|
||||
private readonly ICacheService _cache = Substitute.For<ICacheService>();
|
||||
private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
|
||||
private readonly IDateTimeProvider _clock = Substitute.For<IDateTimeProvider>();
|
||||
private readonly ISearchIndexMaintainer _searchIndex = Substitute.For<ISearchIndexMaintainer>();
|
||||
|
||||
public AdminVerificationHandlersTests()
|
||||
{
|
||||
@@ -44,7 +46,7 @@ public class AdminVerificationHandlersTests
|
||||
}
|
||||
|
||||
private AdminReviewStepCommandHandler ReviewHandler(ICredentialVerifier credentialVerifier)
|
||||
=> new(_currentUser, _unitOfWork, credentialVerifier, _audit, _cache, _clock);
|
||||
=> new(_currentUser, _unitOfWork, credentialVerifier, _audit, _cache, _clock, _searchIndex);
|
||||
|
||||
[Fact]
|
||||
public async Task Review_ApproveCredentialStep_RecordsCredentialAndFlipsVerified()
|
||||
@@ -121,7 +123,7 @@ public class AdminVerificationHandlersTests
|
||||
var profile = new NurseProfile();
|
||||
profile.MarkVerified();
|
||||
_nurses.GetTrackedByIdAsync(42, Arg.Any<CancellationToken>()).Returns(profile);
|
||||
var handler = new AdminSuspendVerificationCommandHandler(_currentUser, _unitOfWork, _audit, _cache, _clock);
|
||||
var handler = new AdminSuspendVerificationCommandHandler(_currentUser, _unitOfWork, _audit, _cache, _clock, _searchIndex);
|
||||
|
||||
var result = await handler.Handle(new AdminSuspendVerificationCommand(10, "Fraud reported"), CancellationToken.None);
|
||||
|
||||
@@ -151,7 +153,7 @@ public class AdminVerificationHandlersTests
|
||||
|
||||
var alerts = Substitute.For<ISupportAlertService>();
|
||||
var notifications = Substitute.For<INotificationDispatcher>();
|
||||
var handler = new ScanExpiringCredentialsCommandHandler(_unitOfWork, alerts, notifications, _cache, _clock);
|
||||
var handler = new ScanExpiringCredentialsCommandHandler(_unitOfWork, alerts, notifications, _cache, _clock, _searchIndex);
|
||||
|
||||
var result = await handler.Handle(new ScanExpiringCredentialsCommand(), CancellationToken.None);
|
||||
|
||||
@@ -193,7 +195,7 @@ public class AdminVerificationHandlersTests
|
||||
_nurses.GetTrackedByIdAsync(99, Arg.Any<CancellationToken>()).Returns(profileB);
|
||||
|
||||
var handler = new ScanExpiringCredentialsCommandHandler(
|
||||
_unitOfWork, Substitute.For<ISupportAlertService>(), Substitute.For<INotificationDispatcher>(), _cache, _clock);
|
||||
_unitOfWork, Substitute.For<ISupportAlertService>(), Substitute.For<INotificationDispatcher>(), _cache, _clock, _searchIndex);
|
||||
|
||||
var result = await handler.Handle(new ScanExpiringCredentialsCommand(), CancellationToken.None);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Baya.Application.Contracts.Common;
|
||||
using Baya.Application.Contracts.Persistence;
|
||||
using Baya.Application.Contracts.Search;
|
||||
using Baya.Application.Contracts.SupportAlerts;
|
||||
using Baya.Application.Features.Verification.Commands.RunBankAccountVerification;
|
||||
using Baya.Application.Features.Verification.Commands.RunIdentityKyc;
|
||||
@@ -24,6 +25,7 @@ public class RunStepHandlersTests
|
||||
private readonly INurseProfileRepository _nurses = Substitute.For<INurseProfileRepository>();
|
||||
private readonly INurseBankAccountRepository _accounts = Substitute.For<INurseBankAccountRepository>();
|
||||
private readonly IDateTimeProvider _clock = Substitute.For<IDateTimeProvider>();
|
||||
private readonly ISearchIndexMaintainer _searchIndex = Substitute.For<ISearchIndexMaintainer>();
|
||||
|
||||
public RunStepHandlersTests()
|
||||
{
|
||||
@@ -54,7 +56,7 @@ public class RunStepHandlersTests
|
||||
var identityKyc = Substitute.For<IIdentityKycProvider>();
|
||||
identityKyc.VerifyAsync("0012345678", null, Arg.Any<CancellationToken>())
|
||||
.Returns(new IdentityKycResult(true, "Verified Nurse", "ref", "{}", null));
|
||||
var handler = new RunIdentityKycCommandHandler(_currentUser, _unitOfWork, identityKyc, _clock);
|
||||
var handler = new RunIdentityKycCommandHandler(_currentUser, _unitOfWork, identityKyc, _clock, _searchIndex);
|
||||
|
||||
var result = await handler.Handle(new RunIdentityKycCommand("0012345678", null), CancellationToken.None);
|
||||
|
||||
@@ -75,7 +77,7 @@ public class RunStepHandlersTests
|
||||
var identityKyc = Substitute.For<IIdentityKycProvider>();
|
||||
identityKyc.VerifyAsync("0000000000", null, Arg.Any<CancellationToken>())
|
||||
.Returns(new IdentityKycResult(false, null, "ref", "{}", "could not verify"));
|
||||
var handler = new RunIdentityKycCommandHandler(_currentUser, _unitOfWork, identityKyc, _clock);
|
||||
var handler = new RunIdentityKycCommandHandler(_currentUser, _unitOfWork, identityKyc, _clock, _searchIndex);
|
||||
|
||||
var result = await handler.Handle(new RunIdentityKycCommand("0000000000", null), CancellationToken.None);
|
||||
|
||||
@@ -96,7 +98,7 @@ public class RunStepHandlersTests
|
||||
shahkar.MatchAsync("09120000000", "0012345678", Arg.Any<CancellationToken>())
|
||||
.Returns(new ShahkarMatchResult(false, true, "ref", "{}", "shared sim"));
|
||||
var alerts = Substitute.For<ISupportAlertService>();
|
||||
var handler = new RunShahkarMatchCommandHandler(_currentUser, _unitOfWork, shahkar, alerts, _clock);
|
||||
var handler = new RunShahkarMatchCommandHandler(_currentUser, _unitOfWork, shahkar, alerts, _clock, _searchIndex);
|
||||
|
||||
var result = await handler.Handle(new RunShahkarMatchCommand(), CancellationToken.None);
|
||||
|
||||
@@ -115,7 +117,7 @@ public class RunStepHandlersTests
|
||||
_verif.GetTrackedUserAsync(7, Arg.Any<CancellationToken>()).Returns(new User { PhoneNumber = "09121112233" });
|
||||
var shahkar = Substitute.For<IShahkarVerifier>();
|
||||
var alerts = Substitute.For<ISupportAlertService>();
|
||||
var handler = new RunShahkarMatchCommandHandler(_currentUser, _unitOfWork, shahkar, alerts, _clock);
|
||||
var handler = new RunShahkarMatchCommandHandler(_currentUser, _unitOfWork, shahkar, alerts, _clock, _searchIndex);
|
||||
|
||||
var result = await handler.Handle(new RunShahkarMatchCommand(), CancellationToken.None);
|
||||
|
||||
@@ -135,7 +137,7 @@ public class RunStepHandlersTests
|
||||
var verifier = Substitute.For<IBankAccountOwnershipVerifier>();
|
||||
verifier.VerifyOwnershipAsync(account.Iban, "0012345678", Arg.Any<CancellationToken>())
|
||||
.Returns(new OwnershipInquiryResult(false, "Someone Else", "ref"));
|
||||
var handler = new RunBankAccountVerificationCommandHandler(_currentUser, _unitOfWork, verifier, _clock);
|
||||
var handler = new RunBankAccountVerificationCommandHandler(_currentUser, _unitOfWork, verifier, _clock, _searchIndex);
|
||||
|
||||
var result = await handler.Handle(new RunBankAccountVerificationCommand(), CancellationToken.None);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user