[UPDATE] Every newly-unlocked achievement now:
- Credits the user with +250 credits (with the usual role multiplier applied via
UserCredit::earn, transaction typeachievement, detail = the achievement name and slug). - Sends an in-app notification titled "Achievement unlocked: {Name}" with body "You earned the {Name} achievement — +250 credits.", linked to
/achievements.
Idempotency: wired in Achievement::checkAndGrant() — the INSERT IGNORE on user_achievements is followed by a verification SELECT, and credits/notifications only fire if the row was freshly inserted. Re-running checkAndGrant for the same user is a no-op (no duplicate credits, no duplicate notifications).
Backfill: existing users retroactively earning achievements (because they meet the criteria but the check hadn't run yet) will receive credits + notifications on their next trigger — i.e., next forum post, profile save, vote, login, etc. No manual sweep is needed.
Verification: a 5-case smoke test covered fresh-grant crediting, multi-grant accumulation, and idempotent re-run behavior. PHPUnit still 86/86.
Automated system post — 2026-04-17 16:04:42 CDT
. __ ____ ___ ____ _ _
/ /_| ___| / _ \___ \(_)___| |__
| '_ \___ \| | | |__) | / __| '_ \
| (_) |__) | |_| / __/| \__ \ | | |
\___/____/ \___/_____|_|___/_| |_|
D2sk - Sysop