Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.24% covered (success)
90.24%
37 / 41
60.00% covered (warning)
60.00%
6 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
InventoryApplicationService
90.00% covered (success)
90.00%
36 / 40
60.00% covered (warning)
60.00%
6 / 10
18.32
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
 enableInventoryForModule
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 addInventoryField
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 getInventoryFields
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updateInventoryField
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteInventoryField
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInventoryTableName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRecordItems
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 saveRecordItems
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 deleteRecordItem
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
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\Repository\InventoryFieldRepositoryInterface;
12use App\Core\Engine\Domain\Repository\InventoryRecordRepositoryInterface;
13use App\Core\Engine\Domain\Repository\MetadataRepositoryInterface;
14use InvalidArgumentException;
15
16final readonly class InventoryApplicationService
17{
18    public function __construct(
19        private InventoryFieldRepositoryInterface $inventoryFieldRepository,
20        private MetadataRepositoryInterface $metadataRepository,
21        private InventorySchemaEngine $schemaEngine,
22        private ?InventoryRecordRepositoryInterface $inventoryRecordRepository = null
23    ) {
24    }
25
26    /**
27     * Enables inventory functionality for a specific module.
28     * This provisions the base dynamic inventory table and registers default columns.
29     */
30    public function enableInventoryForModule(int $moduleId): void
31    {
32        $module = $this->metadataRepository->findModuleById($moduleId);
33        $tableName = $module->tableName;
34        if ($tableName === '') {
35            throw new InvalidArgumentException("Module does not have a base table.");
36        }
37
38        $this->schemaEngine->provisionBaseInventoryTable($moduleId, $tableName, $module->name);
39
40        $existing = $this->inventoryFieldRepository->findByModule($moduleId);
41        if (empty($existing)) {
42            $defaultFields = [
43                [
44                    'field_key' => 'item_name', 'label' => 'Item Name / Description',
45                    'uitype_id' => 1, 'is_mandatory' => 1, 'is_system' => 1, 'sort_order' => 10,
46                ],
47                [
48                    'field_key' => 'quantity', 'label' => 'Quantity',
49                    'uitype_id' => 7, 'is_mandatory' => 1, 'is_system' => 1, 'sort_order' => 20,
50                ],
51                [
52                    'field_key' => 'unit_price', 'label' => 'Unit Price (Net)',
53                    'uitype_id' => 71, 'is_mandatory' => 1, 'is_system' => 1, 'sort_order' => 30,
54                ],
55                [
56                    'field_key' => 'discount_percent', 'label' => 'Discount (%)',
57                    'uitype_id' => 9, 'is_mandatory' => 0, 'is_system' => 1, 'sort_order' => 40,
58                ],
59                [
60                    'field_key' => 'net_amount', 'label' => 'Net Amount',
61                    'uitype_id' => 71, 'is_mandatory' => 1, 'is_system' => 1, 'sort_order' => 50,
62                ],
63                [
64                    'field_key' => 'tax_percent', 'label' => 'Tax (%)',
65                    'uitype_id' => 9, 'is_mandatory' => 0, 'is_system' => 1, 'sort_order' => 60,
66                ],
67                [
68                    'field_key' => 'tax_amount', 'label' => 'Tax Amount',
69                    'uitype_id' => 71, 'is_mandatory' => 0, 'is_system' => 1, 'sort_order' => 70,
70                ],
71                [
72                    'field_key' => 'gross_amount', 'label' => 'Gross Amount',
73                    'uitype_id' => 71, 'is_mandatory' => 0, 'is_system' => 1, 'sort_order' => 80,
74                ],
75                [
76                    'field_key' => 'comment', 'label' => 'Comment',
77                    'uitype_id' => 19, 'is_mandatory' => 0, 'is_system' => 0, 'sort_order' => 90,
78                ],
79            ];
80            foreach ($defaultFields as $field) {
81                $field['module_id'] = $moduleId;
82                $this->inventoryFieldRepository->create($field);
83            }
84        }
85    }
86
87    /**
88     * Adds a new field to the inventory table of a module.
89     *
90     * @param int                  $moduleId Target module ID.
91     * @param array<string, mixed> $data     Field metadata payload.
92     * @return int Inserted field ID.
93     */
94    public function addInventoryField(int $moduleId, array $data): int
95    {
96        $module = $this->metadataRepository->findModuleById($moduleId);
97        $tableName = $module->tableName;
98        if ($tableName === '') {
99            throw new InvalidArgumentException("Module does not have a base table.");
100        }
101
102        $inventoryTableName = str_replace('_records', '_inventory', $tableName);
103        $columnName = (string) ($data['field_key'] ?? '');
104        if ($columnName === '') {
105            throw new InvalidArgumentException("Field key is required.");
106        }
107
108        $sqlType = 'VARCHAR(255)';
109
110        // 1. Alter DB Schema
111        $this->schemaEngine->addInventoryColumn($inventoryTableName, $columnName, $sqlType);
112
113        // 2. Save metadata record
114        $data['module_id'] = $moduleId;
115        return $this->inventoryFieldRepository->create($data);
116    }
117
118    /**
119     * Lists all inventory fields for a module.
120     *
121     * @return array<int, array<string, mixed>>
122     */
123    public function getInventoryFields(int $moduleId): array
124    {
125        return $this->inventoryFieldRepository->findByModule($moduleId);
126    }
127
128    /**
129     * Updates an existing inventory field.
130     *
131     * @param int                  $fieldId Field primary key.
132     * @param array<string, mixed> $data    Updated attributes.
133     */
134    public function updateInventoryField(int $fieldId, array $data): void
135    {
136        $this->inventoryFieldRepository->update($fieldId, $data);
137    }
138
139    /**
140     * Deletes an inventory field by ID.
141     */
142    public function deleteInventoryField(int $fieldId): void
143    {
144        $this->inventoryFieldRepository->delete($fieldId);
145    }
146
147    /**
148     * Resolves the inventory table name for a module.
149     */
150    public function getInventoryTableName(int $moduleId): string
151    {
152        $module = $this->metadataRepository->findModuleById($moduleId);
153        return str_replace('_records', '_inventory', $module->tableName);
154    }
155
156    /**
157     * Retrieves all inventory line items for a specific document.
158     *
159     * @return array<int, array<string, mixed>>
160     */
161    public function getRecordItems(int $moduleId, int $recordId): array
162    {
163        if ($this->inventoryRecordRepository === null) {
164            return [];
165        }
166        $tableName = $this->getInventoryTableName($moduleId);
167        return $this->inventoryRecordRepository->findItems($tableName, $recordId);
168    }
169
170    /**
171     * Saves all inventory line items for a document.
172     *
173     * @param array<int, array<string, mixed>> $items
174     */
175    public function saveRecordItems(int $moduleId, int $recordId, array $items, int $owner = 1): void
176    {
177        if ($this->inventoryRecordRepository === null) {
178            return;
179        }
180        $tableName = $this->getInventoryTableName($moduleId);
181        $this->inventoryRecordRepository->saveItems($tableName, $recordId, $items, $owner);
182    }
183
184    /**
185     * Deletes a specific inventory line item by its primary key.
186     */
187    public function deleteRecordItem(int $moduleId, int $itemId): bool
188    {
189        if ($this->inventoryRecordRepository === null) {
190            return false;
191        }
192        $tableName = $this->getInventoryTableName($moduleId);
193        return $this->inventoryRecordRepository->deleteItem($tableName, $itemId);
194    }
195}
196