Files
Jyotisha/frontend/src/lib/birth-time-evidence-service.ts
T
2026-07-19 06:42:17 +08:00

148 lines
4.7 KiB
TypeScript

import {
withCandidateResult,
withConfirmedCandidate,
} from "./birth-time-evidence.ts";
import type { CandidateResult, LifeEvent } from "./birth-time-evidence.ts";
import type {
BirthTimeJourneyPorts,
JourneyResponse,
LegacyStoredRectificationCase,
StoredRectificationCase,
} from "./birth-time-journey-service.ts";
export class BirthTimeEvidenceContextError extends Error {
readonly name = "BirthTimeEvidenceContextError";
}
export class StaleCandidateConfirmationError extends Error {
readonly name = "StaleCandidateConfirmationError";
readonly caseId: string;
constructor(caseId: string) {
super(`Candidate result for ${caseId} is stale`);
this.caseId = caseId;
}
}
export class EvidenceRectificationCaseNotFoundError extends Error {
readonly name = "EvidenceRectificationCaseNotFoundError";
readonly caseId: string;
constructor(caseId: string) {
super(`Rectification case ${caseId} was not found`);
this.caseId = caseId;
}
}
export class GuidedJourneyLegacyMutationError extends Error {
readonly name = "GuidedJourneyLegacyMutationError";
readonly caseId: string;
constructor(caseId: string) {
super(`Guided journey ${caseId} cannot use a legacy mutation`);
this.caseId = caseId;
}
}
export function assertNotDynamicJourneyMutation(
stored: StoredRectificationCase,
): asserts stored is LegacyStoredRectificationCase {
if (stored.journeyProtocol === "dynamic-choice-v2") {
throw new GuidedJourneyLegacyMutationError(stored.id);
}
}
export function assertLegacyJourneyMutation(
stored: StoredRectificationCase,
): asserts stored is LegacyStoredRectificationCase {
assertNotDynamicJourneyMutation(stored);
if (stored.turnState) {
throw new GuidedJourneyLegacyMutationError(stored.id);
}
}
function response(
stored: StoredRectificationCase,
lifeEvents: readonly LifeEvent[],
candidateResult: CandidateResult | null,
): JourneyResponse {
return {
caseId: stored.id,
snapshot: stored.snapshot,
questionnaire: stored.questionnaire,
scoring: stored.scoring ?? null,
answers: stored.answers,
lifeEvents,
candidateResult,
};
}
async function ownedCase(
ports: BirthTimeJourneyPorts,
userId: string,
caseId: string,
) {
const stored = await ports.store.loadCase(userId, caseId);
if (!stored) throw new EvidenceRectificationCaseNotFoundError(caseId);
assertLegacyJourneyMutation(stored);
return stored;
}
export function createBirthTimeEvidenceActions(ports: BirthTimeJourneyPorts) {
return {
async submitLifeEvents(
userId: string,
caseId: string,
events: readonly LifeEvent[],
): Promise<JourneyResponse> {
const stored = await ownedCase(ports, userId, caseId);
const context = stored.eventContext;
const { startTime, endTime } = stored.snapshot.reportedRange;
const acceptsEvidence = stored.snapshot.input === "life_events"
|| stored.snapshot.input === "candidate_actions"
|| stored.snapshot.input === "candidate_confirmation";
if (!context || !startTime || !endTime || !acceptsEvidence) {
throw new BirthTimeEvidenceContextError();
}
const candidateResult = await ports.engine.scoreEvents({
...context,
startTime,
endTime,
events,
});
const snapshot = withCandidateResult(stored.snapshot, candidateResult);
const updated = { ...stored, snapshot, lifeEvents: events, candidateResult };
await ports.store.saveCandidateResult(updated);
return response(updated, events, candidateResult);
},
async saveCandidate(
userId: string,
caseId: string,
resultId: string,
): Promise<JourneyResponse> {
const stored = await ownedCase(ports, userId, caseId);
const candidateResult = stored.candidateResult ?? null;
if (!candidateResult || candidateResult.resultId !== resultId) {
throw new StaleCandidateConfirmationError(caseId);
}
await ports.store.saveCandidate(stored);
return response(stored, stored.lifeEvents ?? [], candidateResult);
},
async confirmCandidate(
userId: string,
caseId: string,
resultId: string,
time: string,
): Promise<JourneyResponse> {
const stored = await ownedCase(ports, userId, caseId);
const candidateResult = stored.candidateResult ?? null;
if (!candidateResult || candidateResult.resultId !== resultId) {
throw new StaleCandidateConfirmationError(caseId);
}
const snapshot = withConfirmedCandidate(stored.snapshot, candidateResult, time);
const updated = { ...stored, snapshot };
await ports.store.confirmCandidate(updated);
return response(updated, stored.lifeEvents ?? [], candidateResult);
},
};
}