backend phase 6: nurse verification & credentials (mocked vendors)
The trust engine. New `verif` schema (5 tables) + a data-driven verification pipeline: steps are rows (6 seeded step-types), not a code enum. - nurse_verifications.status is the single source of verification truth; nurse_profiles.is_verified is flipped ONLY inside the finalize transaction (VerificationAggregator: tracked verification + tracked profile -> one commit) and reversed on suspension/expiry — no in-between state. - is_automated snapshotted onto each step at submit; steps seeded from active required step-types; automated runs (identity-KYC, Shahkar, IBAN ownership) find their step by code. - users.national_id populated only on identity-KYC pass; Shahkar + IBAN owner compare against it (money-mule guard); shared-SIM -> shared_sim support alert. - Documents are metadata-only behind signed URLs; credential_number encrypted and never serialized; public trust badge exposes credential TYPES, not numbers; holder-name cross-checked against the verified identity before recording. - Admin-triggered credential-expiry scan reverts lapsed steps, re-gates bookability, raises a verification_expired alert + verification_expiry_prompt notification (scheduled cron deferred; config key verification_expiry_scan_cadence_hours). Three new mock vendor seams (IShahkarVerifier / IIdentityKycProvider / ICredentialVerifier) behind DI; reuses b3 IBankAccountOwnershipVerifier and b0 IObjectStorage/IFieldEncryptor. 15 endpoints across 4 controllers. Two migrations (tables + step-type seed). 154 tests pass, zero new warnings. Contract dev/contracts/domains/verification.md + swagger snapshot refreshed; handoff/report/mocks-registry updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Asp.Versioning;
|
||||
using Baya.Application.Features.Verification.Commands.DeactivateStepType;
|
||||
using Baya.Application.Features.Verification.Commands.UpsertStepType;
|
||||
using Baya.Application.Features.Verification.Queries.ListStepTypes;
|
||||
using Baya.Application.Models.Verification;
|
||||
using Baya.Infrastructure.Identity.Identity.PermissionManager;
|
||||
using Baya.WebFramework.Attributes;
|
||||
using Baya.WebFramework.BaseController;
|
||||
using Mediator;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Baya.WebFramework.ServiceConfiguration;
|
||||
|
||||
namespace Baya.Web.Api.Controllers.V1;
|
||||
|
||||
[ApiVersion("1")]
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Authorize(ConstantPolicies.DynamicPermission)]
|
||||
[EnableRateLimiting(RateLimitingServiceExtension.SensitivePolicy)]
|
||||
[Display(Description = "Admin catalog of verification step-types (the data-driven pipeline)")]
|
||||
public sealed class AdminVerificationStepTypesController(ISender sender) : BaseController
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesOkApiResponseType<IReadOnlyList<VerificationStepTypeDto>>]
|
||||
public async Task<IActionResult> List([FromQuery] bool includeInactive, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new AdminListStepTypesQuery(includeInactive), cancellationToken));
|
||||
|
||||
[HttpPost]
|
||||
[ProducesOkApiResponseType<VerificationStepTypeDto>]
|
||||
public async Task<IActionResult> Upsert(AdminUpsertStepTypeCommand command, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(command, cancellationToken));
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[ProducesOkApiResponseType]
|
||||
public async Task<IActionResult> Deactivate(long id, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new AdminDeactivateStepTypeCommand(id), cancellationToken));
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Asp.Versioning;
|
||||
using Baya.Application.Features.Verification.Commands.ReviewStep;
|
||||
using Baya.Application.Features.Verification.Commands.ScanExpiringCredentials;
|
||||
using Baya.Application.Features.Verification.Commands.SuspendVerification;
|
||||
using Baya.Application.Features.Verification.Queries.GetVerificationDetail;
|
||||
using Baya.Application.Features.Verification.Queries.ListPendingSteps;
|
||||
using Baya.Application.Models.Common;
|
||||
using Baya.Application.Models.Verification;
|
||||
using Baya.Infrastructure.Identity.Identity.PermissionManager;
|
||||
using Baya.WebFramework.Attributes;
|
||||
using Baya.WebFramework.BaseController;
|
||||
using Mediator;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Baya.WebFramework.ServiceConfiguration;
|
||||
|
||||
namespace Baya.Web.Api.Controllers.V1;
|
||||
|
||||
[ApiVersion("1")]
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Authorize(ConstantPolicies.DynamicPermission)]
|
||||
[EnableRateLimiting(RateLimitingServiceExtension.SensitivePolicy)]
|
||||
[Display(Description = "Admin verification review queue, decisions, suspension and expiry scan")]
|
||||
public sealed class AdminVerificationsController(ISender sender) : BaseController
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesOkApiResponseType<PagedResult<AdminPendingStepDto>>]
|
||||
public async Task<IActionResult> List([FromQuery] AdminListPendingStepsQuery query, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(query, cancellationToken));
|
||||
|
||||
[HttpGet("{nurseVerificationId}")]
|
||||
[ProducesOkApiResponseType<AdminVerificationDetailDto>]
|
||||
public async Task<IActionResult> Get(long nurseVerificationId, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new AdminGetVerificationDetailQuery(nurseVerificationId), cancellationToken));
|
||||
|
||||
[HttpPost("steps/{stepId}/decide")]
|
||||
[ProducesOkApiResponseType<ReviewStepResult>]
|
||||
public async Task<IActionResult> Decide(long stepId, AdminReviewStepCommand command, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(command with { StepId = stepId }, cancellationToken));
|
||||
|
||||
[HttpPost("{nurseVerificationId}/suspend")]
|
||||
[ProducesOkApiResponseType]
|
||||
public async Task<IActionResult> Suspend(long nurseVerificationId, AdminSuspendVerificationCommand command, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(command with { NurseVerificationId = nurseVerificationId }, cancellationToken));
|
||||
|
||||
[HttpPost("scan_expiring")]
|
||||
[ProducesOkApiResponseType<ScanExpiringResult>]
|
||||
public async Task<IActionResult> ScanExpiring(ScanExpiringCredentialsCommand command, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(command, cancellationToken));
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Asp.Versioning;
|
||||
using Baya.Application.Features.Verification.Commands.ConfirmDocumentUpload;
|
||||
using Baya.Application.Features.Verification.Commands.RequestDocumentUploadUrl;
|
||||
using Baya.Application.Features.Verification.Commands.RunBankAccountVerification;
|
||||
using Baya.Application.Features.Verification.Commands.RunIdentityKyc;
|
||||
using Baya.Application.Features.Verification.Commands.RunShahkarMatch;
|
||||
using Baya.Application.Features.Verification.Commands.SubmitVerification;
|
||||
using Baya.Application.Features.Verification.Queries.GetStatus;
|
||||
using Baya.Application.Models.Verification;
|
||||
using Baya.WebFramework.Attributes;
|
||||
using Baya.WebFramework.BaseController;
|
||||
using Mediator;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Baya.Web.Api.Controllers.V1;
|
||||
|
||||
[ApiVersion("1")]
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Authorize]
|
||||
[Display(Description = "The signed-in nurse's verification pipeline (checklist, uploads, automated runs)")]
|
||||
public sealed class NurseVerificationController(ISender sender) : BaseController
|
||||
{
|
||||
[HttpPost("[action]")]
|
||||
[ProducesOkApiResponseType<VerificationStatusDto>]
|
||||
public async Task<IActionResult> Submit(CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new SubmitNurseVerificationCommand(), cancellationToken));
|
||||
|
||||
[HttpGet]
|
||||
[ProducesOkApiResponseType<VerificationStatusDto>]
|
||||
public async Task<IActionResult> Get(CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new GetNurseVerificationStatusQuery(), cancellationToken));
|
||||
|
||||
[HttpPost("steps/{stepId}/[action]")]
|
||||
[ProducesOkApiResponseType<UploadUrlResult>]
|
||||
public async Task<IActionResult> UploadUrl(long stepId, RequestDocumentUploadUrlCommand command, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(command with { StepId = stepId }, cancellationToken));
|
||||
|
||||
[HttpPost("steps/{stepId}/documents")]
|
||||
[ProducesOkApiResponseType<DocumentConfirmedResult>]
|
||||
public async Task<IActionResult> ConfirmDocument(long stepId, ConfirmDocumentUploadCommand command, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(command with { StepId = stepId }, cancellationToken));
|
||||
|
||||
[HttpPost("steps/identity_kyc/run")]
|
||||
[ProducesOkApiResponseType<RunStepResult>]
|
||||
public async Task<IActionResult> RunIdentityKyc(RunIdentityKycCommand command, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(command, cancellationToken));
|
||||
|
||||
[HttpPost("steps/shahkar_match/run")]
|
||||
[ProducesOkApiResponseType<RunStepResult>]
|
||||
public async Task<IActionResult> RunShahkarMatch(CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new RunShahkarMatchCommand(), cancellationToken));
|
||||
|
||||
[HttpPost("steps/bank_account_verification/run")]
|
||||
[ProducesOkApiResponseType<RunStepResult>]
|
||||
public async Task<IActionResult> RunBankAccountVerification(CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new RunBankAccountVerificationCommand(), cancellationToken));
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Asp.Versioning;
|
||||
using Baya.Application.Features.Verification.Queries.GetTrustBadge;
|
||||
using Baya.Application.Models.Verification;
|
||||
using Baya.WebFramework.Attributes;
|
||||
using Baya.WebFramework.BaseController;
|
||||
using Mediator;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Baya.Web.Api.Controllers.V1;
|
||||
|
||||
[ApiVersion("1")]
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[AllowAnonymous]
|
||||
[Display(Description = "Public nurse read surface (the verified trust badge)")]
|
||||
public sealed class NursesController(ISender sender) : BaseController
|
||||
{
|
||||
// Public: the verified badge exposes credential *types* held, never the encrypted numbers.
|
||||
[HttpGet("{nurseId}/[action]")]
|
||||
[ProducesOkApiResponseType<TrustBadgeDto>]
|
||||
public async Task<IActionResult> TrustBadge(long nurseId, CancellationToken cancellationToken)
|
||||
=> OperationResult(await sender.Send(new GetVerifiedTrustBadgeQuery(nurseId), cancellationToken));
|
||||
}
|
||||
Reference in New Issue
Block a user