refactor(shoraka): move financing records to master database

This commit is contained in:
2026-10-19 13:35:42 +08:00
parent 011fbfe8b2
commit a36cbcf930
6 changed files with 124 additions and 2 deletions
+7
View File
@@ -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=
@@ -0,0 +1,78 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
class MoveShorakaFinancings extends Command
{
protected $signature = 'shoraka:move-financings {--source=mysql} {--apply : Copy records; default only checks}';
protected $description = 'Copy and verify financing records to shoraka_master without deleting the source';
public function handle()
{
$sourceName = $this->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;
}
}
+5
View File
@@ -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'];
}
+16
View File
@@ -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'),
+7
View File
@@ -0,0 +1,7 @@
<?php
return [
// Financing workflow records are centralized in API mysql7's master database.
// Never fall back to the counter database when the master is unavailable.
'financing_connection' => 'shoraka_master',
];
+11 -2
View File
@@ -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"]