Skip to content

feat: add scheduled membership state reconciliation (#53)#67

Open
Anichris-koded wants to merge 4 commits into
Adamantine-guild:mainfrom
Anichris-koded:feat/scheduled-membership-reconciliation
Open

feat: add scheduled membership state reconciliation (#53)#67
Anichris-koded wants to merge 4 commits into
Adamantine-guild:mainfrom
Anichris-koded:feat/scheduled-membership-reconciliation

Conversation

@Anichris-koded

@Anichris-koded Anichris-koded commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

closes #53

Summary

Implements scheduled membership state reconciliation as described in issue #53.

Changes

New files

  • apps/access-api/src/workers/reconciliationWorker.tsrunReconciliation() scans memberships where state IN (active, suspended) AND expiresAt < now, updates each to expired, and emits a MEMBERSHIP_UPDATED audit event per row. startReconciliationWorker(intervalMs) wraps it in a configurable setInterval and returns a stop function.

Modified files

  • apps/access-api/src/services/memberService.ts — added getNormalizedMembershipState() helper; applied across all four public methods so read-time expiry is the first line of defence (not the reconciliation worker)
  • apps/access-api/prisma/schema.prisma — added @@index([state, expiresAt]) on Membership for efficient reconciliation queries; fixed pre-existing BadgeMember missing back-relation; removed duplicate comment
  • apps/access-api/src/config.ts — added RECONCILIATION_INTERVAL_MS (default 300 000 ms)
  • apps/access-api/src/index.ts — starts worker after app boot, stops it on SIGTERM/SIGINT
  • apps/access-api/jest.config.js — added src to roots so src/workers/ tests are discovered
  • .env.example — documented RECONCILIATION_INTERVAL_MS

Tests

10 unit tests in src/workers/reconciliationWorker.test.ts covering all acceptance criteria:

  • Stale active membership → expired
  • Stale suspended membership → expired
  • Already-expired rows never selected (idempotent query)
  • Active with future expiry not touched
  • Active with null expiry not touched
  • Audit event emitted per state change
  • Second pass is a no-op (idempotent)
  • Multi-row batch processing
  • Per-row error tolerance (partial failures counted, not thrown)
  • Timer start/stop lifecycle

Notes

  • Reconciliation is a background correctness pass only; read-time getNormalizedMembershipState() remains the authoritative check for access decisions.

)

- Add reconciliationWorker with runReconciliation() and startReconciliationWorker()
- Query targets state IN [active,suspended] AND expiresAt < now; updates to expired
- Emit MEMBERSHIP_UPDATED audit event per changed row with before/after state
- Wire worker into startup/shutdown in index.ts
- Add RECONCILIATION_INTERVAL_MS config (default 5 min) + .env.example entry
- Add @@index([state, expiresAt]) on Membership for efficient reconciliation queries
- Add getNormalizedMembershipState() to memberService for read-time expiry checks
- Update jest.config.js roots to include src/ so worker tests are discovered
- Fix schema: add Badge<->Member back-relation and remove duplicate comment
- 10 tests covering all acceptance criteria
- 20260615_init: create all base tables (was missing, causing CI migrate deploy to fail)
- 20260618_membership_reconciliation_index: CREATE INDEX for Membership(state, expiresAt)
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.

Add scheduled membership state reconciliation

1 participant