Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.22% covered (warning)
65.22%
15 / 23
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
InventorySchemaEngine
63.64% covered (warning)
63.64%
14 / 22
20.00% covered (danger)
20.00%
1 / 5
21.13
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
 getPdo
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
4.59
 provisionBaseInventoryTable
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 addInventoryColumn
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 dropInventoryColumn
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
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\Instance\Application\Service\InstanceContextManagerInterface;
12use InvalidArgumentException;
13use PDO;
14
15/**
16 * InventorySchemaEngine
17 *
18 * Engine responsible for dynamically building and altering MySQL tables
19 * for module inventory records (e.g. `a_mod_{module}_inventory`).
20 */
21final readonly class InventorySchemaEngine
22{
23    private const string IDENTIFIER_PATTERN = '/^\w+$/';
24
25    public function __construct(
26        private PDO $pdo,
27        private ?InstanceContextManagerInterface $instanceContext = null,
28        private ?PDO $clientPdo = null
29    ) {
30    }
31
32    private function getPdo(): PDO
33    {
34        if ($this->instanceContext !== null && $this->instanceContext->isRemote() && $this->clientPdo !== null) {
35            return $this->clientPdo;
36        }
37
38        return $this->pdo;
39    }
40
41    /**
42     * Provisions the base inventory table for a given module if it doesn't exist.
43     *
44     * @param int $moduleId The target module ID.
45     * @param string $moduleTableName The base module table name (e.g. 'c_mod_quotes_records')
46     * @param string $moduleName The module name (e.g. 'Quotes')
47     * @return void
48     */
49    public function provisionBaseInventoryTable(int $moduleId, string $moduleTableName, string $moduleName): void
50    {
51        if ($moduleId <= 0) {
52            return;
53        }
54
55        // Example: from "c_mod_quotes_records" to "c_mod_quotes_inventory"
56        $inventoryTableName = str_replace('_records', '_inventory', $moduleTableName);
57
58        // Standard base columns for all inventory tables
59        $sql = "CREATE TABLE IF NOT EXISTS `{$inventoryTableName}` (
60            `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
61            `record_id` INT UNSIGNED NOT NULL COMMENT 'FK to base module record',
62            `sort_order` INT UNSIGNED NOT NULL DEFAULT 10,
63            `item_name` VARCHAR(255) NOT NULL DEFAULT '',
64            `quantity` DECIMAL(12, 4) NOT NULL DEFAULT 1.0000,
65            `unit_price` DECIMAL(12, 4) NOT NULL DEFAULT 0.0000,
66            `discount_percent` DECIMAL(5, 2) NOT NULL DEFAULT 0.00,
67            `discount_amount` DECIMAL(12, 4) NOT NULL DEFAULT 0.0000,
68            `net_amount` DECIMAL(12, 4) NOT NULL DEFAULT 0.0000,
69            `tax_percent` DECIMAL(5, 2) NOT NULL DEFAULT 0.00,
70            `tax_amount` DECIMAL(12, 4) NOT NULL DEFAULT 0.0000,
71            `gross_amount` DECIMAL(12, 4) NOT NULL DEFAULT 0.0000,
72            `comment` TEXT NULL DEFAULT NULL,
73            `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
74            `updated_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
75            `created_by` INT UNSIGNED NOT NULL,
76            `owner` INT UNSIGNED NOT NULL,
77            PRIMARY KEY (`id`),
78            KEY `idx_inv_record_id` (`record_id`),
79            KEY `idx_inv_record_sort` (`record_id`, `sort_order`, `id`),
80            KEY `idx_inv_record_group` (`record_id`, `group_name`),
81            CONSTRAINT `fk_{$moduleName}_inv_record` FOREIGN KEY (`record_id`)
82                REFERENCES `{$moduleTableName}` (`id`) ON DELETE CASCADE
83        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
84
85        $this->getPdo()->exec($sql);
86    }
87
88    /**
89     * Adds a dynamic column to the inventory table.
90     */
91    public function addInventoryColumn(string $inventoryTableName, string $columnName, string $sqlType): void
92    {
93        // Sanitize identifiers
94        if (!preg_match(self::IDENTIFIER_PATTERN, $columnName)
95            || !preg_match(self::IDENTIFIER_PATTERN, $inventoryTableName)) {
96            throw new InvalidArgumentException("Invalid table or column name identifier.");
97        }
98
99        $sql = "ALTER TABLE `{$inventoryTableName}` ADD COLUMN `{$columnName}{$sqlType} NULL DEFAULT NULL;";
100        $this->getPdo()->exec($sql);
101    }
102
103    /**
104     * Drops a dynamic column from the inventory table.
105     */
106    public function dropInventoryColumn(string $inventoryTableName, string $columnName): void
107    {
108        if (!preg_match(self::IDENTIFIER_PATTERN, $columnName)
109            || !preg_match(self::IDENTIFIER_PATTERN, $inventoryTableName)) {
110            throw new InvalidArgumentException("Invalid table or column name identifier.");
111        }
112
113        $sql = "ALTER TABLE `{$inventoryTableName}` DROP COLUMN `{$columnName}`;";
114        $this->getPdo()->exec($sql);
115    }
116}