Skip to content

feat(jobs): add idempotency keys for payout-triggering jobs (BE-103)#1873

Open
edrizxabdulganiyu-blip wants to merge 2 commits into
EarnQuestOne:mainfrom
edrizxabdulganiyu-blip:feat/BE-103-idempotency-keys-payout-jobs
Open

feat(jobs): add idempotency keys for payout-triggering jobs (BE-103)#1873
edrizxabdulganiyu-blip wants to merge 2 commits into
EarnQuestOne:mainfrom
edrizxabdulganiyu-blip:feat/BE-103-idempotency-keys-payout-jobs

Conversation

@edrizxabdulganiyu-blip

Copy link
Copy Markdown

feat(jobs): Add idempotency keys for payout-triggering jobs (BE-103)

Summary

Implements job-level idempotency for payout-triggering BullMQ jobs to prevent duplicate Stellar
payment submissions when a job is retried or accidentally enqueued more than once.

Problem

The PayoutProcessor had no idempotency guard — if BullMQ retried a job or the scheduler
enqueued the same payoutId twice, the Stellar payment could be submitted multiple times. The
existing IdempotencyService only covered HTTP-layer POST requests via an interceptor, leaving
the job queue unprotected.

Solution

Introduced a JobIdempotencyService that reuses the existing idempotency_keys table and
IdempotencyService with job-specific semantics:

Key schema: payout-job:{payoutId}:{jobType} — deterministic, based solely on the payout record
and job type.

Lifecycle:

  1. checkAndLock — before processing, check if a completed record exists (return cached result)
    or a lock is held (skip duplicate). If neither, atomically acquire the lock.
  2. complete — after successful processing, persist the result and unlock so future duplicates
    replay the same output.
  3. release — on failure, remove the lock so the next genuine BullMQ retry can re-acquire it.

Changes

┌──────┬────────┐
│ File │ Change │
├──────────────────────────────────────────┼────────────────────────────────────────────────┤
│ jobs/services/job-idempotency.service.ts │ New service — job-specific idempotency wrapper │
├──────────────────────────────────────────┼────────────────────────────────────────────────┤
│ jobs/processors/payout.processor.ts │ Idempotency check before processing; │
│ │ deterministic transactionHash │
├──────────────────────────────────────────┼────────────────────────────────────────────────┤
│ jobs/jobs.module.ts │ Adds IdempotencyKey entity, │
│ │ IdempotencyService, JobIdempotencyService as │
│ │ providers │
├──────────────────────────────────────────┼────────────────────────────────────────────────┤
│ jobs/jobs.service.ts │ Registers PAYOUTS queue; wires PayoutProcessor │
│ jobs/jobs.service.ts │ Registers PAYOUTS queue; wires │
│ │ PayoutProcessor as the queue worker │
├───────────────────────────────────────────┼───────────────────────────────────────────────┤
│ test/jobs/job-idempotency.service.spec.ts │ New unit tests for all idempotency paths │
├───────────────────────────────────────────┼───────────────────────────────────────────────┤
│ test/jobs/job-processors.spec.ts │ Updated with JobIdempotencyService mock │
└───────────────────────────────────────────┴───────────────────────────────────────────────┘

Test Coverage

  • Fresh job: lock acquired, processing proceeds normally
  • Already completed: cached result returned, no re-execution
  • In-flight duplicate: locked=true returned, job skipped gracefully
  • Race condition: tryAcquire fails after concurrent insert — handled correctly
  • complete and release delegate correctly to IdempotencyService
  • Validation errors (missing fields, bad amount, invalid address) still throw as before

Notes

  • No new database migration needed — reuses the existing idempotency_keys table from migration
    1800000000004.
  • No circular dependency introduced — JobsModule imports IdempotencyKey and IdempotencyService
    directly rather than importing PayoutsModule.
  • TTL for job idempotency records is 7 days, covering all realistic retry windows.

close #1130

- Add JobIdempotencyService wrapping IdempotencyService with job-specific
  checkAndLock / complete / release methods
- Idempotency key schema: payout-job:{payoutId}:{jobType} (deterministic)
- Update PayoutProcessor to check idempotency before executing; returns
  cached result on duplicate and releases lock on failure
- Register PAYOUTS queue in JobsService.onModuleInit() and wire
  PayoutProcessor as the queue worker
- Add IdempotencyKey entity and IdempotencyService to JobsModule providers
  so the jobs layer owns its idempotency infrastructure without circular deps
- Add unit tests: job-idempotency.service.spec.ts (new)
- Update job-processors.spec.ts with JobIdempotencyService mock
@drips-wave

drips-wave Bot commented Jun 30, 2026

Copy link
Copy Markdown

@edrizxabdulganiyu-blip Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@RUKAYAT-CODER

Copy link
Copy Markdown
Contributor

Kindly resolve conflict

@RUKAYAT-CODER

Copy link
Copy Markdown
Contributor

Great job so far

There’s just one blocker — the workflow is failing. Could you take a look and fix it so all checks pass?
You could pull from the main to get the changes before pushing.

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.

[BE-103] Add idempotency keys for payout-triggering jobs

2 participants