// @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)
}
}