fix(api): close three retry/duplicate hazards - #82
Conversation
Weekly digest has no duplicate-send guard despite being triggered by an external scheduler over HTTP, so a retry currently double-sends to every subscribed user. Add a per-user lastDigestSentAt timestamp with a 6-day resend window, mirroring the existing reminderSentAt pattern. UpdateApplicationUseCase logged a field_updated activity entry whenever a field was present in the input, not when it actually changed, unlike the correctly-guarded status_changed branch next to it — a retried no-op update polluted the activity timeline. Now compares against the current value before logging. BulkDeleteApplicationsUseCase used Promise.all, so retrying a partially failed bulk-delete failed again on the already-deleted items. Switch to Promise.allSettled and treat NOT_FOUND as an idempotent no-op; other per-item failures still surface as before. No API contract change.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (14)
WalkthroughThe PR adds persisted weekly digest send timestamps and resend-window checks, makes bulk deletion idempotent for missing applications, and restricts field-update activity logs to actual value changes. ChangesDigest resend tracking
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant DigestJob
participant UserRepository
participant Mailer
DigestJob->>UserRepository: load users and lastDigestSentAt
DigestJob->>Mailer: send digest after resend window
DigestJob->>UserRepository: persist send timestamp
Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary
Follow-up to an API idempotency audit. Conclusion: a generic idempotency-key system is not necessary for this app (small trusted client set, no adversarial/high-volume third-party callers, MCP surface is 100% read-only). But three concrete, currently-unprotected retry/duplicate hazards were found and are fixed here — each cheap, and mirroring a pattern already proven elsewhere in the codebase.
/admin/digest/sendis a plain secret-gated HTTP route invoked by an external scheduler not tracked in this repo — exactly the kind of caller that may retry on a timeout/5xx. There was zero duplicate-send protection, so a retry meant every subscribed user got a duplicate digest email. Added a per-userlastDigestSentAttimestamp + a 6-day resend window, mirroring the existingreminderSentAtpattern used by follow-up reminders.UpdateApplicationUseCaseduplicate activity-log entries. Thefield_updatedbranch logged whenever a field was present in the input, not when it actually changed — unlike thestatus_changedbranch right next to it, which already compared against the current value. A retried/duplicate update with identical values polluted the activity timeline every time. Now compares against the pre-update snapshot (with properDateinstant comparison forfollowUpAt).BulkDeleteApplicationsUseCaseusedPromise.all, so retrying a partially-failed bulk-delete failed again on the already-deleted items. Switched toPromise.allSettled, treatingNOT_FOUNDas an idempotent no-op while other per-item failures (e.g.FORBIDDEN) still surface as before. No API contract change — still returnsvoid/ throws on real failures.Deliberately out of scope (investigated, not fixed): MCP tools (confirmed 100% read-only), create-mutation dedup (a UI double-submit concern, not a server-side gap), and
BulkUpdateApplicationsUseCase/BulkAddTagToApplicationsUseCase(no clean "already applied" no-op concept the way delete has).Test plan
pnpm --filter @job-finder/api test— 642 tests passing, including new coverage for all three fixespnpm --filter @job-finder/api typecheck— cleanpnpm --filter @job-finder/api lint— cleanadd_last_digest_sent_at) applied and Prisma client regeneratedDocumenttable's initial creation was never captured in a migration file, so rebuilding a fresh dev DB from migration history fails; not something to fix as part of this PR)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes