public function updateRssSource(): never {$this->guard();Security::verifyCsrf($_POST['_csrf']??null);$id=(int)($_POST['id']??0);$name=trim((string)($_POST['name']??''));$url=trim((string)($_POST['feed_url']??''));if(!$id||!$name||!filter_var($url,FILTER_VALIDATE_URL)){$_SESSION['flash']='Source name and valid RSS URL required';redirect('/admin/rss');}Database::run('UPDATE rss_sources SET name=?,feed_url=?,category_id=? WHERE id=?',[$name,$url,(int)($_POST['category_id']??0)?:null,$id]);$_SESSION['flash']='RSS news link updated';redirect('/admin/rss');} public function rssStatus(): never {$this->guard();Security::verifyCsrf($_POST['_csrf']??null);Database::run('UPDATE rss_sources SET is_active=IF(is_active=1,0,1) WHERE id=?',[(int)$_POST['id']]);redirect('/admin/rss');} public function deleteRssSource(): never {$this->guard();Security::verifyCsrf($_POST['_csrf']??null);Database::run('DELETE FROM rss_sources WHERE id=?',[(int)$_POST['id']]);redirect('/admin/rss');} public function employees(): void {$this->guard();$page=max(1,(int)($_GET['page']??1));$perPage=20;$total=(int)(Database::fetch('SELECT COUNT(*) n FROM users WHERE role="employee"')['n']??0);$pages=max(1,(int)ceil($total/$perPage));$page=min($page,$pages);$items=Database::all('SELECT u.*,(SELECT COUNT(*) FROM businesses b WHERE b.created_by=u.id) referral_count,(SELECT COALESCE(SUM(c.amount),0) FROM commissions c WHERE c.employee_id=u.id AND c.status IN("pending","approved")) pending_amount,(SELECT COALESCE(SUM(c.amount),0) FROM commissions c WHERE c.employee_id=u.id AND c.status="paid") paid_amount,(SELECT COUNT(*) FROM employee_kyc_documents k WHERE k.employee_id=u.id) kyc_count,(SELECT COUNT(*) FROM employee_kyc_documents k WHERE k.employee_id=u.id AND k.status="verified") kyc_verified,(SELECT status FROM employee_bank_details eb WHERE eb.employee_id=u.id LIMIT 1) bank_status FROM users u WHERE u.role="employee" ORDER BY u.id DESC LIMIT '.$perPage.' OFFSET '.(($page-1)*$perPage));view('admin/employees',compact('items','page','pages','total'));} private function paymentsFilters(): array { $status=in_array($_GET['status']??'', ['created','paid','failed','refunded'],true)?$_GET['status']:''; $purpose=in_array($_GET['purpose']??'', ['subscription','classified','popup'],true)?$_GET['purpose']:''; $businessId=(int)($_GET['business_id']??0); $from=trim((string)($_GET['from']??''));$to=trim((string)($_GET['to']??'')); $from=(bool)strtotime($from)?date('Y-m-d',strtotime($from)):''; $to=(bool)strtotime($to)?date('Y-m-d',strtotime($to)):''; $where=[];$params=[]; if($status){$where[]='p.status=?';$params[]=$status;} if($purpose){$where[]='p.purpose=?';$params[]=$purpose;} if($businessId){$where[]='p.business_id=?';$params[]=$businessId;} if($from){$where[]='DATE(p.created_at)>=?';$params[]=$from;} if($to){$where[]='DATE(p.created_at)<=?';$params[]=$to;} $sql=$where?(' WHERE '.implode(' AND ',$where)):''; return [$sql,$params,compact('status','purpose','businessId','from','to')]; } // Payment rows always resolve plan name/price live from the plans table via // subscriptions.plan_id, so renamed/repriced/newly added plans show correctly // without touching this query. private function paymentsBaseSql(): string { return 'SELECT p.*,u.name user_name,u.mobile user_mobile,b.name business_name,pl.name plan_name,pl.code plan_code FROM payments p LEFT JOIN users u ON u.id=p.user_id LEFT JOIN businesses b ON b.id=p.business_id LEFT JOIN subscriptions s ON s.payment_id=p.id LEFT JOIN plans pl ON pl.id=s.plan_id'; } public function payments(): void { $this->guard(); [$whereSql,$params,$filters]=$this->paymentsFilters(); $page=max(1,(int)($_GET['page']??1));$perPage=25; $total=(int)(Database::fetch('SELECT COUNT(*) n FROM payments p'.$whereSql,$params)['n']??0); $pages=max(1,(int)ceil($total/$perPage));$page=min($page,$pages); $items=Database::all($this->paymentsBaseSql().$whereSql.' ORDER BY p.id DESC LIMIT '.$perPage.' OFFSET '.(($page-1)*$perPage),$params); $summary=Database::fetch('SELECT COUNT(*) n,COALESCE(SUM(CASE WHEN status="paid" THEN amount END),0) collected,COALESCE(SUM(CASE WHEN status="created" THEN amount END),0) pending_amount,COALESCE(SUM(CASE WHEN status="refunded" THEN amount END),0) refunded FROM payments p'.$whereSql,$params); $businesses=Database::all('SELECT id,name FROM businesses ORDER BY name'); view('admin/payments',compact('items','page','pages','total','filters','summary','businesses')); } public function updatePaymentStatus(): never { $this->guard(); Security::verifyCsrf($_POST['_csrf']??null); $id=(int)($_POST['id']??0); $status=in_array($_POST['status']??'', ['created','paid','failed','refunded'],true)?$_POST['status']:''; $back=trim((string)($_POST['back']??''))?:'/admin/payments'; if(!$id||!$status){$_SESSION['flash']='Invalid payment status update';redirect($back);} try { match($status){ 'paid'=>PaymentService::markPaidManually($id), 'refunded'=>PaymentService::markRefunded($id), 'failed'=>PaymentService::markFailed($id), 'created'=>Database::run('UPDATE payments SET status="created" WHERE id=?',[$id]), }; $_SESSION['flash']='Payment marked as '.$status.'. Related subscription/business status updated as per plan.'; } catch(\Throwable $e) { $_SESSION['flash']='Could not update payment: '.$e->getMessage(); } redirect($back); } public function exportPayments(): never { $this->guard(); [$whereSql,$params]=$this->paymentsFilters(); $rows=Database::all($this->paymentsBaseSql().$whereSql.' ORDER BY p.id DESC',$params); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="payments-'.date('Ymd-His').'.csv"'); $out=fopen('php://output','w'); fputcsv($out,['Payment ID','Date','Business','Owner Name','Owner Mobile','Purpose','Plan','Amount','Currency','Status','Merchant Order ID','Razorpay Order ID','Razorpay Payment ID','Paid At']); foreach($rows as $paymentRow){ fputcsv($out,[$paymentRow['id'],$paymentRow['created_at'],$paymentRow['business_name'],$paymentRow['user_name'],$paymentRow['user_mobile'],$paymentRow['purpose'],$paymentRow['plan_name']?:'—',$paymentRow['amount'],$paymentRow['currency'],$paymentRow['status'],$paymentRow['merchant_order_id'],$paymentRow['razorpay_order_id'],$paymentRow['razorpay_payment_id'],$paymentRow['paid_at']]); } fclose($out); exit; } public function settlements(): void {$this->guard();$page=max(1,(int)($_GET['page']??1));$perPage=20;$total=(int)(Database::fetch('SELECT COUNT(*) n FROM commissions')['n']??0);$pages=max(1,(int)ceil($total/$perPage));$page=min($page,$pages);$settlements=Database::all('SELECT c.*,u.name employee_name,u.mobile employee_mobile,b.name business_name,p.amount payment_amount,p.razorpay_payment_id FROM commissions c JOIN users u ON u.id=c.employee_id JOIN businesses b ON b.id=c.business_id LEFT JOIN payments p ON p.id=c.payment_id ORDER BY c.id DESC LIMIT '.$perPage.' OFFSET '.(($page-1)*$perPage));view('admin/settlements',compact('settlements','page','pages','total'));} public function employeeStatus(): never {$this->guard();Security::verifyCsrf($_POST['_csrf']??null);$status=in_array($_POST['status']??'', ['active','pending','blocked'],true)?$_POST['status']:'pending';Database::run('UPDATE users SET status=?,commission_rate=? WHERE id=? AND role="employee"',[$status,max(0,min(100,(float)($_POST['commission_rate']??0))),(int)$_POST['id']]);redirect('/admin/employees');} public function createEmployee(): never {$this->guard();Security::verifyCsrf($_POST['_csrf']??null);$mobile=Security::cleanMobile($_POST['mobile']??'');$pin=(string)($_POST['pin']??'');if(strlen($mobile)!==10||!preg_match('/^\d{4}$/',$pin)||Database::fetch('SELECT id FROM users WHERE mobile=? AND role="employee"',[$mobile])){$_SESSION['flash']='Valid mobile/PIN required or employee already exists';redirect('/admin/employees');}$status=($_POST['status']??'pending')==='active'?'active':'pending';Database::run('INSERT INTO users(name,mobile,pin_hash,role,status,commission_rate) VALUES(?,?,?,"employee",?,?)',[trim($_POST['name']),$mobile,Security::pinHash($pin),$status,max(0,min(100,(float)($_POST['commission_rate']??0)))]);$_SESSION['flash']='Employee added';redirect('/admin/employees');} public function commissionStatus(): never {$this->guard();Security::verifyCsrf($_POST['_csrf']??null);$status=in_array($_POST['status']??'', ['pending','approved','paid'],true)?$_POST['status']:'pending';$paidAt=$status==='paid'?date('Y-m-d H:i:s'):null;Database::run('UPDATE commissions SET status=?,paid_at=? WHERE id=?',[$status,$paidAt,(int)($_POST['id']??0)]);$_SESSION['flash']=$status==='paid'?'Settlement marked as paid':'Commission status updated';redirect('/admin/settlements');} public function security(): void { $user=$this->guard(); $record=Database::fetch('SELECT totp_enabled FROM users WHERE id=? AND role="super_admin"',[$user['id']]); $pendingSecret=(string)($_SESSION['totp_setup_secret']??''); $otpauth=$pendingSecret?TotpService::otpauthUri($pendingSecret,'Natepute Express',(string)$user['mobile']):''; view('admin/security',compact('user','record','pendingSecret','otpauth')); } public function securitySetup(): never { $user=$this->guard(); Security::verifyCsrf($_POST['_csrf']??null); $_SESSION['totp_setup_secret']=TotpService::secret(); redirect('/admin/security'); } public function securityEnable(): never { $user=$this->guard(); Security::verifyCsrf($_POST['_csrf']??null); $secret=(string)($_SESSION['totp_setup_secret']??''); if($secret==='' || !TotpService::verify($secret,(string)($_POST['code']??''))){$_SESSION['flash']='Invalid authenticator code. Scan/setup the secret and try again.';redirect('/admin/security');} Database::run('UPDATE users SET totp_secret=?,totp_enabled=1,totp_enabled_at=NOW() WHERE id=? AND role="super_admin"',[$secret,$user['id']]);unset($_SESSION['totp_setup_secret']);$_SESSION['flash']='Super Admin 2FA enabled. Authenticator app will be required at login.';redirect('/admin/security'); } public function securityDisable(): never { $user=$this->guard(); Security::verifyCsrf($_POST['_csrf']??null); Database::run('UPDATE users SET totp_secret=NULL,totp_enabled=0,totp_enabled_at=NULL WHERE id=? AND role="super_admin"',[$user['id']]);unset($_SESSION['totp_setup_secret']);$_SESSION['flash']='Super Admin 2FA disabled.';redirect('/admin/security'); } public function kyc(): void { $this->guard(); $documents=Database::all('SELECT k.*,u.name employee_name,u.mobile FROM employee_kyc_documents k JOIN users u ON u.id=k.employee_id WHERE u.role="employee" ORDER BY k.id DESC'); $banks=Database::all('SELECT eb.*,u.name employee_name,u.mobile FROM employee_bank_details eb JOIN users u ON u.id=eb.employee_id WHERE u.role="employee" ORDER BY eb.id DESC'); foreach($banks as &$bank){$bank['account_number']=CryptoService::decrypt($bank['account_number_encrypted']);$bank['ifsc']=CryptoService::decrypt($bank['ifsc_encrypted']);$bank['upi_id']=CryptoService::decrypt($bank['upi_id_encrypted']);} unset($bank); view('admin/employee_kyc',compact('documents','banks')); } public function kycStatus(): never { $this->guard(); Security::verifyCsrf($_POST['_csrf']??null);$id=(int)($_POST['id']??0);$status=in_array($_POST['status']??'', ['pending','verified','rejected'],true)?$_POST['status']:'pending';$remarks=trim((string)($_POST['remarks']??'')); Database::run('UPDATE employee_kyc_documents SET status=?,remarks=?,verified_at=?,verified_by=? WHERE id=? AND employee_id IN (SELECT id FROM users WHERE role="employee")',[$status,$remarks,$status==='verified'?date('Y-m-d H:i:s'):null,$status==='verified'?(int)Auth::user()['id']:null,$id]);$_SESSION['flash']='Employee KYC status updated';redirect('/admin/employees'); } public function bankStatus(): never { $this->guard(); Security::verifyCsrf($_POST['_csrf']??null);$id=(int)($_POST['id']??0);$status=in_array($_POST['status']??'', ['pending','verified','rejected'],true)?$_POST['status']:'pending';Database::run('UPDATE employee_bank_details SET status=?,verified_at=?,verified_by=? WHERE id=?',[$status,$status==='verified'?date('Y-m-d H:i:s'):null,$status==='verified'?(int)Auth::user()['id']:null,$id]);$_SESSION['flash']='Employee bank status updated';redirect('/admin/employees/kyc'); } public function downloadKyc(int $employeeId,int $docId): never { $this->guard();$doc=Database::fetch('SELECT * FROM employee_kyc_documents WHERE id=? AND employee_id=?',[$docId,$employeeId]);if(!$doc)Security::abort(404,'Document not found');$file=DocumentService::absolute($doc['file_path']);if(!is_file($file))Security::abort(404,'Document file not found');header('Content-Type: '.($doc['mime_type']?:'application/octet-stream'));header('Content-Length: '.filesize($file));header('Content-Disposition: inline; filename="'.preg_replace('/[^A-Za-z0-9._-]/','_',basename($doc['original_name'])).'"');readfile($file);exit; } }