Source: Router/person/externService.js

// @ts-check
/**
 * Extern Service — Pure Business Logic
 *
 * All database operations and data transformations for external members.
 * No HTTP concerns (no req.params / res). Session values are passed
 * as explicit parameters where needed; `req` is forwarded only to the
 * shared personHelpers functions that still require it internally.
 */

import { query, transaction, UUID2hex } from '@commtool/sql-query';
import { phonetikArray } from '../../utils/compileTemplates.js';
import { queueAdd } from '../../tree/treeQueue/treeQueue.js';
import { addToTree } from './migratePerson.js';
import { encryptIbans } from '../../utils/crypto.js';
import {
    updatePersonData,
    handleGroupMembershipMigration,
    updatePersonEmbedding,
    fetchGroup,
    fetchMemberExists,
    fetchLatestObjectValidFrom,
    rebuildMemberAccess,
} from './personHelpers.js';

// Re-export shared helpers so callers of externService have one import point
export { fetchGroup, fetchMemberExists as fetchExternExists, fetchLatestObjectValidFrom };

// ---------------------------------------------------------------------------
// Mutation helpers
// ---------------------------------------------------------------------------

/**
 * Insert a brand-new extern member into ObjectBase, Member, Links, and Visible.
 * Fires tree queue and starts a non-blocking AI-embedding job.
 *
 * @param {Object} object        Rendered extern object (from renderObject)
 * @param {Buffer} UIDgroup      Target group UID
 * @param {string} sessionUser   req.session.user
 * @param {string} sessionRoot   req.session.root
 * @param {number|undefined} timestamp  Unix seconds backdate (or undefined)
 */
export const createNewExtern = async (object, UIDgroup, sessionUser, sessionRoot, 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(sessionUser), 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(sessionRoot), UUID2hex(sessionUser), '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(sessionUser)]
    );

    // Non-blocking AI embedding — failure must not abort the request
    updatePersonEmbedding(object.UID, object.Data, 'extern', UUID2hex(sessionRoot), UUID2hex(sessionUser))
        .catch(err => console.error('Background embedding creation failed:', err));
};

/**
 * Update an existing extern record, or migrate a person record to type 'extern'.
 * Delegates shared update logic to personHelpers.
 *
 * Note: `req` is forwarded because updatePersonData / handleGroupMembershipMigration
 * internally require session information from it.
 *
 * @param {Object} extern    Existing DB record for the extern
 * @param {Object} object    Rendered extern object
 * @param {Buffer} UIDgroup
 * @param {Object} req       Express request object
 * @param {number|undefined} timestamp
 */
export const updateExistingExtern = async (extern, object, UIDgroup, req, timestamp) => {
    object.Type = 'extern';
    object.hierarchie = 0;
    object.stage = 0;

    await updatePersonData(object.UID, object.Data, object, req, timestamp);

    if (!extern.MemberA || !extern.MemberA.equals(UIDgroup) || extern.Type === 'person') {
        await handleGroupMembershipMigration(extern, object.UID, UIDgroup, req, timestamp, 'extern');
    }
};

/**
 * Delete an extern and all its child objects (guest, job, entry) plus associated links.
 *
 * @param {Buffer} UID
 * @param {number|undefined} timestamp
 * @returns {Promise<Buffer[]>} Binary UIDs of affected parent groups (for WebSocket updates)
 */
export const deleteExternById = async (UID, timestamp) => {
    const update = await query(
        `SELECT UIDTarget FROM Links WHERE UID = ? AND Links.Type IN ('member', 'memberA')`,
        [UID]
    );
    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 }
    );
    return update.map(el => el.UIDTarget);
};

/**
 * Rebuild visibility and list access for an extern and all its job objects.
 * Delegates to the shared helper in personHelpers.
 *
 * @param {Buffer} UID
 * @param {string} sessionRoot  req.session.root
 */
export const rebuildExternAccess = rebuildMemberAccess;