Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.18% covered (success)
98.18%
54 / 55
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
UniversalPolymorphicRelationService
100.00% covered (success)
100.00%
54 / 54
100.00% covered (success)
100.00%
6 / 6
27
100.00% covered (success)
100.00%
1 / 1
 synchronizePolymorphicColumns
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 syncRelatedPartyRef
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 syncProcessRef
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 syncSubprocessRef
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 resolveRefId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 autoCalculateWorkTimeDuration
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
14
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Engine\Application\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Application\Transformer\PolymorphicRelationTransformer;
12
13/**
14 * Handles polymorphic foreign key column synchronization and work time duration calculations.
15 */
16final readonly class UniversalPolymorphicRelationService
17{
18    /**
19     * Automatically synchronizes integer foreign key columns from polymorphic reference values.
20     *
21     * @param string               $moduleName Active module name.
22     * @param array<string, mixed> $data       Writable field data to synchronize.
23     * @param array<string, mixed> $snapshot   Existing record snapshot for updates.
24     */
25    public function synchronizePolymorphicColumns(
26        string $moduleName,
27        array &$data,
28        array $snapshot = []
29    ): void {
30        if ($moduleName !== 'calendar' && $moduleName !== 'work_time') {
31            return;
32        }
33
34        $this->syncRelatedPartyRef($data);
35        $this->syncProcessRef($data);
36        $this->syncSubprocessRef($data);
37
38        if ($moduleName === 'work_time') {
39            $this->autoCalculateWorkTimeDuration($data, $snapshot);
40        }
41    }
42
43    /**
44     * Synchronizes related party foreign keys (company, partner, contact).
45     *
46     * @param array<string, mixed> $data Field data.
47     */
48    private function syncRelatedPartyRef(array &$data): void
49    {
50        if (!array_key_exists('related_party_ref', $data)) {
51            return;
52        }
53
54        $parsed = PolymorphicRelationTransformer::parse($data['related_party_ref'] ?? null);
55        $data['company_id'] = $this->resolveRefId($parsed, 'companies');
56        $data['partner_id'] = $this->resolveRefId($parsed, 'partners');
57        $data['contact_id'] = $this->resolveRefId($parsed, 'contacts');
58    }
59
60    /**
61     * Synchronizes process foreign keys (project, contract).
62     *
63     * @param array<string, mixed> $data Field data.
64     */
65    private function syncProcessRef(array &$data): void
66    {
67        if (!array_key_exists('process_ref', $data)) {
68            return;
69        }
70
71        $parsed = PolymorphicRelationTransformer::parse($data['process_ref'] ?? null);
72        $data['project_id'] = $this->resolveRefId($parsed, 'projects');
73        $data['contract_id'] = $this->resolveRefId($parsed, 'contracts');
74    }
75
76    /**
77     * Synchronizes subprocess foreign keys (ticket, task, stage).
78     *
79     * @param array<string, mixed> $data Field data.
80     */
81    private function syncSubprocessRef(array &$data): void
82    {
83        if (!array_key_exists('subprocess_ref', $data)) {
84            return;
85        }
86
87        $parsed = PolymorphicRelationTransformer::parse($data['subprocess_ref'] ?? null);
88        $data['ticket_id'] = $this->resolveRefId($parsed, 'tickets');
89        $data['task_id'] = $this->resolveRefId($parsed, 'project_tasks');
90        $data['stage_id'] = $this->resolveRefId($parsed, 'project_stages');
91    }
92
93    /**
94     * Resolves integer ID if parsed reference matches the expected module.
95     *
96     * @param array{module: string, id: int}|null $parsed         Parsed reference.
97     * @param string                              $expectedModule Machine name of expected target module.
98     * @return int|null Record ID or null.
99     */
100    private function resolveRefId(?array $parsed, string $expectedModule): ?int
101    {
102        if ($parsed !== null && $parsed['module'] === $expectedModule) {
103            return $parsed['id'];
104        }
105
106        return null;
107    }
108
109    /**
110     * Automatically calculates duration and billing amount for work_time records.
111     *
112     * @param array<string, mixed> $data     Work time data payload.
113     * @param array<string, mixed> $snapshot Existing record snapshot (for partial update).
114     */
115    private function autoCalculateWorkTimeDuration(array &$data, array $snapshot = []): void
116    {
117        $startDate = $data['start_date'] ?? $snapshot['start_date'] ?? null;
118        $endDate   = $data['end_date'] ?? $snapshot['end_date'] ?? null;
119        if (empty($startDate) || empty($endDate)) {
120            return;
121        }
122
123        $startTs = strtotime((string) $startDate);
124        $endTs   = strtotime((string) $endDate);
125        if ($startTs <= 0 || $endTs < $startTs) {
126            $data['duration_minutes'] = 0;
127            $data['duration_hours']   = 0.00;
128            return;
129        }
130
131        $rawAllDay = $data['is_all_day'] ?? $snapshot['is_all_day'] ?? false;
132        $isAllDay  = $rawAllDay === true || $rawAllDay === 1 || $rawAllDay === '1';
133
134        if ($isAllDay) {
135            $diffDays = max(1, (int) ceil(($endTs - $startTs) / 86400));
136            $data['duration_hours']   = round($diffDays * 8.00, 2);
137            $data['duration_minutes'] = (int) ($data['duration_hours'] * 60);
138        } else {
139            $diffMins = (int) round(($endTs - $startTs) / 60);
140            $data['duration_minutes'] = max(0, $diffMins);
141            $data['duration_hours']   = round($data['duration_minutes'] / 60, 2);
142        }
143
144        $hourlyRate  = $data['hourly_rate'] ?? $snapshot['hourly_rate'] ?? null;
145        $rawBillable = $data['is_billable'] ?? $snapshot['is_billable'] ?? true;
146        $isBillable  = $rawBillable === true || $rawBillable === 1 || $rawBillable === '1';
147
148        if ($hourlyRate !== null && is_numeric($hourlyRate)) {
149            $rate = (float) $hourlyRate;
150            $data['total_amount'] = ($isBillable && $rate > 0)
151                ? round($data['duration_hours'] * $rate, 2)
152                : 0.00;
153        }
154    }
155}