Ctrl K
ComunidadGuías…

tine20/HumanResources/Controller/AttendanceRecorder.php

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<?php declare(strict_types=1);
/**
 * AttendanceRecorder Controller
 *
 * @package     HumanResources
 * @subpackage  Controller
 * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
 * @author      Paul Mehrer <p.mehrer@metaways.de>
 * @copyright   Copyright (c) 2022 Metaways Infosystems GmbH (http://www.metaways.de)
 *
 */

/**
 * AttendanceRecorder Controller
 *
 * @package     HumanResources
 * @subpackage  Controller
 */
class HumanResources_Controller_AttendanceRecorder implements Tinebase_Controller_Interface
{
    /** @use Tinebase_Controller_SingletonTrait<HumanResources_Controller_AttendanceRecorder> */
    use Tinebase_Controller_SingletonTrait;

    protected $backend;

    protected function __construct()
    {
        $this->backend = HumanResources_Controller_AttendanceRecord::getInstance();
    }

    protected function checkOutOfSequence(HumanResources_Config_AttendanceRecorder $config): ?Closure
    {
        $aheadRecords = $this->backend->search(Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_AttendanceRecord::class, [
            ['field' => HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID, 'operator' => 'equals', 'value' => $config->getAccount()->getId()],
            ['field' => HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP,  'operator' => 'after', 'value' => $config->getTimeStamp()],
        ]), new Tinebase_Model_Pagination(['sort' => HumanResources_Model_AttendanceRecord::FLD_SEQUENCE, 'dir' => 'ASC']));

        if (0 === $aheadRecords->count()) {
            return null;
        }

        if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(
            __METHOD__ . '::' . __LINE__ . ' out of sequence clock action encountered.'
        );
        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
            __METHOD__ . '::' . __LINE__ . ' ' . print_r($config->__serialize(), true)
        );

        // shallow clone
        $blUndo = $aheadRecords->getClone(true);
        $refIdsToOpen = array_unique($aheadRecords->filter(HumanResources_Model_AttendanceRecord::FLD_TYPE,
            HumanResources_Model_AttendanceRecord::TYPE_CLOCK_OUT)->{HumanResources_Model_AttendanceRecord::FLD_REFID});
        if (!empty($refIdsToOpen)) {
            $refIdsToOpen = $this->backend->search(Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_AttendanceRecord::class, [
                ['field' => HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID, 'operator' => 'equals', 'value' => $config->getAccount()->getId()],
                ['field' => HumanResources_Model_AttendanceRecord::FLD_REFID, 'operator' => 'in', 'value' => $refIdsToOpen],
                ['field' => 'id', 'operator' => 'notin', 'value' => $aheadRecords->getArrayOfIds()],
            ]));
            $blUndo->merge($refIdsToOpen);
            $blUndo->sort(HumanResources_Model_AttendanceRecord::FLD_SEQUENCE, 'ASC', 'asort',  SORT_NUMERIC);
        }

        // undo BL
        $deviceIds = array_unique($aheadRecords->{HumanResources_Model_AttendanceRecord::FLD_DEVICE_ID});
        foreach ($deviceIds as $deviceId) {
            $device = HumanResources_Controller_AttendanceRecorderDevice::getInstance()->get($deviceId);
            if (empty($device->{HumanResources_Model_AttendanceRecorderDevice::FLD_BLPIPE}) ||
                    0 === $device->{HumanResources_Model_AttendanceRecorderDevice::FLD_BLPIPE}->count()) {
                continue;
            }
            $deviceRecords = $blUndo->filter(HumanResources_Model_AttendanceRecord::FLD_DEVICE_ID, $deviceId);
            if ($deviceRecords->count() > 0) {
                foreach ($device->{HumanResources_Model_AttendanceRecorderDevice::FLD_BLPIPE} as $blpipe) {
                    $blElem = $blpipe->configRecord->getNewBLElement();
                    if ($blElem instanceof HumanResources_BL_AttendanceRecorder_UndoInterface) {
                        $blElem->undo($deviceRecords);
                    }
                }
            }
        }

        $blUndo->removeRecordsById($aheadRecords);
        // delete
        foreach ($aheadRecords as $record) {
            $this->backend->getBackend()->delete($record->getId());
        }
        if ($refIdsToOpen instanceof Tinebase_Record_RecordSet) {
            foreach ($refIdsToOpen as $record) {
                if (HumanResources_Model_AttendanceRecord::STATUS_OPEN === $record->{HumanResources_Model_AttendanceRecord::FLD_STATUS}) {
                    continue;
                }
                $record->{HumanResources_Model_AttendanceRecord::FLD_STATUS} = HumanResources_Model_AttendanceRecord::STATUS_OPEN;
                $this->backend->getBackend()->update($record);
                $blUndo->removeById($record->getId());
            }
        }
        foreach ($blUndo as $record) {
            if ($record->isDirty()) {
                $this->backend->getBackend()->update($record);
            }
        }

        // return and do this api call
        // then replay deleted api calls!
        return function() use ($aheadRecords) {
            if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(
                __METHOD__ . '::' . __LINE__ . ' replay API actions due to out of sequence clock action.'
            );
            /** @var HumanResources_Model_AttendanceRecord $record */
            foreach($aheadRecords->filter(HumanResources_Model_AttendanceRecord::FLD_AUTOGEN, false) as $record) {
                $cfg = $record->getConfig();
                $cfg->setThrowOnFaultyAction(false);
                if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
                    __METHOD__ . '::' . __LINE__ . ' ' . $record->{HumanResources_Model_AttendanceRecord::FLD_TYPE} .
                    ' ' . print_r($cfg->__serialize(), true)
                );
                switch($record->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) {
                    case HumanResources_Model_AttendanceRecord::TYPE_CLOCK_IN:
                        $this->clockIn($cfg);
                        break;
                    case HumanResources_Model_AttendanceRecord::TYPE_CLOCK_OUT:
                        $this->clockOut($cfg);
                        break;
                    case HumanResources_Model_AttendanceRecord::TYPE_CLOCK_PAUSED:
                        $this->clockPause($cfg);
                        break;
                }
            }
        };
    }

    public function clockIn(HumanResources_Config_AttendanceRecorder $config): HumanResources_Model_AttendanceRecorderClockInOutResult
    {
        if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__);
        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
            __METHOD__ . '::' . __LINE__ . ' ' . print_r($config->__serialize(), true)
        );

        $raii = Tinebase_RAII::getTransactionManagerRAII();

        $this->prepareConfig($config);
        if (null === $config->getStatus()) {
            $config->setStatus(HumanResources_Model_AttendanceRecord::STATUS_OPEN);
        } elseif (HumanResources_Model_AttendanceRecord::STATUS_OPEN !== $config->getStatus()) {
            throw new Tinebase_Exception_UnexpectedValue('can\'t clockIn with other status than ' . HumanResources_Model_AttendanceRecord::STATUS_OPEN);
        }

        $this->registerAfterCommitBLPipeRun($config->getAccount()->getId());
        $result = $this->getClockInOutResult();

        $outOfSequenceClosure = $this->checkOutOfSequence($config);

        $lastRecord = null;
        $openRecords = null;
        if ($config->getDevice()->{HumanResources_Model_AttendanceRecorderDevice::FLD_ALLOW_MULTI_START} &&
                isset($config->getMetaData()[HumanResources_Model_AttendanceRecord::CLOCK_OUT_OTHERS]) &&
                $config->getMetaData()[HumanResources_Model_AttendanceRecord::CLOCK_OUT_OTHERS]) {
            $openRecords = $this->getOpenRecords($config->getAccount()->getId(), $config->getDevice()->getId())
                ->filter(function(HumanResources_Model_AttendanceRecord $val) use($config) {
                    static $refIds = [];
                    if (isset($refIds[$val->{HumanResources_Model_AttendanceRecord::FLD_REFID}]) || $config->getRefId()
                            === $val->{HumanResources_Model_AttendanceRecord::FLD_REFID}) {
                        return false;
                    }
                    $refIds[$val->{HumanResources_Model_AttendanceRecord::FLD_REFID}] = true;
                    return true;
                });
            foreach ($openRecords as $openRecord) {
                $subConfig = clone $config;
                $subConfig->setRefId($openRecord->{HumanResources_Model_AttendanceRecord::FLD_REFID});
                $subConfig->setStatus(HumanResources_Model_AttendanceRecord::STATUS_CLOSED);
                $subConfig->setAutogen(true);
                $subConfig->setMetaData(array_filter($subConfig->getMetaData() ?: [], function($key) {
                    return $key === HumanResources_Config_AttendanceRecorder::METADATA_SOURCE;
                }, ARRAY_FILTER_USE_KEY));
                $subResult = $this->clockOut($subConfig);
                $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_INS}->mergeById(
                    $subResult->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_INS});
                $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_OUTS}->mergeById(
                    $subResult->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_OUTS});
                $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_PAUSES}->mergeById(
                    $subResult->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_PAUSES});
                $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_FAULTY_CLOCKS}->mergeById(
                    $subResult->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_FAULTY_CLOCKS});
            }
            $openRecords = null;
        }

        if (null === $config->getRefId()) {
            if ($config->getDevice()->{HumanResources_Model_AttendanceRecorderDevice::FLD_ALLOW_MULTI_START}) {
                $config->setRefId(Tinebase_Record_Abstract::generateUID());
            } else {
                if ($lastRecord = ($openRecords = $this->getOpenRecords($config->getAccount()->getId(), $config->getDevice()->getId()))->getLastRecord()) {
                    $config->setRefId($lastRecord->{HumanResources_Model_AttendanceRecord::FLD_REFID});
                } else {
                    $config->setRefId(Tinebase_Record_Abstract::generateUID());
                }
            }
        } else {
            $openRecords = $this->getOpenRecords($config->getAccount()->getId(), $config->getDevice()->getId());
            if ($config->getDevice()->{HumanResources_Model_AttendanceRecorderDevice::FLD_ALLOW_MULTI_START}) {
                $lastRecord = $openRecords->filter(HumanResources_Model_AttendanceRecord::FLD_REFID, $config->getRefId())->getLastRecord();
            } else {
                $lastRecord = $openRecords->getLastRecord();
            }
        }

        if ($lastRecord && $lastRecord->{HumanResources_Model_AttendanceRecord::FLD_REFID} !== $config->getRefId()) {
            if ($config->getThrowOnFaultyAction()) {
                throw new Tinebase_Exception_UnexpectedValue('refId mismatch');
            }
        }
        if ($lastRecord && HumanResources_Model_AttendanceRecord::TYPE_CLOCK_PAUSED !==
                $lastRecord->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) {
            // graceful close conditions
            $graceful = isset($config->getMetaData()[HumanResources_Model_AttendanceRecord::CLOCK_OUT_GRACEFULLY]);

            if (!$graceful && $config->getThrowOnFaultyAction()) {
                throw new Tinebase_Exception_UnexpectedValue('can\'t clock in, open attendance record found');
            }

            $config->setRefId(Tinebase_Record_Abstract::generateUID());
            $subConfig = clone $config;
            $subConfig->setRefId($lastRecord->{HumanResources_Model_AttendanceRecord::FLD_REFID});
            if (!$graceful) {
                $subConfig->setStatus(HumanResources_Model_AttendanceRecord::STATUS_FAULTY);
                foreach ($openRecords as $openRecord) {
                    if (HumanResources_Model_AttendanceRecord::STATUS_FAULTY !==
                            $openRecord->{HumanResources_Model_AttendanceRecord::FLD_STATUS}) {
                        $openRecord->{HumanResources_Model_AttendanceRecord::FLD_STATUS} = HumanResources_Model_AttendanceRecord::STATUS_FAULTY;
                        HumanResources_Controller_AttendanceRecord::getInstance()->update($openRecord);
                    }
                }
            } else {
                $subConfig->setStatus(HumanResources_Model_AttendanceRecord::STATUS_CLOSED);
            }
            $subConfig->setAutogen(true);
            $subConfig->setMetaData(array_filter($subConfig->getMetaData() ?: [], function($key) {
                return $key === HumanResources_Config_AttendanceRecorder::METADATA_SOURCE;
            }, ARRAY_FILTER_USE_KEY));
            $subResult = $this->clockOut($subConfig);
            $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_INS}->mergeById(
                $subResult->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_INS});
            $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_OUTS}->mergeById(
                $subResult->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_OUTS});
            $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_PAUSES}->mergeById(
                $subResult->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_PAUSES});
            $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_FAULTY_CLOCKS}->mergeById(
                $subResult->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_FAULTY_CLOCKS});
        }

        /** @var HumanResources_Model_AttendanceRecorderDeviceRef $device */
        foreach ($config->getDevice()->{HumanResources_Model_AttendanceRecorderDevice::FLD_STARTS} as $device) {
            /** @var HumanResources_Model_AttendanceRecorderDevice $device */
            $device = $device->{HumanResources_Model_AttendanceRecorderDeviceRef::FLD_DEVICE_ID};
            if ($device->{HumanResources_Model_AttendanceRecorderDevice::FLD_ALLOW_MULTI_START}) {
                // TODO fixme shouldn't happen... at least log a warn?
                continue;
            }
            if (!($lastRecord = $this->getOpenRecords($config->getAccount()->getId(), $device->getId())->getLastRecord()) ||
                    HumanResources_Model_AttendanceRecord::TYPE_CLOCK_PAUSED === $lastRecord->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) {
                $tmpCfg = clone $config;
                $tmpCfg->setMetaData(array_filter($tmpCfg->getMetaData() ?: [], function($key) {
                    return $key === HumanResources_Config_AttendanceRecorder::METADATA_SOURCE;
                }, ARRAY_FILTER_USE_KEY));
                $tmpCfg->setDevice($device);
                $tmpCfg->setAutogen(true);
                $tmpCfg->setRefId($lastRecord ? $lastRecord->{HumanResources_Model_AttendanceRecord::FLD_REFID}
                    : Tinebase_Record_Abstract::generateUID());
                $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_INS}->addRecord(
                    $this->backend->create(
                        $this->createAttendanceRecord($tmpCfg, HumanResources_Model_AttendanceRecord::TYPE_CLOCK_IN)
                    )
                );
            }
        }

        if ($lastRecord && HumanResources_Model_AttendanceRecord::TYPE_CLOCK_PAUSED ===
                $lastRecord->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) {
            /** @var HumanResources_Model_AttendanceRecorderDeviceRef $device */
            foreach ($config->getDevice()->{HumanResources_Model_AttendanceRecorderDevice::FLD_UNPAUSES} as $device) {
                /** @var HumanResources_Model_AttendanceRecorderDevice $device */
                $device = $device->{HumanResources_Model_AttendanceRecorderDeviceRef::FLD_DEVICE_ID};
                $unpauseOpenRecords = $this->getOpenRecords($config->getAccount()->getId(), $device->getId());
                $refIds = [];
                /** @var HumanResources_Model_AttendanceRecord $record */
                foreach ($unpauseOpenRecords as $record) {
                    if (isset($refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}])) {
                        continue;
                    }
                    $refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}] = true;
                    $record = $unpauseOpenRecords->filter(HumanResources_Model_AttendanceRecord::FLD_REFID,
                            $record->{HumanResources_Model_AttendanceRecord::FLD_REFID})->getLastRecord();
                    if (HumanResources_Model_AttendanceRecord::TYPE_CLOCK_PAUSED !== $record
                            ->{HumanResources_Model_AttendanceRecord::FLD_TYPE} ||
                            !$record->{HumanResources_Model_AttendanceRecord::FLD_AUTOGEN}) {
                        continue;
                    }
                    $tmpCfg = clone $config;
                    $tmpCfg->setMetaData(array_filter($tmpCfg->getMetaData() ?: [], function ($key) {
                        return $key === HumanResources_Config_AttendanceRecorder::METADATA_SOURCE;
                    }, ARRAY_FILTER_USE_KEY));
                    $tmpCfg->setDevice($device);
                    $tmpCfg->setAutogen(true);
                    $tmpCfg->setRefId($record->{HumanResources_Model_AttendanceRecord::FLD_REFID});
                    $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_INS}->addRecord(
                        $this->backend->create(
                            $this->createAttendanceRecord($tmpCfg, HumanResources_Model_AttendanceRecord::TYPE_CLOCK_IN)
                        )
                    );
                }
            }
        }

        $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_INS}->addRecord(
            $this->backend->create(
                $this->createAttendanceRecord($config, HumanResources_Model_AttendanceRecord::TYPE_CLOCK_IN)
            )
        );

        if ($outOfSequenceClosure) {
            $outOfSequenceClosure();
        }

        $raii->release();

        $result->reloadData();
        return $result;
    }

    public function clockPause(HumanResources_Config_AttendanceRecorder $config): HumanResources_Model_AttendanceRecorderClockInOutResult
    {
        if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__);
        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
            __METHOD__ . '::' . __LINE__ . ' ' . print_r($config->__serialize(), true)
        );

        $raii = Tinebase_RAII::getTransactionManagerRAII();

        $this->prepareConfig($config);
        if (null === $config->getStatus()) {
            $config->setStatus(HumanResources_Model_AttendanceRecord::STATUS_OPEN);
        } elseif (HumanResources_Model_AttendanceRecord::STATUS_OPEN !== $config->getStatus()) {
            throw new Tinebase_Exception_UnexpectedValue('can\'t clockPause with other status than ' . HumanResources_Model_AttendanceRecord::STATUS_OPEN);
        }

        $result = $this->getClockInOutResult();
        if (!$config->getDevice()->{HumanResources_Model_AttendanceRecorderDevice::FLD_ALLOW_PAUSE}) {
            if ($config->getThrowOnFaultyAction()) {
                throw new Tinebase_Exception_UnexpectedValue('device doesn\'t allow to clock pauses');
            }
            return $result;
        }

        $this->registerAfterCommitBLPipeRun($config->getAccount()->getId());

        $outOfSequenceClosure = $this->checkOutOfSequence($config);

        $openRecords = $this->getOpenRecords($config->getAccount()->getId(), $config->getDevice()->getId());
        if ($config->getRefId()) {
            $openRecords = $openRecords->filter(HumanResources_Model_AttendanceRecord::FLD_REFID, $config->getRefId());
        }
        $refIds = [];
        $toBePaused = new Tinebase_Record_RecordSet(HumanResources_Model_AttendanceRecord::class);
        /** @var HumanResources_Model_AttendanceRecord $record */
        foreach ($openRecords as $record) {
            if (isset($refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}])) {
                continue;
            }
            $refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}] = true;
            if (HumanResources_Model_AttendanceRecord::TYPE_CLOCK_IN !== $openRecords->filter(
                    HumanResources_Model_AttendanceRecord::FLD_REFID,
                    $record->{HumanResources_Model_AttendanceRecord::FLD_REFID})->getLastRecord()
                    ->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) {
                continue;
            }
            $toBePaused->addRecord($record);
        }

        if (0 === $toBePaused->count()) {
            if ($config->getThrowOnFaultyAction()) {
                throw new Tinebase_Exception_UnexpectedValue('can\'t clock pause, no open records found for refId: ' .
                    $config->getRefId());
            }
            $config->setStatus(HumanResources_Model_AttendanceRecord::STATUS_FAULTY);
            $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_FAULTY_CLOCKS}->addRecord(
                $this->backend->create(
                    $this->createAttendanceRecord($config, HumanResources_Model_AttendanceRecord::TYPE_CLOCK_PAUSED)
                )
            );
        }

        /** @var HumanResources_Model_AttendanceRecorderDeviceRef $device */
        foreach ($config->getDevice()->{HumanResources_Model_AttendanceRecorderDevice::FLD_PAUSES} as $device) {
            /** @var HumanResources_Model_AttendanceRecorderDevice $device */
            $device = $device->{HumanResources_Model_AttendanceRecorderDeviceRef::FLD_DEVICE_ID};
            $pauseOpenRecords = $this->getOpenRecords($config->getAccount()->getId(), $device->getId());
            $refIds = [];
            /** @var HumanResources_Model_AttendanceRecord $record */
            foreach ($pauseOpenRecords as $record) {
                if (isset($refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}])) {
                    continue;
                }
                $refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}] = true;
                if (HumanResources_Model_AttendanceRecord::TYPE_CLOCK_IN !== $pauseOpenRecords->filter(
                        HumanResources_Model_AttendanceRecord::FLD_REFID,
                        $record->{HumanResources_Model_AttendanceRecord::FLD_REFID})->getLastRecord()
                        ->{HumanResources_Model_AttendanceRecord::FLD_TYPE}) {
                    continue;
                }
                $tmpCfg = clone $config;
                $tmpCfg->setMetaData(array_filter($tmpCfg->getMetaData() ?: [], function($key) {
                    return $key === HumanResources_Config_AttendanceRecorder::METADATA_SOURCE;
                }, ARRAY_FILTER_USE_KEY));
                $tmpCfg->setDevice($device);
                $tmpCfg->setAutogen(true);
                $tmpCfg->setRefId($record->{HumanResources_Model_AttendanceRecord::FLD_REFID});
                $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_PAUSES}->addRecord(
                    $this->backend->create(
                        $this->createAttendanceRecord($tmpCfg, HumanResources_Model_AttendanceRecord::TYPE_CLOCK_PAUSED)
                    )
                );
            }
        }

        $refIds = [];
        /** @var HumanResources_Model_AttendanceRecord $record */
        foreach ($toBePaused as $record) {
            if (isset($refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}])) {
                continue;
            }
            $refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}] = true;
            $config->setRefId($record->{HumanResources_Model_AttendanceRecord::FLD_REFID});
            $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_PAUSES}->addRecord(
                $this->backend->create(
                    $this->createAttendanceRecord($config, HumanResources_Model_AttendanceRecord::TYPE_CLOCK_PAUSED)
                )
            );
        }

        if ($outOfSequenceClosure) {
            $outOfSequenceClosure();
        }

        $raii->release();

        $result->reloadData();
        return $result;
    }

    public function clockOut(HumanResources_Config_AttendanceRecorder $config): HumanResources_Model_AttendanceRecorderClockInOutResult
    {
        if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__);
        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
            __METHOD__ . '::' . __LINE__ . ' ' . print_r($config->__serialize(), true)
        );

        $raii = Tinebase_RAII::getTransactionManagerRAII();

        $this->prepareConfig($config);
        if (null === $config->getStatus()) {
            $config->setStatus(HumanResources_Model_AttendanceRecord::STATUS_CLOSED);
        } elseif (HumanResources_Model_AttendanceRecord::STATUS_CLOSED !== $config->getStatus() &&
                HumanResources_Model_AttendanceRecord::STATUS_FAULTY !== $config->getStatus()) {
            throw new Tinebase_Exception_UnexpectedValue('can\'t clockOut with status other than ' .
                HumanResources_Model_AttendanceRecord::STATUS_CLOSED . ' or ' .
                HumanResources_Model_AttendanceRecord::STATUS_FAULTY);
        }

        $this->registerAfterCommitBLPipeRun($config->getAccount()->getId());
        $result = $this->getClockInOutResult();

        $outOfSequenceClosure = $this->checkOutOfSequence($config);

        $openRecords = $this->getOpenRecords($config->getAccount()->getId(), $config->getDevice()->getId());
        if ($config->getRefId()) {
            $toBeClosed = $openRecords->filter(HumanResources_Model_AttendanceRecord::FLD_REFID, $config->getRefId());
        } else {
            $toBeClosed = $openRecords;
        }

        if (0 === $toBeClosed->count()) {
            if ($config->getThrowOnFaultyAction()) {
                throw new Tinebase_Exception_UnexpectedValue('can\'t clock out, no open records found for refId: ' .
                    $config->getRefId());
            }
            $config->setStatus(HumanResources_Model_AttendanceRecord::STATUS_FAULTY);
            $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_FAULTY_CLOCKS}->addRecord(
                $this->backend->create(
                    $this->createAttendanceRecord($config, HumanResources_Model_AttendanceRecord::TYPE_CLOCK_OUT)
                )
            );
        }

        /** @var HumanResources_Model_AttendanceRecorderDeviceRef $device */
        foreach ($config->getDevice()->{HumanResources_Model_AttendanceRecorderDevice::FLD_STOPS} as $device) {
            /** @var HumanResources_Model_AttendanceRecorderDevice $device */
            $device = $device->{HumanResources_Model_AttendanceRecorderDeviceRef::FLD_DEVICE_ID};
            $stopOpenRecords = $this->getOpenRecords($config->getAccount()->getId(), $device->getId());
            $refIds = [];
            /** @var HumanResources_Model_AttendanceRecord $record */
            foreach ($stopOpenRecords as $record) {
                $record->{HumanResources_Model_AttendanceRecord::FLD_STATUS} = HumanResources_Model_AttendanceRecord::STATUS_CLOSED;
                $this->backend->update($record);
                if (isset($refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}])) {
                    continue;
                }
                $refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}] = true;
                $tmpCfg = clone $config;
                $tmpCfg->setMetaData(array_filter($tmpCfg->getMetaData() ?: [], function($key) {
                    return $key === HumanResources_Config_AttendanceRecorder::METADATA_SOURCE;
                }, ARRAY_FILTER_USE_KEY));
                $tmpCfg->setDevice($device);
                $tmpCfg->setAutogen(true);
                $tmpCfg->setRefId($record->{HumanResources_Model_AttendanceRecord::FLD_REFID});
                $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_OUTS}->addRecord(
                    $this->backend->create(
                        $this->createAttendanceRecord($tmpCfg, HumanResources_Model_AttendanceRecord::TYPE_CLOCK_OUT)
                    )
                );
            }
        }

        $refIds = [];
        $orgConfig = clone $config;
        /** @var HumanResources_Model_AttendanceRecord $record */
        foreach ($toBeClosed as $record) {
            $record->{HumanResources_Model_AttendanceRecord::FLD_STATUS} = HumanResources_Model_AttendanceRecord::STATUS_CLOSED;
            $this->backend->update($record);
            if (isset($refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}])) {
                continue;
            }
            $refIds[$record->{HumanResources_Model_AttendanceRecord::FLD_REFID}] = true;
            $config->setRefId($record->{HumanResources_Model_AttendanceRecord::FLD_REFID});
            $result->{HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_OUTS}->addRecord(
                $this->backend->create(
                    $this->createAttendanceRecord($config, HumanResources_Model_AttendanceRecord::TYPE_CLOCK_OUT, $orgConfig)
                )
            );
        }

        if ($outOfSequenceClosure) {
            $outOfSequenceClosure();
        }

        $raii->release();

        $result->reloadData();
        return $result;
    }

    protected function getClockInOutResult(): HumanResources_Model_AttendanceRecorderClockInOutResult
    {
        return new HumanResources_Model_AttendanceRecorderClockInOutResult([
            HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_INS =>
                new Tinebase_Record_RecordSet(HumanResources_Model_AttendanceRecord::class, []),
            HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_OUTS =>
                new Tinebase_Record_RecordSet(HumanResources_Model_AttendanceRecord::class, []),
            HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_CLOCK_PAUSES =>
                new Tinebase_Record_RecordSet(HumanResources_Model_AttendanceRecord::class, []),
            HumanResources_Model_AttendanceRecorderClockInOutResult::FLD_FAULTY_CLOCKS =>
                new Tinebase_Record_RecordSet(HumanResources_Model_AttendanceRecord::class, []),
        ]);
    }

    protected function registerAfterCommitBLPipeRun(string $accountId): void
    {
        Tinebase_TransactionManager::getInstance()->registerAfterCommitCallback(function() use($accountId) {
            $oldContext = $context = (array)HumanResources_Controller_DailyWTReport::getInstance()->getRequestContext();
            $context['tsSyncron'] = true;
            HumanResources_Controller_DailyWTReport::getInstance()->setRequestContext($context);
            try {
                HumanResources_Controller_AttendanceRecorder::runBLPipes($accountId);
            } finally {
                HumanResources_Controller_DailyWTReport::getInstance()->setRequestContext($oldContext);
            }
        });
    }

    protected function createAttendanceRecord(HumanResources_Config_AttendanceRecorder $config, string $type, ?HumanResources_Config_AttendanceRecorder $orgConfig = null): HumanResources_Model_AttendanceRecord
    {
        return new HumanResources_Model_AttendanceRecord([
            HumanResources_Model_AttendanceRecord::FLD_TIMESTAMP => $config->getTimeStamp()->getClone(),
            HumanResources_Model_AttendanceRecord::FLD_TYPE => $type,
            HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID => $config->getAccount()->getId(),
            HumanResources_Model_AttendanceRecord::FLD_DEVICE_ID => $config->getDevice()->getId(),
            HumanResources_Model_AttendanceRecord::FLD_STATUS => $config->getStatus(),
            HumanResources_Model_AttendanceRecord::FLD_REFID => $config->getRefId() ?: Tinebase_Record_Abstract::generateUID(),
            HumanResources_Model_AttendanceRecord::FLD_FREETIMETYPE_ID => $config->getFreetimetypeId(),
            HumanResources_Model_AttendanceRecord::FLD_CREATION_CONFIG => serialize($orgConfig ?: $config),
            HumanResources_Model_AttendanceRecord::FLD_AUTOGEN => $config->getAutogen(),
            'xprops' => [HumanResources_Model_AttendanceRecord::META_DATA => $config->getMetaData()],
        ]);
    }

    protected function getOpenRecords(string $accountId, string $deviceId, array $type = []): Tinebase_Record_RecordSet
    {
        return $this->backend->search(Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_AttendanceRecord::class, array_merge([
                ['field' => HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID, 'operator' => 'equals', 'value' => $accountId],
                ['field' => HumanResources_Model_AttendanceRecord::FLD_DEVICE_ID,  'operator' => 'equals', 'value' => $deviceId],
                ['field' => HumanResources_Model_AttendanceRecord::FLD_STATUS,  'operator' => 'equals', 'value' => HumanResources_Model_AttendanceRecord::STATUS_OPEN],
            ], (empty($type) ? [] :
                [['field' => HumanResources_Model_AttendanceRecord::FLD_TYPE,  'operator' => 'in', 'value' => $type]])
        )), new Tinebase_Model_Pagination(['sort' => HumanResources_Model_AttendanceRecord::FLD_SEQUENCE, 'dir' => 'ASC']));
    }

    protected function prepareConfig(HumanResources_Config_AttendanceRecorder $config): void
    {
        if (null === $config->getDevice()) {
            throw new Tinebase_Exception_UnexpectedValue('device needs to be give');
        }
        Tinebase_Record_Expander::expandRecord($config->getDevice());

        if ($e = $config->getEmployee()) {
            if ($config->getAccount()) {
                if ($config->getAccount()->getId() !== $e->account_id) {
                    throw new Tinebase_Exception_UnexpectedValue(HumanResources_Config_AttendanceRecorder::class .
                        ' employee and account mismatch');
                }
            } else {
                $config->setAccount(Tinebase_User::getInstance()->getFullUserById($e->account_id));
            }
        } else {
            if (!$config->getAccount()) {
                $config->setAccount(Tinebase_Core::getUser());
            }
            $employeeCtrl = HumanResources_Controller_Employee::getInstance();
            $employeeCtrlRaii = new Tinebase_RAII($employeeCtrl->assertPublicUsage());
            /** @var ?HumanResources_Model_Employee $e */
            $e = $employeeCtrl->search(
                Tinebase_Model_Filter_FilterGroup::getFilterForModel(HumanResources_Model_Employee::class, [
                    ['field' => 'account_id', 'operator' => 'equals', 'value' => $config->getAccount()->getId()],
                ])
            )->getFirstRecord();
            unset($employeeCtrlRaii);
            $config->setEmployee($e);
        }

        if (null === $config->getTimeStamp()) {
            $config->setTimeStamp(Tinebase_DateTime::now());
        }
    }

    public static function runBLPipes(?string $accountId = null): bool
    {
        $deviceCtrl = HumanResources_Controller_AttendanceRecorderDevice::getInstance();
        $recordCtrl = HumanResources_Controller_AttendanceRecord::getInstance();

        $devices = $deviceCtrl->getAll();
        /** @var HumanResources_Model_AttendanceRecorderDevice $device */
        foreach ($devices as $device) {
            if (empty($device->{HumanResources_Model_AttendanceRecorderDevice::FLD_BLPIPE}) ||
                    0 === $device->{HumanResources_Model_AttendanceRecorderDevice::FLD_BLPIPE}->count()) {
                continue;
            }

            $transaction = Tinebase_RAII::getTransactionManagerRAII();

            /** @var Tinebase_Backend_Sql_Abstract $backend */
            $backend = $recordCtrl->getBackend();
            $selectForUpdate = Tinebase_Backend_Sql_SelectForUpdateHook::getRAII($backend);

            $data = $recordCtrl->search(Tinebase_Model_Filter_FilterGroup::getFilterForModel(
                HumanResources_Model_AttendanceRecord::class, array_merge([
                ['field' => HumanResources_Model_AttendanceRecord::FLD_DEVICE_ID, 'operator' => 'equals', 'value' => $device->getId()],
                ['field' => HumanResources_Model_AttendanceRecord::FLD_BLPROCESSED, 'operator' => 'equals', 'value' => false],
            ], $accountId ? [
                ['field' => HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID, 'operator' => 'equals', 'value' => $accountId],
            ] : [])), new Tinebase_Model_Pagination(['sort' => [HumanResources_Model_AttendanceRecord::FLD_ACCOUNT_ID, HumanResources_Model_AttendanceRecord::FLD_SEQUENCE], 'dir' => 'ASC']));

            unset($selectForUpdate);

            if (0 === $data->count()) {
                $transaction->release();
                continue;
            }
            $data = new HumanResources_BL_AttendanceRecorder_Data($data);

            $bl = new Tinebase_BL_Pipe($device->{HumanResources_Model_AttendanceRecorderDevice::FLD_BLPIPE});
            $bl->execute($data);

            /** @var HumanResources_Model_AttendanceRecord $record */
            foreach ($data->data as $record) {
                if ($record->isDirty()) {
                    $recordCtrl->update($record);
                }
            }

            $transaction->release();
        }

        return true;
    }
}