Deploy: 2026-05-23 03:23:22
This commit is contained in:
@@ -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
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
86
backend/app/Controllers/BillingController.php
Normal file
86
backend/app/Controllers/BillingController.php
Normal 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()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user