Source: tree/familyAddress.js

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


}