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;