feat(timeline): emit application lifecycle events
The timeline could interpret InterviewScheduled, InterviewCompleted, OfferReceived and FollowUpCompleted, but only StatusChanged and FollowUpSet were ever written, so those branches never rendered. Events are now derived from the status TRANSITION in one shared emitter rather than at each call site, so the two status-change boundaries in JobApplicationsController cannot drift apart and a third would get the behaviour for free. Both boundaries now call it instead of hand-writing the StatusChanged block. Deriving from the transition rather than the resulting state is what prevents duplicates: one user action produces at most one lifecycle event, re-saving an unchanged status produces none, and reaching an offer twice records it once. Moving an application backwards is treated as a correction, not a completed interview, so only a forward move out of an interview stage counts. An Interview to Offer move reports the offer, which is the thing the user cares about. Completing a follow-up checklist item emits FollowUpCompleted, guarded on the same transition rule so re-saving a done item stays silent. The task itself remains a checklist item — this only records that it happened. No new history store: every event is a JobEvent row, which stays the single source of application history. 393 backend tests pass, including timeline rendering of the emitted events. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -193,9 +193,18 @@ public sealed class ApplicationChecklistService : IApplicationChecklistService
|
||||
if (ChecklistCategories.IsValid(input.Category)) item.Category = input.Category!;
|
||||
}
|
||||
|
||||
var wasDone = item.Status == ChecklistStatuses.Done;
|
||||
|
||||
if (ChecklistStatuses.IsValid(input.Status)) Stamp(item, input.Status!);
|
||||
else Stamp(item);
|
||||
|
||||
// Ticking off a follow-up task is a real lifecycle moment, so the timeline records it. Guarded
|
||||
// on the transition, so re-saving an already-done item does not emit a second event.
|
||||
if (!wasDone && item.Status == ChecklistStatuses.Done && item.Category == ChecklistCategories.FollowUp)
|
||||
{
|
||||
JobLifecycleEvents.RecordFollowUpCompleted(_db, jobApplicationId, item.Title);
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return Project(item);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user