ALL-1875 PR-2: Scheduled duplicate-detection BullMQ job
queued## ALL-1875: Contact merge UI + auto duplicate detection — PR 2 of 5
### Goal
Wire the detectDuplicates service (from PR-1) into a scheduled BullMQ job that runs periodically.
### Context
- Linear issue: ALL-1875
- Branch: `wilbo/all-1875-duplicate-detection-job`
- Worktree: `mono-all-1875-duplicate-detection-job`
- Depends on: PR-1 merged (DuplicateCandidate table + detectDuplicates service exist)
### What to build
New BullMQ job at `domains/identity/src/jobs/duplicateDetection/`:
1. **`queue.ts`** — `initDuplicateDetectionQueue` / `shutdownDuplicateDetectionQueue`
- Queue name: `duplicate-detection`
- Use repeatable job with cron pattern defaulting to `0 2 * * *` (2 AM daily) — configurable via env/config
2. **`worker.ts`** — `initDuplicateDetectionWorker` / `shutdownDuplicateDetectionWorker`
- Fetch all workspaceIds that have CanonicalContacts (skip empty workspaces)
- For each workspace, call `detectDuplicates(workspaceId)` from PR-1
- Emit metrics: candidates_found, candidates_dismissed_total counters
- Handle errors per-workspace (log + skip, do not fail the whole job)
3. **Register in `domains/identity/src/jobs/index.ts`**:
- Add `initDuplicateDetectionQueue()` + `initDuplicateDetectionWorker()` to `initializeJobs()`
- Add corresponding shutdown calls to `shutdownJobs()`
### Key existing files to copy patterns from
- `domains/identity/src/jobs/bulkImport/importJob/queue.ts` — BullMQ queue init pattern
- `domains/identity/src/jobs/bulkImport/importJob/worker.ts` — BullMQ worker pattern
- `domains/identity/src/jobs/index.ts` — where to register
- `domains/identity/src/jobs/stagingProfile/` — another good repeatable-job pattern
### Acceptance test
- Job appears in BullMQ dashboard when service starts
- Worker processes a workspace with known duplicate emails and writes DuplicateCandidate rows
- Failed workspace does not halt remaining workspaces
- Metrics increment (check Datadog or console logs in dev)
### Depends on
- PR-1 merged (fleet-task dd69e15f-601c-468a-a128-9a9bd795e237)
### Out of scope
- GraphQL queries for the frontend (PR-3)
- Frontend UI (PR-4)
Event Timeline
created