adminRecipient()); } /** * Send a car notification to the admin via WhatsApp Cloud API. * * @param \App\Models\Category $category * @param \App\Models\User|null $customer * @return array */ public function sendCarNotificationToAdmin(Category $category, ?User $customer = null) { if (!$this->isConfigured()) { return [ 'success' => false, 'message' => 'WhatsApp Business API belum dikonfigurasi.', ]; } $recipient = $this->formatPhoneNumber($this->adminRecipient()); $endpoint = $this->graphEndpoint('messages'); $imageUrl = $this->getCarImageUrl($category); $mediaId = $this->uploadCarImage($category); $canSendImage = filled($mediaId) || $this->isPublicMediaUrl($imageUrl); $caption = $this->buildCaption($category, $customer, $canSendImage); $payload = [ 'messaging_product' => 'whatsapp', 'recipient_type' => 'individual', 'to' => $recipient, 'type' => $canSendImage ? 'image' : 'text', ]; if ($mediaId) { $payload['image'] = [ 'id' => $mediaId, 'caption' => $caption, ]; } elseif ($canSendImage) { $payload['image'] = [ 'link' => $imageUrl, 'caption' => $caption, ]; } else { $payload['text'] = [ 'preview_url' => false, 'body' => $caption, ]; } $response = Http::withToken(config('services.whatsapp.token')) ->acceptJson() ->post($endpoint, $payload); if ($response->successful()) { return [ 'success' => true, 'message' => $canSendImage ? 'Notifikasi gambar katalog mobil berhasil dikirim ke admin.' : 'Notifikasi teks berhasil dikirim ke admin.', 'data' => $response->json(), ]; } Log::warning('Gagal mengirim WhatsApp Business notification.', [ 'category_id' => $category->id, 'status' => $response->status(), 'response' => $response->json() ?: $response->body(), ]); return [ 'success' => false, 'message' => 'Gagal mengirim notifikasi WhatsApp Business ke admin.', 'error' => $response->json() ?: $response->body(), ]; } /** * Build message caption sent to admin. * * @param \App\Models\Category $category * @param \App\Models\User|null $customer * @param bool $withImage * @return string */ private function buildCaption(Category $category, ?User $customer = null, $withImage = true) { $lines = [ 'Customer tertarik dengan mobil katalog ini.', 'Mobil: ' . $category->name, 'Harga: Rp ' . number_format((float) $category->price, 0, ',', '.'), 'Tahun: ' . ($category->year ?: '-'), ]; if ($customer) { $lines[] = 'Username customer: ' . $customer->username; $lines[] = 'Email customer: ' . $customer->email; if ($customer->phone_number) { $lines[] = 'No. HP customer: ' . $customer->phone_number; } } else { $lines[] = 'Customer: pengunjung belum login'; } $detailUrl = route('mobil-bekas.detail', ['category' => $category->id]); $lines[] = 'Link detail: ' . $detailUrl; if (!$withImage) { $lines[] = 'Link foto katalog: ' . ($this->getCarImageUrl($category) ?: '-'); $lines[] = 'Catatan: gambar dikirim sebagai teks karena file katalog belum bisa dikirim sebagai media.'; } return implode("\n", $lines); } /** * Upload the local catalog image to WhatsApp and return its media ID. * * @param \App\Models\Category $category * @return string|null */ private function uploadCarImage(Category $category) { if (!$category->image || !Storage::disk('public')->exists($category->image)) { return null; } $path = Storage::disk('public')->path($category->image); $mimeType = Storage::disk('public')->mimeType($category->image) ?: 'image/jpeg'; if (!str_starts_with($mimeType, 'image/')) { return null; } $handle = fopen($path, 'r'); if (!$handle) { return null; } try { $response = Http::withToken(config('services.whatsapp.token')) ->acceptJson() ->attach('file', $handle, basename($path), ['Content-Type' => $mimeType]) ->post($this->graphEndpoint('media'), [ 'messaging_product' => 'whatsapp', 'type' => $mimeType, ]); } finally { fclose($handle); } if ($response->successful()) { return $response->json('id'); } Log::warning('Gagal upload media WhatsApp Business.', [ 'category_id' => $category->id, 'status' => $response->status(), 'response' => $response->json() ?: $response->body(), ]); return null; } /** * Get admin WhatsApp recipient from the dedicated WhatsApp setting, * with fallback to the existing direct chat number. * * @return string|null */ private function adminRecipient() { return config('services.whatsapp.admin_recipient'); } /** * Get a public absolute image URL if the car has an image. * * @param \App\Models\Category $category * @return string|null */ private function getCarImageUrl(Category $category) { if (!$category->image) { return null; } return $this->publicUrl(Storage::disk('public')->url($category->image)); } /** * Check whether the media URL is public enough for WhatsApp to fetch. * * @param string|null $url * @return bool */ private function isPublicMediaUrl($url) { if (!$url) { return false; } return !str_contains($url, 'localhost') && !str_contains($url, '127.0.0.1'); } /** * Build an absolute URL from APP_URL so the Cloud API can fetch media. * * @param string $path * @return string */ private function publicUrl($path) { if (filter_var($path, FILTER_VALIDATE_URL)) { return $path; } return rtrim(config('app.url'), '/') . '/' . ltrim($path, '/'); } /** * Build WhatsApp Graph API endpoint for the configured phone number. * * @param string $path * @return string */ private function graphEndpoint($path) { return rtrim(config('services.whatsapp.base_url'), '/') . '/' . config('services.whatsapp.api_version') . '/' . config('services.whatsapp.phone_number_id') . '/' . ltrim($path, '/'); } /** * Normalize phone number to WhatsApp format. * * @param string $phoneNumber * @return string */ private function formatPhoneNumber($phoneNumber) { $phoneNumber = preg_replace('/[^0-9]/', '', $phoneNumber); if (strlen($phoneNumber) === 10 && substr($phoneNumber, 0, 1) === '8') { return '62' . $phoneNumber; } if (strlen($phoneNumber) >= 11 && substr($phoneNumber, 0, 2) === '08') { return '62' . substr($phoneNumber, 1); } if (substr($phoneNumber, 0, 2) !== '62') { return '62' . $phoneNumber; } return $phoneNumber; } }