From 18ae044a97cc11ceb98b57755d7aea110b6df4ab Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 2 May 2026 05:05:46 +0000 Subject: [PATCH] update last --- complete_schema.sql | 2 +- db/install_baseline_migrations.php | 56 ++++++ ...260502_zzz_payment_methods_schema_sync.php | 177 ++++++++++++++++++ includes/DatabaseInstaller.php | 52 +++++ index.php | 142 +++++++++++++- login_debug.log | 2 + post_debug.log | 23 +++ refresh_complete_schema.php | 73 +++++++- runtime_debug.log | 1 + 9 files changed, 518 insertions(+), 10 deletions(-) create mode 100644 db/install_baseline_migrations.php create mode 100644 db/migrations/20260502_zzz_payment_methods_schema_sync.php create mode 100644 runtime_debug.log diff --git a/complete_schema.sql b/complete_schema.sql index fbe6e17..f4b9367 100644 --- a/complete_schema.sql +++ b/complete_schema.sql @@ -12,7 +12,7 @@ /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- Auto-generated full schema snapshot for fresh installs. --- Re-run refresh_complete_schema.php after schema changes so new installations stay current. +-- Re-run refresh_complete_schema.php after schema or migration changes so new installations stay current. -- -- Table structure for table `acc_accounts` diff --git a/db/install_baseline_migrations.php b/db/install_baseline_migrations.php new file mode 100644 index 0000000..3fe5232 --- /dev/null +++ b/db/install_baseline_migrations.php @@ -0,0 +1,56 @@ + 'VARCHAR(255) DEFAULT NULL', + 'name_ar' => 'VARCHAR(255) DEFAULT NULL', + 'created_at' => 'TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP', + ]; + + foreach ($columns as $column => $definition) { + if (payment_methods_schema_sync_20260502_column_exists($pdo, 'payment_methods', $column)) { + continue; + } + + payment_methods_schema_sync_20260502_exec( + $pdo, + sprintf( + 'ALTER TABLE `payment_methods` ADD COLUMN `%s` %s', + str_replace('`', '', $column), + $definition + ) + ); + } + + payment_methods_schema_sync_20260502_backfill_legacy_names($pdo); + payment_methods_schema_sync_20260502_seed_defaults($pdo); + } + + function payment_methods_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 payment_methods_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 payment_methods_schema_sync_20260502_backfill_legacy_names(PDO $pdo): void + { + if (!payment_methods_schema_sync_20260502_table_exists($pdo, 'payment_methods')) { + return; + } + + $hasLegacyName = payment_methods_schema_sync_20260502_column_exists($pdo, 'payment_methods', 'name'); + $hasNameEn = payment_methods_schema_sync_20260502_column_exists($pdo, 'payment_methods', 'name_en'); + $hasNameAr = payment_methods_schema_sync_20260502_column_exists($pdo, 'payment_methods', 'name_ar'); + + if ($hasLegacyName && $hasNameEn) { + $pdo->exec( + "UPDATE `payment_methods` + SET `name_en` = CASE + WHEN `name_en` IS NULL OR TRIM(`name_en`) = '' THEN `name` + ELSE `name_en` + END + WHERE `name` IS NOT NULL AND TRIM(`name`) <> ''" + ); + } + + if ($hasNameAr) { + $sourceExpression = $hasLegacyName ? "COALESCE(NULLIF(`name_en`, ''), `name`)" : '`name_en`'; + $pdo->exec( + "UPDATE `payment_methods` + SET `name_ar` = $sourceExpression + WHERE (`name_ar` IS NULL OR TRIM(`name_ar`) = '') + AND $sourceExpression IS NOT NULL + AND TRIM($sourceExpression) <> ''" + ); + } + } + + function payment_methods_schema_sync_20260502_seed_defaults(PDO $pdo): void + { + if (!payment_methods_schema_sync_20260502_table_exists($pdo, 'payment_methods')) { + return; + } + + $count = (int)$pdo->query('SELECT COUNT(*) FROM `payment_methods`')->fetchColumn(); + if ($count > 0) { + return; + } + + $defaults = [ + ['Cash', 'كاش'], + ['Credit Card', 'بطاقة بنكية'], + ['Bank Transfer', 'تحويل بنكي'], + ]; + + $stmt = $pdo->prepare('INSERT INTO `payment_methods` (`name_en`, `name_ar`) VALUES (?, ?)'); + foreach ($defaults as [$nameEn, $nameAr]) { + $stmt->execute([$nameEn, $nameAr]); + } + } + + function payment_methods_schema_sync_20260502_exec(PDO $pdo, string $statement): void + { + try { + $pdo->exec($statement); + } catch (PDOException $exception) { + if (payment_methods_schema_sync_20260502_is_ignorable($exception)) { + return; + } + + throw $exception; + } + } + + function payment_methods_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; + } +} + +payment_methods_schema_sync_20260502_run(); + +return true; diff --git a/includes/DatabaseInstaller.php b/includes/DatabaseInstaller.php index 28bf08f..25dfd4e 100644 --- a/includes/DatabaseInstaller.php +++ b/includes/DatabaseInstaller.php @@ -29,6 +29,7 @@ class DatabaseInstaller { self::executeSqlFile($seedFile); } + self::seedInstallMigrationBaseline($pdo, $schemaFile); self::ensureCurrentSchema(); return true; @@ -52,6 +53,57 @@ class DatabaseInstaller { return (int) $stmt->fetchColumn() === 0; } + private static function seedInstallMigrationBaseline(PDO $pdo, string $schemaFile): void { + if (!self::shouldSeedInstallMigrationBaseline($schemaFile)) { + return; + } + + $baselineMigrations = self::getInstallBaselineMigrations(); + if ($baselineMigrations === []) { + return; + } + + self::ensureMigrationsTable($pdo); + foreach ($baselineMigrations as $migrationName) { + self::recordMigration($pdo, $migrationName); + } + } + + private static function shouldSeedInstallMigrationBaseline(string $schemaFile): bool { + $completeSchemaFile = __DIR__ . '/../complete_schema.sql'; + $schemaRealPath = realpath($schemaFile); + $completeSchemaRealPath = realpath($completeSchemaFile); + + if ($schemaRealPath === false || $completeSchemaRealPath === false) { + return false; + } + + return $schemaRealPath === $completeSchemaRealPath; + } + + private static function getInstallBaselineMigrations(): array { + $baselineFile = __DIR__ . '/../db/install_baseline_migrations.php'; + if (!is_file($baselineFile)) { + return []; + } + + $baseline = require $baselineFile; + if (!is_array($baseline)) { + return []; + } + + $normalized = []; + foreach ($baseline as $migrationName) { + if (!is_string($migrationName) || trim($migrationName) === '') { + continue; + } + + $normalized[basename($migrationName)] = true; + } + + return array_keys($normalized); + } + public static function ensureCurrentSchema(): void { require_once __DIR__ . '/../db/config.php'; diff --git a/index.php b/index.php index 3fad45c..27131b1 100644 --- a/index.php +++ b/index.php @@ -1973,11 +1973,97 @@ function getPromotionalPrice($item) { } if (isset($_POST['add_payment_method'])) { - $name = $_POST['name'] ?? ''; - db()->prepare("INSERT INTO payment_methods (name) VALUES (?)")->execute([$name]); + $name_en = trim((string)($_POST['name_en'] ?? '')); + $name_ar = trim((string)($_POST['name_ar'] ?? '')); + + if ($name_en === '' && $name_ar === '') { + redirectWithMessage("Please enter a payment method name.", "index.php?page=payment_methods"); + } + + if ($name_en === '') { + $name_en = $name_ar; + } + if ($name_ar === '') { + $name_ar = $name_en; + } + + if (db_column_exists('payment_methods', 'name_en') || db_column_exists('payment_methods', 'name_ar')) { + $columns = []; + $placeholders = []; + $values = []; + + if (db_column_exists('payment_methods', 'name_en')) { + $columns[] = 'name_en'; + $placeholders[] = '?'; + $values[] = $name_en; + } + if (db_column_exists('payment_methods', 'name_ar')) { + $columns[] = 'name_ar'; + $placeholders[] = '?'; + $values[] = $name_ar; + } + + db()->prepare("INSERT INTO payment_methods (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")")->execute($values); + } elseif (db_column_exists('payment_methods', 'name')) { + db()->prepare("INSERT INTO payment_methods (`name`) VALUES (?)")->execute([$name_en]); + } else { + throw new RuntimeException('payment_methods table is missing a usable name column.'); + } + redirectWithMessage("Payment method added!", "index.php?page=payment_methods"); } + if (isset($_POST['edit_payment_method'])) { + $id = (int)($_POST['id'] ?? 0); + $name_en = trim((string)($_POST['name_en'] ?? '')); + $name_ar = trim((string)($_POST['name_ar'] ?? '')); + + if ($id <= 0) { + redirectWithMessage("Invalid payment method.", "index.php?page=payment_methods"); + } + if ($name_en === '' && $name_ar === '') { + redirectWithMessage("Please enter a payment method name.", "index.php?page=payment_methods"); + } + + if ($name_en === '') { + $name_en = $name_ar; + } + if ($name_ar === '') { + $name_ar = $name_en; + } + + if (db_column_exists('payment_methods', 'name_en') || db_column_exists('payment_methods', 'name_ar')) { + $sets = []; + $values = []; + + if (db_column_exists('payment_methods', 'name_en')) { + $sets[] = 'name_en = ?'; + $values[] = $name_en; + } + if (db_column_exists('payment_methods', 'name_ar')) { + $sets[] = 'name_ar = ?'; + $values[] = $name_ar; + } + + $values[] = $id; + db()->prepare("UPDATE payment_methods SET " . implode(', ', $sets) . " WHERE id = ?")->execute($values); + } elseif (db_column_exists('payment_methods', 'name')) { + db()->prepare("UPDATE payment_methods SET `name` = ? WHERE id = ?")->execute([$name_en, $id]); + } else { + throw new RuntimeException('payment_methods table is missing a usable name column.'); + } + + redirectWithMessage("Payment method updated!", "index.php?page=payment_methods"); + } + + if (isset($_POST['delete_payment_method'])) { + $id = (int)($_POST['id'] ?? 0); + if ($id > 0) { + db()->prepare("DELETE FROM payment_methods WHERE id = ?")->execute([$id]); + } + redirectWithMessage("Payment method deleted!", "index.php?page=payment_methods"); + } + if (isset($_POST['delete_invoice'])) { $id = (int)$_POST['id']; $type = ($page === 'purchases') ? 'purchase' : 'sale'; @@ -7647,6 +7733,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; +
Payment Methods
@@ -7665,22 +7752,61 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; - + - - - + No payment methods found yet. Add one to get started. + + + + + + + +
- +
- +
+ + +
diff --git a/login_debug.log b/login_debug.log index f8c527f..fec5440 100644 --- a/login_debug.log +++ b/login_debug.log @@ -3,3 +3,5 @@ 2026-02-26 17:58:46 - Failed login for 'admin'. Reason: Password mismatch 2026-02-26 17:59:03 - Failed login for 'admin'. Reason: Password mismatch 2026-03-17 10:12:37 - Failed login for 'moosa'. Reason: Password mismatch +2026-05-02 04:46:38 - Failed login for 'moosa'. Reason: Password mismatch +2026-05-02 04:46:47 - Failed login for 'admin'. Reason: Password mismatch diff --git a/post_debug.log b/post_debug.log index ff49ba4..ef61d0a 100644 --- a/post_debug.log +++ b/post_debug.log @@ -151,3 +151,26 @@ 2026-05-02 02:56:23 - POST: {"username":"admin","password":"admin","login":"1"} 2026-05-02 03:00:39 - POST: {"username":"admin","password":"admin","login":"1"} 2026-05-02 03:32:58 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:46:38 - POST: {"username":"moosa","password":"moosa123","login":""} +2026-05-02 04:46:47 - POST: {"username":"admin","password":"admin123","login":""} +2026-05-02 04:46:55 - POST: {"username":"admin","password":"admin","login":""} +2026-05-02 04:50:45 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:50:50 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:50:50 - POST: {"name_en":"Wallet","name_ar":"\u0645\u062d\u0641\u0638\u0629","add_payment_method":"1"} +2026-05-02 04:54:23 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:54:23 - POST: {"name_en":"QA Wallet","name_ar":"\u0645\u062d\u0641\u0638\u0629 \u062a\u062c\u0631\u064a\u0628\u064a\u0629","add_payment_method":"1"} +2026-05-02 04:54:32 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:54:39 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:54:45 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:54:49 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:54:54 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:55:00 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:55:44 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:55:52 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:55:52 - POST: {"name_en":"QA Wallet","name_ar":"\u0645\u062d\u0641\u0638\u0629 \u062a\u062c\u0631\u064a\u0628\u064a\u0629","add_payment_method":"1"} +2026-05-02 04:55:52 - POST: {"id":"5","name_en":"QA Wallet Updated","name_ar":"\u0645\u062d\u0641\u0638\u0629 \u0645\u062d\u062f\u062b\u0629","edit_payment_method":"1"} +2026-05-02 04:55:52 - POST: {"id":"5","delete_payment_method":"1"} +2026-05-02 04:56:04 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:56:12 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:56:19 - POST: {"username":"admin","password":"admin","login":"1"} +2026-05-02 04:56:42 - POST: {"id":"3","name_en":"Bank Transfer","name_ar":"\u062a\u062d\u0648\u064a\u0644 \u0628\u0646\u0643\u064a","edit_payment_method":""} diff --git a/refresh_complete_schema.php b/refresh_complete_schema.php index 7d7abc4..04337ff 100644 --- a/refresh_complete_schema.php +++ b/refresh_complete_schema.php @@ -83,6 +83,66 @@ function normalizeCreateTableSql(string $sql): string return rtrim($sql, ';') . ';'; } +function schemaSnapshotMigrationSortKey(string $filePath): string +{ + $basename = basename($filePath); + + return match ($basename) { + '20260318_add_outlet_id_to_purchases.sql' => '20260318_10_add_outlet_id_to_purchases.sql', + '20260318_create_outlets_table.sql' => '20260318_20_create_outlets_table.sql', + '20260318_multi_outlet_schema.sql' => '20260318_30_multi_outlet_schema.sql', + '20260318_local_definitions.sql' => '20260318_40_local_definitions.sql', + '20260318_user_outlets_table.sql' => '20260318_50_user_outlets_table.sql', + default => $basename, + }; +} + +function fetchInstallBaselineMigrationNames(): array +{ + $files = array_merge( + glob(__DIR__ . '/db/migrations/*.sql') ?: [], + glob(__DIR__ . '/db/migrations/*.php') ?: [] + ); + + usort($files, static function (string $left, string $right): int { + return strnatcasecmp(schemaSnapshotMigrationSortKey($left), schemaSnapshotMigrationSortKey($right)); + }); + + $names = []; + foreach ($files as $filePath) { + $migrationName = basename($filePath); + if ($migrationName === '') { + continue; + } + + $names[$migrationName] = true; + } + + return array_keys($names); +} + +function buildInstallBaselineContent(array $migrationNames): string +{ + $lines = [ + 'query("SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"); @@ -245,7 +305,7 @@ function buildSnapshotContent(PDO $pdo, array &$tableOrder = []): string '/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;', '', '-- Auto-generated full schema snapshot for fresh installs.', - '-- Re-run refresh_complete_schema.php after schema changes so new installations stay current.', + '-- Re-run refresh_complete_schema.php after schema or migration changes so new installations stay current.', '', ]; @@ -291,9 +351,20 @@ function runSchemaSnapshotRefresh(): int throw new RuntimeException('Unable to write complete_schema.sql'); } + $baselineNames = fetchInstallBaselineMigrationNames(); + $baselineTargetFile = __DIR__ . '/db/install_baseline_migrations.php'; + $baselineBytesWritten = file_put_contents($baselineTargetFile, buildInstallBaselineContent($baselineNames)); + + if ($baselineBytesWritten === false) { + throw new RuntimeException('Unable to write db/install_baseline_migrations.php'); + } + snapshotOutput('OK: refreshed complete_schema.sql'); + snapshotOutput('OK: refreshed db/install_baseline_migrations.php'); snapshotOutput('Tables: ' . count($tableOrder)); + snapshotOutput('Baseline migrations: ' . count($baselineNames)); snapshotOutput('Path: complete_schema.sql'); + snapshotOutput('Path: db/install_baseline_migrations.php'); return 0; } catch (Throwable $throwable) { if (PHP_SAPI !== 'cli') { diff --git a/runtime_debug.log b/runtime_debug.log new file mode 100644 index 0000000..eca171c --- /dev/null +++ b/runtime_debug.log @@ -0,0 +1 @@ +2026-05-02 04:50:50 || [PDOException] || SQLSTATE[42S22]: Column not found: 1054 Unknown column 'name' in 'INSERT INTO' || file=/home/ubuntu/executor/workspace/index.php:1977 || page=payment_methods || uri=/index.php?page=payment_methods || user_id=1 || hint=Missing column: name || error_info=["42S22",1054,"Unknown column 'name' in 'INSERT INTO'"] || trace=#0 /home/ubuntu/executor/workspace/index.php(1977): PDOStatement->execute() | #1 {main}