Source: Router/person/externController.js

/**
 * External Member Controller
 *
 * Contains all the business logic for external member operations:
 * - Creating or updating external members in groups
 * - Updating group memberships for existing external members
 * - Managing membership migrations between groups
 * - Deleting external members and their associated data
 * - Publishing exit events when members leave organizations
 */
// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from './../../types.js'
 */
import './../../types.js';
import { query, transaction, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { parseTimestampToSeconds } from '../../utils/parseTimestamp.js'
import { renderObject } from '../../utils/renderTemplates.js';
import { Templates, phonetikArray, getConfig } from '../../utils/compileTemplates.js';
import { getUID, isValidUID } from '../../utils/UUIDs.js';
import { queueAdd } from '../../tree/treeQueue/treeQueue.js';
import { addUpdateList, addUpdateEntry } from '../../server.ws.js';
import { isObjectAdmin } from '../../utils/authChecks.js';
import { publishEvent } from '../../utils/events.js';
import { addToTree, migratePerson } from './migratePerson.js';
import _ from 'lodash';
import { encryptIbans } from '../../utils/crypto.js';
import { requestUpdateLogger, readLogger, errorLoggerRead, errorLoggerUpdate } from '../../utils/requestLogger.js';
import { updatePerson, getPerson, personAdmin, personHistorie, personDuplicates } from '../person.js';
import { 
    updatePersonData, 
    handleGroupMembershipMigration, 
    publishGroupEvent,
    updateGroupMembershipShared,
    updatePersonEmbedding
} from './personHelpers.js';

/**
 * Create or update external member (PUT /:group)
 * 
 * Handles the creation or update of an "extern" member in a group.
 * Validates input, fetches group and member data, renders and encrypts member data,
 * and performs the necessary database operations (insert or update) depending on
 * whether the extern already exists.
 */
/**
 * Create or update external member (PUT /:group)
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const createOrUpdateExtern = async (req, res) => {
    try {
        const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);
        const asOf = timestamp ? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : '';

        // Validate group parameter
        if (!req.params.group) {
            res.json({ success: false, message: 'group null or undefined' });
            return;
        }

        const UIDgroup = UUID2hex(req.params.group);
        const UIDextern = await getUID(req);
        const template = Templates[req.session.root].extern;

        // Fetch group data
        const groups = await query(`SELECT ObjectBase.UID,ObjectBase.Title,Member.Display,
            ObjectBase.dindex,Member.Data,  ObjectBase.hierarchie, ObjectBase.gender,
            ObjectBase.stage, ObjectBase.validFrom, ObjectBase.UIDBelongsTo  
            FROM ObjectBase 
            INNER JOIN Member ON (Member.UID=ObjectBase.UID)
            WHERE ObjectBase.UID=? `, [UIDgroup], { cast: ['json', 'UUID'] });

        if (groups.length === 0) {
            res.json({ success: false, message: 'invalid group UID' });
            return;
        }

        const group = groups[0];
        const groupData = group.Data || {};

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

        // Render object data
        const object = await renderObject(template, { ...req.body, group: groupData }, req);
        object.Data = { ...req.body, UID: undefined };
        encryptIbans(await getConfig('db', req), object.Data);

        // Check if extern already exists
        const externs = await query(`SELECT Member.UID,Member.Data,Links.UIDTarget AS MemberA, ObjectBase.Data AS BaseData,
            ObjectBase.dindex, ObjectBase.Type, ObjectBase.SortName AS SortBase
            FROM ObjectBase ${asOf}
            INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
            INNER JOIN Links ${asOf} ON(Links.Type='memberA' AND Links.UID=ObjectBase.UID) 
            INNER JOIN ObjectBase ${asOf} AS MainGroup ON (Links.UIDTarget=MainGroup.UID AND MainGroup.Type='group')
            WHERE ObjectBase.UID=? AND ObjectBase.Type IN ('person','extern')`,
            [UIDextern], { cast: ['json'] }
        );

        if (externs.length === 0) {
            // Create new extern
            await createNewExtern(object, UIDgroup, req, timestamp);
        } else {
            // Update existing extern
            await updateExistingExtern(externs[0], object, UIDgroup, req, timestamp);
        }

        // Update WebSocket clients
        addUpdateEntry(UIDextern, {
            person: { Type: 'extern', ...object, Data: { ...req.body, UID: undefined }, UID: HEX2uuid(object.UID) },
            parent: group
        });
        addUpdateList([UIDgroup, UIDextern]);

        res.json({ success: true, result: { ...object, UID: HEX2uuid(object.UID) } });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Helper function to create a new extern member
 */
const createNewExtern = async (object, UIDgroup, req, timestamp) => {
    await transaction(async (connection) => {
        await connection.query(`
            INSERT INTO ObjectBase(UID,UIDuser,Type,SortName,UIDBelongsTo,Title,hierarchie,stage,gender,Data)
            VALUES (?,?,'extern',?,?,?,?,?,?,'{}')
            `,
            [object.UID, UUID2hex(req.session.user), object.SortBase, object.UID, object.Title, 0, 0, object.gender],
        );

        await query(`
            INSERT INTO Member(UID,Display,SortName, FullTextIndex,PhonetikIndex,Data)
            VALUES (?,?,?,?,?,?)
            `,
            [
                object.UID,
                object.Display,
                object.SortIndex,
                object.FullTextIndex,
                phonetikArray([object.Data.firstName, object.Data.lastName]),
                JSON.stringify(object.Data)
            ]);
        
        await connection.query(`INSERT IGNORE INTO Links(UID, Type, UIDTarget)
            VALUES (?,'memberA',?)`,
            [object.UID, UIDgroup]);
    }, { backDate: timestamp });

    await addToTree(object.UID, UIDgroup, timestamp);
    queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'extern', object.UID, object.UID, null, UIDgroup, timestamp);
    await query(`INSERT INTO Visible (UID,Type,UIDUser) VALUES (?,'changeable',?)
            ON DUPLICATE KEY UPDATE Type='changeable'`, [object.UID, UUID2hex(req.session.user)]);
    await publishGroupEvent('new', object.UID,'extern',UIDgroup, req.session.root, timestamp);
    
    // Asynchronously create AI embeddings (non-blocking)
    updatePersonEmbedding(object.UID, object.Data, 'extern', UUID2hex(req.session.root)).catch(err => {
        console.error('Background embedding creation failed:', err);
    });
};

/**
 * Helper function to update an existing extern member
 */
const updateExistingExtern = async (extern, object, UIDgroup, req, timestamp) => {
    // Set extern-specific properties
    object.Type = 'extern';
    object.hierarchie = 0;
    object.stage = 0;
    
    // Use the shared updatePersonData helper function
    await updatePersonData(object.UID, object.Data, object, req, timestamp);

    // Extern-specific: Handle group migration if necessary
    if (!extern.MemberA || !extern.MemberA.equals(UIDgroup) || extern.Type === 'person') {
        await handleGroupMembershipMigration(extern, object.UID, UIDgroup, req, timestamp, 'extern');
    }
};



/**
 * Update group membership (POST /:group/:UID)
 * 
 * Updates the group membership of an external member, mutating their membership if required.
 * Handles the mutation of group membership for a person or extern, including updating
 * their group, stage, hierarchy, and related data.
 */
/**
 * Update group membership (POST /:group/:UID)
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const updateGroupMembership = async (req, res) => {
    return updateGroupMembershipShared(req, res, 'extern', isObjectAdmin);
};

/**
 * Delete external member (DELETE /:UID)
 * 
 * Deletes a user and associated links from the database based on the provided UID.
 * This removes all external member data, guest entries, jobs, and other associated records.
 */
/**
 * Delete external member (DELETE /:UID)
 *
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const deleteExtern = async (req, res) => {
    try {
        const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);

        // Validate UID parameter
        if (!req.params.UID) {
            res.json({ success: false, message: 'UID null or undefined' });
            return;
        }

        const UID = UUID2hex(req.params.UID);
        const update = await query(`SELECT UIDTarget FROM Links WHERE UID = ? AND Links.Type IN ('member','memberA') `, [UID]);

        // Delete all associated data
        await query(`DELETE ObjectBase, Links 
            FROM ObjectBase
            INNER JOIN Links ON (Links.UID=ObjectBase.UID OR Links.UIDTarget=ObjectBase.UID)
            WHERE ObjectBase.UIDBelongsTo =? AND ObjectBase.Type IN ('extern','guest','job','entry')`,
            [UID], { backDate: timestamp });

        addUpdateList(update.map(el => el.UIDTarget));
        res.json({ success: true });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get external member details (GET /:UID)
 * Delegates to the person controller since externs use the same data structure
 */
export const getExtern = getPerson;

/**
 * Check admin rights for external member (GET /admin/:UID)
 * Delegates to the person controller since admin checks are the same
 */
export const getExternAdmin = personAdmin;

/**
 * Get external member history (GET /history/:UID)
 * Delegates to the person controller since history tracking is the same
 */
export const getExternHistory = personHistorie;

/**
 * Find duplicate external members (GET /duplicates/:firstName/:lastName)
 * Delegates to the person controller since duplicate detection logic is the same
 */
export const getExternDuplicates = personDuplicates;

/**
 * Update external member data (POST /:UID)
 * Delegates to the person controller since data update logic is the same
 */
export const updateExtern = updatePerson;