Deploy: 2026-05-23 03:23:22

This commit is contained in:
Hamza-Ayed
2026-05-23 03:23:22 +03:00
parent 30301151c3
commit d686f8928b
10 changed files with 463 additions and 24 deletions

View File

@@ -51,8 +51,19 @@ class AuthController extends BaseController
'role' => 'admin'
]);
// Automatically Assign 14-Day Free Trial (Plan ID: 4)
$startsAt = date('Y-m-d H:i:s');
$endsAt = date('Y-m-d H:i:s', strtotime('+14 days'));
\App\Models\CompanySubscription::subscribeCompany(
$companyId,
4, // Trial Plan ID
14, // Duration in days
'auto_trial',
'system'
);
$response->json([
'message' => 'Company and Admin user registered successfully.',
'message' => 'Company and Admin user registered successfully. 14-Day Free Trial activated.',
'company_id' => $companyId,
'user_id' => $userId
], 201);
@@ -133,15 +144,35 @@ class AuthController extends BaseController
return;
}
$isSuperAdmin = (int)$user['company_id'] === 1;
// Fetch subscription info for the UI
$subscription = \App\Models\CompanySubscription::findActiveByCompany($user['company_id']);
$subStatus = $subscription ? $subscription['status'] : 'expired';
$trialDaysLeft = 0;
if ($subscription && $subscription['status'] === 'active' && strpos((string)$subscription['payment_gateway'], 'auto_trial') !== false) {
$subStatus = 'trialing'; // Treat as trialing
}
if ($subStatus === 'trialing' || ($subscription && $subscription['plan_id'] == 4)) {
$endsAt = strtotime($subscription['ends_at']);
$now = time();
$trialDaysLeft = max(0, ceil(($endsAt - $now) / 86400));
}
$response->json([
'user' => [
'id' => $user['id'],
'company_id' => $user['company_id'],
'name' => $user['name'],
'email' => Security::decrypt($user['email']), // Decrypt email before sending back
'email' => Security::decrypt($user['email']),
'role' => $user['role'],
'status' => $user['status'],
'created_at' => $user['created_at']
'created_at' => $user['created_at'],
'is_super_admin' => $isSuperAdmin,
'subscription_status' => $subStatus,
'trial_days_left' => $trialDaysLeft
]
]);
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Controllers;
use App\Core\Request;
use App\Core\Response;
use App\Core\Database;
class BillingController extends BaseController
{
/**
* Get all available subscription plans
* GET /api/plans
*/
public function getPlans(Request $request, Response $response): void
{
// Don't expose the Trial plan (ID 4) as an upgrade option, only paid ones.
$plans = Database::select("SELECT * FROM subscription_plans WHERE price > 0 ORDER BY price ASC");
$response->json([
'status' => 'success',
'data' => $plans
]);
}
/**
* Upgrade or submit payment for a plan
* POST /api/billing/upgrade
*/
public function upgrade(Request $request, Response $response): void
{
$companyId = $request->company_id;
$body = $request->getBody();
$planId = $body['plan_id'] ?? null;
$paymentMethod = $body['payment_method'] ?? 'manual'; // 'paymob', 'cliq', 'binance', etc.
$receiptReference = $body['receipt_reference'] ?? null;
if (!$planId) {
$response->status(400)->json(['error' => 'Missing plan_id']);
return;
}
$plan = Database::selectOne("SELECT * FROM subscription_plans WHERE id = ?", [$planId]);
if (!$plan) {
$response->status(404)->json(['error' => 'Plan not found']);
return;
}
try {
if ($paymentMethod === 'paymob') {
// Here we would integrate Paymob API to generate a payment link
// For now, we simulate returning a checkout URL.
$checkoutUrl = "https://paymob.com/checkout/mock_url_for_plan_{$planId}_company_{$companyId}";
$response->json([
'status' => 'success',
'message' => 'Redirect to Paymob to complete payment',
'checkout_url' => $checkoutUrl
]);
} else {
// Manual Payment (CliQ, Binance, Bank Transfer)
if (empty($receiptReference)) {
$response->status(400)->json(['error' => 'Please provide a receipt reference or transaction ID']);
return;
}
// Delete any existing pending requests for this company to avoid spam
Database::execute("DELETE FROM company_subscriptions WHERE company_id = ? AND status = 'pending_approval'", [$companyId]);
// Insert a pending subscription request
Database::execute("
INSERT INTO company_subscriptions (company_id, plan_id, status, starts_at, ends_at, payment_method, receipt_reference)
VALUES (?, ?, 'pending_approval', NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), ?, ?)
", [$companyId, $planId, $paymentMethod, $receiptReference]);
$response->json([
'status' => 'success',
'message' => 'Payment receipt submitted successfully. Your account will be upgraded after admin approval.'
]);
}
} catch (\Exception $e) {
$response->status(500)->json(['error' => 'Failed to process upgrade request: ' . $e->getMessage()]);
}
}
}

View File

@@ -46,24 +46,28 @@ class SuperAdminController extends BaseController
cs.plan_id,
sp.name as plan_name,
cs.status as subscription_status,
cs.starts_at as subscription_starts,
cs.ends_at as subscription_ends,
(SELECT COUNT(*) FROM whatsapp_sessions WHERE company_id = c.id) as sessions_count,
(SELECT COUNT(*) FROM whatsapp_sessions WHERE company_id = c.id AND status = 'connected') as active_sessions,
COALESCE(cu.request_count, 0) as request_usage,
COALESCE(cu.voice_count, 0) as voice_usage,
COALESCE(cu.ocr_count, 0) as ocr_usage
SELECT c.id, c.name, c.status as company_status, c.created_at,
cs.plan_id, cs.status as sub_status, cs.starts_at, cs.ends_at, cs.payment_method, cs.receipt_reference,
p.name as plan_name
FROM companies c
LEFT JOIN company_subscriptions cs ON cs.company_id = c.id AND cs.status = 'active'
LEFT JOIN subscription_plans sp ON cs.plan_id = sp.id
LEFT JOIN company_subscription_usage cu ON cu.company_id = c.id
AND cu.billing_start <= CURRENT_DATE()
AND cu.billing_end >= CURRENT_DATE()
ORDER BY c.id ASC
LEFT JOIN company_subscriptions cs ON c.id = cs.company_id AND cs.status IN ('active', 'trialing', 'pending_approval')
LEFT JOIN subscription_plans p ON cs.plan_id = p.id
ORDER BY c.created_at DESC
");
// Fetch list of available subscription plans
$plans = Database::select("SELECT id, name, price, max_sessions FROM subscription_plans ORDER BY price ASC");
$pendingApprovals = [];
$activeCompanies = [];
foreach ($companies as $c) {
if ($c['sub_status'] === 'pending_approval') {
$pendingApprovals[] = $c;
} else {
$activeCompanies[] = $c;
}
}
// 4. Fetch all available plans to allow admin to upgrade them manually
$plans = Database::select("SELECT id, name, price FROM subscription_plans ORDER BY price ASC");
$response->json([
'status' => 'success',
@@ -73,7 +77,8 @@ class SuperAdminController extends BaseController
'total_sessions' => (int)$sessionsCount,
'connected_sessions' => (int)$connectedSessions
],
'companies' => $companies,
'companies' => $activeCompanies,
'pending_approvals' => $pendingApprovals,
'plans' => $plans
]
]);
@@ -138,4 +143,46 @@ class SuperAdminController extends BaseController
$response->status(500)->json(['error' => 'Failed to update subscription: ' . $e->getMessage()]);
}
}
/**
* Approve a pending billing request
* POST /api/admin/companies/approve-billing
*/
public function approveBilling(Request $request, Response $response): void
{
$body = $request->getBody();
$targetCompanyId = isset($body['company_id']) ? (int)$body['company_id'] : null;
if (!$targetCompanyId) {
$response->status(400)->json(['error' => 'Missing company_id']);
return;
}
try {
// Find the pending subscription
$pending = Database::selectOne("SELECT * FROM company_subscriptions WHERE company_id = ? AND status = 'pending_approval' ORDER BY id DESC LIMIT 1", [$targetCompanyId]);
if (!$pending) {
$response->status(404)->json(['error' => 'No pending approval found for this company']);
return;
}
// Deactivate other active/trialing subscriptions
Database::execute("UPDATE company_subscriptions SET status = 'expired' WHERE company_id = ? AND status IN ('active', 'trialing')", [$targetCompanyId]);
// Mark the pending one as active
Database::execute("UPDATE company_subscriptions SET status = 'active' WHERE id = ?", [$pending['id']]);
// Clear active subscription cache for the company
if (class_exists('App\Core\Cache')) {
\App\Core\Cache::delete("company_subscription_{$targetCompanyId}");
}
$response->json([
'status' => 'success',
'message' => 'Billing approved and subscription activated successfully'
]);
} catch (\Exception $e) {
$response->status(500)->json(['error' => 'Failed to approve billing: ' . $e->getMessage()]);
}
}
}

View File

@@ -442,7 +442,27 @@ class WhatsAppController extends BaseController
} elseif ($body['state'] === 'connected') {
$updateData['qr_code'] = null; // Clear QR when connected
if (!empty($body['phone'])) {
$updateData['phone'] = $body['phone'];
// Anti-Abuse: Prevent Duplicate Phone Numbers Across Companies
$existingPhoneSession = \App\Models\WhatsAppSession::findByPhone($body['phone']);
if ($existingPhoneSession && (int)$existingPhoneSession['company_id'] !== (int)$session['company_id']) {
// This phone is already linked to another company! We must disconnect this malicious session.
$nodeUrl = 'http://127.0.0.1:3722/api/sessions/delete';
$payload = json_encode(['session_key' => $session['session_key']]);
$ch = curl_init($nodeUrl);
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']);
curl_exec($ch);
curl_close($ch);
error_log("Anti-Abuse Block: Company ID {$session['company_id']} tried to link phone {$body['phone']} which is already linked to Company ID {$existingPhoneSession['company_id']}. Session disconnected.");
$updateData['status'] = 'disconnected';
$updateData['qr_code'] = null;
} else {
$updateData['phone'] = $body['phone'];
}
}
} elseif ($body['state'] === 'disconnected') {
$updateData['qr_code'] = null;

View File

@@ -86,6 +86,25 @@ class WhatsAppSession extends BaseModel
return $session;
}
/**
* Find session by phone number (useful to prevent duplicates across companies)
*/
public static function findByPhone(string $phone)
{
$phoneHash = Security::blindIndex($phone);
$session = Database::selectOne(
"SELECT * FROM " . static::$table . " WHERE phone_hash = ? LIMIT 1",
[$phoneHash]
);
if ($session) {
$session['phone'] = $session['phone'] ? Security::decrypt($session['phone']) : null;
$session['qr_code'] = $session['qr_code'] ? Security::decrypt($session['qr_code']) : null;
}
return $session;
}
/**
* Create or retrieve a new session for a company
*/

View File

@@ -22,11 +22,13 @@ CREATE TABLE IF NOT EXISTS `company_subscriptions` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`company_id` INT NOT NULL,
`plan_id` INT NOT NULL,
`status` ENUM('active', 'trialing', 'canceled', 'expired', 'past_due') DEFAULT 'active',
`status` ENUM('active', 'trialing', 'canceled', 'expired', 'past_due', 'pending_approval') DEFAULT 'active',
`starts_at` TIMESTAMP NOT NULL,
`ends_at` TIMESTAMP NOT NULL,
`canceled_at` TIMESTAMP NULL DEFAULT NULL,
`payment_gateway` VARCHAR(50) NULL,
`payment_method` VARCHAR(50) NULL COMMENT 'paymob, cliq, binance',
`receipt_reference` VARCHAR(255) NULL COMMENT 'External transaction ID or receipt URL',
`subscription_ref` VARCHAR(255) NULL COMMENT 'External subscription ID reference',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

View File

@@ -0,0 +1,39 @@
<?php
if (php_sapi_name() !== 'cli') {
http_response_code(403);
exit('Access denied.');
}
require_once __DIR__ . '/app/bootstrap.php';
use App\Core\Database;
try {
echo "=== Migrating Billing Tables ===\n";
// 1. Alter company_subscriptions to support new fields and ENUM
Database::execute("
ALTER TABLE company_subscriptions
MODIFY COLUMN status ENUM('active', 'trialing', 'canceled', 'expired', 'past_due', 'pending_approval') DEFAULT 'active';
");
echo "✅ Updated 'status' ENUM in company_subscriptions.\n";
// Add payment_method if it doesn't exist
try {
Database::execute("ALTER TABLE company_subscriptions ADD COLUMN payment_method VARCHAR(50) NULL COMMENT 'paymob, cliq, binance' AFTER payment_gateway");
echo "✅ Added 'payment_method' column.\n";
} catch (\Exception $e) {
echo " Column 'payment_method' may already exist. Skipping.\n";
}
// Add receipt_reference if it doesn't exist
try {
Database::execute("ALTER TABLE company_subscriptions ADD COLUMN receipt_reference VARCHAR(255) NULL COMMENT 'External transaction ID or receipt URL' AFTER payment_method");
echo "✅ Added 'receipt_reference' column.\n";
} catch (\Exception $e) {
echo " Column 'receipt_reference' may already exist. Skipping.\n";
}
echo "=== Migration Completed Successfully! ===\n";
} catch (\Exception $e) {
echo "❌ Migration Failed: " . $e->getMessage() . "\n";
}

View File

@@ -760,7 +760,7 @@
<!-- Left Sidebar Nav -->
<div class="nav-menu">
<!-- Super Admin Dashboard -->
<button class="nav-item" x-show="user?.is_super_admin" :class="{ 'active': activeDashboardTab === 'super_admin' }" @click="activeDashboardTab = 'super_admin'; fetchSuperAdminStats()" id="nav-superadmin-btn">
<button class="nav-item" x-show="user?.is_super_admin" :class="{ 'active': activeDashboardTab === 'super_admin' }" @click="activeDashboardTab = 'super_admin'; fetchSuperAdminStats()" id="nav-superadmin-btn">
<span class="nav-icon">👑</span>
<span class="nav-text" x-text="lang === 'ar' ? 'لوحة المشرف العام' : 'Super Admin'"></span>
</button>
@@ -768,6 +768,9 @@
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'whatsapp' }" @click="activeDashboardTab = 'whatsapp'" id="nav-whatsapp-btn">
<span>📱</span> <span x-text="lang === 'ar' ? 'اتصال الواتساب' : 'WhatsApp Connection'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'billing' }" @click="activeDashboardTab = 'billing'; fetchPlans()" id="nav-billing-btn">
<span>💳</span> <span x-text="lang === 'ar' ? 'الباقات والاشتراكات' : 'Billing & Plans'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'contacts' }" @click="activeDashboardTab = 'contacts'; fetchContacts(); fetchGroups()" id="nav-contacts-btn">
<span>👥</span> <span x-text="lang === 'ar' ? 'دليل جهات الاتصال' : 'Contacts Directory'"></span>
</button>
@@ -790,6 +793,11 @@
<!-- Right Dashboard Panels -->
<div style="flex: 1;">
<template x-if="user?.subscription_status === 'trialing' || (user?.subscription_status === 'active' && user?.trial_days_left < 14)">
<div class="banner banner-warning" style="margin-bottom: 1.5rem;">
<span x-text="lang === 'ar' ? 'متبقي لك ' + user.trial_days_left + ' أيام من الفترة التجريبية. اشترك الآن لضمان استمرار الخدمة.' : 'You have ' + user.trial_days_left + ' days left in your trial. Subscribe now to ensure service continuity.'"></span>
</div>
</template>
<!-- Global Dashboard Banner -->
<template x-if="dashboardSuccess">
<div class="banner banner-success" style="margin-bottom: 1.5rem;" id="dashboard-success-banner">
@@ -821,6 +829,38 @@
</div>
</div>
<div style="margin-top: 2rem; margin-bottom: 2rem;" x-show="superAdminPending?.length > 0">
<h3 style="margin-bottom: 1rem;"><span style="color: var(--warning-color);"></span> <span x-text="lang === 'ar' ? 'طلبات الدفع بانتظار الموافقة' : 'Pending Approvals'"></span></h3>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th x-text="lang === 'ar' ? 'معرف الشركة' : 'Company ID'"></th>
<th x-text="lang === 'ar' ? 'اسم الشركة' : 'Company Name'"></th>
<th x-text="lang === 'ar' ? 'الباقة المطلوبة' : 'Requested Plan'"></th>
<th x-text="lang === 'ar' ? 'طريقة الدفع' : 'Payment Method'"></th>
<th x-text="lang === 'ar' ? 'رقم الحوالة' : 'Receipt Reference'"></th>
<th x-text="lang === 'ar' ? 'الإجراء' : 'Action'"></th>
</tr>
</thead>
<tbody>
<template x-for="req in superAdminPending" :key="req.company_id">
<tr>
<td x-text="'#' + req.id"></td>
<td x-text="req.name"></td>
<td><span class="badge badge-info" x-text="req.plan_name"></span></td>
<td x-text="req.payment_method?.toUpperCase()"></td>
<td x-text="req.receipt_reference"></td>
<td>
<button class="btn btn-primary btn-sm" @click="approveBilling(req.id)" x-text="lang === 'ar' ? 'موافقة وتفعيل' : 'Approve & Activate'"></button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<h3 style="margin-top:2rem;" x-text="lang === 'ar' ? 'قائمة الشركات' : 'Companies List'"></h3>
<div class="data-table" style="overflow-x: auto;">
<table>
@@ -1020,6 +1060,30 @@
</div>
</div>
<!-- Panel: Billing & Plans -->
<div class="panel" x-show="activeDashboardTab === 'billing'" id="panel-billing">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<h2 style="font-size: 1.4rem; margin: 0;" x-text="lang === 'ar' ? 'الباقات والاشتراكات' : 'Billing & Plans'"></h2>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem;" :style="lang === 'ar' ? 'direction: rtl;' : ''">
<template x-for="plan in availablePlans" :key="plan.id">
<div class="card" style="border: 2px solid transparent; transition: 0.3s; padding: 2rem; text-align: center;" :style="userPlanId == plan.id ? 'border-color: var(--primary-accent);' : ''">
<h3 style="font-size: 1.5rem; margin-bottom: 0.5rem;" x-text="plan.name"></h3>
<div style="font-size: 2rem; font-weight: bold; color: var(--primary-accent); margin-bottom: 1rem;">
$<span x-text="plan.price"></span> <span style="font-size: 1rem; color: var(--text-muted);">/ <span x-text="lang === 'ar' ? 'شهر' : 'mo'"></span></span>
</div>
<ul style="list-style: none; padding: 0; margin-bottom: 1.5rem; text-align: start; line-height: 1.8;">
<li><span style="margin-right: 0.5rem;"></span> <span x-text="plan.max_sessions"></span> <span x-text="lang === 'ar' ? 'أرقام واتساب' : 'WhatsApp Numbers'"></span></li>
<li><span style="margin-right: 0.5rem;"></span> <span x-text="plan.max_requests"></span> <span x-text="lang === 'ar' ? 'رسالة نصية' : 'Text Messages'"></span></li>
<li><span style="margin-right: 0.5rem;"></span> <span x-text="plan.max_voice_requests"></span> <span x-text="lang === 'ar' ? 'رسالة صوتية' : 'Voice Notes'"></span></li>
</ul>
<button class="btn btn-primary" style="width: 100%;" @click="openCheckoutModal(plan)" :disabled="userPlanId == plan.id" x-text="userPlanId == plan.id ? (lang === 'ar' ? 'باقتك الحالية' : 'Current Plan') : (lang === 'ar' ? 'ترقية الآن' : 'Upgrade Now')"></button>
</div>
</template>
</div>
</div>
<!-- Panel: Contacts Directory -->
<div class="panel" x-show="activeDashboardTab === 'contacts'" id="panel-contacts">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
@@ -1216,7 +1280,7 @@
💡 نصائح وتوجيهات لكتابة تعليمات ممتازة:
</strong>
<ul style="list-style-type: disc; margin-right: 1.25rem; padding-left: 0; line-height: 1.5; margin-bottom: 0;">
<li><strong>الهوية والاسم:</strong> حدد اسم الروبوت بوضوح (مثال: "أنا سارة من فريق تطبيق نبيه").</li>
<li><strong>الهوية والاسم:</strong> حدد اسم الروبوت بوحضوح (مثال: "أنا سارة من فريق تطبيق نبيه").</li>
<li><strong>اللهجة والأسلوب:</strong> اطلب من الذكاء الاصطناعي الرد بلهجة معينة (مثال: اللهجة السورية أو الفصحى المبسطة).</li>
<li><strong>البيانات الأساسية:</strong> اكتب ساعات عمل المتجر، طرق الدفع والتوصيل، وسياسة الاستبدال لكي يجيب الروبوت بدقة.</li>
<li><strong>التعليمات اللغوية:</strong> قمنا بتضمين ميزة مطابقة اللغة تلقائياً (الرد بالإنجليزية على الرسائل الإنجليزية، وبالعربية على العربية).</li>
@@ -1455,6 +1519,45 @@
</div>
</div>
<!-- Modal: Checkout -->
<div class="modal-overlay" x-show="showCheckoutModal" id="modal-checkout" style="display: none;">
<div class="modal-card">
<div class="modal-header">
<h3 class="modal-title" x-text="lang === 'ar' ? 'إتمام عملية الدفع' : 'Complete Checkout'"></h3>
<button class="modal-close" @click="showCheckoutModal = false">&times;</button>
</div>
<div class="modal-body">
<p x-text="lang === 'ar' ? 'أنت تقوم بترقية حسابك إلى باقة: ' + selectedCheckoutPlan?.name : 'You are upgrading to: ' + selectedCheckoutPlan?.name"></p>
<p style="font-weight: bold; font-size: 1.2rem; margin-bottom: 1rem;">
<span x-text="lang === 'ar' ? 'المبلغ المطلوب: ' : 'Total Amount: '"></span> $<span x-text="selectedCheckoutPlan?.price"></span>
</p>
<div class="form-group">
<label class="form-label" x-text="lang === 'ar' ? 'طريقة الدفع' : 'Payment Method'"></label>
<select x-model="checkoutPaymentMethod" class="form-input">
<option value="paymob" x-text="lang === 'ar' ? 'البطاقة الائتمانية (Paymob)' : 'Credit Card (Paymob)'"></option>
<option value="cliq" x-text="lang === 'ar' ? 'كليك (CliQ) - الأردن' : 'CliQ Transfer (Jordan)'"></option>
<option value="binance" x-text="lang === 'ar' ? 'بينانس (Binance Pay)' : 'Crypto (Binance)'"></option>
</select>
</div>
<template x-if="checkoutPaymentMethod !== 'paymob'">
<div class="form-group">
<label class="form-label" x-text="lang === 'ar' ? 'رقم الحوالة / Transaction ID' : 'Receipt Reference / TXID'"></label>
<input type="text" x-model="checkoutReceipt" class="form-input" :placeholder="lang === 'ar' ? 'أدخل رقم الحوالة هنا' : 'Enter reference here'">
</div>
</template>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="showCheckoutModal = false" x-text="lang === 'ar' ? 'إلغاء' : 'Cancel'"></button>
<button class="btn btn-primary" @click="submitCheckout()" :disabled="actionLoading">
<span x-show="!actionLoading" x-text="lang === 'ar' ? 'تأكيد الدفع' : 'Confirm Payment'"></span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</div>
</div>
</div>
<!-- Modal: Add Contact -->
<div class="modal-overlay" x-show="showAddContactModal" id="modal-add-contact" style="display: none;">
<div class="modal-card">
@@ -1711,6 +1814,7 @@
// Dashboard States
superAdminStats: null,
superAdminCompanies: [],
superAdminPending: [],
superAdminPlans: [],
activeDashboardTab: 'whatsapp',
whatsappSession: null,
@@ -1725,6 +1829,16 @@
password: '',
whatsapp_session_id: ''
},
// Billing State
availablePlans: [],
userPlanId: null,
showCheckoutModal: false,
selectedCheckoutPlan: null,
checkoutPaymentMethod: 'paymob',
checkoutReceipt: '',
checkoutError: '',
checkoutSuccess: '',
woocommerceStatus: null,
woocommerceLoading: false,
wooForm: {
@@ -1934,6 +2048,71 @@
}
},
// Billing & Checkout Methods
async fetchPlans() {
try {
const response = await fetch('/api/plans', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok && data.status === 'success') {
this.availablePlans = data.data;
}
} catch (err) {
console.error('Failed to fetch plans', err);
}
},
openCheckoutModal(plan) {
this.selectedCheckoutPlan = plan;
this.checkoutPaymentMethod = 'paymob';
this.checkoutReceipt = '';
this.checkoutError = '';
this.checkoutSuccess = '';
this.showCheckoutModal = true;
},
async submitCheckout() {
if (this.checkoutPaymentMethod !== 'paymob' && !this.checkoutReceipt) {
this.checkoutError = this.lang === 'ar' ? 'الرجاء إدخال رقم الحوالة' : 'Please enter receipt reference';
return;
}
this.actionLoading = true;
this.checkoutError = '';
try {
const response = await fetch('/api/billing/upgrade', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify({
plan_id: this.selectedCheckoutPlan.id,
payment_method: this.checkoutPaymentMethod,
receipt_reference: this.checkoutReceipt
})
});
const data = await response.json();
if (response.ok && data.status === 'success') {
if (this.checkoutPaymentMethod === 'paymob' && data.checkout_url) {
window.location.href = data.checkout_url;
} else {
this.checkoutSuccess = data.message;
setTimeout(() => {
this.showCheckoutModal = false;
}, 3000);
}
} else {
this.checkoutError = data.error || 'Failed to submit payment request';
}
} catch (err) {
this.checkoutError = 'Network error while submitting payment';
} finally {
this.actionLoading = false;
}
},
async fetchWhatsappStatus() {
if (!this.token) return;
const queryParam = this.whatsappSession ? `?session_id=${this.whatsappSession.id}` : '';
@@ -1971,6 +2150,7 @@
if (response.ok && data.status === 'success') {
this.superAdminStats = data.data.stats;
this.superAdminCompanies = data.data.companies;
this.superAdminPending = data.data.pending_approvals;
this.superAdminPlans = data.data.plans;
}
} catch (err) {

View File

@@ -80,6 +80,11 @@ $router->post('/api/otp/send', [\App\Controllers\OTPController::class
// Super Admin Routes
$router->get('/api/admin/stats', [\App\Controllers\SuperAdminController::class, 'getStats'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/admin/companies/subscribe', [\App\Controllers\SuperAdminController::class, 'subscribeCompany'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/admin/companies/approve-billing', [\App\Controllers\SuperAdminController::class, 'approveBilling'], [\App\Middlewares\AuthMiddleware::class]);
// Billing & Subscription Routes
$router->get('/api/plans', [\App\Controllers\BillingController::class, 'getPlans'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/billing/upgrade', [\App\Controllers\BillingController::class, 'upgrade'], [\App\Middlewares\AuthMiddleware::class]);
// Phase 4 & 5: CRM, Templates & Campaigns Routes
$router->get('/api/contacts', [\App\Controllers\ContactController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);

View File

@@ -42,7 +42,17 @@ try {
'max_requests' => 15000,
'max_voice_requests' => 2000,
'max_ocr_requests' => 2000,
'features' => json_encode(['text_chatbot' => true, 'campaigns' => true, 'woocommerce' => true, 'salla' => true, 'webhooks' => true])
'features' => json_encode(['text_chatbot' => true, 'campaigns' => true, 'woocommerce' => true, 'salla' => true, 'webhooks' => true, 'voice' => true])
],
[
'id' => 4,
'name' => 'Trial (14 Days)',
'price' => 0.00,
'max_sessions' => 1,
'max_requests' => 5000,
'max_voice_requests' => 100,
'max_ocr_requests' => 100,
'features' => json_encode(['text_chatbot' => true, 'campaigns' => true, 'woocommerce' => true, 'salla' => true, 'webhooks' => true, 'voice' => true])
]
];