<?php
namespace App\Services;
use App\Models\Setting;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class MetaConversionsService
{
private string $pixelId;
private string $accessToken;
private string $testEventCode;
private string $apiVersion = 'v19.0';
public function __construct()
{
$this->pixelId = (string) Setting::get('facebook_pixel_id', '');
$this->accessToken = (string) Setting::get('conversions_api_token', '');
$this->testEventCode = (string) Setting::get('conversions_api_test_code', '');
}
public function isEnabled(): bool
{
return !empty($this->pixelId) && !empty($this->accessToken);
}
/**
* Send event to Meta Conversions API
* Returns the event_id so browser pixel can use same ID (deduplication)
*/
public function send(
string $eventName,
array $customData = [],
array $extraUserData = [],
string $eventId = null
): string {
$eventId = $eventId ?? $this->generateUuid();
if (!$this->isEnabled()) {
return $eventId;
}
$req = request();
$userData = array_filter([
'client_ip_address' => $req->ip(),
'client_user_agent' => $req->userAgent(),
'fbp' => $req->cookie('_fbp'),
'fbc' => $this->getFbc($req),
]);
$userData = array_merge($userData, $extraUserData);
$payload = [
'data' => [[
'event_name' => $eventName,
'event_time' => time(),
'event_id' => $eventId,
'event_source_url' => $req->fullUrl(),
'action_source' => 'website',
'user_data' => $userData,
'custom_data' => array_filter($customData),
]],
'access_token' => $this->accessToken,
];
if (!empty($this->testEventCode)) {
$payload['test_event_code'] = $this->testEventCode;
}
try {
Http::timeout(3)->post(
"https://graph.facebook.com/{$this->apiVersion}/{$this->pixelId}/events",
$payload
);
} catch (\Exception $e) {
Log::warning('[Meta CAPI] ' . $eventName . ' failed: ' . $e->getMessage());
}
return $eventId;
}
/**
* Send Purchase event with customer data (most important event)
*/
public function sendPurchase(
float $value,
string $phone,
array $contentIds = [],
int $numItems = 1,
string $orderId = '',
string $eventId = null
): string {
$userData = [];
if (!empty($phone)) {
$userData['ph'] = $this->hashPhone($phone);
}
return $this->send(
'Purchase',
[
'value' => round($value, 2),
'currency' => 'BDT',
'content_ids' => $contentIds,
'content_type' => 'product',
'num_items' => $numItems,
'order_id' => $orderId,
],
$userData,
$eventId
);
}
/**
* Send ViewContent (product page)
*/
public function sendViewContent(int $productId, string $productName, float $price, string $eventId = null): string
{
return $this->send(
'ViewContent',
[
'content_ids' => [(string) $productId],
'content_name' => $productName,
'content_type' => 'product',
'value' => round($price, 2),
'currency' => 'BDT',
],
[],
$eventId
);
}
/**
* Send AddToCart
*/
public function sendAddToCart(int $productId, string $productName, float $price, int $qty, string $eventId = null): string
{
return $this->send(
'AddToCart',
[
'content_ids' => [(string) $productId],
'content_name' => $productName,
'content_type' => 'product',
'value' => round($price * $qty, 2),
'currency' => 'BDT',
],
[],
$eventId
);
}
/**
* Send InitiateCheckout
*/
public function sendInitiateCheckout(float $value, int $numItems, string $eventId = null): string
{
return $this->send(
'InitiateCheckout',
[
'value' => round($value, 2),
'currency' => 'BDT',
'num_items' => $numItems,
],
[],
$eventId
);
}
// ── Helpers ────────────────────────────────────────────────────────────────
private function hashPhone(string $phone): string
{
$phone = preg_replace('/[\s\-\(\)]/', '', $phone);
// Bangladesh: 01XXXXXXXXX → +8801XXXXXXXXX
if (preg_match('/^01[3-9]\d{8}$/', $phone)) {
$phone = '+880' . $phone;
} elseif (preg_match('/^8801[3-9]\d{8}$/', $phone)) {
$phone = '+' . $phone;
}
return hash('sha256', $phone);
}
private function getFbc($req): ?string
{
if ($req->cookie('_fbc')) {
return $req->cookie('_fbc');
}
if ($fbclid = $req->query('fbclid')) {
return 'fb.1.' . (time() * 1000) . '.' . $fbclid;
}
return null;
}
private function generateUuid(): string
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}