[ 'permissions' => 'TEXT NULL', ], 'users' => [ 'theme' => "VARCHAR(20) DEFAULT 'default'", 'outlet_id' => 'INT(11) DEFAULT NULL', ], 'customers' => [ 'outlet_id' => 'INT(11) DEFAULT NULL', 'balance' => 'DECIMAL(15,3) DEFAULT 0.000', 'credit_limit' => 'DECIMAL(15,3) DEFAULT 0.000', ], 'suppliers' => [ 'outlet_id' => 'INT(11) DEFAULT 1', 'balance' => 'DECIMAL(15,3) DEFAULT NULL', 'credit_limit' => 'DECIMAL(15,3) DEFAULT NULL', ], 'stock_categories' => [ 'outlet_id' => 'INT(11) DEFAULT 1', ], 'stock_units' => [ 'outlet_id' => 'INT(11) DEFAULT 1', ], 'stock_items' => [ 'outlet_id' => 'INT(11) DEFAULT 1', ], 'expenses' => [ 'outlet_id' => 'INT(11) DEFAULT NULL', ], 'invoices' => [ 'transaction_no' => 'VARCHAR(50) DEFAULT NULL', 'register_session_id' => 'INT(11) DEFAULT NULL', 'is_pos' => 'TINYINT(1) DEFAULT 0', 'discount_amount' => 'DECIMAL(15,3) DEFAULT 0.000', 'loyalty_points_earned' => 'DECIMAL(15,3) DEFAULT 0.000', 'loyalty_points_redeemed' => 'DECIMAL(15,3) DEFAULT 0.000', 'created_by' => 'INT(11) DEFAULT NULL', 'outlet_id' => 'INT(11) DEFAULT NULL', ], 'invoice_items' => [ 'vat_amount' => 'DECIMAL(15,3) DEFAULT 0.000', ], 'payments' => [ 'outlet_id' => 'INT(11) DEFAULT NULL', ], 'pos_held_carts' => [ 'outlet_id' => 'INT(11) DEFAULT NULL', ], 'pos_transactions' => [ 'outlet_id' => 'INT(11) DEFAULT NULL', ], 'purchase_payments' => [ 'outlet_id' => 'INT(11) DEFAULT NULL', ], 'purchase_returns' => [ 'outlet_id' => 'INT(11) DEFAULT NULL', ], 'purchases' => [ 'outlet_id' => 'INT(11) DEFAULT 1', ], 'quotations' => [ 'outlet_id' => 'INT(11) DEFAULT NULL', ], 'sales_returns' => [ 'outlet_id' => 'INT(11) DEFAULT NULL', ], 'lpos' => [ 'outlet_id' => 'INT(11) DEFAULT NULL', ], ]; foreach ($columns as $table => $tableColumns) { if (!full_schema_sync_20260502_table_exists($pdo, $table)) { continue; } foreach ($tableColumns as $column => $definition) { if (full_schema_sync_20260502_column_exists($pdo, $table, $column)) { continue; } $statement = sprintf( 'ALTER TABLE `%s` ADD COLUMN `%s` %s', str_replace('`', '', $table), str_replace('`', '', $column), $definition ); full_schema_sync_20260502_exec($pdo, $statement); } } } function full_schema_sync_20260502_seed_defaults(PDO $pdo): void { if (full_schema_sync_20260502_table_exists($pdo, 'outlets')) { $pdo->exec( "INSERT INTO outlets (id, name, address, phone, status, created_at) " . "SELECT 1, 'Main Outlet', 'Head Office', '', 'active', NOW() " . "WHERE NOT EXISTS (SELECT 1 FROM outlets WHERE id = 1)" ); } if (full_schema_sync_20260502_table_exists($pdo, 'role_groups') && full_schema_sync_20260502_column_exists($pdo, 'role_groups', 'permissions')) { $pdo->exec("UPDATE role_groups SET permissions = 'all' WHERE permissions IS NULL AND LOWER(name) IN ('administrator', 'admin')"); } if ( full_schema_sync_20260502_table_exists($pdo, 'role_groups') && full_schema_sync_20260502_table_exists($pdo, 'role_permissions') ) { $pdo->exec( "INSERT INTO role_permissions (role_id, permission) " . "SELECT rg.id, 'all' " . "FROM role_groups rg " . "LEFT JOIN role_permissions rp ON rp.role_id = rg.id AND rp.permission = 'all' " . "WHERE LOWER(rg.name) IN ('administrator', 'admin') AND rp.id IS NULL" ); } $outletTables = [ 'users', 'customers', 'suppliers', 'stock_categories', 'stock_units', 'stock_items', 'expenses', 'invoices', 'payments', 'pos_held_carts', 'pos_transactions', 'purchase_payments', 'purchase_returns', 'purchases', 'quotations', 'sales_returns', 'lpos', ]; foreach ($outletTables as $table) { if ( full_schema_sync_20260502_table_exists($pdo, $table) && full_schema_sync_20260502_column_exists($pdo, $table, 'outlet_id') ) { $pdo->exec(sprintf('UPDATE `%s` SET `outlet_id` = 1 WHERE `outlet_id` IS NULL', $table)); } } if ( full_schema_sync_20260502_table_exists($pdo, 'user_outlets') && full_schema_sync_20260502_table_exists($pdo, 'users') && full_schema_sync_20260502_table_exists($pdo, 'outlets') ) { $pdo->exec( 'INSERT INTO user_outlets (user_id, outlet_id) ' . 'SELECT u.id, 1 ' . 'FROM users u ' . 'LEFT JOIN user_outlets uo ON uo.user_id = u.id AND uo.outlet_id = 1 ' . 'WHERE uo.user_id IS NULL' ); } if ( full_schema_sync_20260502_table_exists($pdo, 'outlet_stock') && full_schema_sync_20260502_table_exists($pdo, 'stock_items') && full_schema_sync_20260502_table_exists($pdo, 'outlets') && full_schema_sync_20260502_column_exists($pdo, 'stock_items', 'stock_quantity') ) { $pdo->exec( 'INSERT INTO outlet_stock (outlet_id, item_id, quantity) ' . 'SELECT 1, si.id, COALESCE(si.stock_quantity, 0) ' . 'FROM stock_items si ' . 'LEFT JOIN outlet_stock os ON os.outlet_id = 1 AND os.item_id = si.id ' . 'WHERE os.id IS NULL' ); } } function full_schema_sync_20260502_table_exists(PDO $pdo, string $table): bool { $stmt = $pdo->prepare( 'SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table LIMIT 1' ); $stmt->execute(['table' => $table]); return (bool) $stmt->fetchColumn(); } function full_schema_sync_20260502_column_exists(PDO $pdo, string $table, string $column): bool { $stmt = $pdo->prepare( 'SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table AND COLUMN_NAME = :column LIMIT 1' ); $stmt->execute([ 'table' => $table, 'column' => $column, ]); return (bool) $stmt->fetchColumn(); } function full_schema_sync_20260502_exec(PDO $pdo, string $statement): void { try { $pdo->exec($statement); } catch (PDOException $exception) { if (full_schema_sync_20260502_is_ignorable($exception)) { return; } throw $exception; } } function full_schema_sync_20260502_is_ignorable(PDOException $exception): bool { $driverCode = isset($exception->errorInfo[1]) ? (int) $exception->errorInfo[1] : null; $message = strtolower($exception->getMessage()); $ignorableCodes = [1050, 1060, 1061, 1062, 1091, 1826]; $ignorableSnippets = [ 'already exists', 'duplicate column name', 'duplicate key name', 'duplicate entry', 'duplicate foreign key constraint name', 'duplicate key on write or update', 'errno: 121', 'check that column/key exists', ]; if ($driverCode !== null && in_array($driverCode, $ignorableCodes, true)) { return true; } foreach ($ignorableSnippets as $snippet) { if (str_contains($message, $snippet)) { return true; } } return false; } } full_schema_sync_20260502_run(); return true;