Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.00% covered (success)
94.00%
47 / 50
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SystemLevelResolver
95.92% covered (success)
95.92%
47 / 49
66.67% covered (warning)
66.67%
4 / 6
15
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
 isLevelScopedModule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolvePrefix
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 resolveTableName
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 adaptFieldsForLevel
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 getAvailableLevels
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
4
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\Domain\Model\FieldMetadata;
12use App\Core\Engine\Domain\Model\ModuleMetadata;
13use PDO;
14
15/**
16 * System Level Resolver.
17 *
18 * Manages operational system levels (admin, client, helpdesk, shop)
19 * across log and audit modules, dynamically adjusting database tables and foreign keys.
20 *
21 * @package App\Core\Engine\Application\Service
22 */
23final readonly class SystemLevelResolver implements SystemLevelResolverInterface
24{
25    public const string DEFAULT_LEVEL = 'admin';
26
27    /** @var list<string> Module names that support level switching. */
28    public const array SCOPED_MODULES = [
29        'logs_auth',
30        'logs_sessions',
31        'logs_audit_create',
32        'logs_audit_read',
33        'logs_audit_update',
34        'logs_audit_delete',
35    ];
36
37    /** @var array<string, string> Mapping of system level to table prefix. */
38    private const array LEVEL_PREFIX_MAP = [
39        'admin'    => 'a_',
40        'client'   => 'c_',
41        'helpdesk' => 'h_',
42        'shop'     => 's_',
43    ];
44
45    /**
46     * SystemLevelResolver constructor.
47     *
48     * @param PDO|null $pdo Database connection handle for loading picklist values.
49     */
50    public function __construct(private ?PDO $pdo = null)
51    {
52    }
53
54    /**
55     * {@inheritdoc}
56     */
57    public function isLevelScopedModule(string $moduleName): bool
58    {
59        return in_array($moduleName, self::SCOPED_MODULES, true);
60    }
61
62    /**
63     * {@inheritdoc}
64     */
65    public function resolvePrefix(string $level): string
66    {
67        $normalized = strtolower(trim($level));
68        return self::LEVEL_PREFIX_MAP[$normalized] ?? self::LEVEL_PREFIX_MAP[self::DEFAULT_LEVEL];
69    }
70
71    /**
72     * {@inheritdoc}
73     */
74    public function resolveTableName(ModuleMetadata $module, string $level): string
75    {
76        if (!$this->isLevelScopedModule($module->name)) {
77            return $module->tableName;
78        }
79
80        $targetPrefix = $this->resolvePrefix($level);
81        $baseName = $module->tableName;
82
83        if (preg_match('/^[a-z]_/', $baseName) === 1) {
84            return $targetPrefix . substr($baseName, 2);
85        }
86
87        return $baseName;
88    }
89
90    /**
91     * {@inheritdoc}
92     */
93    public function adaptFieldsForLevel(array $fields, string $level): array
94    {
95        $targetPrefix = $this->resolvePrefix($level);
96        $targetUserTable = $targetPrefix . 'mod_users_records';
97        $adapted = [];
98
99        foreach ($fields as $field) {
100            if ($field->hasRelation()
101                && $field->relationTable !== null
102                && str_ends_with($field->relationTable, 'mod_users_records')
103            ) {
104                $adapted[] = $field->withRelationTable($targetUserTable);
105                continue;
106            }
107            $adapted[] = $field;
108        }
109
110        return $adapted;
111    }
112
113    /**
114     * {@inheritdoc}
115     */
116    public function getAvailableLevels(): array
117    {
118        if ($this->pdo !== null) {
119            try {
120                $stmt = $this->pdo->prepare(
121                    'SELECT v.`value`, v.`label`, v.`color`, v.`icon_class` AS `icon` '
122                    . 'FROM `a_core_picklist_records` p '
123                    . 'JOIN `a_core_picklist_value_records` v ON v.`picklist_id` = p.`id` '
124                    . 'WHERE p.`name` = :name AND v.`is_active` = 1 AND v.`is_pending_delete` = 0 '
125                    . 'ORDER BY v.`sort_order` ASC'
126                );
127                $stmt->execute([':name' => 'system_level']);
128                /** @var list<array{value: string, label: string, color: ?string, icon: ?string}> $rows */
129                $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
130                if (!empty($rows)) {
131                    return $rows;
132                }
133            } catch (\Throwable) {
134                // Fallback to defaults if table is not reachable
135            }
136        }
137
138        return [
139            [
140                'value' => 'admin',
141                'label' => 'Admin',
142                'color' => '#0d6efd',
143                'icon'  => 'bi bi-shield-lock',
144            ],
145            [
146                'value' => 'client',
147                'label' => 'Client',
148                'color' => '#198754',
149                'icon'  => 'bi bi-people',
150            ],
151        ];
152    }
153}