Source: Router/person/externController.js

/**
 * External Member Controller — HTTP Request/Response Handling
 *
 * Extracts request parameters and delegates all business logic to externService.
 * Read/shared operations reuse the person controller directly since externs and
 * persons share the same data structure.
 */
// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from './../../types.js'
 */
import { UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { parseTimestampToSeconds } from '../../utils/parseTimestamp.js';
import { renderObject } from '../../utils/renderTemplates.js';
import { Templates, getConfig } from '../../utils/compileTemplates.js';
import { getUID, isValidUID } from '../../utils/UUIDs.js';
import { addUpdateList, addUpdateEntry } from '../../server.ws.js';
import { isObjectAdmin } from '../../utils/authChecks.js';
import { encryptIbans } from '../../utils/crypto.js';
import { errorLoggerUpdate } from '../../utils/requestLogger.js';
import { updateGroupMembershipShared } from './personHelpers.js';
import { updatePerson, getPerson, personAdmin, personHistorie, personDuplicates } from '../person.js';
import * as externService from './externService.js';

// ---------------------------------------------------------------------------
// Mutation handlers
// ---------------------------------------------------------------------------

/**
 * PUT /:group — Create or update an extern in a 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})` : '';

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

        const group = await externService.fetchGroup(UIDgroup);
        if (!group) {
            res.json({ success: false, message: 'invalid group UID' });
            return;
        }

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

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

        const externs = await externService.fetchExternExists(UIDextern, asOf);

        // Safeguard: when changing an existing person → extern with a backdate,
        // the timestamp must not land before the latest ObjectBase history row.
        if (externs.length > 0 && timestamp && externs[0].Type !== 'extern') {
            const latestValidFrom = await externService.fetchLatestObjectValidFrom(UIDextern);
            if (latestValidFrom !== null && timestamp < latestValidFrom) {
                res.status(300).json({
                    success: false,
                    message: `Cannot backdate to ${timestamp}: type change (person → extern) must not precede the latest existing history row (${latestValidFrom}).`,
                });
                return;
            }
        }

        if (externs.length === 0) {
            await externService.createNewExtern(object, UIDgroup, req.session.user, req.session.root, timestamp);
        } else {
            await externService.updateExistingExtern(externs[0], object, UIDgroup, req, timestamp);
        }

        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: e.message });
    }
};

/**
 * POST /:group/:UID — Update group membership for an extern.
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const updateGroupMembership = async (req, res) => {
    return updateGroupMembershipShared(req, res, 'extern', isObjectAdmin);
};

/**
 * DELETE /:UID — Delete an extern and all associated data.
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const deleteExtern = async (req, res) => {
    try {
        const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);
        const UID = UUID2hex(req.params.UID);

        const affectedGroups = await externService.deleteExternById(UID, timestamp);
        addUpdateList(affectedGroups);
        res.json({ success: true });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: e.message });
    }
};

/**
 * POST /rebuildAccess/:UID — Rebuild visibility and list access for an extern.
 * @param {ExpressRequestAuthorized} req
 * @param {ExpressResponse} res
 */
export const rebuildExternAccess = async (req, res) => {
    try {
        const UID = UUID2hex(req.params.UID);
        await externService.rebuildExternAccess(UID, req.session.root);
        res.json({ success: true });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, error: e.message });
    }
};

// ---------------------------------------------------------------------------
// Delegations — externs share data structure and read logic with persons
// ---------------------------------------------------------------------------

/** GET /:UID — Delegates to person controller (same data structure) */
export const getExtern = getPerson;

/** GET /admin/:UID — Delegates to person controller (same admin check) */
export const getExternAdmin = personAdmin;

/** GET /history/:UID — Delegates to person controller (same history logic) */
export const getExternHistory = personHistorie;

/** GET /duplicates/:firstName/:lastName — Delegates to person controller */
export const getExternDuplicates = personDuplicates;

/** POST /:UID — Partial update; delegates to person controller (same structure) */
export const updateExtern = updatePerson;