Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.65% covered (success)
95.65%
132 / 138
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqlTaxGroupRepository
95.62% covered (success)
95.62%
131 / 137
62.50% covered (warning)
62.50%
5 / 8
33
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
 findById
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
5
 findByCode
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
5
 findAll
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
8
 save
78.95% covered (warning)
78.95%
15 / 19
0.00% covered (danger)
0.00%
0 / 1
6.34
 delete
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 loadGroupItems
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
2
 saveGroupItems
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
5.01
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\Tax\Infrastructure\Repository;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Instance\Application\Service\InstanceContextManagerInterface;
12use App\Modules\Tax\Domain\Model\TaxGroup;
13use App\Modules\Tax\Domain\Model\TaxGroupItem;
14use App\Modules\Tax\Domain\Repository\TaxGroupRepositoryInterface;
15use App\Core\Database\Repository\TenantAwareRepositoryTrait;
16use App\Modules\Tax\Domain\Repository\TaxRateRepositoryInterface;
17use PDO;
18
19/**
20 * SQL/PDO repository for managing TaxGroup aggregates and their assigned rates.
21 */
22final readonly class SqlTaxGroupRepository implements TaxGroupRepositoryInterface
23{
24    use TenantAwareRepositoryTrait;
25
26    private const string PARAM_GROUP_ID = ':group_id';
27    /**
28     * SqlTaxGroupRepository constructor.
29     */
30    public function __construct(
31        protected PDO $pdo,
32        private TaxRateRepositoryInterface $taxRateRepo,
33        private string $tablePrefix = 'a_',
34        protected ?InstanceContextManagerInterface $instanceManager = null,
35        protected ?PDO $clientPdo = null
36    ) {
37    }
38
39    /**
40     * @inheritDoc
41     */
42    public function findById(int $id): ?TaxGroup
43    {
44        $table = $this->tablePrefix . 'mod_tax_groups_records';
45        $sql = "SELECT `id`, `group_code`, `group_name`, `description`, `is_active`, `owner`,
46                       `created_at`, `updated_at`
47                FROM `{$table}`
48                WHERE `id` = :id AND `special_access` != 0
49                LIMIT 1";
50
51        $stmt = $this->getPdo()->prepare($sql);
52        $stmt->bindValue(':id', $id, PDO::PARAM_INT);
53        $stmt->execute();
54        $row = $stmt->fetch(PDO::FETCH_ASSOC);
55
56        if (!$row) {
57            return null;
58        }
59
60        $items = $this->loadGroupItems((int) $row['id']);
61
62        return new TaxGroup(
63            id: (int) $row['id'],
64            groupCode: (string) $row['group_code'],
65            groupName: (string) $row['group_name'],
66            description: $row['description'] !== null ? (string) $row['description'] : null,
67            isActive: (bool) $row['is_active'],
68            items: $items,
69            owner: (int) ($row['owner'] ?? 1),
70            createdAt: isset($row['created_at']) ? (string) $row['created_at'] : null,
71            updatedAt: isset($row['updated_at']) ? (string) $row['updated_at'] : null,
72        );
73    }
74
75    /**
76     * @inheritDoc
77     */
78    public function findByCode(string $code): ?TaxGroup
79    {
80        $table = $this->tablePrefix . 'mod_tax_groups_records';
81        $sql = "SELECT `id`, `group_code`, `group_name`, `description`, `is_active`, `owner`,
82                       `created_at`, `updated_at`
83                FROM `{$table}`
84                WHERE `group_code` = :code AND `special_access` != 0
85                LIMIT 1";
86
87        $stmt = $this->getPdo()->prepare($sql);
88        $stmt->bindValue(':code', $code, PDO::PARAM_STR);
89        $stmt->execute();
90        $row = $stmt->fetch(PDO::FETCH_ASSOC);
91
92        if (!$row) {
93            return null;
94        }
95
96        $items = $this->loadGroupItems((int) $row['id']);
97
98        return new TaxGroup(
99            id: (int) $row['id'],
100            groupCode: (string) $row['group_code'],
101            groupName: (string) $row['group_name'],
102            description: $row['description'] !== null ? (string) $row['description'] : null,
103            isActive: (bool) $row['is_active'],
104            items: $items,
105            owner: (int) ($row['owner'] ?? 1),
106            createdAt: isset($row['created_at']) ? (string) $row['created_at'] : null,
107            updatedAt: isset($row['updated_at']) ? (string) $row['updated_at'] : null,
108        );
109    }
110
111    /**
112     * @inheritDoc
113     */
114    public function findAll(array $filters = []): array
115    {
116        $table = $this->tablePrefix . 'mod_tax_groups_records';
117        $where = ['`special_access` != 0'];
118        $params = [];
119
120        if (isset($filters['is_active'])) {
121            $where[] = '`is_active` = :is_active';
122            $params[':is_active'] = (int) $filters['is_active'];
123        }
124
125        if (!empty($filters['tax_status'])) {
126            $where[] = '`tax_status` = :tax_status';
127            $params[':tax_status'] = (string) $filters['tax_status'];
128        }
129
130        $whereSql = implode(' AND ', $where);
131        $sql = "SELECT `id`, `group_code`, `group_name`, `description`, `is_active`, `owner`,
132                       `created_at`, `updated_at`
133                FROM `{$table}`
134                WHERE {$whereSql}
135                ORDER BY `id` ASC";
136
137        $stmt = $this->getPdo()->prepare($sql);
138        foreach ($params as $key => $val) {
139            $stmt->bindValue($key, $val);
140        }
141        $stmt->execute();
142
143        $groups = [];
144        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
145            $items = $this->loadGroupItems((int) $row['id']);
146            $groups[] = new TaxGroup(
147                id: (int) $row['id'],
148                groupCode: (string) $row['group_code'],
149                groupName: (string) $row['group_name'],
150                description: $row['description'] !== null ? (string) $row['description'] : null,
151                isActive: (bool) $row['is_active'],
152                items: $items,
153                owner: (int) ($row['owner'] ?? 1),
154                createdAt: isset($row['created_at']) ? (string) $row['created_at'] : null,
155                updatedAt: isset($row['updated_at']) ? (string) $row['updated_at'] : null,
156            );
157        }
158
159        return $groups;
160    }
161
162    /**
163     * @inheritDoc
164     */
165    public function save(TaxGroup $taxGroup): TaxGroup
166    {
167        $table = $this->tablePrefix . 'mod_tax_groups_records';
168
169        if ($taxGroup->id !== null && $taxGroup->id > 0) {
170            $sql = "UPDATE `{$table}` SET
171                        `group_code` = :group_code,
172                        `group_name` = :group_name,
173                        `description` = :description,
174                        `is_active` = :is_active,
175                        `owner` = :owner
176                    WHERE `id` = :id";
177            $stmt = $this->getPdo()->prepare($sql);
178            $stmt->bindValue(':id', $taxGroup->id, PDO::PARAM_INT);
179        } else {
180            $sql = "INSERT INTO `{$table}` (`group_code`, `group_name`, `description`, `is_active`, `owner`)
181                    VALUES (:group_code, :group_name, :description, :is_active, :owner)";
182            $stmt = $this->getPdo()->prepare($sql);
183        }
184
185        $stmt->bindValue(':group_code', $taxGroup->groupCode, PDO::PARAM_STR);
186        $stmt->bindValue(':group_name', $taxGroup->groupName, PDO::PARAM_STR);
187        $stmt->bindValue(':description', $taxGroup->description, PDO::PARAM_STR);
188        $stmt->bindValue(':is_active', $taxGroup->isActive ? 1 : 0, PDO::PARAM_INT);
189        $stmt->bindValue(':owner', $taxGroup->owner, PDO::PARAM_INT);
190        $stmt->execute();
191
192        if ($taxGroup->id === null || $taxGroup->id <= 0) {
193            $taxGroup->id = (int) $this->getPdo()->lastInsertId();
194        }
195
196        $this->saveGroupItems($taxGroup->id, $taxGroup->items);
197
198        return $taxGroup;
199    }
200
201    /**
202     * @inheritDoc
203     */
204    public function delete(int $id): bool
205    {
206        $table = $this->tablePrefix . 'mod_tax_groups_records';
207        $sql = "UPDATE `{$table}` SET `special_access` = 0 WHERE `id` = :id";
208        $stmt = $this->getPdo()->prepare($sql);
209        $stmt->bindValue(':id', $id, PDO::PARAM_INT);
210
211        return $stmt->execute();
212    }
213
214    /**
215     * Load assigned tax rate items for a group.
216     *
217     * @return TaxGroupItem[]
218     */
219    private function loadGroupItems(int $groupId): array
220    {
221        $table = $this->tablePrefix . 'mod_tax_group_items';
222        $sql = "SELECT `id`, `tax_group_id`, `tax_rate_id`, `sequence_order`, `is_compound`
223                FROM `{$table}`
224                WHERE `tax_group_id` = :group_id
225                ORDER BY `sequence_order` ASC";
226
227        $stmt = $this->getPdo()->prepare($sql);
228        $stmt->bindValue(self::PARAM_GROUP_ID, $groupId, PDO::PARAM_INT);
229        $stmt->execute();
230
231        $items = [];
232        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
233            $rate = $this->taxRateRepo->findById((int) $row['tax_rate_id']);
234            $items[] = new TaxGroupItem(
235                id: (int) $row['id'],
236                taxGroupId: (int) $row['tax_group_id'],
237                taxRateId: (int) $row['tax_rate_id'],
238                sequenceOrder: (int) $row['sequence_order'],
239                isCompound: (bool) $row['is_compound'],
240                taxRate: $rate,
241            );
242        }
243
244        return $items;
245    }
246
247    /**
248     * Save items for a given group.
249     *
250     * @param TaxGroupItem[] $items
251     */
252    private function saveGroupItems(int $groupId, array $items): void
253    {
254        $table = $this->tablePrefix . 'mod_tax_group_items';
255        $del = $this->getPdo()->prepare("DELETE FROM `{$table}` WHERE `tax_group_id` = :group_id");
256        $del->bindValue(self::PARAM_GROUP_ID, $groupId, PDO::PARAM_INT);
257        $del->execute();
258
259        if (empty($items)) {
260            return;
261        }
262
263        $ins = $this->getPdo()->prepare(
264            "INSERT INTO `{$table}` (`tax_group_id`, `tax_rate_id`, `sequence_order`, `is_compound`)
265             VALUES (:group_id, :rate_id, :seq, :compound)"
266        );
267
268        foreach ($items as $idx => $item) {
269            $ins->bindValue(self::PARAM_GROUP_ID, $groupId, PDO::PARAM_INT);
270            $ins->bindValue(':rate_id', $item->taxRateId, PDO::PARAM_INT);
271            $ins->bindValue(':seq', $item->sequenceOrder ?: ($idx + 1), PDO::PARAM_INT);
272            $ins->bindValue(':compound', $item->isCompound ? 1 : 0, PDO::PARAM_INT);
273            $ins->execute();
274        }
275    }
276}