Deploy: 2026-05-22 02:09:48
This commit is contained in:
87
backend/app/Controllers/EndpointController.php
Normal file
87
backend/app/Controllers/EndpointController.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Request;
|
||||
use App\Core\Response;
|
||||
use App\Models\CompanyEndpoint;
|
||||
|
||||
class EndpointController extends BaseController
|
||||
{
|
||||
/**
|
||||
* List all custom API endpoints for the company
|
||||
*/
|
||||
public function index(Request $request, Response $response)
|
||||
{
|
||||
$endpoints = CompanyEndpoint::findAllByCompany($request->company_id);
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'data' => $endpoints
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a custom API endpoint configuration
|
||||
*/
|
||||
public function store(Request $request, Response $response)
|
||||
{
|
||||
$errors = $this->validate($request, [
|
||||
'name' => 'required',
|
||||
'endpoint_url' => 'required',
|
||||
'action_type' => 'required'
|
||||
]);
|
||||
|
||||
if (!empty($errors)) {
|
||||
$response->status(400)->json(['status' => 'error', 'errors' => $errors]);
|
||||
return;
|
||||
}
|
||||
|
||||
$body = $request->getBody();
|
||||
$saveData = [
|
||||
'company_id' => $request->company_id,
|
||||
'name' => $body['name'],
|
||||
'endpoint_url' => $body['endpoint_url'],
|
||||
'action_type' => $body['action_type'],
|
||||
'description' => $body['description'] ?? null,
|
||||
'headers' => $body['headers'] ?? null
|
||||
];
|
||||
|
||||
if (isset($body['id'])) {
|
||||
$saveData['id'] = (int)$body['id'];
|
||||
}
|
||||
|
||||
$id = CompanyEndpoint::saveSecure($saveData);
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Integration endpoint saved successfully',
|
||||
'id' => $id
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an API endpoint configuration
|
||||
*/
|
||||
public function delete(Request $request, Response $response)
|
||||
{
|
||||
$body = $request->getBody();
|
||||
if (empty($body['id'])) {
|
||||
$response->status(400)->json(['status' => 'error', 'message' => 'Missing endpoint ID']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
$endpoint = CompanyEndpoint::find($body['id']);
|
||||
if (!$endpoint || $endpoint['company_id'] !== $request->company_id) {
|
||||
$response->status(404)->json(['status' => 'error', 'message' => 'Endpoint not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
CompanyEndpoint::delete($body['id']);
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Integration endpoint deleted successfully'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -335,8 +335,21 @@ class WhatsAppController extends BaseController
|
||||
error_log("[Chatbot Warning] Gemini API Key is not set globally or for company " . $session['company_id']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dynamically fetch customer/driver info from configured endpoints if set
|
||||
$infoContext = "";
|
||||
$infoEndpoint = \App\Models\CompanyEndpoint::findByAction($session['company_id'], 'fetch_user_info');
|
||||
if ($infoEndpoint && !empty($msgData['phone'])) {
|
||||
$infoContext = $this->fetchUserInfoFromEndpoint($infoEndpoint, $msgData['phone']);
|
||||
}
|
||||
|
||||
$systemPrompt = $rule['ai_prompt'] ?: 'You are a helpful customer support assistant.';
|
||||
|
||||
// Append real-time info context to Gemini system prompt
|
||||
if (!empty($infoContext)) {
|
||||
$systemPrompt .= "\n\n" . $infoContext;
|
||||
}
|
||||
|
||||
// Enforce language matching rule dynamically
|
||||
$systemPrompt .= "\n\nIMPORTANT LANGUAGE RULE: Detect the language of the incoming message. If the incoming message is in English, you MUST reply in English. If the incoming message is in Arabic, you MUST reply in Arabic. Override any default language instruction to match the user's language.";
|
||||
|
||||
@@ -368,8 +381,8 @@ class WhatsAppController extends BaseController
|
||||
// Strip the tag from the final reply sent to user
|
||||
$replyText = trim(str_replace($matches[0], '', $replyText));
|
||||
|
||||
// Call the payment verification API
|
||||
$verificationResult = $this->verifyPaymentSlip($msgData['phone'], $jsonStr);
|
||||
// Call the payment verification API (passing company_id)
|
||||
$verificationResult = $this->verifyPaymentSlip($session['company_id'], $msgData['phone'], $jsonStr);
|
||||
if ($verificationResult) {
|
||||
$replyText .= "\n\n" . $verificationResult;
|
||||
}
|
||||
@@ -436,9 +449,9 @@ class WhatsAppController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* Call external Entaleq API to verify payment slip
|
||||
* Call external API to verify payment slip
|
||||
*/
|
||||
private function verifyPaymentSlip(string $phone, string $jsonStr): ?string
|
||||
private function verifyPaymentSlip(int $companyId, string $phone, string $jsonStr): ?string
|
||||
{
|
||||
try {
|
||||
$data = json_decode($jsonStr, true);
|
||||
@@ -454,11 +467,17 @@ class WhatsAppController extends BaseController
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine API URL (default to localhost mock endpoint)
|
||||
$apiUrl = getenv('ENTALEQ_PAYMENT_API_URL');
|
||||
// Find configured endpoint for verify_payment
|
||||
$endpoint = \App\Models\CompanyEndpoint::findByAction($companyId, 'verify_payment');
|
||||
$apiUrl = $endpoint ? $endpoint['endpoint_url'] : null;
|
||||
|
||||
if (empty($apiUrl)) {
|
||||
$appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/');
|
||||
$apiUrl = $appUrl . '/api/external/verify-payment';
|
||||
// Fallback to local default mock
|
||||
$apiUrl = getenv('ENTALEQ_PAYMENT_API_URL');
|
||||
if (empty($apiUrl)) {
|
||||
$appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/');
|
||||
$apiUrl = $appUrl . '/api/external/verify-payment';
|
||||
}
|
||||
}
|
||||
|
||||
$payload = json_encode([
|
||||
@@ -468,14 +487,23 @@ class WhatsAppController extends BaseController
|
||||
'method' => $method
|
||||
]);
|
||||
|
||||
$headers = ['Content-Type: application/json'];
|
||||
if ($endpoint && !empty($endpoint['headers'])) {
|
||||
$customHeaders = json_decode($endpoint['headers'], true);
|
||||
if (is_array($customHeaders)) {
|
||||
foreach ($customHeaders as $key => $value) {
|
||||
$headers[] = "$key: $value";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$headers[] = 'X-API-Key: ' . (getenv('ENTALEQ_API_KEY') ?: 'mock-key');
|
||||
}
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'X-API-Key: ' . (getenv('ENTALEQ_API_KEY') ?: 'mock-key')
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
@@ -499,4 +527,45 @@ class WhatsAppController extends BaseController
|
||||
return "⏳ تم استلام وصل الدفع بنجاح. يجري الآن مراجعته وتدقيقه يدوياً من قبل الإدارة الفنية لتأكيد شحن رصيدك.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch user/driver info from external API endpoint configured by the company
|
||||
*/
|
||||
private function fetchUserInfoFromEndpoint(array $endpoint, string $phone): string
|
||||
{
|
||||
try {
|
||||
$apiUrl = $endpoint['endpoint_url'];
|
||||
$payload = json_encode([
|
||||
'phone' => $phone
|
||||
]);
|
||||
|
||||
$headers = ['Content-Type: application/json'];
|
||||
if (!empty($endpoint['headers'])) {
|
||||
$customHeaders = json_decode($endpoint['headers'], true);
|
||||
if (is_array($customHeaders)) {
|
||||
foreach ($customHeaders as $key => $value) {
|
||||
$headers[] = "$key: $value";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5); // Short timeout to avoid blocking chatbot flow
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode === 200 && !empty($response)) {
|
||||
return "\n\n[معلومات العميل المسترجعة من النظام الخارجي للمؤسسة:\n" . $response . "\nاستخدم هذه المعلومات للإجابة بدقة على أسئلة العميل المتعلقة بحسابه ورصيده وحالته]";
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("[Fetch User Info Exception] " . $e->getMessage());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
84
backend/app/Models/CompanyEndpoint.php
Normal file
84
backend/app/Models/CompanyEndpoint.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Database;
|
||||
|
||||
/**
|
||||
* CompanyEndpoint Model
|
||||
* Handles configuration of dynamic integration endpoints for multi-tenant SaaS integration.
|
||||
*/
|
||||
class CompanyEndpoint extends BaseModel
|
||||
{
|
||||
protected static string $table = 'company_endpoints';
|
||||
|
||||
/**
|
||||
* Find all endpoints for a company
|
||||
*/
|
||||
public static function findAllByCompany(int $companyId): array
|
||||
{
|
||||
self::ensureTableExists();
|
||||
return Database::select(
|
||||
"SELECT * FROM " . static::$table . " WHERE company_id = ? ORDER BY id DESC",
|
||||
[$companyId]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a specific endpoint by action type for a company
|
||||
*/
|
||||
public static function findByAction(int $companyId, string $actionType): ?array
|
||||
{
|
||||
self::ensureTableExists();
|
||||
return Database::selectOne(
|
||||
"SELECT * FROM " . static::$table . " WHERE company_id = ? AND action_type = ? LIMIT 1",
|
||||
[$companyId, $actionType]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update an endpoint configuration
|
||||
*/
|
||||
public static function saveSecure(array $data)
|
||||
{
|
||||
self::ensureTableExists();
|
||||
|
||||
if (isset($data['id'])) {
|
||||
$id = $data['id'];
|
||||
unset($data['id']);
|
||||
self::update($id, $data);
|
||||
return $id;
|
||||
} else {
|
||||
return self::create($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the table exists dynamically
|
||||
*/
|
||||
public static function ensureTableExists(): void
|
||||
{
|
||||
static $checked = false;
|
||||
if ($checked) return;
|
||||
try {
|
||||
Database::execute("
|
||||
CREATE TABLE IF NOT EXISTS `company_endpoints` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`company_id` INT NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`endpoint_url` VARCHAR(512) NOT NULL,
|
||||
`action_type` VARCHAR(100) NOT NULL,
|
||||
`description` TEXT NULL,
|
||||
`headers` TEXT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX `idx_endpoint_company` (`company_id`),
|
||||
INDEX `idx_endpoint_action` (`action_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
");
|
||||
$checked = true;
|
||||
} catch (\Exception $e) {
|
||||
error_log("Failed to ensure company_endpoints table: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user