ready to test

Testing

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-22 10:06:58 +08:00
parent bf48c95467
commit 439bb33f77
189 changed files with 17559 additions and 686 deletions
+159
View File
@@ -0,0 +1,159 @@
/***********************************************************************************************************************************************************************
* File Name: achievements.service.js
* Type of Program: Service
* Description: Registry-based achievement auto-grant system.
* To add a new achievement — add an entry to utils/achievements.data.js.
* To trigger one — call grantAchievement(user_id, key, metadata).
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 11, 2026
***********************************************************************************************************************************************************************/
'use strict';
const { Op } = require('sequelize');
const mdl_Achievements = require('../models/users/achievements.mdl');
const mdl_Users = require('../models/users/users.mdl');
const { EARLY_ACCESS_CUTOFF, ACHIEVEMENT_REGISTRY } = require('../data/achievements.data');
const UserNotification = require('../models/notifications/user_notification.mdl');
const { NOTIFICATION_REGISTRY } = require('../data/notifications.data');
// ─── Core grant function ──────────────────────────────────────────────────────
/**
* Grant an achievement to a user if not already granted.
* Safe to call multiple times — idempotent via findOrCreate.
*
* @param {string|number} user_id
* @param {string} key — must exist in ACHIEVEMENT_REGISTRY
* @param {Object} metadata — optional extra data (course_id, score, etc.)
* @param {string|number} granted_by — null = system, user_id = admin manual grant
* @returns {{ achievement, created }} or null on error
*/
async function grantAchievement(user_id, key, metadata = {}, granted_by = null) {
const def = ACHIEVEMENT_REGISTRY[key];
if (!def) {
console.warn(`[ACHIEVEMENTS] Unknown achievement key: "${key}"`);
return null;
}
try {
// CockroachDB doesn't support findOrCreate — use findOne + create
const existing = await mdl_Achievements.findOne({ where: { user_id, key } });
if (existing) return { achievement: existing, created: false };
const achievement = await mdl_Achievements.create({
user_id,
type: def.type,
key: def.key,
label: def.label,
description: def.description,
granted_by: granted_by ?? null,
granted_at: new Date(),
metadata,
});
console.log(`[ACHIEVEMENTS] Granted "${key}" to user ${user_id}`);
// Fire-and-forget — notification failure never blocks the achievement grant
UserNotification.create({
user_id,
...NOTIFICATION_REGISTRY.achievement.build({
label: def.label,
description: def.description,
key: def.key,
}),
}).catch(err => console.error(`[ACHIEVEMENTS] Failed to emit notification for "${key}":`, err));
return { achievement, created: true };
} catch (err) {
// Ignore unique constraint violation (race condition) — already granted
if (err?.parent?.code === '23505') return null;
console.error(`[ACHIEVEMENTS] Failed to grant "${key}" to user ${user_id}:`, err);
return null;
}
}
// ─── Convenience triggers ─────────────────────────────────────────────────────
// Call these from controllers after the relevant event occurs.
/** Call after verifyOTP / register succeeds */
async function onUserRegistered(user_id) {
const user = await mdl_Users.findByPk(user_id, { attributes: ['user_id', 'createdAt'] });
if (!user) return;
if (new Date(user.createdAt) <= EARLY_ACCESS_CUTOFF) {
await grantAchievement(user_id, 'early_access');
}
}
/** Call after captureOrder succeeds and tier is activated */
async function onTierActivated(user_id, tier) {
if (tier === 'premium') await grantAchievement(user_id, 'premium_first_time');
if (tier === 'exclusive') await grantAchievement(user_id, 'exclusive_first_time');
}
/** Call after a course is marked complete for a user */
async function onCourseCompleted(user_id, course_id, totalCompleted, course_title = null) {
await grantAchievement(user_id, 'first_course_completed', { course_id, course_title });
if (totalCompleted >= 5) await grantAchievement(user_id, 'courses_completed_5');
if (totalCompleted >= 10) await grantAchievement(user_id, 'courses_completed_10');
}
/** Call after a perfect quiz score */
async function onPerfectQuiz(user_id, quiz_id) {
await grantAchievement(user_id, 'perfect_quiz_score', { quiz_id });
}
/** Call after profile is fully filled out */
async function onProfileCompleted(user_id) {
await grantAchievement(user_id, 'profile_completed');
}
/** Call after a successful referral */
async function onReferral(user_id, referred_user_id) {
await grantAchievement(user_id, 'first_referral', { referred_user_id });
}
// ─── Admin manual grant ───────────────────────────────────────────────────────
/** Manually grant any achievement from admin panel */
async function adminGrantAchievement(user_id, key, admin_id, metadata = {}) {
return grantAchievement(user_id, key, metadata, admin_id);
}
// ─── Backfill ─────────────────────────────────────────────────────────────────
/** Run once to grant early_access to all existing eligible users */
async function backfillEarlyAccess() {
const users = await mdl_Users.findAll({
where: { createdAt: { [Op.lte]: EARLY_ACCESS_CUTOFF } },
attributes: ['user_id'],
paranoid: false,
});
console.log(`[ACHIEVEMENTS] Backfilling early_access for ${users.length} users...`);
for (const user of users) {
await grantAchievement(user.user_id, 'early_access');
}
console.log('[ACHIEVEMENTS] Backfill complete.');
}
// ─── Exports ──────────────────────────────────────────────────────────────────
module.exports = {
ACHIEVEMENT_REGISTRY,
grantAchievement,
// Convenience triggers
onUserRegistered,
onTierActivated,
onCourseCompleted,
onPerfectQuiz,
onProfileCompleted,
onReferral,
// Admin
adminGrantAchievement,
// Backfill
backfillEarlyAccess,
};