Source: Router/achievement/insertOrUpdate.js

// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from '../../types.js'
 */
import {query,transaction,UUID2hex,HEX2uuid,HEX2base64} from '@commtool/sql-query'
import {Templates} from '../../utils/compileTemplates.js'
import {getUID, isValidUID} from '../../utils/UUIDs.js'
import { queueAdd } from '../../tree/treeQueue/treeQueue.js';
import {addUpdateList} from '../../server.ws.js'
import {isObjectAdmin} from '../../utils/authChecks.js'
import { errorLoggerUpdate} from '../../utils/requestLogger.js'
import {authorizeUser, updateAchievements} from './utilities.js'
import {renderObject} from '../../utils/renderTemplates.js'
import {isAdmin} from '../../utils/authChecks.js'
import { recreateJobsPerson } from '../job/utilities.js';
import { parseTimestampToSeconds } from '../../utils/parseTimestamp.js'

/**
 * Updates an existing achievement in the database.
 * 
 * @async
 * @function updateAchievement
 * @param {Object} object - Achievement object with updated data
 * @param {string|Buffer} achievementUID - UUID (hex string or Buffer) of the achievement
 * @param {string|Buffer} templateUID - UUID (hex string or Buffer) of the template
 * @param {Object} session - User session data
 * @param {Object} connection - Database connection for transaction
 * @returns {Promise<void>}
 */
async function updateAchievement(object, achievementUID, templateUID, session, connection) {
    await connection.query(
        `UPDATE ObjectBase SET Title=?,SortName=?,dindex=?,Data=?, UIDuser=?
         WHERE UID=?`,
        [object.Title, object.SortIndex, object.dindex, JSON.stringify(object.Data), UUID2hex(session.user), object.UID]
    );
    
    await query(
        `INSERT IGNORE INTO Links(UID, Type, UIDTarget, UIDuser) VALUES (?,'achievement',?,?)`,
        [achievementUID, templateUID, UUID2hex(session.user)]
    );
}

/**
    import { parseTimestampToSeconds } from '../../utils/parseTimestamp.js'
 * Inserts a new achievement into the database.
 * 
 * @async
 * @function insertAchievement
 * @param {Object} object - Achievement object to insert
 * @param {string|Buffer} achievementUID - UUID (hex string or Buffer) of the achievement
 * @param {string|Buffer} templateUID - UUID (hex string or Buffer) of the template
 * @param {Object} session - User session data
 * @param {string|Buffer} memberUID - UUID (hex string or Buffer) of the member
 * @param {Object} connection - Database connection for transaction
 * @returns {Promise<void>}
 */
async function insertAchievement(object, achievementUID, templateUID, session, memberUID, connection) {
    await query(
        `INSERT INTO ObjectBase (UID,Type,UIDuser,UIDBelongsTo,Title,SortName,dindex,Data)
         VALUES (?,'achievement',?,?,?,?,?,?) ON DUPLICATE KEY UPDATE
         Title=VALUE(Title),SortName=VALUE(SortName),dindex=VALUE(dindex),Data=VALUE(Data), UIDuser=VALUE(UIDuser)`,
        [object.UID, UUID2hex(session.user), memberUID, object.Title, object.SortIndex, object.dindex, JSON.stringify(object.Data)],
        { connection: connection }
    );

    await connection.query(
        `INSERT IGNORE INTO Links(UID, Type, UIDTarget, UIDuser) VALUES (?,'achievement',?,?)`,
        [achievementUID, templateUID, UUID2hex(session.user)]
    );
}


/**
 * Handles the insertion or update of an achievement for a member.
 * 
 * This function processes achievement data from the request, checks user authorization,
 * validates the member and template, and either creates a new achievement or updates an existing one.
 * It also handles achievement renewal logic and queues necessary updates.
 * 
 * @async
 * @function insertOrUpdateAchievement
 * @param {Object} req - The request object
 * @param {Object} req.params - URL parameters
 * @param {string} req.params.member - UUID of the member
 * @param {string} req.params.template - UUID of the achievement template
 * @param {Object} req.body - Request body
 * @param {string} req.body.UID - Achievement UUID
 * @param {number} [req.body.date] - Date of the achievement (timestamp)
 * @param {string} [req.body.userAdded] - User who added the achievement
 * @param {Object} req.session - User session data
 * @param {string} req.session.user - Current user's UUID
 * @param {string} req.session.root - Root UUID
 * @param {Object} res - The response object
 * @returns {Promise<void>} - Sends JSON response with success status and result
 * @throws {Error} - Logs errors with errorLoggerUpdate
 */
export const insertOrUpdateAchievement = async (req, res) => {
    try {
        // Extract and prepare parameters
        const timestamp = parseTimestampToSeconds(req.body.date);
        const memberUID = UUID2hex(req.params.member);
        const templateUID = UUID2hex(req.params.template);
        const achievementUID = await getUID(req);

        // Validate achievement template
        const aTemplates = await query(
            `SELECT Data FROM ObjectBase WHERE UID=? AND Type='achievementT'`,
            [templateUID],
            { cast: ['json'] }
        );
        
        if (!aTemplates.length) {
            return res.status(400).json({ success: false, message: 'Missing or unknown template' });
        }

        // Extract template data
        const aTemplate = aTemplates[0];
        const achievementData = aTemplate.Data;

        // Check user authorization
        const authorized = await authorizeUser(req.session.user, achievementData.authorized);
        
        if (!isObjectAdmin(req, memberUID)) {
            return res.status(403).json({
                success: false,
                message: 'User not authorized to change information for this person'
            });
        }
        
        if (!authorized && !await isAdmin(req.session)) {
            return res.status(403).json({
                success: false,
                message: 'User not authorized to add this achievement'
            });
        }

        // Validate member exists
        const baseMembers = await query(`SELECT Data FROM ObjectBase WHERE UID=?`, [memberUID]);
        if (!baseMembers.length) {
            return res.status(400).json({ success: false, message: 'Invalid base member UID' });
        }

        const members = await query(
            `SELECT Data FROM Member WHERE UID=?`,
            [memberUID],
            { cast: ['json'] }
        );
        
        if (!members.length) {
            return res.status(400).json({ success: false, message: 'Invalid member UID' });
        }

        // Prepare object data
        const member = members[0];
        const personData = member.Data;
        const template = Templates[req.session.root].achievement;
        const userAdded = req.body.userAdded || req.session.user;
        let date = req.body.date !== null && req.body.date !== undefined && req.body.date > 0 
            ? req.body.date 
            : Date.now();

        // Render the achievement object
        const object = await renderObject(
            template,
            {
                person: personData,
                achievement: achievementData,
                UID: req.body.UID,
                date: date,
                dateAdded: Date.now()/1000,
            },
            req
        );
        
        object.Data = {
            renewal: achievementData.renewal,
            date: date,
            userAdded: userAdded
        };

        // Check for existing achievement
        let achievement = null;
        let treeQueue = true;
        
        const achievements = await query(
            `SELECT ObjectBase.UID, UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom, ObjectBase.dindex
             FROM ObjectBase
             INNER JOIN Links ON (Links.UID=ObjectBase.UID)
             WHERE ObjectBase.Type='achievement' AND ObjectBase.UIDBelongsTo=? AND Links.UIDTarget=?`,
            [memberUID, templateUID]
        );

        // Handle existing achievement
        if (achievements.length > 0) {
            achievement = achievements[0];
            
            // Use existing date if newer or if renewal isn't allowed
            if (achievement.validFrom > date || !achievementData.renewal) {
                date = achievement.validFrom;
            }
            treeQueue = false;
            // Update existing achievement
            object.UID = achievement.UID;
        }

        // Calculate expiration for renewable achievements
        object.dindex = null;
        if (achievementData.renewal) {
            const sinceDays = (Date.now() - object.Data.date) / 86400000;
            const expire = Math.floor(parseInt(achievementData.renewal) - sinceDays);
            
            if (expire < 0) {
                object.dindex = 1;  // 1 means expired
            }
            
            if (achievement && achievement.dindex !== object.dindex) {
                treeQueue = true;
            }
        }

        // Validate UID format
        if (!isValidUID(req.body.UID)) {
            return res.status(400).json({ success: false, message: 'Invalid UID format in body.UID' });
        }

        // Database transaction
        await transaction(async (connection) => {
            if (achievement) {
                await updateAchievement(
                    object,
                    achievementUID,
                    templateUID,
                    req.session,
                    connection,
                );
            } else {
                await insertAchievement(
                    object,
                    achievementUID,
                    templateUID,
                    req.session,
                    memberUID,
                    connection
                );
            }

            // Queue updates if needed (only when achievement is added or expiry update)
            if (treeQueue) {
                // Recreate jobs for the person when an achievement is added or updated
                // This ensures job assignments are updated based on new qualification achievements
                recreateJobsPerson(req,memberUID);
                
                // Queue tree updates for list membership
                queueAdd(
                    UUID2hex(req.session.root),
                    UUID2hex(req.session.user),
                    'listMember',
                    memberUID,
                    memberUID,
                    null,
                    memberUID,
                    timestamp
                );
            }
        }, {
            backDate: achievement ? Math.max(timestamp, achievement.validFrom) : timestamp
        });

        // Send success response
        res.json({
            success: true,
            result: { ...object, UID: HEX2uuid(object.UID) }
        });
        
        // Update achievements and lists
        updateAchievements(memberUID);
        addUpdateList(memberUID);
        
    } catch (e) {
        res.status(500).json({ success: false, message: 'Internal server error' });
        errorLoggerUpdate(e);
    }
};