src/features/reviews/handlers/shared.ts
Linked repository file from this build.
import type { ActorRef } from "@/lib/auth/actor";
import { RuleError } from "@/lib/errors";
import { runFullEditCheck } from "@/programs/snap-negative/validation";
import type { SnapReview } from "@/programs/snap-negative/types";
import type { ReviewCommand } from "../command-schema";
import { canEdit } from "../permissions";
import { roleLabels, type Activity, type Actor, type StoredFinding } from "../types";
/** What execute gives each command handler. */
export interface CommandContext<C extends ReviewCommand> {
/** The review before the command. Check rules against it. */
review: SnapReview;
/** A private copy that the handler changes in place. */
next: SnapReview;
command: C;
actor: Actor;
/** The actor as saved in records. */
by: ActorRef;
meta: { id: string; at: string };
/** The activity entry that execute appends after the handler returns. */
event: Activity;
}
/** One handler per command type. A handler returns a review only to replace `next`. */
export type CommandHandlers = {
[T in ReviewCommand["type"]]: (
context: CommandContext<Extract<ReviewCommand, { type: T }>>,
) => SnapReview | void;
};
export function roleTag(actor: Actor): string {
return `${actor.name} (${roleLabels[actor.role]})`;
}
export function assertReviewEdit(review: SnapReview, actor: Actor): void {
if (!canEdit(review, actor)) {
if (actor.role === "pec") throw new RuleError("PEC stakeholders cannot edit review facts.");
throw new RuleError("Only the examiner can edit or check an active or returned review.");
}
}
export function storeEditCheck(review: SnapReview, at: string): StoredFinding[] {
const previous =
review.validation?.revision === review.revision ? review.validation.findings : [];
const findings = runFullEditCheck(review).map((finding) => {
const existing = previous.find(
(entry) =>
entry.field === finding.field &&
entry.code === finding.code &&
entry.message === finding.message,
);
return existing?.refused ? { ...finding, refused: existing.refused } : finding;
});
review.validation = { revision: review.revision, findings, createdAt: at };
return findings;
}
export function assertStoredFinding(review: SnapReview, findingId: string): StoredFinding {
const finding = review.validation?.findings.find((entry) => entry.id === findingId);
if (!finding) throw new RuleError("Edit-check finding not found on the current review.");
if (finding.refused) throw new RuleError("This finding is already refused.");
return finding;
}
export function clearApprovals(review: SnapReview) {
delete review.supervisorApproved;
delete review.pecApproved;
delete review.memoApproved;
delete review.errorSummaryReleased;
delete review.dropApproved;
delete review.caoResponse;
delete review.caoResponseDeadline;
delete review.pecDecision;
}