Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
65.22% |
15 / 23 |
|
20.00% |
1 / 5 |
CRAP | |
0.00% |
0 / 1 |
| InventorySchemaEngine | |
63.64% |
14 / 22 |
|
20.00% |
1 / 5 |
21.13 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getPdo | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
4.59 | |||
| provisionBaseInventoryTable | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
| addInventoryColumn | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
| dropInventoryColumn | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** @license For full copyright and license information, please see the LICENSE.md file. */ |
| 6 | |
| 7 | namespace App\Core\Engine\Application\Service; |
| 8 | |
| 9 | defined('AMMONLY_APP') || exit('Direct script access is forbidden.'); |
| 10 | |
| 11 | use App\Core\Instance\Application\Service\InstanceContextManagerInterface; |
| 12 | use InvalidArgumentException; |
| 13 | use 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 | */ |
| 21 | final 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 | } |