Skip to content

fix(attendance): gate webhook emission and response on actual saga outcome#3868

Open
Srejoye wants to merge 1 commit into
Premshaw23:masterfrom
Srejoye:fix/attendance-webhook-duplicate-gating
Open

fix(attendance): gate webhook emission and response on actual saga outcome#3868
Srejoye wants to merge 1 commit into
Premshaw23:masterfrom
Srejoye:fix/attendance-webhook-duplicate-gating

Conversation

@Srejoye

@Srejoye Srejoye commented Jun 26, 2026

Copy link
Copy Markdown

Problem

In app/api/attendance/record/route.js, after calling AttendanceService.recordAttendance(...), the route unconditionally:

  1. Called emitWebhookEvent("attendance.recorded", {...}) with no check on sagaResult.success or sagaResult.context?._alreadyRecorded.
  2. Returned jsonSuccess({ alreadyRecorded: false }, 201) — a hardcoded false, never derived from the saga result.

The saga in lib/services/attendanceService.js already tracks idempotency: write_attendance sets ctx._alreadyRecorded = true and skips the MongoDB write/XP award when a document already exists for that userId_date key. The publishEvent SSE call already correctly gates on sagaResult.success && !sagaResult.context?._alreadyRecorded — the webhook dispatch and HTTP response did not apply the same gate.

This meant a duplicate submission (retry, double-tap, re-marking an already-checked-in student) fired the external attendance.recorded webhook a second time for an event that didn't actually happen, and API consumers had no way to tell a genuine new record from a no-op duplicate from the response body.

Fix

  • Compute alreadyRecorded from sagaResult.context?._alreadyRecorded.
  • Only call emitWebhookEvent when sagaResult.success && !alreadyRecorded — the same condition already used for publishEvent.
  • Return { alreadyRecorded } (no longer hardcoded) with status 200 for a duplicate or 201 for a genuinely new record.

Tests

Added app/api/attendance/record/__tests__/webhook-gating.test.js:

  • New record → webhook fires once; response is { alreadyRecorded: false } / 201.
  • Duplicate (existing Firestore doc) → webhook is not called; response is { alreadyRecorded: true } / 200.

The pre-existing duplicate-check-in assertion in app/api/attendance/record/route.test.js, which expected alreadyRecorded: true / 200 and was failing against the hardcoded response, now passes as well.

Closes #3803

…tcome

After AttendanceService.recordAttendance(), the route unconditionally called emitWebhookEvent('attendance.recorded', ...) and returned a hardcoded { alreadyRecorded: false }, regardless of whether the saga's write_attendance step had short-circuited as a duplicate (ctx._alreadyRecorded = true). The publishEvent SSE call already correctly gated on sagaResult.success && !sagaResult.context?._alreadyRecorded; the webhook dispatch and HTTP response did not.

Derive alreadyRecorded from the saga result and apply the same gate to the webhook, and use it (instead of a hardcoded false) for the response body and status code (200 for a duplicate, 201 for a new record).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] : Attendance webhook fires and response claims "not already recorded" even when the saga short-circuited as a duplicate

1 participant