Source: utils/syncAccountingVisibility.js

// @ts-check
/**
 * Accounting Visibility Synchronization
 *
 * Synchronizes the `Visible.Accounting` column based on financialMaster flags
 * from function templates and per-job accounting blocks.
 *
 * financialMaster=true  →  Accounting='admin' on the job's group + all subgroup
 * accountingAccess/writeAccess rules  →  Filter-object-based (handled separately)
 */

import { query, UUID2hex, HEX2uuid } from '@commtool/sql-query'

/**
 * Extracts the financialMaster flag from a job or function template.
 *
 * Priority: job-level `accounting.financialMaster` > function template `financialMaster`
 *
 * @param {Object} functionData - Data from the function template (ObjectBase.Data)
 * @param {Object} [jobData] - Data from the job (ObjectBase.Data), may contain accounting block
 * @returns {{ hasFinancialMaster: boolean }}
 */
export function getFinancialMasterFlag(functionData, jobData) {
    if (jobData?.accounting?.financialMaster === true) {
        return { hasFinancialMaster: true }
    }
    return {
        hasFinancialMaster: functionData?.financialMaster === true,
    }
}

/**
 * Synchronizes `Visible.Accounting` for a single job.
 *
 * - If financialMaster=true: sets Accounting='admin' on the job's group AND recursively
 *   on all sub-groups of that group using a recursive CTE.
 * - If financialMaster=false: removes Accounting rows for this person+group.
 *   Does NOT remove if the person has another job with financialMaster in same group.
 *
 * @param {Buffer|string} jobUID - UID of the job (hex buffer or UUID string)
 * @param {Object} [opts] - Options
 * @param {boolean} [opts.delete] - If true, the job is being deleted
 * @param {string} [opts.personUID] - Person UID (required when opts.delete is true)
 * @param {string} [opts.groupUID] - Group UID (required when opts.delete is true)
 * @returns {Promise<void>}
 */
export async function syncAccountingVisibility(jobUID, opts = {}) {
    const jobBuffer = UUID2hex(jobUID)

    // ── Delete-Fall: Job is being removed ──────────────────────────────
    if (opts.delete) {
        if (!opts.personUID || !opts.groupUID) return
        const personBuffer = UUID2hex(opts.personUID)
        const groupBuffer = UUID2hex(opts.groupUID)

        // Check if the person still has another job with financialMaster in this group or its subgroups
        const [other] = await query(`
            SELECT COUNT(*) AS cnt
            FROM ObjectBase AS job
            INNER JOIN Links AS gLink ON (gLink.UID = job.UID AND gLink.Type = 'memberA')
            INNER JOIN Links AS fLink ON (fLink.UIDTarget = job.UID AND fLink.Type = 'function')
            INNER JOIN ObjectBase AS func ON (func.UID = fLink.UID)
            WHERE job.UIDBelongsTo = ?
                AND job.UID != ?
                AND job.Type = 'job'
                AND (
                    JSON_EXTRACT(func.Data, '$.financialMaster') = true
                    OR JSON_EXTRACT(job.Data, '$.accounting.financialMaster') = true
                )
        `, [personBuffer, jobBuffer])

        if (Number(other.cnt) === 0) {
            // No other job with accounting access — remove all accounting rows
            // for this person on the group and all subgroups
            await query(`
                UPDATE Visible
                INNER JOIN (
                    WITH RECURSIVE SubGroups AS (
                        SELECT UID FROM ObjectBase WHERE UID = ? AND Type IN ('group','ggroup')
                        UNION ALL
                        SELECT ObjectBase.UID FROM ObjectBase
                        INNER JOIN Links ON (Links.UIDTarget = ObjectBase.UID AND Links.Type IN ('member','memberA'))
                        INNER JOIN SubGroups ON (SubGroups.UID = Links.UID)
                        WHERE ObjectBase.Type IN ('group','ggroup')
                    )
                    SELECT UID FROM SubGroups
                ) AS G ON (Visible.UID = G.UID)
                SET Visible.Accounting = NULL
                WHERE Visible.UIDUser = ?
            `, [groupBuffer, personBuffer])
        }
        return
    }

    // ── Normaler Fall: Job existiert noch ──────────────────────────────
    const [job] = await query(`
        SELECT job.UID, job.UIDBelongsTo AS personUID,
               gLink.UIDTarget AS groupUID,
               JSON_EXTRACT(func.Data, '$') AS functionData,
               job.Data AS jobData
        FROM ObjectBase AS job
        INNER JOIN Links AS gLink ON (gLink.UID = job.UID AND gLink.Type = 'memberA')
        INNER JOIN Links AS fLink ON (fLink.UIDTarget = job.UID AND fLink.Type = 'function')
        INNER JOIN ObjectBase AS func ON (func.UID = fLink.UID)
        WHERE job.UID = ? AND job.Type = 'job'
    `, [jobBuffer], { cast: ['json'] })

    if (!job) return

    const funcData = job.functionData || {}
    const jobData = job.jobData || {}
    const { hasFinancialMaster } = getFinancialMasterFlag(funcData, jobData)

    if (hasFinancialMaster) {
        // Set Accounting='admin' on the group and ALL subgroups recursively
        await query(`
            INSERT INTO Visible (UID, Type, Accounting, UIDUser)
            SELECT G.UID, 'visible', 'admin', ?
            FROM (
                WITH RECURSIVE SubGroups AS (
                    SELECT UID FROM ObjectBase WHERE UID = ? AND Type IN ('group','ggroup')
                    UNION ALL
                    SELECT ObjectBase.UID FROM ObjectBase
                    INNER JOIN Links ON (Links.UIDTarget = ObjectBase.UID AND Links.Type IN ('member','memberA'))
                    INNER JOIN SubGroups ON (SubGroups.UID = Links.UID)
                    WHERE ObjectBase.Type IN ('group','ggroup')
                )
                SELECT UID FROM SubGroups
            ) AS G
            ON DUPLICATE KEY UPDATE Accounting = 'admin'
        `, [job.personUID, job.groupUID])
        return
    }

    // ── financialMaster=false: Remove accounting if no other job provides it ──
    const [other] = await query(`
        SELECT COUNT(*) AS cnt
        FROM ObjectBase AS job
        INNER JOIN Links AS gLink ON (gLink.UID = job.UID AND gLink.Type = 'memberA')
        INNER JOIN Links AS fLink ON (fLink.UIDTarget = job.UID AND fLink.Type = 'function')
        INNER JOIN ObjectBase AS func ON (func.UID = fLink.UID)
        WHERE job.UIDBelongsTo = ?
            AND gLink.UIDTarget = ?
            AND job.UID != ?
            AND job.Type = 'job'
            AND (
                JSON_EXTRACT(func.Data, '$.financialMaster') = true
                OR JSON_EXTRACT(job.Data, '$.accounting.financialMaster') = true
            )
    `, [job.personUID, job.groupUID, jobBuffer])

    if (Number(other.cnt) === 0) {
        // Remove accounting rows for this person on the group and all subgroups
        await query(`
            UPDATE Visible
            INNER JOIN (
                WITH RECURSIVE SubGroups AS (
                    SELECT UID FROM ObjectBase WHERE UID = ? AND Type IN ('group','ggroup')
                    UNION ALL
                    SELECT ObjectBase.UID FROM ObjectBase
                    INNER JOIN Links ON (Links.UIDTarget = ObjectBase.UID AND Links.Type IN ('member','memberA'))
                    INNER JOIN SubGroups ON (SubGroups.UID = Links.UID)
                    WHERE ObjectBase.Type IN ('group','ggroup')
                )
                SELECT UID FROM SubGroups
            ) AS G ON (Visible.UID = G.UID)
            SET Visible.Accounting = NULL
            WHERE Visible.UIDUser = ?
        `, [job.groupUID, job.personUID])
    }
}

/**
 * Synchronizes Visible.Accounting for ALL jobs linked to a function template.
 *
 * Called when a function template's financialMaster flag or accounting rules change.
 *
 * @param {Buffer|string} functionUID - UID of the function template
 * @returns {Promise<void>}
 */
export async function syncAccountingForFunction(functionUID) {
    const funcBuffer = UUID2hex(functionUID)

    const jobs = await query(`
        SELECT job.UID
        FROM ObjectBase AS job
        INNER JOIN Links AS fLink ON (fLink.UIDTarget = job.UID AND fLink.Type = 'function')
        WHERE fLink.UID = ? AND job.Type = 'job'
    `, [funcBuffer])

    for (const job of jobs) {
        await syncAccountingVisibility(job.UID)
    }
}