Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.50% covered (success)
95.50%
106 / 111
75.00% covered (warning)
75.00%
9 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommentHierarchyRollupService
95.45% covered (success)
95.45%
105 / 110
75.00% covered (warning)
75.00%
9 / 12
45
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveHierarchy
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
10
 resolveCompany
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 resolvePartner
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 resolveContact
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 resolveProject
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 resolveContract
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 resolveTicket
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 resolveTask
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
4.01
 resolveStage
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 applyPartyFromRow
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
5.68
 applyRelationRef
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
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\Modules\Comments\Application\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use PDO;
12
13/**
14 * Service resolving multi-level parent hierarchy rollup for comment references.
15 *
16 * Automatically traces and cascades foreign keys upwards:
17 * e.g., Ticket -> Contract/Project -> Company/Contact.
18 *
19 * @package App\Modules\Comments\Application\Service
20 */
21final readonly class CommentHierarchyRollupService implements CommentHierarchyRollupServiceInterface
22{
23    private const string MOD_COMPANIES = 'companies';
24    private const string MOD_PARTNERS = 'partners';
25    private const string MOD_CONTACTS = 'contacts';
26    private const string MOD_PROJECTS = 'projects';
27    private const string MOD_CONTRACTS = 'contracts';
28    private const string MOD_TICKETS = 'tickets';
29    private const string MOD_PROJECT_TASKS = 'project_tasks';
30    private const string MOD_PROJECT_STAGES = 'project_stages';
31
32    /**
33     * CommentHierarchyRollupService constructor.
34     */
35    public function __construct(
36        private PDO $pdo
37    ) {
38    }
39
40    /**
41     * Resolves upward hierarchy references based on target module and record ID.
42     *
43     * @param string $module   Target module name.
44     * @param int    $recordId Target record ID.
45     * @return array{
46     *     related_party_ref: string|null,
47     *     company_id: int|null,
48     *     partner_id: int|null,
49     *     contact_id: int|null,
50     *     process_ref: string|null,
51     *     project_id: int|null,
52     *     contract_id: int|null,
53     *     subprocess_ref: string|null,
54     *     ticket_id: int|null,
55     *     task_id: int|null,
56     *     stage_id: int|null
57     * }
58     */
59    public function resolveHierarchy(string $module, int $recordId): array
60    {
61        $hierarchy = [
62            'related_party_ref' => null,
63            'company_id'        => null,
64            'partner_id'        => null,
65            'contact_id'        => null,
66            'process_ref'       => null,
67            'project_id'        => null,
68            'contract_id'       => null,
69            'subprocess_ref'    => null,
70            'ticket_id'         => null,
71            'task_id'           => null,
72            'stage_id'          => null,
73        ];
74
75        match ($module) {
76            self::MOD_COMPANIES      => $this->resolveCompany($recordId, $hierarchy),
77            self::MOD_PARTNERS       => $this->resolvePartner($recordId, $hierarchy),
78            self::MOD_CONTACTS       => $this->resolveContact($recordId, $hierarchy),
79            self::MOD_PROJECTS       => $this->resolveProject($recordId, $hierarchy),
80            self::MOD_CONTRACTS      => $this->resolveContract($recordId, $hierarchy),
81            self::MOD_TICKETS        => $this->resolveTicket($recordId, $hierarchy),
82            self::MOD_PROJECT_TASKS  => $this->resolveTask($recordId, $hierarchy),
83            self::MOD_PROJECT_STAGES => $this->resolveStage($recordId, $hierarchy),
84            default                  => null,
85        };
86
87        return $hierarchy;
88    }
89
90    /**
91     * @param array<string, mixed> $h
92     */
93    private function resolveCompany(int $id, array &$h): void
94    {
95        $h['related_party_ref'] = self::MOD_COMPANIES . ':' . $id;
96        $h['company_id'] = $id;
97    }
98
99    /**
100     * @param array<string, mixed> $h
101     */
102    private function resolvePartner(int $id, array &$h): void
103    {
104        $h['related_party_ref'] = self::MOD_PARTNERS . ':' . $id;
105        $h['partner_id'] = $id;
106    }
107
108    /**
109     * @param array<string, mixed> $h
110     */
111    private function resolveContact(int $id, array &$h): void
112    {
113        $h['related_party_ref'] = self::MOD_CONTACTS . ':' . $id;
114        $h['contact_id'] = $id;
115
116        $stmt = $this->pdo->prepare('SELECT company_id FROM c_mod_contacts_records WHERE id = :id');
117        $stmt->execute([':id' => $id]);
118        $companyId = $stmt->fetchColumn();
119        if ($companyId !== false && $companyId !== null && (int) $companyId > 0) {
120            $h['company_id'] = (int) $companyId;
121        }
122    }
123
124    /**
125     * @param array<string, mixed> $h
126     */
127    private function resolveProject(int $id, array &$h): void
128    {
129        $h['process_ref'] = self::MOD_PROJECTS . ':' . $id;
130        $h['project_id'] = $id;
131
132        $stmt = $this->pdo->prepare(
133            'SELECT company_id, contract_id, relation_ref FROM c_mod_projects_records WHERE id = :id'
134        );
135        $stmt->execute([':id' => $id]);
136        $row = $stmt->fetch(PDO::FETCH_ASSOC);
137        if ($row !== false) {
138            $this->applyPartyFromRow($row, $h);
139            if (!empty($row['contract_id'])) {
140                $h['contract_id'] = (int) $row['contract_id'];
141            }
142            if (!empty($row['relation_ref']) && $h['related_party_ref'] === null) {
143                $this->applyRelationRef((string) $row['relation_ref'], $h);
144            }
145        }
146    }
147
148    /**
149     * @param array<string, mixed> $h
150     */
151    private function resolveContract(int $id, array &$h): void
152    {
153        $h['process_ref'] = self::MOD_CONTRACTS . ':' . $id;
154        $h['contract_id'] = $id;
155
156        $stmt = $this->pdo->prepare(
157            'SELECT company_id, contact_id FROM c_mod_contracts_records WHERE id = :id'
158        );
159        $stmt->execute([':id' => $id]);
160        $row = $stmt->fetch(PDO::FETCH_ASSOC);
161        if ($row !== false) {
162            $this->applyPartyFromRow($row, $h);
163        }
164    }
165
166    /**
167     * @param array<string, mixed> $h
168     */
169    private function resolveTicket(int $id, array &$h): void
170    {
171        $h['subprocess_ref'] = self::MOD_TICKETS . ':' . $id;
172        $h['ticket_id'] = $id;
173
174        $stmt = $this->pdo->prepare(
175            'SELECT company_id, contract_id FROM c_mod_tickets_records WHERE id = :id'
176        );
177        $stmt->execute([':id' => $id]);
178        $row = $stmt->fetch(PDO::FETCH_ASSOC);
179        if ($row === false) {
180            return;
181        }
182
183        $this->applyPartyFromRow($row, $h);
184
185        if (!empty($row['contract_id'])) {
186            $h['contract_id'] = (int) $row['contract_id'];
187            $h['process_ref'] = self::MOD_CONTRACTS . ':' . $row['contract_id'];
188        }
189    }
190
191    /**
192     * @param array<string, mixed> $h
193     */
194    private function resolveTask(int $id, array &$h): void
195    {
196        $h['subprocess_ref'] = self::MOD_PROJECT_TASKS . ':' . $id;
197        $h['task_id'] = $id;
198
199        $stmt = $this->pdo->prepare(
200            'SELECT stage_id, project_id FROM c_mod_project_tasks_records WHERE id = :id'
201        );
202        $stmt->execute([':id' => $id]);
203        $row = $stmt->fetch(PDO::FETCH_ASSOC);
204        if ($row === false) {
205            return;
206        }
207
208        if (!empty($row['stage_id'])) {
209            $h['stage_id'] = (int) $row['stage_id'];
210        }
211        if (!empty($row['project_id'])) {
212            $this->resolveProject((int) $row['project_id'], $h);
213        }
214    }
215
216    /**
217     * @param array<string, mixed> $h
218     */
219    private function resolveStage(int $id, array &$h): void
220    {
221        $h['subprocess_ref'] = self::MOD_PROJECT_STAGES . ':' . $id;
222        $h['stage_id'] = $id;
223
224        $stmt = $this->pdo->prepare(
225            'SELECT project_id FROM c_mod_project_stages_records WHERE id = :id'
226        );
227        $stmt->execute([':id' => $id]);
228        $projectId = $stmt->fetchColumn();
229        if ($projectId !== false && $projectId !== null && (int) $projectId > 0) {
230            $this->resolveProject((int) $projectId, $h);
231        }
232    }
233
234    /**
235     * @param array<string, mixed> $row
236     * @param array<string, mixed> $h
237     */
238    private function applyPartyFromRow(array $row, array &$h): void
239    {
240        if (!empty($row['company_id'])) {
241            $h['company_id'] = (int) $row['company_id'];
242            $h['related_party_ref'] = self::MOD_COMPANIES . ':' . $row['company_id'];
243        } elseif (!empty($row['partner_id'])) {
244            $h['partner_id'] = (int) $row['partner_id'];
245            $h['related_party_ref'] = self::MOD_PARTNERS . ':' . $row['partner_id'];
246        }
247
248        if (!empty($row['contact_id'])) {
249            $h['contact_id'] = (int) $row['contact_id'];
250            if ($h['related_party_ref'] === null) {
251                $h['related_party_ref'] = self::MOD_CONTACTS . ':' . $row['contact_id'];
252            }
253        }
254    }
255
256    /**
257     * @param array<string, mixed> $h
258     */
259    private function applyRelationRef(string $ref, array &$h): void
260    {
261        $parts = explode(':', $ref, 2);
262        if (count($parts) === 2 && is_numeric($parts[1])) {
263            $h['related_party_ref'] = $ref;
264            if ($parts[0] === self::MOD_COMPANIES) {
265                $h['company_id'] = (int) $parts[1];
266            } elseif ($parts[0] === self::MOD_PARTNERS) {
267                $h['partner_id'] = (int) $parts[1];
268            }
269        }
270    }
271}