314 lines
11 KiB
PHP
314 lines
11 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../config.php';
|
|
|
|
if (!function_exists('full_schema_sync_20260502_run')) {
|
|
function full_schema_sync_20260502_run(): void
|
|
{
|
|
$pdo = db();
|
|
|
|
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/../schema.sql');
|
|
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/20260318_create_outlets_table.sql');
|
|
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/20260318_multi_outlet_schema.sql');
|
|
full_schema_sync_20260502_apply_create_tables_from_file($pdo, __DIR__ . '/20260318_user_outlets_table.sql');
|
|
|
|
full_schema_sync_20260502_ensure_columns($pdo);
|
|
full_schema_sync_20260502_seed_defaults($pdo);
|
|
}
|
|
|
|
function full_schema_sync_20260502_apply_create_tables_from_file(PDO $pdo, string $filePath): void
|
|
{
|
|
if (!is_file($filePath)) {
|
|
return;
|
|
}
|
|
|
|
$sql = file_get_contents($filePath);
|
|
if ($sql === false) {
|
|
throw new RuntimeException('Unable to read schema source: ' . basename($filePath));
|
|
}
|
|
|
|
foreach (full_schema_sync_20260502_extract_create_table_statements($sql) as $statement) {
|
|
$normalized = full_schema_sync_20260502_normalize_create_table_statement($statement);
|
|
full_schema_sync_20260502_exec($pdo, $normalized);
|
|
}
|
|
}
|
|
|
|
function full_schema_sync_20260502_extract_create_table_statements(string $sql): array
|
|
{
|
|
if (!preg_match_all('/CREATE\s+TABLE\b.*?;/is', $sql, $matches)) {
|
|
return [];
|
|
}
|
|
|
|
return $matches[0] ?? [];
|
|
}
|
|
|
|
function full_schema_sync_20260502_normalize_create_table_statement(string $statement): string
|
|
{
|
|
$statement = trim($statement);
|
|
$statement = preg_replace('/^CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+/i', 'CREATE TABLE IF NOT EXISTS ', $statement, 1) ?? $statement;
|
|
|
|
$lines = preg_split('/\R/', $statement) ?: [];
|
|
$kept = [];
|
|
|
|
foreach ($lines as $line) {
|
|
if (preg_match('/^\s*CONSTRAINT\b/i', $line)) {
|
|
continue;
|
|
}
|
|
$kept[] = $line;
|
|
}
|
|
|
|
$statement = implode("\n", $kept);
|
|
$statement = preg_replace('/,\s*\)(?=\s*(ENGINE|DEFAULT|COMMENT|CHARSET|COLLATE|;))/is', "\n)", $statement) ?? $statement;
|
|
|
|
return rtrim($statement, "\n\r\t ;") . ';';
|
|
}
|
|
|
|
function full_schema_sync_20260502_ensure_columns(PDO $pdo): void
|
|
{
|
|
$columns = [
|
|
'role_groups' => [
|
|
'permissions' => 'TEXT NULL',
|
|
],
|
|
'users' => [
|
|
'theme' => "VARCHAR(20) DEFAULT 'default'",
|
|
'outlet_id' => 'INT(11) DEFAULT NULL',
|
|
],
|
|
'customers' => [
|
|
'outlet_id' => 'INT(11) DEFAULT NULL',
|
|
],
|
|
'suppliers' => [
|
|
'outlet_id' => 'INT(11) DEFAULT 1',
|
|
],
|
|
'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;
|