// @ts-check
/**
* @typedef {Object} AddressItem
* @property {string} type - Address type (e.g., 'family', 'personal', 'secondary')
* @property {string} [road] - Street name
* @property {string} [houseNumber] - House number
* @property {string} [postcode] - Postal code
* @property {string} [countryCode] - Country code
*/
/**
* @typedef {Object} EmailItem
* @property {string} type - Email type (e.g., 'family', 'personal', 'secondary')
* @property {string} [email] - Email address
*/
/**
* @typedef {Object} PhoneItem
* @property {string} type - Phone type (e.g., 'family', 'personal', 'secondary')
* @property {string} [number] - Phone number
*/
/**
* @typedef {Object} AccountItem
* @property {string} type - Account type (e.g., 'family', 'familyFees', 'personal')
* @property {string} [IBAN] - Bank account IBAN
*/
/**
* @typedef {Object} FamilyMemberObject
* @property {Buffer} UID - Member unique identifier
* @property {'person'|'extern'|'family'} Type - Object type (required)
* @property {Object} Data - Member data object (required)
* @property {AddressItem[]} [Data.address] - Address array
* @property {EmailItem[]} [Data.email] - Email array
* @property {PhoneItem[]} [Data.phone] - Phone array
* @property {AccountItem[]} [Data.accounts] - Account array
*/
import {query} from '@commtool/sql-query'
import { publishChangeEvent } from '../Router/person.js'
import { addUpdateEntry } from '../server.ws.js';
import _ from 'lodash'
import { keysEqual } from '../utils/keyCompare.js';
import { errorLoggerUpdate } from '../utils/requestLogger.js';
import { decryptIbans, decryptAccount } from '../utils/crypto.js';
import { getConfig } from '../utils/compileTemplates.js';
// handle potential family address changes
function difference(object, base) {
function changes(object, base) {
return _.transform(object, function(result, value, key) {
if (!_.isEqual(value, base[key])) {
result[key] = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value;
}
});
}
//console.log(changes(object, base));
}
/**
* Adjusts member data with family-shared information (address, email, phone, accounts)
*
* @param {FamilyMemberObject} member - The family member to update
* @param {FamilyMemberObject} object - The source object containing family data
* @param {string} organization - Organization UID for event publishing
* @returns {Promise<Object|undefined>} Updated member data or undefined
*/
const adjustMemberData=async(member,object,organization)=>
{
try {
const famAddress= /** @type {AddressItem|AddressItem[]|undefined} */ ((object.Type==='person' || object.Type==='extern') && object.Data.address ? (object.Data.address?.find(a=>(a.type==='family'))) : object.Data.address)
if(!member.Data)
return
const origData=_.cloneDeep(member.Data)
if(famAddress && !Array.isArray(famAddress))
{
famAddress.type='family'
let Index=member.Data.address ? member.Data.address.findIndex(a=>(a.type==='family')) : null
if(Index<0)
Index=member.Data.address.findIndex(a=>(
famAddress.houseNumber===a.houseNumber &&
famAddress.road===a.road &&
famAddress.countryCode===a.countryCode &&
famAddress.postcode===a.postcode
))
const famPrimary= Index===0 // primary address is family address, make new family address the primary one
if(Index>=0)
if(famPrimary)
{
member.Data.address= [{type:'family',...famAddress},
...member.Data.address.filter((a,i)=>i!==Index)
.map(a=> (a.type==='family'? {...a,type:'personal'} :a))]
}
else
{
member.Data.address= member.Data.address ? member.Data.address.map((a,i)=>
{
if (i===Index)
return famAddress
else
return {...a,type:a.type==='family' ? i===0 ? 'personal' : 'secondary' : a.type}
})
:
[famAddress]
}
else
{
if(member.Data.address)
member.Data.address.push(famAddress)
else
member.Data.address=[famAddress]
}
}
const famMail= /** @type {EmailItem|EmailItem[]|undefined} */ ((object.Type==='person' || object.Type==='extern') && object.Data.email ? object.Data.email?.find(e=>(e.type==='family')) : object.Data.email)
if(famMail && !Array.isArray(famMail))
{
let Index=member.Data.email ? member.Data.email.findIndex(a=>(a.type==='family')) : null
if(Index<0)
Index=member.Data.email.findIndex(p=>(famMail.email===p.email))
if(Index>=0)
member.Data.email= member.Data.email ? member.Data.email.map((e,i)=>
{
if (i===Index)
return famMail
else
return {...e,type:(e.type==='family' ? i===0 ? 'personal' : 'secondary' : e.type)}
})
:
[famMail]
else
{
if(member.Data.email)
member.Data.email.push(famMail)
else
member.Data.email=[famMail]
}
}
const famPhone= /** @type {PhoneItem|PhoneItem[]|undefined} */ ((object.Type==='person' || object.Type==='extern') && object.Data.phone ? object.Data.phone?.find(p=>(p.type==='family')) : object.Data.phone)
if(famPhone && !Array.isArray(famPhone))
{
let Index=member.Data.phone ? member.Data.phone.findIndex(a=>(a.type==='family')): null
if(Index<0)
Index=member.Data.phone.findIndex(p=>(famPhone.number===p.number))
if(Index>=0)
member.Data.phone= member.Data.phone ? member.Data.phone.map((p,i)=>
{
if (i===Index)
return famPhone
else
{
return p.number? {number:p.number,type:p.type==='family' ? 'personal' : p.type} : {type:'personal'}
}
})
:
[famPhone]
else
{
if(member.Data.phone)
member.Data.phone.push(famPhone)
else
member.Data.phone=[famPhone]
}
}
const famAccounts= (object.Type==='person' || object.Type==='extern') && object.Data.accounts ? object.Data.accounts?.filter(a=>(a.type==='family' || a.type==='familyFees')) :
object.Data.accounts
if(famAccounts && famAccounts.length>0)
{
decryptIbans( member.Data)
for (let account of famAccounts)
{
account =decryptAccount(account)
if(account.type==='familyFees')
{
// we have to make it the first one
// delete the entry, if it exists
let accounts
if(member.Data.accounts.length>0)
{
accounts=member.Data.accounts.filter(a=>a.IBAN!==account.IBAN).map(a=>a.type==='familyFees'?{...a,type:'family'} :a)
}
else
accounts=[]
member.Data.accounts=[account,...accounts]
}
else
{
let Index=member.Data.accounts ? member.Data.accounts.findIndex(a=>(a.IBAN===account.IBAN)) :null
if(Index>=0)
{
// replace the
member.Data.accounts= member.Data.accounts? member.Data.accounts.map((a,i)=>
{
if (i===Index)
return account
else
return a
})
:
[account]
}
else
{
if(member.Data.accounts)
member.Data.accounts.push(account )
else
member.Data.accounts=[account]
}
}
}
}
//difference(member.Data,origData)
const [equal,myDiff]= keysEqual(origData,member.Data)
if(!equal)
{
query(`UPDATE Member SET Data=? WHERE UID=?`,[JSON.stringify(member.Data),member.UID])
addUpdateEntry(member.UID,{data:member.Data})
publishChangeEvent({Type:member.Type,UID:member.UID},myDiff,Date.now()/1000,organization)
}
return member.Data
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Synchronizes family-shared data (address, email, phone, accounts) across all family members
*
* When a person/extern/family object is updated with family data, this function propagates
* those changes to all other members of the same family. Handles:
* - Family addresses
* - Family email addresses
* - Family phone numbers
* - Family accounts (including familyFees accounts)
*
* @param {FamilyMemberObject} object - The source object with updated family data (requires: UID, Type, Data properties)
* @param {string} organization - Organization UID for multi-tenant context and event publishing
* @returns {Promise<void>}
*/
export const familyAddress=async (object, organization)=>
{
try {
let result
//get all family members
if(object.Type==='person' || object.Type==='extern')
result =await query(`SELECT Family.UID, Family.Data, Members.Data AS MemberData, Members.UID AS UIDmember,MObject.Type
FROM Member AS Family
INNER JOIN Links ON (Links.UIDTarget =Family.UID )
INNER JOIN Links AS FamLinks ON ( FamLinks.UIDTarget=Family.UID AND FamLinks.Type IN ('family','familyFees'))
INNER JOIN Member AS Members ON (FamLinks.UID=Members.UID)
INNER JOIN ObjectBase AS MObject ON (MObject.UID=Members.UID)
WHERE Links.UID=? AND Links.Type IN ('family','familyFees')
`,[object.UID])
else if(object.Type==='family')
{
result =await query(`SELECT Family.UID, Family.Data, Members.Data AS MemberData, Members.UID AS UIDmember,MObject.Type
FROM Member AS Family
LEFT JOIN ObjectBase ON (Family.UID=ObjectBase.UID)
INNER JOIN Links ON (Links.UIDTarget =Family.UID )
INNER JOIN Links AS FamLinks ON ( FamLinks.UIDTarget=Family.UID AND FamLinks.Type IN ('family','familyFees'))
INNER JOIN Member AS Members ON (FamLinks.UID=Members.UID)
INNER JOIN ObjectBase AS MObject ON (MObject.UID=Members.UID)
WHERE Links.UID=? AND Links.Type IN ('family','familyFees') AND ObjectBase.UID IS NULL
`,[object.UID])
}
else
return
if(result.length>0)
{
const family= result.map(m=>({UID:m.UIDmember,Type:m.Type,Data:JSON.parse(m.MemberData)}))
for (const member of family)
{
await adjustMemberData(member,object,organization)
}
}
}
catch(e)
{
errorLoggerUpdate(e)
}
}
/**
* Adds family updates to a new member when they join a family
*
* Synchronizes family data (address, email, phone, accounts) from existing family members
* to the newly added family member.
*
* @param {Buffer} UIDnewMember - UID of the new family member
* @param {Buffer} UIDfamily - UID of the family
* @param {string} [organization] - Optional organization UID for event publishing
* @returns {Promise<Object|undefined>} Updated member data or original data if no family members exist
*/
export const addFamilyUpdate=async (UIDnewMember,UIDfamily,organization)=>
{
try {
// get an old member of the family
const result=await query(`SELECT ObjectBase.UID,ObjectBase.Type,Member.Data FROM Links
INNER JOIN ObjectBase ON (ObjectBase.UID=Links.UID AND Links.Type IN ('family','familyFees'))
INNER JOIN Member ON (Member.UID=ObjectBase.UID)
WHERE Links.UID<>? AND Links.UIDTarget=?`,[UIDnewMember,UIDfamily],
{cast:['json']})
const [memberObject]=await query(`SELECT Member.UID,Member.Data,ObjectBase.Type
FROM Member
INNER JOIN ObjectBase ON (ObjectBase.UID=Member.UID)
WHERE Member.UID=?`,
[UIDnewMember],{cast:['json']})
if(result.length>0)
{
const newData=await adjustMemberData(memberObject,result[0],organization)
return newData
}
else
return memberObject.Data
}
catch(e)
{
errorLoggerUpdate(e)
}
}