param = $param; } /** * Assemble the readiness notification and hand it to notify(). * * @return void */ public function handle() { $cwFactory = new CloudWatchLoggerFactory(); $param = $this->param; $id = Arr::get($param, 'id', null); if (!$id) { $cwFactory->logger(json_encode(['title' => self::LOG_TITLE, 'message' => 'Missing parameter id'])); return; } try { $changes = Arr::get($param, 'changes', []); // Keyed by applicant type, then tier — [1 => ['approval' => [...], // 'rejection' => [...]], 2 => ['rejection' => [...]]]. Key 2 is // present only when the application has a co-applicant, and only the // tiers that apply to an applicant type appear under it (there is no // co-applicant approval tier). $pending = Arr::get($param, 'pending', []); // A tier with nothing pending is answerable now — the one thing a // consumer of this notification almost always wants to know. Derived // from $pending so it inherits its shape: absent applicant types and // absent tiers stay absent rather than defaulting to "ready", which // is what a flat tier loop over $tiers would have produced. $ready = []; foreach ($pending as $applicantType => $tiers) { foreach ($tiers as $tier => $missing) { $ready[$applicantType][$tier] = empty($missing); } } $notification = [ 'application_id' => (int) $id, 'trigger' => Arr::get($param, 'trigger', DatapointHelper::NOTIFY_CHANGED), 'has_co_applicant' => (bool) Arr::get($param, 'has_co_applicant', false), 'changed' => array_keys($changes), 'changes' => $changes, 'flags' => Arr::get($param, 'flags', []), 'pending' => $pending, 'ready' => $ready, ]; $this->notify($notification, $cwFactory); } catch (\Exception $e) { $cwFactory->logger(json_encode(['title' => self::LOG_TITLE, 'application_id' => $id, 'message' => $e->getMessage(), 'line' => $e->getLine(), 'trace' => $e->getTraceAsString()])); } } /** * Deliver the pending data-point notification via WhatsApp group (Narad). * * Sends a single generalised message listing all fields still needed for * auto-decision. No rejection/approval split — the merchant only needs to * know what is missing, not which rule tier it belongs to. * * @param array $notification application_id, trigger, changed, changes, flags, pending, ready * @return void */ protected function notify(array $notification, CloudWatchLoggerFactory $cwFactory) { // Always log the snapshot to CloudWatch for observability. $cwFactory->logger(json_encode(array_merge(['title' => self::LOG_TITLE], $notification))); $id = $notification['application_id']; // Collect all unique pending fields across every tier and applicant type. // The message is generalised — we do not label which tier a field belongs to. $allPending = []; foreach ($notification['pending'] ?? [] as $tiers) { foreach ($tiers as $fields) { foreach ($fields as $field) { $allPending[$field] = true; } } } if (empty($allPending)) { // Nothing pending — no message to send. return; } // Resolve the WhatsApp group via ServiceController (single source of truth). $groupName = (new ServiceController(new Request()))->getGroupNameByApplication($id); if (!$groupName) { return; } $application = \App\ConsumerApplication::where('id', $id)->first(['name', 'mobile_number']); $customerInfo = $application ? "Customer Name: " . $application->name . "\nMobile Number: " . $application->mobile_number . "\n" : ''; $message = $customerInfo . "Application ID: " . $id . "\n" . "Pending Data Points for Auto-Decision:\n- " . implode("\n- ", DatapointHelper::labelled(array_keys($allPending))); $payload = [ 'group_name' => $groupName, 'application_id' => (string) $id, 'status' => 'pending_datapoints', 'message' => $message, ]; $this->sendNaradRequest('/_admin/send-group-status', $payload, 'DatapointNotificationError', $cwFactory); } /** * Send a POST request to the Narad WhatsApp gateway. * Mirrors ServiceController::sendNaradRequest() without the controller dependency. */ private function sendNaradRequest(string $endpoint, array $payload, string $errorTitle, CloudWatchLoggerFactory $cwFactory) { $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => _env('NARAD_HOST') . $endpoint, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'CF_ACCESS_KEY: ' . _env('NARAD_CF_ACCESS_KEY'), 'CF_SECRET_KEY: ' . _env('NARAD_CF_SECRET_KEY'), ], ]); $response = curl_exec($curl); $error = curl_error($curl); curl_close($curl); if ($error) { $cwFactory->logger(json_encode([ 'title' => $errorTitle, 'error' => $error, 'payload' => $payload, 'response' => $response, ], JSON_UNESCAPED_UNICODE)); } return $response; } }