From 0c404eda5bf8a60dc808f46c4845edebf8c97727 Mon Sep 17 00:00:00 2001 From: ISMAIL MASSERAN Date: Thu, 24 Sep 2026 11:26:29 +0800 Subject: [PATCH] DONE: update .env for production --- .env.example | 181 +----------------- .env.production | 24 ++- app/Http/Controllers/KelulusanController.php | 141 ++++++++++++-- .../Controllers/WhatsappWebhookController.php | 122 ++++++++++++ config/whatsapp.php | 2 + routes/web.php | 3 + 6 files changed, 287 insertions(+), 186 deletions(-) create mode 100644 app/Http/Controllers/WhatsappWebhookController.php diff --git a/.env.example b/.env.example index 8a444f0..7c39266 100644 --- a/.env.example +++ b/.env.example @@ -87,182 +87,21 @@ DB_DATABASE_11=arrahn_pputeh DB_USERNAME_11=spider DB_PASSWORD_11=G0dz!ll42020 -DB_CONNECTION_12=mysql -DB_HOST_12=127.0.0.1 -DB_PORT_12=3306 -DB_DATABASE_12=arrahn_rpanjang -DB_USERNAME_12=spider -DB_PASSWORD_12=G0dz!ll42020 - -DB_CONNECTION_13=mysql -DB_HOST_13=127.0.0.1 -DB_PORT_13=3306 -DB_DATABASE_13=arrahn_tmerah -DB_USERNAME_13=spider -DB_PASSWORD_13=G0dz!ll42020 - -DB_CONNECTION_14=mysql -DB_HOST_14=127.0.0.1 -DB_PORT_14=3306 -DB_DATABASE_14=arrahn_wb -DB_USERNAME_14=spider -DB_PASSWORD_14=G0dz!ll42020 - -DB_CONNECTION_15=mysql -DB_HOST_15=127.0.0.1 -DB_PORT_15=3306 -DB_DATABASE_15=arrahn_wcy -DB_USERNAME_15=spider -DB_PASSWORD_15=G0dz!ll42020 - -DB_CONNECTION_16=mysql -DB_HOST_16=127.0.0.1 -DB_PORT_16=3306 -DB_DATABASE_16=arrahn_bentong -DB_USERNAME_16=spider -DB_PASSWORD_16=G0dz!ll42020 - -DB_CONNECTION_17=mysql -DB_HOST_17=192.168.0.102 -DB_PORT_17=3306 -DB_DATABASE_17=arrahn_kajang -DB_USERNAME_17=spider -DB_PASSWORD_17=G0dz!ll42020 - -DB_CONNECTION_18=mysql -DB_HOST_18=192.168.0.102 -DB_PORT_18=3306 -DB_DATABASE_18=arrahn_kangar -DB_USERNAME_18=spider -DB_PASSWORD_18=G0dz!ll42020 - -DB_CONNECTION_19=mysql -DB_HOST_19=192.168.0.102 -DB_PORT_19=3306 -DB_DATABASE_19=arrahn_kuantan -DB_USERNAME_19=spider -DB_PASSWORD_19=G0dz!ll42020 - -DB_CONNECTION_20=mysql -DB_HOST_20=192.168.0.102 -DB_PORT_20=3306 -DB_DATABASE_20=arrahn_pgudang -DB_USERNAME_20=spider -DB_PASSWORD_20=G0dz!ll42020 - -DB_CONNECTION_21=mysql -DB_HOST_21=192.168.0.102 -DB_PORT_21=3306 -DB_DATABASE_21=arrahn_raub -DB_USERNAME_21=spider -DB_PASSWORD_21=G0dz!ll42020 - -DB_CONNECTION_22=mysql -DB_HOST_22=192.168.0.102 -DB_PORT_22=3306 -DB_DATABASE_22=arrahn_rlaut -DB_USERNAME_22=spider -DB_PASSWORD_22=G0dz!ll42020 - -DB_CONNECTION_23=mysql -DB_HOST_23=192.168.0.102 -DB_PORT_23=3306 -DB_DATABASE_23=arrahn_skudai -DB_USERNAME_23=spider -DB_PASSWORD_23=G0dz!ll42020 - -DB_CONNECTION_24=mysql -DB_HOST_24=192.168.0.102 -DB_PORT_24=3306 -DB_DATABASE_24=arrahn_spetani -DB_USERNAME_24=spider -DB_PASSWORD_24=G0dz!ll42020 - -DB_CONNECTION_25=mysql -DB_HOST_25=192.168.0.102 -DB_PORT_25=3306 -DB_DATABASE_25=arrahn_temerloh -DB_USERNAME_25=spider -DB_PASSWORD_25=G0dz!ll42020 - -DB_CONNECTION_26=mysql -DB_HOST_26=192.168.0.103 -DB_PORT_26=3306 -DB_DATABASE_26=arrahn_api2 -DB_USERNAME_26=spider -DB_PASSWORD_26=G0dz!ll42020 - -DB_CONNECTION_27=mysql -DB_HOST_27=192.168.0.103 -DB_PORT_27=3306 -DB_DATABASE_27=arrahn_beaufort -DB_USERNAME_27=spider -DB_PASSWORD_27=G0dz!ll42020 - -DB_CONNECTION_28=mysql -DB_HOST_28=192.168.0.103 -DB_PORT_28=3306 -DB_DATABASE_28=arrahn_kbelud -DB_USERNAME_28=spider -DB_PASSWORD_28=G0dz!ll42020 - -DB_CONNECTION_29=mysql -DB_HOST_29=192.168.0.103 -DB_PORT_29=3306 -DB_DATABASE_29=arrahn_keningau -DB_USERNAME_29=spider -DB_PASSWORD_29=G0dz!ll42020 - -DB_CONNECTION_30=mysql -DB_HOST_30=192.168.0.103 -DB_PORT_30=3306 -DB_DATABASE_30=arrahn_putatan -DB_USERNAME_30=spider -DB_PASSWORD_30=G0dz!ll42020 - -DB_CONNECTION_31=mysql -DB_HOST_31=192.168.0.103 -DB_PORT_31=3306 -DB_DATABASE_31=arrahn_sandakan -DB_USERNAME_31=spider -DB_PASSWORD_31=G0dz!ll42020 - -DB_CONNECTION_32=mysql -DB_HOST_32=192.168.0.103 -DB_PORT_32=3306 -DB_DATABASE_32=arrahn_tawau -DB_USERNAME_32=spider -DB_PASSWORD_32=G0dz!ll42020 - -DB_CONNECTION_33=mysql -DB_HOST_33=192.168.0.103 -DB_PORT_33=3306 -DB_DATABASE_33=arrahn_tuaran -DB_USERNAME_33=spider -DB_PASSWORD_33=G0dz!ll42020 - CACHE_DRIVER=file QUEUE_CONNECTION=sync +MAX_MUTUEMAS=true -WABOT_API=https://md.wabot.my/api/send.php -WABOT_INSTANCES_ID=63F40BD120DB4 -# WABOT_INSTANCES_ID=64892FD7440A6 -WABOT_TOKEN_KEYS=cbee7e95cf3ffbfa0531d5fddfd44f87 +TENNANT_MASTER_DB=master_erahn +TENNANT_KAUNTER_DB=kaunter_erahn -# Meta WhatsApp Cloud (test number). Paste token locally; do not commit it. -WHATSAPP_TOKEN= +KAUNTER_URL=http://host.docker.internal:8081 +INTERNAL_NOTIFICATION_SECRET= + +WHATSAPP_TOKEN= # Paste token from Graph API explorer. WHATSAPP_PHONE_NUMBER_ID=1323449077519027 WHATSAPP_API_VERSION=v25.0 WHATSAPP_TEMPLATE=hello_world WHATSAPP_TEMPLATE_LANG=en_US - -ONEWAYSMS_API=http://gateway.onewaysms.com.my:10001/api.aspx -ONEWAYSMS_AUTH_USER=APIUK330HDAFO -ONEWAYSMS_AUTH_PASS=APIUK330HDAFOUK330 - -ADMIN_PHONE_NUMBER=60193450165 -ACTIVE_JOB_SERVER=false - -# Kaunter inbox (PC modal request → operasi bell). Same secret as kaunter INTERNAL_NOTIFICATION_SECRET. -KAUNTER_URL=http://host.docker.internal:8081 -INTERNAL_NOTIFICATION_SECRET= +WHATSAPP_VERIFY_TOKEN= # webhook verify token +WHATSAPP_APP_SECRET= # X-Hub-Signature-256 checks; need to be enable first +ADMIN_PHONE_NUMBER=60193450165 \ No newline at end of file diff --git a/.env.production b/.env.production index 90443a6..ec105d3 100644 --- a/.env.production +++ b/.env.production @@ -59,9 +59,29 @@ DB_DATABASE_7=master_erahn DB_USERNAME_7=spider DB_PASSWORD_7=arrahn@123 -MAX_MUTUEMAS=true +DB_CONNECTION_9=mysql +DB_HOST_9=192.168.1.216 +DB_PORT_9=3306 +DB_DATABASE_9=annur_tampoi +DB_USERNAME_9=spider +DB_PASSWORD_9=arrahn@123 CACHE_DRIVER=file QUEUE_CONNECTION=sync -TENNANT_MASTER_DB=master_erahn \ No newline at end of file +MAX_MUTUEMAS=true + +TENNANT_MASTER_DB=master_erahn +TENNANT_KAUNTER_DB=kaunter_erahn + +KAUNTER_URL=https://ujrah.erahn.com.my +INTERNAL_NOTIFICATION_SECRET="nIDTe(u@6[)VLJyd" + +WHATSAPP_TOKEN=EAAZAonT56AO0BSlxZAKa8QX6HIWJpp5Czsz2KQ8BYE4iyZAe5fg8ZAmaSwZCXZCPf44xazDZAhaXLHJKHKUrUZCxei86EWl6Ar3RingfybZAlXp79Yd6WwOG5IZAnu9o5mevm895ygrs39H11G01fpYU2lYRpIWdn6URkTA0kMJdqZAlTaRILTjUM5KhvV05Q4lLAZDZD +WHATSAPP_PHONE_NUMBER_ID=1323449077519027 +WHATSAPP_API_VERSION=v25.0 +WHATSAPP_TEMPLATE=pembiayaan_online_paid +WHATSAPP_TEMPLATE_LANG=ms +WHATSAPP_VERIFY_TOKEN=418ed86d49422071681c76bed25ae1cb7e90456c794a92f1c9ea417945a35a0a +WHATSAPP_APP_SECRET= +ADMIN_PHONE_NUMBER=60193450165 \ No newline at end of file diff --git a/app/Http/Controllers/KelulusanController.php b/app/Http/Controllers/KelulusanController.php index 89e7647..87cb123 100644 --- a/app/Http/Controllers/KelulusanController.php +++ b/app/Http/Controllers/KelulusanController.php @@ -8,6 +8,7 @@ use App\Cawangan; use App\Mohonid; use App\MohonAnggota; use App\Customer; +use App\Services\StaffNotificationClient; use Carbon\Carbon; @@ -444,6 +445,8 @@ public function create(REQUEST $request,$kodcaw,$norujukan,$roles) $audit->updated_at = $harini; $audit->save(); + $this->notifyOperasiAssignAnggota($kodcaw, $cawangan, $kelulusan, $request); + // ✅ Return JSON response ke frontend return response()->json([ 'success' => true, @@ -466,6 +469,58 @@ public function create(REQUEST $request,$kodcaw,$norujukan,$roles) } } + /** + * Inbox for all operasi staff. Failures must not block the teller request. + * + * @param string $kodcaw + * @param \App\Cawangan $cawangan + * @param \App\MohonAnggota $kelulusan + * @param \Illuminate\Http\Request $request + * @return void + */ + protected function notifyOperasiAssignAnggota($kodcaw, $cawangan, $kelulusan, Request $request) + { + try { + $nama = trim((string) ($request->nama ?? '')); + if ($nama === '' && !empty($kelulusan->nokp)) { + $customer = Customer::on('mysql7') + ->where('kpbaru', $kelulusan->nokp) + ->orWhere('kplama', $kelulusan->nokp) + ->first(); + if ($customer && !empty($customer->nama)) { + $nama = trim((string) $customer->nama); + } + } + + $namaCaw = $kodcaw; + if ($cawangan) { + $resolved = trim((string) $cawangan->nama_cawangan); + if ($resolved !== '') { + $namaCaw = $resolved; + } + } + + app(StaffNotificationClient::class)->notify([ + 'type' => 'anggota.pending_operasi', + 'cawangan' => $kodcaw, + 'audience' => ['role' => 'operasi'], + 'data' => [ + 'id' => $kelulusan->id, + 'nokp' => $kelulusan->nokp, + 'nama' => $nama, + 'teller' => $kelulusan->teller, + 'komen' => $kelulusan->komen, + 'cawangan_nama' => $namaCaw, + ], + ]); + } catch (\Throwable $e) { + Log::warning('Assign anggota operasi notification failed: '.$e->getMessage(), [ + 'kodcaw' => $kodcaw, + 'nokp' => isset($kelulusan->nokp) ? $kelulusan->nokp : null, + ]); + } + } + //KEPOHONAN EDIT DAN HAPUS public function kepohonancreate(REQUEST $request,$kodcaw,$norujukan) @@ -600,26 +655,86 @@ public function create(REQUEST $request,$kodcaw,$norujukan,$roles) return response()->json($arraykelulusan,200); } - public function listoperasikelulusan() + public function listoperasikelulusan(Request $request) { - $harini = Carbon::now(); - $harini = new Carbon; + $dateFrom = $request->query('date_from'); + $dateTo = $request->query('date_to'); + $search = trim((string) $request->query('search', '')); + + // Default to last 30 days to avoid loading entire history + if (!$dateFrom && !$dateTo) { + $dateTo = Carbon::now()->toDateString(); + $dateFrom = Carbon::now()->subDays(30)->toDateString(); + } + + $cawangan_all = Cawangan::on('mysql7') + ->select('mysql', 'details_caw', 'kodcaw') + ->get(); - $cawangan_all = Cawangan::on('mysql7')->get(); $arraykelulusan = []; - foreach($cawangan_all as $index=>$cawangan){ - $kelulusan=Mohon::on($cawangan['mysql']) - ->where('peloperasi','=','operasi') - ->where('operasi','!=','') - ->get(); + foreach ($cawangan_all as $cawangan) { + $query = Mohon::on($cawangan['mysql']) + ->select([ + 'tarikh', + 'norujukan', + 'komen', + 'jumpinj', + 'nilaimarhun', + 'berat', + 'margin', + 'status', + 'operasi', + 'masalulus', + ]) + ->where('peloperasi', '=', 'operasi') + ->where('operasi', '!=', ''); - if(sizeof($kelulusan) != 0){ - array_push($arraykelulusan,["kelulusanall" => $kelulusan,"cawangan" => $cawangan['details_caw'],"cawangankod" => $cawangan['kodcaw']]); + if ($dateFrom) { + $query->where('tarikh', '>=', $dateFrom); + } + if ($dateTo) { + $query->where('tarikh', '<=', $dateTo); + } + if ($search !== '') { + $query->where(function ($q) use ($search) { + $q->where('norujukan', 'like', "%{$search}%") + ->orWhere('operasi', 'like', "%{$search}%") + ->orWhere('status', 'like', "%{$search}%") + ->orWhere('komen', 'like', "%{$search}%"); + }); } - } - return response()->json($arraykelulusan,200); + $kelulusan = $query->orderBy('tarikh', 'desc')->get(); + + foreach ($kelulusan as $kel) { + $komenParts = explode('|', (string) ($kel->komen ?? '')); + $komenParts = array_values(array_filter($komenParts, function ($part) { + return $part !== ''; + })); + + $arraykelulusan[] = [ + 'tarikh' => $kel->tarikh ?? '-', + 'norujukan' => $kel->norujukan ?? '-', + 'status' => $kel->status ?? '-', + 'komen' => $komenParts[0] ?? ($kel->komen ?: '-'), + 'pinjaman' => $kel->jumpinj ?? '-', + 'nilaimarhun' => $komenParts[1] ?? ($kel->nilaimarhun ?? '-'), + 'berat' => $komenParts[2] ?? ($kel->berat ?? '-'), + 'margin' => $komenParts[3] ?? ($kel->margin ?? '-'), + 'pelulus' => $kel->operasi ?? '-', + 'masalulus' => $kel->masalulus ?? '-', + 'cawangan' => $cawangan['details_caw'], + 'cawangankod' => $cawangan['kodcaw'], + ]; + } + } + + usort($arraykelulusan, function ($a, $b) { + return strcmp((string) ($b['tarikh'] ?? ''), (string) ($a['tarikh'] ?? '')); + }); + + return response()->json($arraykelulusan, 200); } public function UpdateHapusAnsuranOperasi(Request $request){ diff --git a/app/Http/Controllers/WhatsappWebhookController.php b/app/Http/Controllers/WhatsappWebhookController.php new file mode 100644 index 0000000..11b4f40 --- /dev/null +++ b/app/Http/Controllers/WhatsappWebhookController.php @@ -0,0 +1,122 @@ +query('hub_mode', $request->query('hub.mode')); + $token = $request->query('hub_verify_token', $request->query('hub.verify_token')); + $challenge = $request->query('hub_challenge', $request->query('hub.challenge')); + + $expected = (string) config('whatsapp.verify_token'); + + if ($mode === 'subscribe' && $expected !== '' && hash_equals($expected, (string) $token)) { + Log::info('WhatsApp webhook verified'); + return response((string) $challenge, 200) + ->header('Content-Type', 'text/plain'); + } + + Log::warning('WhatsApp webhook verify failed', [ + 'mode' => $mode, + 'token_match' => $expected !== '' && hash_equals($expected, (string) $token), + ]); + + return response('Forbidden', 403); + } + + /** + * POST /api/whatsapp/webhook + * Acknowledge quickly; optionally check X-Hub-Signature-256 when app secret is set. + */ + public function handle(Request $request) + { + $raw = $request->getContent(); + + if (!$this->signatureOk($request, $raw)) { + Log::warning('WhatsApp webhook invalid signature'); + return response('Invalid signature', 403); + } + + $payload = json_decode($raw, true); + if (!is_array($payload)) { + return response('OK', 200); + } + + try { + $this->logEvents($payload); + } catch (\Throwable $e) { + Log::error('WhatsApp webhook log failed: ' . $e->getMessage()); + } + + return response('OK', 200); + } + + protected function signatureOk(Request $request, $raw) + { + $secret = trim((string) config('whatsapp.app_secret')); + if ($secret === '') { + return true; + } + + $header = (string) $request->header('X-Hub-Signature-256', ''); + if ($header === '' || strpos($header, 'sha256=') !== 0) { + return false; + } + + $expected = 'sha256=' . hash_hmac('sha256', $raw, $secret); + + return hash_equals($expected, $header); + } + + protected function logEvents(array $payload) + { + $entries = $payload['entry'] ?? []; + if (!is_array($entries)) { + return; + } + + foreach ($entries as $entry) { + $changes = $entry['changes'] ?? []; + if (!is_array($changes)) { + continue; + } + + foreach ($changes as $change) { + $value = $change['value'] ?? []; + if (!is_array($value)) { + continue; + } + + foreach ($value['statuses'] ?? [] as $status) { + Log::info('WhatsApp status', [ + 'id' => $status['id'] ?? null, + 'status' => $status['status'] ?? null, + 'recipient' => $status['recipient_id'] ?? null, + 'errors' => $status['errors'] ?? null, + ]); + } + + foreach ($value['messages'] ?? [] as $message) { + Log::info('WhatsApp inbound', [ + 'from' => $message['from'] ?? null, + 'type' => $message['type'] ?? null, + 'id' => $message['id'] ?? null, + ]); + } + } + } + } +} diff --git a/config/whatsapp.php b/config/whatsapp.php index a169c04..546b489 100644 --- a/config/whatsapp.php +++ b/config/whatsapp.php @@ -6,4 +6,6 @@ return [ 'api_version' => env('WHATSAPP_API_VERSION', 'v25.0'), 'template' => env('WHATSAPP_TEMPLATE', 'hello_world'), 'template_lang' => env('WHATSAPP_TEMPLATE_LANG', 'en_US'), + 'verify_token' => env('WHATSAPP_VERIFY_TOKEN'), + 'app_secret' => env('WHATSAPP_APP_SECRET'), ]; diff --git a/routes/web.php b/routes/web.php index 39fc2c3..e76b125 100644 --- a/routes/web.php +++ b/routes/web.php @@ -699,6 +699,9 @@ $router->group(['prefix'=>'api'], function () use ($router){ $router->get('initiate/modal-online', ['uses' => 'ModalOnlineController@initiate']); // Operasi kelulusan pembiayaan online (must be before {kodcaw} routes) + $router->get('whatsapp/webhook', ['uses' => 'WhatsappWebhookController@verify']); + $router->post('whatsapp/webhook', ['uses' => 'WhatsappWebhookController@handle']); + $router->get('pembiayaan-online/operasi/batches', ['uses' => 'PembiayaanOnlineController@operasiListBatches']); $router->get('pembiayaan-online/operasi/{kodcaw}/batches/{batchId}', ['uses' => 'PembiayaanOnlineController@operasiShowBatch']); $router->post('pembiayaan-online/operasi/{kodcaw}/lines/{id}/approve', ['uses' => 'PembiayaanOnlineController@operasiApproveLine']);