/** * Plugin Name: TSN Booking Backend — Repaired * Description: Booking storage, private manager access, date-specific availability and approval allocation. * Version: 2.0.1 * Requires PHP: 7.4 */ if (!defined('ABSPATH')) exit; class TSN_Booking_Fixed { private $manager = false; function __construct() { add_action('init', function(){ register_post_type('tsn_booking', ['label'=>'TSN Bookings','public'=>false,'show_ui'=>true,'supports'=>['title'],'capabilities'=>['edit_posts'=>'manage_options','create_posts'=>'manage_options'],'map_meta_cap'=>true]); }); add_action('rest_api_init', [$this,'routes']); add_action('add_meta_boxes', function(){add_meta_box('tsn_details','Stored booking details',[$this,'details'],'tsn_booking');}); } function details($post) { echo '
'.esc_html(wp_json_encode(get_post_meta($post->ID,'tsn_booking_data',true), JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)).'
'; } function catalog() { return apply_filters('tsn_room_catalog', [ 'VIP Suite (AC)'=>['total'=>0,'rates'=>['army'=>1000,'iaf'=>1500,'civil'=>2000]], 'Premier Room (AC)'=>['total'=>5,'rates'=>['army'=>800,'iaf'=>1500,'civil'=>2000]], 'Super Deluxe (AC)'=>['total'=>4,'rates'=>['army'=>750,'iaf'=>1100,'civil'=>1350]], 'Deluxe Single-Bed (AC)'=>['total'=>8,'rates'=>['army'=>450,'iaf'=>650,'civil'=>900]], 'Dormitory'=>['total'=>5,'rates'=>['army'=>150,'iaf'=>150,'civil'=>150]] ]); } function routes() { foreach ([['/test','GET','test',false],['/booking-receipt','POST','receipt',false],['/manager-login','POST','login',false],['/manager-logout','POST','logout',true],['/availability','GET','availability',false],['/bookings','GET','listing',true],['/bookings','POST','create',false],['/booking-status','POST','status',true],['/manager-booking','POST','create_manager',true],['/bookings-import','POST','import',true]] as $r) { register_rest_route('tsn/v1',$r[0],['methods'=>$r[1],'callback'=>[$this,$r[2]],'permission_callback'=>$r[3]?[$this,'authorize']:'__return_true']); } } function err($m,$s=400) { return new WP_Error('tsn_error',$m,['status'=>$s]); } function response($d) { $r=new WP_REST_Response($d); $r->header('Cache-Control','no-store, private, max-age=0'); return $r; } function token_key($r) { return 'tsn_session_'.hash('sha256',preg_replace('/^Bearer\s+/i','',$r->get_header('authorization'))); } function authorize($r) { $uid=get_transient($this->token_key($r)); if (!$uid || !user_can((int)$uid,'manage_options')) return $this->err('Manager session expired or access denied. Sign in with a WordPress administrator account.',401); $this->manager=true; return true; } function throttle($scope,$limit) { $key='tsn_rl_'.hash('sha256',$scope.'|'.($_SERVER['REMOTE_ADDR']??'')); $n=(int)get_transient($key); if($n >= $limit) return false; set_transient($key,$n+1,10*MINUTE_IN_SECONDS); return true; } function login($r) { if(!$this->throttle('login',20)) return $this->err('Too many login attempts. Try again in ten minutes.',429); $p=$r->get_json_params(); $u=wp_authenticate(sanitize_user($p['username']??''),(string)($p['password']??'')); if(is_wp_error($u) || !user_can($u,'manage_options')) return $this->err('Invalid credentials or administrator access required.',403); $token=bin2hex(random_bytes(32)); set_transient('tsn_session_'.hash('sha256',$token),$u->ID,8*HOUR_IN_SECONDS); return $this->response(['success'=>true,'token'=>$token]); } function logout($r) {delete_transient($this->token_key($r)); return $this->response(['success'=>true]);} function canonical($name) { return $name==='Premium (AC)' ? 'Premier Room (AC)' : $name; } function all() { $out=[]; foreach(get_posts(['post_type'=>'tsn_booking','post_status'=>'publish','numberposts'=>-1,'orderby'=>'date','order'=>'DESC']) as $p) { $b=get_post_meta($p->ID,'tsn_booking_data',true); if(!is_array($b)) { $legacy=get_post_meta($p->ID,'_tsn_booking_payload',true); $b=is_string($legacy)?json_decode($legacy,true):null; if(!is_array($b)) continue; foreach(['approvalStatus'=>'_tsn_approval_status','paymentStatus'=>'_tsn_payment_status'] as $field=>$meta) { $status=get_post_meta($p->ID,$meta,true); if($status!=='')$b[$field]=$status; } } $b['bookingId']=$b['bookingId']??get_post_meta($p->ID,'tsn_booking_id',true); $b['roomType']=$this->canonical($b['roomType']??''); $b['approvalStatus']=$b['approvalStatus']??'Pending Approval'; $b['_post']=$p->ID; $out[]=$b; } return $out; } function find($id) { foreach($this->all() as $b) if((string)$b['bookingId']===(string)$id) return $b; return null; } function lines($b) {return !empty($b['rooms']) && is_array($b['rooms']) ? $b['rooms'] : [['roomType'=>$b['roomType']??'','quantity'=>$b['roomCount']??1]];} function date_ok($s) {if(!is_string($s)||!preg_match('/^\d{4}-\d{2}-\d{2}$/',$s))return false;return date('Y-m-d',strtotime($s.' UTC'))===$s;} function dates($p) { $a=$p['checkin']??''; $b=$p['checkout']??''; if(!$this->date_ok($a)||!$this->date_ok($b)||$b<=$a) return $this->err('Valid check-in and check-out dates are required.'); $n=(strtotime($b.' UTC')-strtotime($a.' UTC'))/86400; if($n>2) return $this->err('Maximum stay is two nights.'); $today=current_datetime()->format('Y-m-d'); if($a<$today || $a>current_datetime()->modify('+60 days')->format('Y-m-d')) return $this->err('Check-in must be today or within the next 60 days.'); if($a<='2026-10-31' && $b>'2026-10-31')return $this->err('October 31, 2026 is closed.'); return (int)$n; } function occupied($room,$night,$exclude='') { $n=0; foreach($this->all() as $b) { if(($b['bookingId']??'')===$exclude || ($b['approvalStatus']??'')!=='Approved' || ($b['checkin']??'')>$night || ($b['checkout']??'')<=$night)continue; foreach($this->lines($b) as $line) if($this->canonical($line['roomType']??'')===$room)$n+=max(1,(int)($line['quantity']??1)); } return $n; } function inventory($a,$b,$exclude='') { $out=[]; foreach($this->catalog() as $name=>$r) { $free=(int)$r['total']; if($this->date_ok($a)&&$this->date_ok($b)&&$b>$a && strtotime($b)-strtotime($a)<=60*86400) { for($d=$a;$d<$b;$d=gmdate('Y-m-d',strtotime($d.' UTC')+86400)) $free=min($free,max(0,$r['total']-$this->occupied($name,$d,$exclude))); } $out[]=['roomType'=>$name,'total'=>$r['total'],'available'=>$free,'rate'=>$r['rates']['army'],'category'=>'']; } return $out; } function availability($r) { $out=[];foreach($this->all() as $b) if($b['approvalStatus']==='Approved') foreach($this->lines($b) as $line)$out[]=['roomType'=>$this->canonical($line['roomType']??''),'roomCount'=>(int)($line['quantity']??1),'checkin'=>$b['checkin']??'','checkout'=>$b['checkout']??'','approvalStatus'=>'Approved']; return $this->response(['bookings'=>$out,'inventory'=>$this->inventory($r['checkin'],$r['checkout'])]); } function listing($r) { $out=$this->all();foreach($out as &$b)unset($b['_post']); return $this->response(['bookings'=>$out,'inventory'=>$this->inventory($r['checkin'],$r['checkout'])]); } function locked($fn) { global $wpdb; $key='tsn_'.md5($wpdb->prefix.'inventory'); if((int)$wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 5)',$key))!==1)return $this->err('Booking system is busy. Please try again.',503); try{return $fn();}finally{$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)',$key));} } function normalize($p,$manager=false) { if(!is_array($p))return $this->err('JSON booking data is required.'); $n=$this->dates($p);if(is_wp_error($n))return $n; $g=$p['guest']??[];if(!is_array($g))return $this->err('Guest details are invalid.'); $guest=[];foreach(['name','mobile','email','dob','idType','idNumber','rank','service','address'] as $k)$guest[$k]=sanitize_text_field($g[$k]??''); $guest['email']=sanitize_email($g['email']??''); if(!$guest['name'])return $this->err('Guest name is required.'); if(!$manager && (!preg_match('/^\d{10}$/',$guest['mobile'])||!is_email($guest['email'])||empty($p['transactionId'])))return $this->err('A ten-digit mobile, email and bank transaction reference are required.'); if(!$manager && (!$guest['idType'] || !$guest['idNumber'] || !$guest['rank'])) return $this->err('Complete defence ID type, defence ID number and rank.'); $category=$p['category']??'army'; if(!in_array($category,['army','iaf','civil'],true))return $this->err('Invalid service category.'); $lines=$manager ? $this->lines($p) : [['roomType'=>$p['roomType']??'','quantity'=>$p['roomCount']??1]]; $catalog=$this->catalog();$total=0;$qty=0;$clean=[];$seen=[]; foreach($lines as $line) { $name=$this->canonical(sanitize_text_field($line['roomType']??''));$q=filter_var($line['quantity']??1,FILTER_VALIDATE_INT); if(!isset($catalog[$name])||!$q||$q<1||$q>$catalog[$name]['total']||isset($seen[$name]))return $this->err('Invalid room type or quantity.'); $seen[$name]=true;$rate=$catalog[$name]['rates'][$category];$clean[]=['roomType'=>$name,'quantity'=>$q,'rate'=>$rate];$total+=$rate*$n*$q;$qty+=$q; } if(!$clean)return $this->err('Select accommodation.'); return ['bookingId'=>sanitize_text_field($p['bookingId']??('TSN-'.wp_generate_uuid4())),'checkin'=>$p['checkin'],'checkout'=>$p['checkout'],'roomType'=>count($clean)>1?'Multiple Rooms':$clean[0]['roomType'],'roomCount'=>$qty,'rooms'=>$clean,'nights'=>$n,'rate'=>count($clean)===1?$clean[0]['rate']:0,'total'=>$total,'category'=>$category,'guest'=>$guest,'transactionId'=>sanitize_text_field($p['transactionId']??''),'approvalStatus'=>$manager?'Approved':'Pending Approval','paymentStatus'=>'Pending','notes'=>sanitize_textarea_field($p['notes']??''),'createdAt'=>current_time('mysql')]; } function allocate($b) { $inv=$this->inventory($b['checkin'],$b['checkout'],$b['bookingId']);$free=array_column($inv,'available','roomType'); foreach($this->lines($b) as $line)if(($free[$this->canonical($line['roomType'])]??0)<$line['quantity'])return $this->err('Not enough accommodation remains for these dates. Booking was not approved.',409); // Stable inventory unit identifiers, not physical room numbers. $units=[];foreach($this->lines($b) as $line) { $name=$this->canonical($line['roomType']);$used=[]; foreach($this->all() as $other) { if($other['bookingId']===$b['bookingId'] || $other['approvalStatus']!=='Approved'||($other['checkin']??'')>=$b['checkout']||($other['checkout']??'')<=$b['checkin'])continue; if(!empty($other['roomNumbers'])) {foreach($other['roomNumbers'] as $unit)$used[$unit]=true;} else { foreach($this->lines($other) as $legacy) if($this->canonical($legacy['roomType']??'')===$name) { $reserved=0; for($j=1;$j<=$this->catalog()[$name]['total'];$j++) { $unit=$name.' / Unit '.str_pad($j,2,'0',STR_PAD_LEFT);if(!isset($used[$unit])){$used[$unit]=true;$reserved++;}if($reserved>=($legacy['quantity']??1))break; } } } } $chosen=[];for($i=1;$i<=$this->catalog()[$name]['total'];$i++){ $id=$name.' / Unit '.str_pad($i,2,'0',STR_PAD_LEFT);if(!isset($used[$id]))$chosen[]=$id;if(count($chosen)===$line['quantity'])break; } if(count($chosen)<$line['quantity'])return $this->err('No consistent unit allocation is available for this stay.',409); $units=array_merge($units,$chosen); }$b['roomNumbers']=$units;return $b; } function store($b,$post=0) { if(!$post) { $post=wp_insert_post(['post_type'=>'tsn_booking','post_status'=>'publish','post_title'=>'Booking: '.$b['bookingId']],true); if(is_wp_error($post)||!$post)return $this->err('WordPress could not create the booking.',500); } unset($b['_post']);update_post_meta($post,'tsn_booking_id',$b['bookingId']);update_post_meta($post,'tsn_booking_data',wp_slash($b)); if(get_post_meta($post,'tsn_booking_data',true)!==$b)return $this->err('Booking storage verification failed. Contact the administrator.',500); return $this->saved_response($b,$post); } function test($r) { return $this->response(['success'=>true,'message'=>'TSN Booking Backend is active.','version'=>'2.0.1','routes'=>['availability','bookings','booking-receipt','manager-login','booking-status','manager-booking']]); } function receipt_token($post) { $secret=get_post_meta($post,'_tsn_guest_receipt_secret',true); return $secret?hash_hmac('sha256',$post.'|'.$secret,wp_salt('auth')):''; } function receipt_view($b) { return array_intersect_key($b,array_flip(['bookingId','approvalStatus','paymentStatus','roomType','roomCount','checkin','checkout','nights','rate','total'])); } function saved_response($b,$post) { unset($b['_post']); $data=['success'=>true,'stored'=>true,'bookingId'=>$b['bookingId'],'booking_id'=>$b['bookingId'],'message'=>'Booking saved successfully.','booking'=>$this->manager?$b:$this->receipt_view($b)]; if(!$this->manager) { if(!get_post_meta($post,'_tsn_guest_receipt_secret',true)) { add_post_meta($post,'_tsn_guest_receipt_secret',wp_generate_password(64,false,false),true); } $data['bookingToken']=$this->receipt_token($post); } return $this->response($data); } function receipt($r) { $p=$r->get_json_params(); if(!is_array($p))return $this->err('Invalid receipt request.'); $b=$this->find(sanitize_text_field($p['bookingId']??'')); $token=is_string($p['token']??null)?$p['token']:''; $expected=$b?$this->receipt_token($b['_post']):''; if(!$expected || !$token || !hash_equals($expected,$token))return $this->err('Receipt unavailable or token invalid.',403); return $this->response(['success'=>true,'booking'=>$this->receipt_view($b)]); } function create($r) { if(!$this->manager&&!$this->throttle('booking',30))return $this->err('Too many requests. Try again later.',429); $p=$r->get_json_params();$b=$this->normalize($p,$this->manager);if(is_wp_error($b))return $b; return $this->locked(function()use($b){ $existing=$this->find($b['bookingId']);if($existing){foreach(['checkin','checkout','roomType','roomCount','guest','transactionId'] as $k)if(($existing[$k]??null)!==$b[$k])return $this->err('Booking reference already exists with different details.',409);return $this->saved_response($existing,$existing['_post']);} if($this->manager){$b=$this->allocate($b);if(is_wp_error($b))return $b;} else { $inv=array_column($this->inventory($b['checkin'],$b['checkout']),'available','roomType');if(($inv[$b['roomType']]??0)<$b['roomCount'])return $this->err('Selected dates are no longer available.',409); } $result=$this->store($b); if(!is_wp_error($result)) { $sent=wp_mail(get_option('admin_email'),'TSN booking request '.$b['bookingId'],wp_json_encode($b,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE));$d=$result->get_data();$d['notificationAccepted']=(bool)$sent;$result->set_data($d); }return $result; }); } function create_manager($r) {return $this->create($r);} function status($r) { $p=$r->get_json_params();return $this->locked(function()use($p){ $b=$this->find(sanitize_text_field($p['bookingId']??''));if(!$b)return $this->err('Booking not found.',404);$post=$b['_post']; if(isset($p['approvalStatus'])) {if(!in_array($p['approvalStatus'],['Pending Approval','Approved','Rejected'],true))return $this->err('Invalid approval status.');$b['approvalStatus']=$p['approvalStatus']; if($b['approvalStatus']==='Approved'){ $b=$this->allocate($b);if(is_wp_error($b))return $b; }else $b['roomNumbers']=[]; } if(isset($p['transactionId']))$b['transactionId']=sanitize_text_field($p['transactionId']); if(isset($p['paymentStatus'])){if(!in_array($p['paymentStatus'],['Paid','Pending'],true))return $this->err('Invalid payment status.');if($p['paymentStatus']==='Paid'&&empty($b['transactionId']))return $this->err('Transaction reference is required.');$b['paymentStatus']=$p['paymentStatus'];} $b['updatedAt']=current_time('mysql');$result=$this->store($b,$post); if(!is_wp_error($result)&&isset($p['approvalStatus'])&&!empty($b['guest']['email']))wp_mail($b['guest']['email'],'TSN booking '.$b['bookingId'].' — '.$b['approvalStatus'],'Your booking status is '.$b['approvalStatus'].'. Check-in: '.$b['checkin'].'. Check-out: '.$b['checkout'].'.'); return $result; }); } function import($r) { $p=$r->get_json_params();$rows=$p['bookings']??[];if(!is_array($rows)||!count($rows)||count($rows)>200)return $this->err('Import between 1 and 200 rows.'); $results=[];foreach($rows as $i=>$b){$req=new WP_REST_Request('POST');$req->set_header('Content-Type','application/json');$req->set_body(wp_json_encode($b)); // Imported rows remain pending until reviewed; never silently approve CSV rows. $b['bookingId']=$b['bookingId']??('TSN-IMP-'.wp_generate_uuid4());$normalized=$this->normalize($b,true);if(is_wp_error($normalized)){$results[]=['row'=>$i+1,'error'=>$normalized->get_error_message()];continue;} $normalized['approvalStatus']='Pending Approval';$result=$this->locked(function()use($normalized){if($this->find($normalized['bookingId']))return $this->err('Reference already exists; row skipped.',409);return $this->store($normalized);});$results[]=['row'=>$i+1,'success'=>!is_wp_error($result),'error'=>is_wp_error($result)?$result->get_error_message():'']; }$failed=count(array_filter($results,function($r){return empty($r['success']);}));return $this->response(['success'=>true,'results'=>$results,'message'=>(count($rows)-$failed).' imported; '.$failed.' skipped or failed. Imported bookings need approval.']); } } new TSN_Booking_Fixed(); https://tirumalatirupatisainikniwas.com/post-sitemap.xml 2026-07-22T07:47:07+00:00 https://tirumalatirupatisainikniwas.com/page-sitemap.xml 2026-10-23T01:23:07+00:00 https://tirumalatirupatisainikniwas.com/elementskit_template-sitemap.xml 2026-10-17T08:27:00+00:00 https://tirumalatirupatisainikniwas.com/category-sitemap.xml 2026-07-22T07:47:07+00:00 https://tirumalatirupatisainikniwas.com/author-sitemap.xml 2026-08-07T14:12:10+00:00