From 22279af86c374e0385b6cd4567edeed863375f43 Mon Sep 17 00:00:00 2001 From: ISMAIL MASSERAN Date: Tue, 8 Sep 2026 14:48:13 +0800 Subject: [PATCH] DONE: feature flags by branches --- README.md | 8 +- app/CawanganFeature.php | 21 +++ app/Feature.php | 147 ++++++++++++++++++ app/Http/Controllers/CawanganController.php | 11 +- app/Http/Controllers/FeatureController.php | 59 +++++++ .../PembiayaanOnline/ModalOnlineService.php | 9 ++ .../PembiayaanOnlineService.php | 5 + ...8_100000_create_cawangan_feature_table.php | 36 +++++ routes/web.php | 4 + 9 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 app/CawanganFeature.php create mode 100644 app/Feature.php create mode 100644 app/Http/Controllers/FeatureController.php create mode 100644 database/migrations/2026_09_08_100000_create_cawangan_feature_table.php diff --git a/README.md b/README.md index 5636bf2..bd2926b 100644 --- a/README.md +++ b/README.md @@ -21,4 +21,10 @@ Tebus=penyelesaian penuh/ansuran docker exec -i api_arrahn_staging_mysql mysql -uroot -proot erahn_test < erahn_test.sql ``` -## Disable the cawangan in arrahn_master_db \ No newline at end of file +## Disable the cawangan in arrahn_master_db + +## Feature Flag +1. Register the flag in api_arrahn/app/Feature.php +2. Block the real work in the API +3. Hide the UI in kaunter from the +4. Enable a pilot from Admin → Ciri Cawangan (or SQL): \ No newline at end of file diff --git a/app/CawanganFeature.php b/app/CawanganFeature.php new file mode 100644 index 0000000..eaae1f6 --- /dev/null +++ b/app/CawanganFeature.php @@ -0,0 +1,21 @@ + 'integer', + ]; +} diff --git a/app/Feature.php b/app/Feature.php new file mode 100644 index 0000000..398ed5f --- /dev/null +++ b/app/Feature.php @@ -0,0 +1,147 @@ + + */ + public static function defaults() + { + $map = []; + foreach (self::keys() as $key) { + $map[$key] = 0; + } + + return $map; + } + + /** + * Display labels for admin UI. New features: add a label here. + * + * @return array + */ + public static function labels() + { + return [ + self::PEMBIAYAAN_ONLINE => 'Pembiayaan Online', + ]; + } + + /** + * Missing row is off (fail closed). + * + * @param string $kodcaw + * @param string $feature + * @return bool + */ + public static function enabled($kodcaw, $feature) + { + return CawanganFeature::on('mysql7') + ->where('kodcaw', $kodcaw) + ->where('feature', $feature) + ->where('enabled', 1) + ->exists(); + } + + /** + * @param string $kodcaw + * @return array + */ + public static function mapFor($kodcaw) + { + $map = self::defaults(); + + $rows = CawanganFeature::on('mysql7') + ->where('kodcaw', $kodcaw) + ->get(['feature', 'enabled']); + + foreach ($rows as $row) { + $map[$row->feature] = ((int) $row->enabled) === 1 ? 1 : 0; + } + + return $map; + } + + /** + * @param string[] $kodcaws + * @return array> + */ + public static function mapForMany(array $kodcaws) + { + $defaults = self::defaults(); + $result = []; + foreach ($kodcaws as $kodcaw) { + $result[$kodcaw] = $defaults; + } + + if (count($kodcaws) === 0) { + return $result; + } + + $rows = CawanganFeature::on('mysql7') + ->whereIn('kodcaw', $kodcaws) + ->get(['kodcaw', 'feature', 'enabled']); + + foreach ($rows as $row) { + $result[$row->kodcaw][$row->feature] = ((int) $row->enabled) === 1 ? 1 : 0; + } + + return $result; + } + + /** + * @param string $kodcaw + * @param string $feature + * @param bool|int $enabled + * @return bool + */ + public static function setEnabled($kodcaw, $feature, $enabled) + { + if (!in_array($feature, self::keys(), true)) { + return false; + } + + CawanganFeature::on('mysql7')->updateOrCreate( + [ + 'kodcaw' => $kodcaw, + 'feature' => $feature, + ], + [ + 'enabled' => $enabled ? 1 : 0, + ] + ); + + return true; + } + + /** + * @param string|null $message + * @return array{ok:bool,status:int,payload:array} + */ + public static function denied($message = null) + { + return [ + 'ok' => false, + 'status' => 403, + 'payload' => [ + 'message' => $message ?: 'Ciri ini tidak diaktifkan untuk cawangan ini', + ], + ]; + } +} diff --git a/app/Http/Controllers/CawanganController.php b/app/Http/Controllers/CawanganController.php index c235dca..f6f5df8 100644 --- a/app/Http/Controllers/CawanganController.php +++ b/app/Http/Controllers/CawanganController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Cawangan; +use App\Feature; use Illuminate\Http\Request; class CawanganController extends Controller @@ -18,7 +19,15 @@ class CawanganController extends Controller $cawangan = Cawangan::on('mysql7') ->where('kodcaw','=',$code_cawangan) ->first(); - return response()->json($cawangan); + + if (!$cawangan) { + return response()->json($cawangan); + } + + $data = $cawangan->toArray(); + $data['features'] = Feature::mapFor($code_cawangan); + + return response()->json($data); } public function create(Request $request) diff --git a/app/Http/Controllers/FeatureController.php b/app/Http/Controllers/FeatureController.php new file mode 100644 index 0000000..a17d236 --- /dev/null +++ b/app/Http/Controllers/FeatureController.php @@ -0,0 +1,59 @@ +json([ + 'keys' => Feature::keys(), + 'labels' => Feature::labels(), + ]); + } + + public function cawangan() + { + $cawangan = Cawangan::on('mysql7') + ->orderBy('kodcaw') + ->get(['kodcaw', 'details_caw']); + + $maps = Feature::mapForMany($cawangan->pluck('kodcaw')->all()); + + $data = $cawangan->map(function ($c) use ($maps) { + return [ + 'kodcaw' => $c->kodcaw, + 'details_caw' => $c->nama_cawangan, + 'features' => $maps[$c->kodcaw] ?? Feature::defaults(), + ]; + }); + + return response()->json($data); + } + + public function update($kodcaw, $feature, Request $request) + { + $cawangan = Cawangan::on('mysql7')->where('kodcaw', $kodcaw)->first(); + if (!$cawangan) { + return response()->json(['message' => 'Cawangan tidak dijumpai'], 404); + } + + if (!in_array($feature, Feature::keys(), true)) { + return response()->json(['message' => 'Ciri tidak sah'], 422); + } + + $enabled = (int) $request->input('enabled') ? 1 : 0; + Feature::setEnabled($kodcaw, $feature, $enabled); + + return response()->json([ + 'kodcaw' => $kodcaw, + 'feature' => $feature, + 'enabled' => $enabled, + 'features' => Feature::mapFor($kodcaw), + ]); + } +} diff --git a/app/Services/PembiayaanOnline/ModalOnlineService.php b/app/Services/PembiayaanOnline/ModalOnlineService.php index ecd57b6..6579b6d 100644 --- a/app/Services/PembiayaanOnline/ModalOnlineService.php +++ b/app/Services/PembiayaanOnline/ModalOnlineService.php @@ -3,6 +3,7 @@ namespace App\Services\PembiayaanOnline; use App\Cawangan; +use App\Feature; use App\ModalOnline; use App\RequestModalOnline; use Carbon\Carbon; @@ -208,6 +209,10 @@ class ModalOnlineService return $resolved; } + if (!Feature::enabled($kodcaw, Feature::PEMBIAYAAN_ONLINE)) { + return Feature::denied('Pembiayaan online tidak diaktifkan untuk cawangan ini'); + } + $mysql = $resolved['mysql']; $amaun = round((float) $amaun, 2); @@ -294,6 +299,10 @@ class ModalOnlineService } $mysql = $resolved['mysql']; + if (!Feature::enabled($kodcaw, Feature::PEMBIAYAAN_ONLINE)) { + return Feature::denied('Pembiayaan online tidak diaktifkan untuk cawangan ini'); + } + $amaun = round((float) $amaun, 2); $dimohonOleh = trim((string) $dimohonOleh); diff --git a/app/Services/PembiayaanOnline/PembiayaanOnlineService.php b/app/Services/PembiayaanOnline/PembiayaanOnlineService.php index b57e7e4..abd0231 100644 --- a/app/Services/PembiayaanOnline/PembiayaanOnlineService.php +++ b/app/Services/PembiayaanOnline/PembiayaanOnlineService.php @@ -7,6 +7,7 @@ use App\Bank; use App\BatchPembiayaanOnline; use App\Cawangan; use App\Customer; +use App\Feature; use App\Gadai; use App\PembiayaanOnline; use Carbon\Carbon; @@ -67,6 +68,10 @@ class PembiayaanOnlineService } $mysql = $resolved['mysql']; + if (!Feature::enabled($kodcaw, Feature::PEMBIAYAAN_ONLINE)) { + return Feature::denied('Pembiayaan online tidak diaktifkan untuk cawangan ini'); + } + $norujukan = trim((string) ($data['norujukan'] ?? '')); $nokp = trim((string) ($data['nokp'] ?? '')); $pinjaman = round((float) ($data['pinjaman'] ?? 0), 2); diff --git a/database/migrations/2026_09_08_100000_create_cawangan_feature_table.php b/database/migrations/2026_09_08_100000_create_cawangan_feature_table.php new file mode 100644 index 0000000..62eb683 --- /dev/null +++ b/database/migrations/2026_09_08_100000_create_cawangan_feature_table.php @@ -0,0 +1,36 @@ +hasTable('cawangan_feature')) { + Schema::connection('mysql7')->create('cawangan_feature', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('kodcaw', 20); + $table->string('feature', 64); + $table->tinyInteger('enabled')->default(0); + $table->timestamps(); + $table->unique(['kodcaw', 'feature'], 'uniq_cawangan_feature'); + }); + } + } + + public function down() + { + Schema::connection('mysql7')->dropIfExists('cawangan_feature'); + } +} diff --git a/routes/web.php b/routes/web.php index 7b6e877..464075c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -240,6 +240,10 @@ $router->group(['prefix'=>'api'], function () use ($router){ $router->get('cawangan/{code_cawangan}',['uses'=>'CawanganController@showOneCawangan']); $router->get('cawangan/detail/{kodcaw}',['uses'=>'CawanganDetailController@getCawanganDetail']); + $router->get('features',['uses'=>'FeatureController@index']); + $router->get('features/cawangan',['uses'=>'FeatureController@cawangan']); + $router->put('features/{kodcaw}/{feature}',['uses'=>'FeatureController@update']); + //Poskod API $router->post('poskod',['uses'=>'PoskodController@store']); $router->put('poskod/{poskod}/update',['uses'=>'PoskodController@update']);