From 9629a075d7ac1ef6a997dd3b51edab9569051f7d Mon Sep 17 00:00:00 2001 From: ISMAIL MASSERAN Date: Sun, 20 Sep 2026 16:20:36 +0800 Subject: [PATCH] WIP: wired whatsapp api using hello world template --- .env.example | 8 +- app/Console/Commands/WhatsappTestSend.php | 55 +++++++ app/Console/Kernel.php | 1 + app/Jobs/WhatsappPembiayaanOnlinePaid.php | 151 ++++++++++++++++++ .../PembiayaanOnlineService.php | 52 +++++- app/Services/Whatsapp/WhatsappCloudClient.php | 100 ++++++++++++ bootstrap/app.php | 1 + config/whatsapp.php | 9 ++ 8 files changed, 373 insertions(+), 4 deletions(-) create mode 100644 app/Console/Commands/WhatsappTestSend.php create mode 100644 app/Jobs/WhatsappPembiayaanOnlinePaid.php create mode 100644 app/Services/Whatsapp/WhatsappCloudClient.php create mode 100644 config/whatsapp.php diff --git a/.env.example b/.env.example index 9f8a734..2013ec2 100644 --- a/.env.example +++ b/.env.example @@ -249,10 +249,16 @@ WABOT_INSTANCES_ID=63F40BD120DB4 # WABOT_INSTANCES_ID=64892FD7440A6 WABOT_TOKEN_KEYS=cbee7e95cf3ffbfa0531d5fddfd44f87 +# Meta WhatsApp Cloud (test number). Paste token locally; do not commit it. +WHATSAPP_TOKEN= +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=60178916936 +ADMIN_PHONE_NUMBER=60193450165 ACTIVE_JOB_SERVER=false diff --git a/app/Console/Commands/WhatsappTestSend.php b/app/Console/Commands/WhatsappTestSend.php new file mode 100644 index 0000000..44fb700 --- /dev/null +++ b/app/Console/Commands/WhatsappTestSend.php @@ -0,0 +1,55 @@ +isConfigured()) { + $this->error('Set WHATSAPP_TOKEN and WHATSAPP_PHONE_NUMBER_ID in .env first.'); + return 1; + } + + $to = $client->normalizeMsisdn($this->option('to') ?: env('ADMIN_PHONE_NUMBER')); + if (!$to) { + $this->error('Pass --to=60193450165 or set ADMIN_PHONE_NUMBER.'); + return 1; + } + + $template = config('whatsapp.template', 'hello_world'); + $lang = config('whatsapp.template_lang', 'en_US'); + $bodyParams = []; + + if (strtolower($template) !== 'hello_world') { + $bodyParams = [ + 'Tampoi', + 'TEST-SAG', + '100.00', + 'Maybank ****1234', + date('d/m/Y'), + 'TEST-REF', + ]; + } + + $this->info("Sending {$template} ({$lang}) to {$to} …"); + $response = $client->sendTemplate($to, $template, $lang, $bodyParams); + + if ($response->successful()) { + $this->info('OK ' . $response->status() . ' ' . $response->body()); + return 0; + } + + $this->error('FAIL ' . $response->status() . ' ' . $response->body()); + return 1; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 9308414..6723bdd 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -24,6 +24,7 @@ class Kernel extends ConsoleKernel Commands\ClearCachesAll::class, Commands\RestartSupervisor::class, Commands\InsertSempornaData::class, + Commands\WhatsappTestSend::class, //Commands\ImportCsv::class, ]; diff --git a/app/Jobs/WhatsappPembiayaanOnlinePaid.php b/app/Jobs/WhatsappPembiayaanOnlinePaid.php new file mode 100644 index 0000000..3ec528d --- /dev/null +++ b/app/Jobs/WhatsappPembiayaanOnlinePaid.php @@ -0,0 +1,151 @@ +kodcaw = $data['kodcaw'] ?? null; + $this->cawName = $data['caw_name'] ?? null; + $this->nokp = $data['nokp'] ?? null; + $this->norujukan = $data['norujukan'] ?? null; + $this->pinjaman = $data['pinjaman'] ?? null; + $this->namaBank = $data['nama_bank'] ?? null; + $this->noakaun = $data['noakaun'] ?? null; + $this->tarikhBayaran = $data['tarikh_bayaran'] ?? null; + $this->noRujukanBayaran = $data['no_rujukan_bayaran'] ?? null; + } + + public function handle() + { + $client = new WhatsappCloudClient(); + $now = Carbon::now(); + + if (!$client->isConfigured()) { + Log::warning('WhatsApp Cloud skipped: token or phone_number_id missing', [ + 'norujukan' => $this->norujukan, + ]); + return; + } + + $customer = Customer::on('mysql7') + ->where(function ($q) { + $q->where('kpbaru', $this->nokp)->orWhere('kplama', $this->nokp); + }) + ->first(); + + $storedPhone = $customer ? ($customer->telefon ?: $customer->telefon2) : null; + $to = $client->normalizeMsisdn($storedPhone); + + if (env('APP_ENV') !== 'production') { + $admin = $client->normalizeMsisdn(env('ADMIN_PHONE_NUMBER')); + if (!$admin) { + Log::warning('WhatsApp Cloud skipped: ADMIN_PHONE_NUMBER required when not production', [ + 'norujukan' => $this->norujukan, + ]); + $this->logBlast($now, $storedPhone, 'skipped: ADMIN_PHONE_NUMBER empty'); + return; + } + $to = $admin; + } + + if (!$to) { + Log::warning('WhatsApp Cloud skipped: no phone', [ + 'nokp' => $this->nokp, + 'norujukan' => $this->norujukan, + ]); + $this->logBlast($now, $storedPhone, 'skipped: no phone'); + return; + } + + $template = config('whatsapp.template', 'hello_world'); + $lang = config('whatsapp.template_lang', 'en_US'); + $bodyParams = $this->isHelloWorld($template) ? [] : $this->bodyParams(); + + try { + $response = $client->sendTemplate($to, $template, $lang, $bodyParams); + $this->logBlast($now, $to, $response->body()); + } catch (\Throwable $e) { + Log::error('WhatsappPembiayaanOnlinePaid failed: ' . $e->getMessage(), [ + 'norujukan' => $this->norujukan, + ]); + $this->logBlast($now, $to, 'exception: ' . $e->getMessage()); + } + } + + protected function isHelloWorld($template) + { + return strtolower((string) $template) === 'hello_world'; + } + + /** + * Matches pembiayaan_online_paid body: cawangan, SAG, amaun, akaun, tarikh, rujukan. + * + * @return array + */ + protected function bodyParams() + { + $akaun = trim((string) $this->namaBank); + $tail = $this->accountTail($this->noakaun); + if ($tail !== '') { + $akaun = trim($akaun . ' ' . $tail); + } + + $tarikh = $this->tarikhBayaran + ? Carbon::parse($this->tarikhBayaran)->format('d/m/Y') + : Carbon::now()->format('d/m/Y'); + + return [ + $this->cawName ?: $this->kodcaw, + $this->norujukan ?: '-', + number_format((float) $this->pinjaman, 2, '.', ''), + $akaun !== '' ? $akaun : '-', + $tarikh, + $this->noRujukanBayaran ?: '-', + ]; + } + + protected function accountTail($noakaun) + { + $digits = preg_replace('/\D+/', '', (string) $noakaun); + if ($digits === '') { + return ''; + } + + return '****' . substr($digits, -4); + } + + protected function logBlast($now, $telefon, $status) + { + try { + $log = new BlastWasap(); + $log->kodcaw = $this->kodcaw; + $log->nokp = $this->nokp; + $log->notelefon = $telefon; + $log->norujukan = $this->norujukan; + $log->status = is_string($status) ? $status : json_encode($status); + $log->created_at = $now; + $log->updated_at = $now; + $log->save(); + } catch (\Throwable $e) { + Log::error('log_blast_wasap save failed: ' . $e->getMessage()); + } + } +} diff --git a/app/Services/PembiayaanOnline/PembiayaanOnlineService.php b/app/Services/PembiayaanOnline/PembiayaanOnlineService.php index 27c2c24..c8ed689 100644 --- a/app/Services/PembiayaanOnline/PembiayaanOnlineService.php +++ b/app/Services/PembiayaanOnline/PembiayaanOnlineService.php @@ -9,7 +9,9 @@ use App\Cawangan; use App\Customer; use App\Feature; use App\Gadai; +use App\Jobs\WhatsappPembiayaanOnlinePaid; use App\PembiayaanOnline; +use App\Services\Whatsapp\WhatsappCloudClient; use Carbon\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -1613,8 +1615,8 @@ class PembiayaanOnlineService } /** - * PC marks PAID line(s) as tallied / blast done (blast_sent=1). - * Actual WhatsApp/SMS dispatch can be wired later. + * PC marks PAID line(s) as tallied / blast done (blast_sent=1), + * then queues WhatsApp Cloud template to each customer (or ADMIN_PHONE_NUMBER off-prod). * * @param array|int|string $ids * @return array{ok:bool,status:int,payload:mixed} @@ -1643,7 +1645,7 @@ class PembiayaanOnlineService $updated = 0; try { - return DB::connection($mysql)->transaction(function () use ($mysql, $kodcaw, $ids, $blastBy, $now, &$updated) { + $result = DB::connection($mysql)->transaction(function () use ($mysql, $kodcaw, $ids, $blastBy, $now, &$updated) { $lines = PembiayaanOnline::on($mysql) ->where('kodcaw', $kodcaw) ->whereIn('id', $ids) @@ -1697,6 +1699,50 @@ class PembiayaanOnlineService 'payload' => ['message' => 'Gagal tanda blast/tally'], ]; } + + if (!empty($result['ok'])) { + $this->queuePaidWhatsapp($kodcaw, $result['payload']['data'] ?? []); + } + + return $result; + } + + /** + * Dispatch Cloud API jobs after tally commit. Failures are logged; tally stays PAID/blast_sent. + * + * @param \Illuminate\Support\Collection|array $lines + */ + protected function queuePaidWhatsapp($kodcaw, $lines) + { + $client = new WhatsappCloudClient(); + if (!$client->isConfigured()) { + Log::info('WhatsApp Cloud not configured; tally saved without send', ['kodcaw' => $kodcaw]); + return; + } + + $cawangan = Cawangan::on('mysql7')->where('kodcaw', $kodcaw)->first(); + $cawName = $cawangan ? $cawangan->nama_cawangan : $kodcaw; + + foreach ($lines as $line) { + try { + dispatch(new WhatsappPembiayaanOnlinePaid([ + 'kodcaw' => $kodcaw, + 'caw_name' => $cawName, + 'nokp' => $line->nokp ?? null, + 'norujukan' => $line->norujukan ?? null, + 'pinjaman' => $line->pinjaman ?? null, + 'nama_bank' => $line->nama_bank ?? null, + 'noakaun' => $line->noakaun ?? null, + 'tarikh_bayaran' => $line->tarikh_bayaran ?? null, + 'no_rujukan_bayaran' => $line->no_rujukan_bayaran ?? null, + ])); + } catch (\Throwable $e) { + Log::error('queuePaidWhatsapp dispatch failed: ' . $e->getMessage(), [ + 'kodcaw' => $kodcaw, + 'id' => $line->id ?? null, + ]); + } + } } /** diff --git a/app/Services/Whatsapp/WhatsappCloudClient.php b/app/Services/Whatsapp/WhatsappCloudClient.php new file mode 100644 index 0000000..6c5ac2d --- /dev/null +++ b/app/Services/Whatsapp/WhatsappCloudClient.php @@ -0,0 +1,100 @@ + $bodyParams + * @return \Illuminate\Http\Client\Response + */ + public function sendTemplate($to, $templateName, $lang, array $bodyParams = []) + { + $payload = [ + 'messaging_product' => 'whatsapp', + 'to' => $to, + 'type' => 'template', + 'template' => [ + 'name' => $templateName, + 'language' => ['code' => $lang], + ], + ]; + + if (count($bodyParams) > 0) { + $payload['template']['components'] = [[ + 'type' => 'body', + 'parameters' => array_map(function ($text) { + return [ + 'type' => 'text', + 'text' => $this->sanitizeParam($text), + ]; + }, array_values($bodyParams)), + ]]; + } + + $version = config('whatsapp.api_version', 'v25.0'); + $phoneId = config('whatsapp.phone_number_id'); + $url = 'https://graph.facebook.com/' . $version . '/' . $phoneId . '/messages'; + + $response = Http::withToken(config('whatsapp.token')) + ->acceptJson() + ->asJson() + ->post($url, $payload); + + if (!$response->successful()) { + Log::error('WhatsApp Cloud send failed', [ + 'to' => $to, + 'template' => $templateName, + 'status' => $response->status(), + 'body' => $response->json() ?: $response->body(), + ]); + } + + return $response; + } + + protected function sanitizeParam($text) + { + $text = trim(preg_replace('/\s+/', ' ', (string) $text)); + if ($text === '') { + return '-'; + } + + return mb_substr($text, 0, 200); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index e5febcc..b487d31 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -93,6 +93,7 @@ $app->configure('tennant_database'); $app->configure('report'); $app->configure('app_parameter'); $app->configure('parameter'); +$app->configure('whatsapp'); /* |-------------------------------------------------------------------------- | Register Middleware diff --git a/config/whatsapp.php b/config/whatsapp.php new file mode 100644 index 0000000..a169c04 --- /dev/null +++ b/config/whatsapp.php @@ -0,0 +1,9 @@ + env('WHATSAPP_TOKEN'), + 'phone_number_id' => env('WHATSAPP_PHONE_NUMBER_ID'), + 'api_version' => env('WHATSAPP_API_VERSION', 'v25.0'), + 'template' => env('WHATSAPP_TEMPLATE', 'hello_world'), + 'template_lang' => env('WHATSAPP_TEMPLATE_LANG', 'en_US'), +];