From a36cbcf930993dde3f8ddfcab1ddccc525370dfe Mon Sep 17 00:00:00 2001 From: afrina Date: Mon, 19 Oct 2026 13:35:42 +0800 Subject: [PATCH] refactor(shoraka): move financing records to master database --- .env.example | 7 ++ .../Commands/MoveShorakaFinancings.php | 78 +++++++++++++++++++ app/ShorakaFinancing.php | 5 ++ config/database.php | 16 ++++ config/shoraka.php | 7 ++ docker/php/Dockerfile | 13 +++- 6 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/MoveShorakaFinancings.php create mode 100644 config/shoraka.php diff --git a/.env.example b/.env.example index 25665a2d..fb6baa2c 100644 --- a/.env.example +++ b/.env.example @@ -52,3 +52,10 @@ MYKAD_READER_ENABLED_CAWANGAN='' INTERNAL_NOTIFICATION_SECRET= LOGVIEWER_PASSWORD= + +# Same master database as api_arrahn mysql7; required for financing records. +SHORAKA_MASTER_DB_HOST= +SHORAKA_MASTER_DB_PORT=3306 +SHORAKA_MASTER_DB_DATABASE= +SHORAKA_MASTER_DB_USERNAME= +SHORAKA_MASTER_DB_PASSWORD= diff --git a/app/Console/Commands/MoveShorakaFinancings.php b/app/Console/Commands/MoveShorakaFinancings.php new file mode 100644 index 00000000..549d7491 --- /dev/null +++ b/app/Console/Commands/MoveShorakaFinancings.php @@ -0,0 +1,78 @@ +option('source'); + $targetName = 'shoraka_master'; + $source = DB::connection($sourceName); + $target = DB::connection($targetName); + if ($sourceName === $targetName || ($source->getConfig('host') === $target->getConfig('host') + && $source->getDatabaseName() === $target->getDatabaseName() + && $source->getConfig('port') === $target->getConfig('port'))) { + $this->error('Source and target must be different databases.'); + return 1; + } + if (!Schema::connection($sourceName)->hasTable('shoraka_financings') + || !Schema::connection($targetName)->hasTable('shoraka_financings')) { + $this->error('Create the target table using the API master migration first. Source and target tables are required.'); + return 1; + } + if ($this->option('apply') && !app()->isDownForMaintenance()) { + $this->error('Stop traffic on every counter instance and run artisan down before applying the transfer.'); + return 1; + } + if ($source->table('shoraka_financings')->where('state', 'PROCESSING')->exists()) { + $this->error('PROCESSING records require reconciliation before transfer.'); + return 1; + } + $count = $source->table('shoraka_financings')->count(); + if (!$this->option('apply')) { + $this->info($count . ' source records. Check only; no data was copied.'); + return 0; + } + try { + $target->transaction(function () use ($source, $target) { + $source->table('shoraka_financings')->orderBy('id')->chunkById(200, function ($rows) use ($target) { + foreach ($rows as $row) { + $data = (array) $row; + unset($data['id']); // IDs may overlap across counter databases. + $key = ['kodcaw' => $row->kodcaw, 'norujukan' => $row->norujukan]; + $existing = $target->table('shoraka_financings')->where($key)->first(); + if (!$existing) { + $target->table('shoraka_financings')->insert($data); + $existing = $target->table('shoraka_financings')->where($key)->first(); + } + foreach ($data as $field => $value) { + $actual = $existing->$field; + if ($field === 'context') { + if (json_decode($actual, true) != json_decode($value, true)) { + throw new RuntimeException('Conflicting context: ' . $row->kodcaw . '/' . $row->norujukan); + } + } elseif ((string) $actual !== (string) $value) { + throw new RuntimeException('Conflicting field ' . $field . ': ' . $row->kodcaw . '/' . $row->norujukan); + } + } + } + }); + }); + } catch (\Throwable $exception) { + $this->error('Transfer rolled back. ' . $exception->getMessage()); + return 1; + } + $this->info($count . ' source records copied and verified. Source retained.'); + $this->info('Ensure shoraka.financing_connection is shoraka_master and reload configuration before reopening traffic.'); + return 0; + } +} diff --git a/app/ShorakaFinancing.php b/app/ShorakaFinancing.php index 3e1e8564..e86b3983 100644 --- a/app/ShorakaFinancing.php +++ b/app/ShorakaFinancing.php @@ -6,6 +6,11 @@ use Illuminate\Database\Eloquent\Model; class ShorakaFinancing extends Model { + public function getConnectionName() + { + return $this->connection ?: config('shoraka.financing_connection'); + } + protected $guarded = []; protected $casts = ['context' => 'array']; } diff --git a/config/database.php b/config/database.php index 25be54e9..a108dd5d 100644 --- a/config/database.php +++ b/config/database.php @@ -63,6 +63,22 @@ return [ ]) : [], ], + // Point these settings at the same database as api_arrahn's mysql7. + 'shoraka_master' => [ + 'driver' => 'mysql', + 'host' => env('SHORAKA_MASTER_DB_HOST'), + 'port' => env('SHORAKA_MASTER_DB_PORT', '3306'), + 'database' => env('SHORAKA_MASTER_DB_DATABASE'), + 'username' => env('SHORAKA_MASTER_DB_USERNAME'), + 'password' => env('SHORAKA_MASTER_DB_PASSWORD'), + 'unix_socket' => env('SHORAKA_MASTER_DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + ], + 'pgsql' => [ 'driver' => 'pgsql', 'url' => env('DATABASE_URL'), diff --git a/config/shoraka.php b/config/shoraka.php new file mode 100644 index 00000000..7c7bcd57 --- /dev/null +++ b/config/shoraka.php @@ -0,0 +1,7 @@ + 'shoraka_master', +]; diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index e2a2c4f8..e5d5b986 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -2,7 +2,14 @@ FROM php:7.4-fpm AS base WORKDIR /var/www/html -RUN apt-get update \ +# PHP 7.4 uses Debian Bullseye; use its archived package repositories. +# Archived security metadata expires, but package signatures remain verified. +RUN printf '%s\n' \ + 'deb https://archive.debian.org/debian bullseye main' \ + 'deb [check-valid-until=no] https://archive.debian.org/debian-security bullseye-security main' \ + > /etc/apt/sources.list \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get -o Acquire::Retries=3 update \ && apt-get install -y --no-install-recommends \ git \ pkg-config \ @@ -28,7 +35,9 @@ COPY docker/php/php.ini /usr/local/etc/php/conf.d/app.ini COPY docker/php/zz-clear-env.conf /usr/local/etc/php-fpm.d/zz-clear-env.conf COPY docker/php/entrypoint.sh /usr/local/bin/app-entrypoint.sh -RUN chmod +x /usr/local/bin/app-entrypoint.sh \ +# Normalize Windows checkout line endings before executing the script. +RUN sed -i 's/\r$//' /usr/local/bin/app-entrypoint.sh \ + && chmod +x /usr/local/bin/app-entrypoint.sh \ && usermod -u 1000 www-data && groupmod -g 1000 www-data ENTRYPOINT ["app-entrypoint.sh"]