Skip to content

feat(backend): user achievement badges calculator worker (#1247) - #1403

Closed
brunomeno wants to merge 1 commit into
FinesseStudioLab:mainfrom
brunomeno:feat/issue-1247-achievement-badges-worker
Closed

brunomeno wants to merge 1 commit into
FinesseStudioLab:mainfrom
brunomeno:feat/issue-1247-achievement-badges-worker

Conversation

@brunomeno

Copy link
Copy Markdown

Summary

Closes #1247

Adds a background worker that calculates aggregate milestone progress per user and grants global achievement badges.

The source data is the indexer projection tables from migration 033 (participants, credit_events, claim_events, referral_credits). The worker reads that on-chain-derived state, so it needs no new contract calls and changes no Soroban contracts, SDKs or existing APIs.

What's included

File Purpose
services/achievementBadgeService.js Frozen badge catalog (9 badges across campaigns joined, points earned, claims and referrals), plus pure validateBadgeCatalog, normalizeMetrics, evaluateBadges and badgeProgress
dal/sqliteUserBadgeRepository.js Keyset-paginated metric aggregation and a transactional save that stores the metric snapshot and grants badges
jobs/achievementBadgesJob.js Batched worker: one user's failure is logged and counted without stopping the run, and the run fails fast if the pagination cursor stops advancing
db/migrations/049_user_achievement_badges.js user_badges (PK (user, badge_id)) and user_milestone_progress
index.js, dal/index.js, .env.example Registers the achievement_badges handler on the existing job runner (retries, dead-letter and distributed lock) and schedules it hourly

Design notes

  • Idempotent and append-only. Badges are granted with INSERT OR IGNORE on (user, badge_id). They are never revoked, so a user who leaves a campaign keeps what they earned. Re-running the job only refreshes the snapshots.
  • Bounded memory. Users are paged with user > ? ORDER BY user LIMIT ? (default 500, capped at 5,000). Each branch of the user UNION filters on an indexed column.
  • Defensive checks. The catalog is validated when the job is constructed. Metric values that are missing, negative or not numbers count as 0, so a corrupt row can't earn a badge. The metric snapshot and the badge grants for a user are written in one transaction.
  • Progress is computed when read. Only raw metrics are stored, and badgeProgress() derives per-badge progress from them. Changing the catalog therefore needs no data migration.

Migration runner fix (needed for this PR)

046_session_tokens, 047_campaign_tenant_isolation and 048_campaign_reward_notifications export migration = { name, description, up } instead of the documented version / description / up. As a result, runMigrations throws Migration 046_session_tokens.js must export a numeric "version", and createDal fails before migration 049 can apply.

migrate.js now adapts that shape the same way it already adapts .sql files, taking the version from the NNN_ prefix. Files that use the documented exports behave exactly as before.

Configuration

Env var Default Meaning
BADGE_CALCULATOR_INTERVAL_MS 3600000 How often the calculator runs
BADGE_CALCULATOR_BATCH_SIZE 500 Users aggregated per batch (max 5,000)

The job is skipped when disableJobs is set, like the other scheduled jobs.

Release / rollback / operational impact

  • Release: additive only. Migration 049 creates two new tables, and the first run backfills badges for all users already indexed.
  • Rollback: revert the commit. The new tables are unused without the worker and can be left in place or dropped.
  • Operational: one hourly job, bounded by batch size. It logs [achievementBadges] Calculation completed with usersScanned, badgesGranted and failures. Failed runs follow the existing retry and dead-letter path.

Tests

  • services/achievementBadgeService.test.js: catalog validation, metric normalisation, badge evaluation and progress.
  • jobs/achievementBadgesJob.test.js: badge grants, idempotency, keyset paging including an exactly full last page, per-user failure isolation, the non-advancing cursor guard, and batch-size clamping.
  • dal/sqliteUserBadgeRepository.test.js: runs the real migrations on in-memory SQLite and covers aggregation, paging, upsert without revoking, transaction rollback, and the job end to end.

…ioLab#1247)

Add a background worker that aggregates per-user milestone metrics from
the indexer projection tables (participants, credit_events, claim_events,
referral_credits) and grants global achievement badges.

- services/achievementBadgeService: validated badge catalog plus pure
  evaluation and progress helpers
- dal/sqliteUserBadgeRepository: keyset-paginated metric aggregation and
  a transactional, idempotent save (badges are never revoked)
- jobs/achievementBadgesJob: batched run with per-user error isolation and
  a guard against a non-advancing cursor
- migration 049: user_badges and user_milestone_progress tables
- wired into the job runner hourly (BADGE_CALCULATOR_INTERVAL_MS,
  BADGE_CALCULATOR_BATCH_SIZE)

Also teach the migration runner the `export const migration = {...}`
shape used by 046_session_tokens, 047_campaign_tenant_isolation and
048_campaign_reward_notifications. Those files export no numeric
`version`, so runMigrations threw at 046 and no later migration
(including 049) could apply.

Closes FinesseStudioLab#1247

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@drips-wave

drips-wave Bot commented Sep 29, 2026

Copy link
Copy Markdown

@brunomeno 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

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.

feat(backend): User Achievement Badges calculator worker service

2 participants