- Shipped
- September 7, 2026 at 5:05 AM UTC
- Author
- Kamo
- Commit
- 9dac861
Adds the Gamification app and the domain behind it: a catalogue of achievements in code, a member's standing in data, and a log of every unlock so that nothing a member earns can be lost between earning it and being told. THE SPLIT THAT MATTERS is member_achievements (state) against member_achievement_unlocks (history). A single table with an `acknowledged` flag cannot answer "they went from I to III while their laptop was shut, so they are owed two popups" — the flag has one value and the member sees one card for two things they did. One row per member per achievement always says where they are now; one row per event, acknowledged on its own, is the queue. The catalogue is CODE and never a table. An achievement that could be edited in a database is one that can be taken away or quietly redefined under the people who earned it, and "you have held this since March" has to stay true. Two rights, and the split is deliberate rather than symmetric. VIEW_ACHIEVEMENTS opens a member's own record; VIEW_ACHIEVEMENT_LEADERBOARD opens the comparison against colleagues, which publishes how much each of them has done, to all of them. That is a disclosure an organization decides to make. Neither is seeded: there is no lineage to carry, and no "everybody" to seed from, so they arrive off and an administrator switches them on. GAMIFICATION is id 33, COMPLETED and enabled, so FeatureController provisions it for every organization exactly as it does CRM and Reports. That changes what an administrator CAN grant, not what anybody can currently see. PhiModule.GAMIFICATION is not optional bookkeeping: PhiServiceTypeMappingTest asserts every ServiceType maps to a module, and an unmapped one fails closed.