fix: DEBT-028 - wire ActivateModelHandler, fix data-corrupting activation

Systematic sweep of every *Handler registered in Program.cs (same
method that found DEBT-026/027) found ActivateModelHandler was the
last orphan in Features/ApprovalWorkflow/: no POST /approvals/{id}/activate
endpoint existed, so an Approved proposal could never reach Active -
the entire point of this maker-checker slice.

While wiring it up, found the handler's original call would have
overwritten the checker's approved_by/approval_notes with the
activating SRE's identity (it passed userEmail through
UpdateProposalStatusAsync's approvedBy parameter), and never set
activated_by/activated_at at all despite those columns existing since
migration 0036. Added a dedicated ApprovalWorkflowSql.ActivateProposalAsync
that only touches activation-specific columns, and a regression test
asserting the checker's approval record survives activation unchanged.

Also documents DEBT-029 (discovered, not fixed - genuine cross-cutting
scope): LogAuditEventCommandHandler is never called by any other
slice, so VS-27's audit trail is empty in production regardless of
activity even though its own tests pass. Downgraded AEG-VS-27-01 from
COMPLETED to BLOCKED in the tracker to reflect that honestly.

dotnet build KArtSell.sln -c Release: clean. Not run against a live
database this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 00:32:02 +09:00
parent cbcc4849ec
commit 9ffb740f07
6 changed files with 75 additions and 5 deletions
@@ -90,6 +90,43 @@ public class ProposeForReviewEndpoint : EndpointWithoutRequest<ProposeForReviewR
}
}
public record ActivateApprovalResponse(Guid Id, string Status, DateTime? ActivatedAt);
/// <summary>
/// DEBT-028: before this endpoint existed, ActivateModelHandler was registered in DI and fully
/// implemented but had no HTTP entry point — an Approved proposal could never reach Active, the
/// final step of the maker-checker gate this whole slice exists for.
/// </summary>
public class ActivateApprovalEndpoint : EndpointWithoutRequest<ActivateApprovalResponse>
{
private readonly ActivateModelHandler _handler;
private readonly ApprovalWorkflowSql _sql;
public ActivateApprovalEndpoint(ActivateModelHandler handler, ApprovalWorkflowSql sql)
{
_handler = handler;
_sql = sql;
}
public override void Configure()
{
Post("/approvals/{id}/activate");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken ct)
{
var proposalId = Route<Guid>("id");
var userEmail = HttpContext.User.FindFirst("email")?.Value ?? "anonymous";
var userRole = HttpContext.User.FindFirst("role")?.Value ?? "Guest";
await _handler.Handle(proposalId, userEmail, userRole, Guid.NewGuid(), ct);
var proposal = await _sql.GetProposalAsync(proposalId, ct);
await Send.OkAsync(new ActivateApprovalResponse(proposalId, "ACTIVE", proposal!.ActivatedAt), ct);
}
}
public record ApprovalDetailResponse(Guid Id, Guid ModelId, string Status, string CreatedBy, DateTime CreatedAt,
string Justification, DateOnly EffectiveAt, string? ApprovedBy, DateTime? ApprovedAt, string? ApprovalNotes,
string? ActivatedBy, DateTime? ActivatedAt, List<EvidenceDto> Evidence);