| Server IP : 65.1.209.187 / Your IP : 216.73.216.144 Web Server : nginx/1.24.0 System : Linux ip-172-31-5-206 6.14.0-1015-aws #15~24.04.1-Ubuntu SMP Tue Sep 23 22:44:48 UTC 2025 x86_64 User : ( 1001) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/ttc_wordpress/wp-content/themes/ttc/ |
Upload File : |
<?php
require_once ABSPATH . '/vendor/autoload.php';
// Define Template Path
define('MARPATH', get_template_directory_uri());
define('ISSPATH', get_template_directory_uri());
define('THEMEPATH', get_template_directory_uri());
add_filter('wpcf7_autop_or_not', '__return_false');
// Widgets
register_sidebar(array(
'name' => 'Home right sidebar',
'id' => 'home_right_1',
'before_widget' => '',
'after_widget' => '',
'before_title' => '<h3>',
'after_title' => '</h3>',
));
register_sidebar(array(
'name' => 'Inner right sidebar',
'id' => 'inner_right_1',
'before_widget' => '',
'after_widget' => '',
'before_title' => '',
'after_title' => '',
));
// rss thingy
add_theme_support('automatic-feed-links');
// Clean up the <head>
function removeHeadLinks() {
remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wlwmanifest_link');
}
add_action('init', 'removeHeadLinks');
remove_action('wp_head', 'wp_generator');
/*
Add Includes
*/
require_once('functions/functions.php');
require_once('functions/enqueue.php');
// require_once('wp_gopu_navwalker.php');
//
add_theme_support('post-thumbnails');
if (function_exists('add_image_size')) {
add_image_size('banner', 1000, 820, true);
add_image_size('package_grid', 352, 416, true);
add_image_size('square', 280, 280, true);
add_image_size('guest-rules-image', 600, 450, true); // 4:3 aspect ratio, cropped
}
add_filter('image_size_names_choose', 'add_guest_rules_image_size_to_media_library');
function add_guest_rules_image_size_to_media_library($sizes) {
return array_merge($sizes, array(
'guest-rules-image' => __('Guest Rules Image')
));
}
// Shortern Text---------------------------------------------------
function shortentext($text, $chars_limit) {
$text = strip_tags($text);
$chars_text = strlen($text);
$text = $text . " ";
$text = substr($text, 0, $chars_limit);
if ($chars_text > $chars_limit) {
$text = $text . "...";
}
return $text;
}
// Add Java Script
// Excerpt in Page
function add_page_excerpt_support() {
add_post_type_support('page', 'excerpt');
}
add_action('admin_init', 'add_page_excerpt_support');
// Display Image in column
// Add the posts and pages columns filter. They can both use the same function.
add_filter('manage_posts_columns', 'post_thumbnail_column', 5);
add_filter('manage_news_events_columns', 'post_thumbnail_column', 5);
// Add the column
function post_thumbnail_column($cols) {
$cols['tcb_post_thumb'] = __('Photo');
return $cols;
}
// Hook into the posts an pages column managing. Sharing function callback again.
add_action('manage_posts_custom_column', 'tcb_display_post_thumbnail_column', 5, 2);
add_action('manage_news_events_custom_column', 'tcb_display_post_thumbnail_column', 5, 2);
// Grab featured-thumbnail size post thumbnail and display it.
function tcb_display_post_thumbnail_column($col, $id) {
switch ($col) {
case 'tcb_post_thumb':
if (function_exists('the_post_thumbnail'))
echo the_post_thumbnail('thumbnail');
else
echo 'Not supported in theme';
break;
}
}
add_filter('post_thumbnail_html', 'remove_width_attribute', 10);
add_filter('image_send_to_editor', 'remove_width_attribute', 10);
function remove_width_attribute($html) {
$html = preg_replace('/(width|height)="\d*"\s/', "", $html);
return $html;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
// wpnbr Pagination by Gopu
////////////////////////////////////////////////////////////////////////////
if (!function_exists('wpneighbour_pagenations')):
function wpneighbour_pagenations($pages = '') {
if ($pages == '') {
global $wp_query;
$pages = $wp_query->max_num_pages;
if (!$pages) {
$pages = 1;
}
}
global $wp_query;
$large = 999999999;
$current_page = max(1, get_query_var('paged'));
$pagination_links = paginate_links(array(
'base' => str_replace(999999999, '%#%', esc_url(get_pagenum_link(999999999))),
'format' => '?paged=%#%',
'current' => $current_page,
'total' => $pages,
'type' => 'array',
'prev_text' => '<i class="fa fa-angle-double-left"></i>',
'next_text' => '<i class="fa fa-angle-double-right"></i>',
));
// echo paginate_links(array(
// 'base' => str_replace($large, '%#%', esc_url(get_pagenum_link($large))),
// 'format' => '?paged=%#%',
// 'current' => max(1, get_query_var('paged')),
// 'total' => $pages,
// 'type'=>'list'
// ));
if (is_array($pagination_links)) {
echo '<ul class="pagination">';
// Previous Page Link
// if ($current_page > 1) {
// echo '<li>' . $pagination_links[0] . '</li>';
// }
foreach ($pagination_links as $link) {
if (strpos($link, 'current') !== false) {
echo '<li class="active"><a href="#">' . $link . '</a></li>';
} else {
echo '<li>' . $link . '</li>';
}
}
// Next Page Link
if ($current_page < $total_pages) {
echo '<li>' . end($pagination_links) . '</li>';
}
echo '</ul>';
}
}
endif; // end wpneighbour_pagenations
/////// Metabox Above Editor
// Enable SVG uploads only for trusted users.
function ttc_can_upload_svg() {
return current_user_can('manage_options');
}
function allow_svg_upload($mimes) {
if (!ttc_can_upload_svg()) {
return $mimes;
}
$mimes['svg'] = 'image/svg+xml';
return $mimes;
}
add_filter('upload_mimes', 'allow_svg_upload');
// Sanitize SVG payloads on upload and reject dangerous content.
function ttc_sanitize_svg_upload($file) {
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if ($extension !== 'svg') {
return $file;
}
if (!ttc_can_upload_svg()) {
$file['error'] = __('SVG uploads are restricted to administrators.', 'ttc');
return $file;
}
if (empty($file['tmp_name']) || !is_readable($file['tmp_name'])) {
$file['error'] = __('Unable to validate SVG upload.', 'ttc');
return $file;
}
$svg_content = file_get_contents($file['tmp_name']);
if ($svg_content === false || trim($svg_content) === '') {
$file['error'] = __('Invalid SVG file.', 'ttc');
return $file;
}
if (!preg_match('/<svg[\s>]/i', $svg_content)) {
$file['error'] = __('Invalid SVG markup.', 'ttc');
return $file;
}
// Reject known dangerous patterns outright.
if (preg_match('/<\?(?:php|=)|<script\b|<foreignObject\b|on\w+\s*=|javascript:/i', $svg_content)) {
$file['error'] = __('SVG contains unsafe content and was rejected.', 'ttc');
return $file;
}
// Additional cleanup for defensive depth.
$svg_content = preg_replace('/<\?(?:php|=)[\s\S]*?\?>/i', '', $svg_content);
$svg_content = preg_replace('/<script\b[^>]*>[\s\S]*?<\/script>/i', '', $svg_content);
$svg_content = preg_replace('/<foreignObject\b[^>]*>[\s\S]*?<\/foreignObject>/i', '', $svg_content);
$svg_content = preg_replace('/\son\w+\s*=\s*(".*?"|\'.*?\'|[^\s>]+)/i', '', $svg_content);
$svg_content = preg_replace('/\s(?:href|xlink:href)\s*=\s*("|\')\s*javascript:[\s\S]*?\1/i', '', $svg_content);
if (file_put_contents($file['tmp_name'], $svg_content) === false) {
$file['error'] = __('Failed to sanitize SVG file.', 'ttc');
return $file;
}
return $file;
}
add_filter('wp_handle_upload_prefilter', 'ttc_sanitize_svg_upload');
// Allow SVG files to be uploaded via WordPress media library.
function allow_svg_in_media_library($data, $file, $filename, $mimes) {
if (!ttc_can_upload_svg()) {
return $data;
}
$filetype = wp_check_filetype($filename, $mimes);
if ('svg' === $filetype['ext']) {
$data['type'] = 'image/svg+xml';
$data['ext'] = 'svg';
}
return $data;
}
add_filter('wp_check_filetype_and_ext', 'allow_svg_in_media_library', 10, 4);
// AJAX handler for loading more gallery images
function load_more_gallery_images() {
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'gallery_ajax_nonce')) {
wp_send_json_error('Invalid nonce');
return;
}
$gallery_id = intval($_POST['gallery_id']);
$page = intval($_POST['page']);
$images_per_page = 8;
$offset = ($page - 1) * $images_per_page;
$gallery_details = get_field('gallery_details', $gallery_id);
$gallery_images = isset($gallery_details['gallery_images']) && is_array($gallery_details['gallery_images'])
? $gallery_details['gallery_images']
: array();
// Get next set of images
$paginated_images = array_slice($gallery_images, $offset, $images_per_page);
ob_start();
if ($paginated_images) {
foreach ($paginated_images as $item) {
if (($item['media_type'] ?? '') === 'image' && !empty($item['image'])) {
$image = $item['image'];
?>
<div class="gallery-grid-cell">
<div class="gallery-preview-frame gallery-detail-frame">
<a href="<?php echo esc_url($image['url']); ?>" class="gallery-preview-link">
<img class="gallery-preview-img"
src="<?php echo esc_url($image['sizes']['medium'] ?? $image['url']); ?>"
alt="<?php echo esc_attr($image['alt'] ?? ''); ?>"
loading="lazy"
decoding="async" />
</a>
</div>
</div>
<?php
} elseif (($item['media_type'] ?? '') === 'video' && !empty($item['video'])) {
$video = $item['video'];
?>
<div class="gallery-grid-cell">
<div class="gallery-preview-frame gallery-detail-frame">
<img src="<?php echo esc_url(get_template_directory_uri() . '/images/v-play.png'); ?>" alt="" class="video-crl gallery-preview-play" width="31" height="31" />
<a class="video gallery-preview-link" href="<?php echo esc_url($video['url']); ?>">
<video class="gallery-preview-video" muted playsinline preload="metadata" aria-hidden="true">
<source src="<?php echo esc_url($video['url']); ?>"
type="<?php echo esc_attr($video['mime_type'] ?? ''); ?>" />
</video>
</a>
</div>
</div>
<?php
}
}
}
$html = ob_get_clean();
wp_send_json_success(array(
'html' => $html,
'hasMore' => (count($gallery_images) > ($offset + $images_per_page))
));
}
add_action('wp_ajax_load_more_gallery_images', 'load_more_gallery_images');
add_action('wp_ajax_nopriv_load_more_gallery_images', 'load_more_gallery_images');
// Add this function to check setup
function check_pdf_setup() {
$theme_dir = get_template_directory();
$fpdf_path = $theme_dir . '/lib/fpdf.php';
$font_path = $theme_dir . '/lib/font';
$issues = array();
if (!file_exists($fpdf_path)) {
$issues[] = "FPDF file not found at: " . $fpdf_path;
}
if (!file_exists($font_path)) {
$issues[] = "Font directory not found at: " . $font_path;
}
if (!is_readable($fpdf_path)) {
$issues[] = "FPDF file is not readable. Please check file permissions.";
}
return empty($issues) ? true : $issues;
}
// Add this action to show admin notices if there are issues
function pdf_setup_admin_notice() {
$check = check_pdf_setup();
if ($check !== true) {
echo '<div class="error"><p><strong>PDF Generation Setup Issues:</strong></p><ul>';
foreach ($check as $issue) {
echo '<li>' . esc_html($issue) . '</li>';
}
echo '</ul></div>';
}
}
add_action('admin_notices', 'pdf_setup_admin_notice');
// Include PDF generator
require_once(get_template_directory() . '/pdf-generator.php');
// Hook the PDF generation function
add_action('template_redirect', 'generate_affiliate_clubs_pdf');
//login
// Hide admin bar from front-end
add_filter('show_admin_bar', '__return_false');
// Hide admin bar from front-end except for administrators
function custom_show_admin_bar($show) {
if (!current_user_can('administrator')) {
return false;
}
return $show;
}
add_filter('show_admin_bar', 'custom_show_admin_bar');
// Disable admin bar preference in user profile for non-administrators
function remove_admin_bar_prefs() {
if (!current_user_can('administrator')) {
remove_action('personal_options', '_admin_bar_preferences');
}
}
add_action('admin_init', 'remove_admin_bar_prefs');
// Add custom CSS to remove admin bar space for non-administrators
function remove_admin_bar_space() {
if (!is_admin() && !current_user_can('administrator')) {
echo '<style type="text/css">
html { margin-top: 0 !important; }
* html body { margin-top: 0 !important; }
</style>';
}
}
add_action('wp_head', 'remove_admin_bar_space', 99);
// Add custom logout redirect
function custom_logout_redirect($redirect_to, $requested_redirect_to, $user) {
return get_permalink(365); // Change this to your login page URL
}
add_filter('logout_redirect', 'custom_logout_redirect', 10, 3);
// Create custom role on theme activation
function create_ttc_member_role() {
// Remove the role if it exists
if (get_role('ttc-website-member')) {
remove_role('ttc-website-member');
}
// Add the custom role
add_role(
'ttc-website-member',
'TTC Website Member',
array(
'read' => true, // Allows a user to read
'edit_posts' => false, // Disable post creation
'delete_posts' => false, // Disable post deletion
'upload_files' => false, // Disable file uploads
'publish_posts' => false, // Disable publishing
'ttc_view_members' => true, // Custom capability to view member content
'ttc_book_courts' => true, // Custom capability to book courts
'ttc_view_events' => true, // Custom capability to view events
'ttc_register_events' => true, // Custom capability to register for events
'ttc_view_notices' => true, // Custom capability to view notices
'ttc_update_profile' => true, // Custom capability to update profile
)
);
}
// Register the activation hook
register_activation_hook(__FILE__, 'create_ttc_member_role');
// Ensure role exists even if theme was activated before
function ensure_ttc_member_role_exists() {
if (!get_role('ttc-website-member')) {
create_ttc_member_role();
}
}
add_action('init', 'ensure_ttc_member_role_exists');
// Register custom capabilities
function register_ttc_capabilities() {
$role = get_role('administrator');
// Add custom capabilities to administrator role
$custom_caps = array(
'ttc_view_members',
'ttc_book_courts',
'ttc_view_events',
'ttc_register_events',
'ttc_view_notices',
'ttc_update_profile'
);
foreach ($custom_caps as $cap) {
if (!$role->has_cap($cap)) {
$role->add_cap($cap);
}
}
}
add_action('admin_init', 'register_ttc_capabilities');
// Function to check if user has TTC member capabilities
function is_ttc_member($user_id = null) {
if (!$user_id) {
$user_id = get_current_user_id();
}
$user = get_userdata($user_id);
return $user && in_array('ttc-website-member', (array) $user->roles);
}
// Custom Walker Class for Nav Menu
class TTC_Menu_Walker extends Walker_Nav_Menu {
public function start_lvl(&$output, $depth = 0, $args = array()) {
$output .= "<div class='dropdown-menu' aria-labelledby='dropdown01'>";
}
public function end_lvl(&$output, $depth = 0, $args = array()) {
$output .= "</div>";
}
public function start_el(&$output, $item, $depth = 0, $args = array(), $id = 0) {
$classes = empty($item->classes) ? array() : (array) $item->classes;
// Check if item has children
$has_children = in_array('menu-item-has-children', $classes);
if ($depth === 0) {
$output .= "<li class='" . ($has_children ? 'dropdown' : '') . "'>";
$output .= "<a href='" . esc_url($item->url) . "' class='nav-link" .
($has_children ? ' dropdown-toggle' : '') . "'" .
($has_children ? " id='dropdown01' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'" : "") .
">";
$output .= esc_html($item->title);
$output .= "</a>";
} else {
// For dropdown items
$output .= "<a class='dropdown-item page-scroll' href='" . esc_url($item->url) . "'>";
$output .= esc_html($item->title);
$output .= "</a>";
// Add divider after each dropdown item except the last one
if ($depth === 1) {
// Get the next menu item
global $wp_menu_items;
if (!isset($wp_menu_items)) {
$wp_menu_items = wp_get_nav_menu_items(get_nav_menu_locations()['primary-menu']);
}
// Find current item position
$current_position = array_search($item->ID, array_column($wp_menu_items, 'ID'));
$next_item = isset($wp_menu_items[$current_position + 1]) ? $wp_menu_items[$current_position + 1] : null;
// Add divider if there's a next item and it's at the same depth
if ($next_item && $next_item->menu_item_parent === $item->menu_item_parent) {
$output .= "<div class='dropdown-divider'></div>";
}
}
}
}
public function end_el(&$output, $item, $depth = 0, $args = array()) {
if ($depth === 0) {
$output .= "</li>";
}
}
}
// Register Navigation Menu
function register_ttc_menus() {
register_nav_menus(array(
'primary-menu' => __('Primary Menu', 'ttc'),
));
}
add_action('init', 'register_ttc_menus');
// Register REST API for Location Manager
function register_location_manager_api() {
// Register the states endpoint
register_rest_route('location-manager/v1', '/states/(?P<country_id>\d+)', array(
'methods' => 'GET',
'callback' => 'api_get_states',
'permission_callback' => '__return_true',
'args' => array(
'country_id' => array(
'required' => true,
'validate_callback' => function($param) {
return is_numeric($param);
}
)
)
));
// Register the districts endpoint
register_rest_route('location-manager/v1', '/districts/(?P<state_id>\d+)', array(
'methods' => 'GET',
'callback' => 'api_get_districts',
'permission_callback' => '__return_true',
'args' => array(
'state_id' => array(
'required' => true,
'validate_callback' => function($param) {
return is_numeric($param);
}
)
)
));
}
add_action('rest_api_init', 'register_location_manager_api');
// API callback for states
function api_get_states($request) {
$country_id = $request['country_id'];
global $wpdb;
$states = $wpdb->get_results($wpdb->prepare(
"SELECT id, state_name
FROM {$wpdb->prefix}custom_states
WHERE country_id = %d AND status = 1
ORDER BY state_name ASC",
$country_id
));
return rest_ensure_response($states);
}
// API callback for districts
function api_get_districts($request) {
$state_id = $request['state_id'];
global $wpdb;
$districts = $wpdb->get_results($wpdb->prepare(
"SELECT id, district_name
FROM {$wpdb->prefix}custom_districts
WHERE state_id = %d AND status = 1
ORDER BY district_name ASC",
$state_id
));
return rest_ensure_response($districts);
}
// ======= Razorpay Integration =======
function razorpay_enqueue_scripts() {
wp_enqueue_script('jquery');
wp_enqueue_script('razorpay-checkout', 'https://checkout.razorpay.com/v1/checkout.js', [], null, true);
wp_enqueue_script('razorpay-custom', get_template_directory_uri() . '/Script/razorpay-scripts.js', ['jquery'], null, true);
wp_localize_script('razorpay-custom', 'razorpay_ajax', [
'ajax_url' => admin_url('admin-ajax.php'),
'key_id' => RAZORPAY_KEY_ID,
'color' => RAZORPAY_COLOR,
'currency' => RAZORPAY_CURRENCY,
'nonce' => wp_create_nonce('razorpay_payment_nonce'),
]);
}
add_action('wp_enqueue_scripts', 'razorpay_enqueue_scripts');
function create_razorpay_order() {
if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'razorpay_payment_nonce')) {
wp_send_json_error(['error' => 'Security verification failed. Please refresh the page and try again.']);
return;
}
$api = new \Razorpay\Api\Api(RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET);
$dueId = isset($_POST['dueId']) ? intval($_POST['dueId']) : 0;
$paymentAmount = isset($_POST['payment_amount']) ? round(floatval(wp_unslash($_POST['payment_amount'])), 2) : 0.0;
$paymentType = isset($_POST['payment_type']) ? sanitize_key(wp_unslash($_POST['payment_type'])) : 'full';
$partial_payment_enabled = ttc_is_partial_payment_enabled();
if (!in_array($paymentType, ['full', 'partial', 'advance'], true)) {
$paymentType = 'full';
}
// Handle advance payments (dueId = 0)
if ($dueId === 0) {
if ($paymentAmount <= 0) {
wp_send_json_error(['error' => 'Invalid payment amount']);
return;
}
// For advance payments, we need to get member details from the session or request
// For now, we'll create a generic order for advance payment
$amountInCents = intval($paymentAmount * 100); // Convert to cents/paise
$orderData = [
'receipt' => uniqid(),
'amount' => $amountInCents,
'currency' => RAZORPAY_CURRENCY,
'payment_capture' => 1,
'notes' => [
'due_id' => 0, // Advance payment
'payment_type' => 'advance',
'payment_amount' => $paymentAmount,
'created_at' => current_time('mysql')
]
];
$order = $api->order->create($orderData);
wp_send_json_success([
'id' => $order['id'],
'amount' => $orderData['amount'],
]);
return;
}
// Regular payment due processing
$paymentDetails = get_payment_due_details($dueId);
if (!$paymentDetails) {
wp_send_json_error(['error' => 'Invalid payment due ID']);
return;
}
if (!in_array($paymentDetails['status'], ['pending', 'overdue'], true)) {
wp_send_json_error(['error' => 'This due is no longer payable']);
return;
}
// Get the membership number from the member post
$membership_number = get_field('membership_number', $paymentDetails['member_id']);
if (!$membership_number) {
wp_send_json_error(['error' => 'Invalid membership number']);
return;
}
$remainingAmount = isset($paymentDetails['remaining_amount']) && $paymentDetails['remaining_amount'] !== null
? floatval($paymentDetails['remaining_amount'])
: floatval($paymentDetails['amount']);
if ($remainingAmount <= 0) {
wp_send_json_error(['error' => 'This due has already been paid']);
return;
}
if (!$partial_payment_enabled && $paymentType === 'partial') {
wp_send_json_error(['error' => 'Partial payment is disabled']);
return;
}
if ($paymentType === 'full') {
$amount = round($remainingAmount, 2);
} else {
// Partial payment or any fallback uses the submitted amount with strict bounds.
$amount = $paymentAmount;
if ($amount <= 0 || $amount > round($remainingAmount, 2)) {
wp_send_json_error(['error' => 'Invalid payment amount for this due']);
return;
}
}
$amountInCents = intval($amount * 100); // Convert to cents/paise
$orderData = [
'receipt' => uniqid(),
'amount' => $amountInCents,
'currency' => RAZORPAY_CURRENCY,
'payment_capture' => 1,
'notes' => [
'due_id' => $dueId,
'membership_number' => $membership_number,
'payment_type' => $paymentType,
'original_amount' => $paymentDetails['amount'],
'payment_amount' => $amount,
'created_at' => current_time('mysql')
]
];
$order = $api->order->create($orderData);
wp_send_json_success([
'id' => $order['id'],
'amount' => $orderData['amount'],
]);
}
function update_payment_due_successfully_paid($dueId, $razorpayPaymentId, $paymentAmount = null, $paymentType = 'full') {
global $wpdb;
$payment_dues_table = $wpdb->prefix . 'payment_dues';
$payment_history_table = $wpdb->prefix . 'payment_history';
// Get current due details
$due = get_payment_due_details($dueId);
if (!$due) {
throw new Exception('Payment due not found');
}
// Calculate new remaining amount
$remainingAmount = $due['remaining_amount'] ?? $due['amount'];
if ($paymentAmount === null) {
$paymentAmount = $remainingAmount;
}
// Insert into payment history
$wpdb->insert(
$payment_history_table,
array(
'payment_due_id' => $dueId,
'member_id' => $due['member_id'],
'amount' => $paymentAmount,
'payment_date' => current_time('mysql'),
'transaction_id' => $razorpayPaymentId,
'payment_type' => $paymentType,
'created_at' => current_time('mysql')
),
array('%d', '%d', '%f', '%s', '%s', '%s', '%s')
);
if ($wpdb->last_error) {
throw new Exception('Failed to insert payment history: ' . $wpdb->last_error);
}
// Calculate new remaining amount
$newRemainingAmount = $remainingAmount - $paymentAmount;
// Determine new status
$newStatus = $newRemainingAmount <= 0 ? 'paid' : 'pending';
$data = [
'status' => $newStatus,
'remaining_amount' => max(0, $newRemainingAmount),
'updated_at' => current_time('mysql')
];
// Set original amount if this is the first payment
if (empty($due['original_amount'])) {
$data['original_amount'] = $due['amount'];
}
$updated = $wpdb->update(
$payment_dues_table,
$data,
['id' => $dueId],
['%s', '%f', '%s', '%f'],
['%d']
);
if ($updated === false) {
throw new Exception('Failed to update payment due: ' . $wpdb->last_error);
}
// Get updated payment due details
$updatedDue = get_payment_due_details($dueId);
return [
'success' => $updated,
'payment_due' => $updatedDue
];
}
function get_payment_due_details($dueId) {
global $wpdb;
if (empty($dueId) || !is_numeric($dueId)) {
return false; // Invalid ID
}
$table_name = $wpdb->prefix . 'payment_dues';
$due = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$dueId
),
ARRAY_A // Return as associative array
);
return $due ? $due : false;
}
function verify_razorpay_payment() {
if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'razorpay_payment_nonce')) {
wp_send_json_error(['message' => 'Security verification failed. Please refresh the page and try again.']);
return;
}
$api = new \Razorpay\Api\Api(RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET);
try {
$razorpay_order_id = isset($_POST['razorpay_order_id']) ? sanitize_text_field(wp_unslash($_POST['razorpay_order_id'])) : '';
$razorpay_payment_id = isset($_POST['razorpay_payment_id']) ? sanitize_text_field(wp_unslash($_POST['razorpay_payment_id'])) : '';
$razorpay_signature = isset($_POST['razorpay_signature']) ? sanitize_text_field(wp_unslash($_POST['razorpay_signature'])) : '';
if ($razorpay_order_id === '' || $razorpay_payment_id === '' || $razorpay_signature === '') {
throw new Exception('Invalid payment verification payload');
}
$clientAmount = isset($_POST['payment_amount']) ? round(floatval(wp_unslash($_POST['payment_amount'])), 2) : null;
$paymentType = isset($_POST['payment_type']) ? sanitize_key(wp_unslash($_POST['payment_type'])) : 'full';
$dueId = isset($_POST['due_id']) ? intval($_POST['due_id']) : 0;
$partial_payment_enabled = ttc_is_partial_payment_enabled();
if (!in_array($paymentType, ['full', 'partial', 'advance'], true)) {
$paymentType = 'full';
}
$attributes = [
'razorpay_order_id' => $razorpay_order_id,
'razorpay_payment_id' => $razorpay_payment_id,
'razorpay_signature' => $razorpay_signature,
];
$api->utility->verifyPaymentSignature($attributes);
// Use gateway-confirmed values for integrity checks and ledger updates.
$gatewayPayment = $api->payment->fetch($razorpay_payment_id);
$gatewayAmount = round(floatval($gatewayPayment['amount']) / 100, 2);
$gatewayOrderId = sanitize_text_field((string) $gatewayPayment['order_id']);
$gatewayStatus = sanitize_key((string) $gatewayPayment['status']);
if ($gatewayOrderId !== $razorpay_order_id) {
throw new Exception('Order ID mismatch detected');
}
if (!in_array($gatewayStatus, ['captured', 'authorized'], true)) {
throw new Exception('Payment status is not valid for settlement');
}
if ($gatewayAmount <= 0) {
throw new Exception('Invalid payment amount received from gateway');
}
// Handle advance payments (dueId = 0)
if ($dueId === 0) {
if ($paymentType !== 'advance') {
$paymentType = 'advance';
}
// For advance payments, we need to get member details from the session
// This would need to be implemented based on how you store member info during the quick pay process
// For now, we'll create a generic advance payment record
global $wpdb;
$payment_history_table = $wpdb->prefix . 'payment_history';
// Insert into payment history for advance payment
$wpdb->insert(
$payment_history_table,
array(
'payment_due_id' => 0, // No specific due
'member_id' => 0, // Will need to be updated based on session
'amount' => $gatewayAmount,
'payment_date' => current_time('mysql'),
'transaction_id' => $razorpay_payment_id,
'payment_type' => 'advance',
'created_at' => current_time('mysql')
),
array('%d', '%d', '%f', '%s', '%s', '%s', '%s')
);
if ($wpdb->last_error) {
throw new Exception('Failed to insert advance payment history: ' . $wpdb->last_error);
}
wp_send_json_success([
'message' => 'Advance payment successful',
'payment_id' => $razorpay_payment_id,
'amount' => $gatewayAmount
]);
return;
}
$paymentDetails = get_payment_due_details($dueId);
if (!$paymentDetails) {
throw new Exception('Invalid payment due ID');
}
if (!in_array($paymentDetails['status'], ['pending', 'overdue'], true)) {
throw new Exception('This due is no longer payable');
}
$remainingAmount = isset($paymentDetails['remaining_amount']) && $paymentDetails['remaining_amount'] !== null
? floatval($paymentDetails['remaining_amount'])
: floatval($paymentDetails['amount']);
$remainingAmount = round($remainingAmount, 2);
if ($paymentType === 'partial') {
if (!$partial_payment_enabled) {
throw new Exception('Partial payment is disabled');
}
if ($gatewayAmount <= 0 || $gatewayAmount > $remainingAmount) {
throw new Exception('Invalid partial payment amount');
}
} else {
// Full payment must settle the remaining amount exactly.
if (abs($gatewayAmount - $remainingAmount) > 0.01) {
throw new Exception('Payment amount mismatch');
}
$paymentType = 'full';
}
// Optional anti-tampering check between posted amount and gateway amount.
if ($clientAmount !== null && abs($clientAmount - $gatewayAmount) > 0.01) {
throw new Exception('Payment amount mismatch');
}
// Regular payment due processing
$update_result = update_payment_due_successfully_paid(
$dueId,
$razorpay_payment_id,
$gatewayAmount,
$paymentType
);
if (!$update_result['success']) {
throw new Exception('Failed to update payment due');
}
// Extract member ID from payment due data
error_log('Update result: ' . print_r($update_result, true));
$member_id = $update_result['payment_due']['member_id'];
// Get WordPress user ID from member post
$user_id = get_field('member_user_id', $member_id);
if (!$user_id) {
throw new Exception('No WordPress user found for member ID: ' . $member_id);
}
// Get member post ID linked to this user (should be the same as member_id)
$member_post_id = get_user_meta($user_id, 'member_post_id', true);
// Initialize member details
$member_name = '';
$member_email = '';
$membership_number = '';
// Get data from ACF fields
$acf_first_name = get_field('first_name', $member_id);
$acf_last_name = get_field('last_name', $member_id);
$acf_email = get_field('email', $member_id);
$acf_membership_number = get_field('membership_number', $member_id);
// Use ACF fields if available
$member_name = ($acf_first_name && $acf_last_name) ? $acf_first_name . ' ' . $acf_last_name : '';
$member_email = $acf_email ?: '';
$membership_number = $acf_membership_number ?: '';
// Fallback to user meta if ACF fields are empty
if (empty($member_name)) {
$first_name = get_user_meta($user_id, 'first_name', true);
$last_name = get_user_meta($user_id, 'last_name', true);
$member_name = ($first_name && $last_name) ? $first_name . ' ' . $last_name : '';
}
if (empty($member_email)) {
$user = get_userdata($user_id);
$member_email = $user ? $user->user_email : '';
}
if (empty($membership_number)) {
$membership_number = get_user_meta($user_id, 'membership_number', true);
}
// Send payment notification email
$email_result = send_payment_notification_email(
$member_name,
$member_email,
$membership_number,
$gatewayAmount
);
// Log email status
if (!$email_result['success']) {
error_log('Payment notification email failed: ' . $email_result['message']);
}
wp_send_json_success(['verified' => 1]);
} catch (\Razorpay\Api\Errors\SignatureVerificationError $e) {
wp_send_json_success(['verificationError' => 1]);
} catch (Exception $e) {
wp_send_json_error(['message' => $e->getMessage()]);
}
}
add_action('wp_ajax_create_razorpay_order', 'create_razorpay_order'); // For logged-in users
add_action('wp_ajax_nopriv_create_razorpay_order', 'create_razorpay_order'); // For guests
add_action('wp_ajax_verify_razorpay_payment', 'verify_razorpay_payment'); // For logged-in users
add_action('wp_ajax_nopriv_verify_razorpay_payment', 'verify_razorpay_payment'); // For guests
/**
* Send payment notification email to admin
*
* @param string $member_name Member's full name
* @param string $member_email Member's email address
* @param string $member_id Member's ID
* @param float $amount Payment amount
* @return array Array containing success status and message
*/
function send_payment_notification_email($member_name, $member_email, $member_id, $amount) {
$ttc_primary_email = get_option('ttc_primary_email', '');
if (!$ttc_primary_email) {
return [
'success' => false,
'message' => 'Email not sent: TTC primary email not configured'
];
}
// Get WordPress timezone setting
$timezone_string = get_option('timezone_string');
if (empty($timezone_string)) {
// Fallback to UTC if timezone is not set
$timezone_string = 'UTC';
}
// Create DateTime object with WordPress timezone
$date = new DateTime('now', new DateTimeZone($timezone_string));
$formatted_date = $date->format('F j, Y, g:i a');
$subject = 'New Payment Received - TTC Member Payment';
// Create HTML email template
$message = '
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #f8f9fa; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
.details { background-color: #ffffff; padding: 20px; border: 1px solid #dee2e6; border-radius: 5px; }
.footer { margin-top: 20px; font-size: 12px; color: #6c757d; }
.amount-container {
margin-top: 25px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 5px;
text-align: center;
}
.amount-label {
font-size: 16px;
color: #495057;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
.amount-value {
font-size: 32px;
color: #28a745;
font-weight: bold;
display: block;
margin: 10px 0;
}
.amount-date {
font-size: 14px;
color: #6c757d;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>New Payment Received</h2>
</div>
<div class="details">
<p>A new payment has been received from a TTC member.</p>
<p><strong>Member Details:</strong></p>
<ul>
<li><strong>Name:</strong> ' . esc_html($member_name) . '</li>
<li><strong>Email:</strong> ' . esc_html($member_email) . '</li>
<li><strong>Member ID:</strong> ' . esc_html($member_id) . '</li>
</ul>
<div class="amount-container">
<span class="amount-label">Payment Amount</span>
<span class="amount-value">₹' . esc_html($amount) . '</span>
<span class="amount-date">Paid on ' . esc_html($formatted_date) . ' (' . esc_html($timezone_string) . ')</span>
</div>
</div>
<div class="footer">
<p>This is an automated message from the TTC website. Please do not reply to this email.</p>
</div>
</div>
</body>
</html>';
$headers = [
'Content-Type: text/html; charset=UTF-8',
'From: TTC Website <noreply@ttc.com>'
];
$attachments = []; // Optional: Add file paths here if needed
$sent = wp_mail($ttc_primary_email, $subject, $message, $headers, $attachments);
if ($sent) {
return [
'success' => true,
'message' => 'Payment notification email sent successfully to: ' . $ttc_primary_email
];
} else {
return [
'success' => false,
'message' => 'Failed to send payment notification email to: ' . $ttc_primary_email
];
}
}
// ======= Admin Scripts - In-House Developer =======
function enqueue_admin_ajax_script() {
wp_enqueue_script('jquery');
wp_enqueue_script('admin-scripts', get_template_directory_uri() . '/Script/admin-scripts.js', ['jquery'], null, true);
wp_localize_script('admin-scripts', 'admin_ajax', [
'ajax_url' => admin_url('admin-ajax.php'),
'admin_action_nonce' => wp_create_nonce('admin_action_nonce'),
]);
}
function handle_verify_user_email() {
check_ajax_referer('admin_action_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['error' => 'unauthorized']);
}
$user_id = isset($_POST['user_id']) ? intval($_POST['user_id']) : 0;
if (!$user_id || !get_user_by('ID', $user_id)) {
wp_send_json_error(['error' => 'invalid userId']);
}
update_user_meta($user_id, 'email_verified', true);
wp_send_json_success(['user_email_verified' => 1]);
}
add_action('admin_enqueue_scripts', 'enqueue_admin_ajax_script');
add_action('wp_ajax_verify_user_email', 'handle_verify_user_email');
// Add new function to get payment history
function get_payment_history($dueId) {
global $wpdb;
$payment_history_table = $wpdb->prefix . 'payment_history';
$payment_dues_table = $wpdb->prefix . 'payment_dues';
// Get the due details
$due = $wpdb->get_row(
$wpdb->prepare(
"SELECT original_amount, remaining_amount
FROM $payment_dues_table
WHERE id = %d",
$dueId
)
);
if (!$due) {
return false;
}
// Get payment history from new table
$history = $wpdb->get_results(
$wpdb->prepare(
"SELECT
amount,
payment_date as date,
transaction_id,
payment_type as type,
payment_method,
receipt_file,
receipt_number
FROM $payment_history_table
WHERE payment_due_id = %d
ORDER BY payment_date DESC",
$dueId
)
);
return [
'history' => $history,
'original_amount' => $due->original_amount,
'remaining_amount' => $due->remaining_amount
];
}
// ======= TTC Settings Page =======
function ttc_add_settings_page() {
add_options_page(
'TTC Settings', // Page title
'TTC Settings', // Menu title
'manage_options', // Capability required
'ttc-settings', // Menu slug
'ttc_settings_page_html' // Callback function
);
}
function ttc_register_settings() {
// Register settings section
add_settings_section(
'ttc_email_settings_section', // ID
'Email Settings', // Title
'ttc_email_settings_section_callback', // Callback
'ttc-settings' // Page
);
// Register payment settings section
add_settings_section(
'ttc_payment_settings_section', // ID
'Payment Settings', // Title
'ttc_payment_settings_section_callback', // Callback
'ttc-settings' // Page
);
// Register the primary email setting
register_setting('ttc-settings', 'ttc_primary_email', [
'type' => 'string',
'sanitize_callback' => 'sanitize_email',
'default' => ''
]);
// Register the enable member login setting
register_setting('ttc-settings', 'ttc_enable_member_login', [
'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean',
'default' => false
]);
// Register the enable partial payment setting
register_setting('ttc-settings', 'ttc_enable_partial_payment', [
'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean',
'default' => true
]);
// Register OTP security settings
register_setting('ttc-settings', 'ttc_enable_otp_security', [
'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean',
'default' => true
]);
register_setting('ttc-settings', 'ttc_otp_request_limit', [
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 3
]);
register_setting('ttc-settings', 'ttc_otp_rate_limit_minutes', [
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 15
]);
register_setting('ttc-settings', 'ttc_otp_resend_limit', [
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 2
]);
register_setting('ttc-settings', 'ttc_otp_resend_minutes', [
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 10
]);
register_setting('ttc-settings', 'ttc_otp_max_attempts', [
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 5
]);
register_setting('ttc-settings', 'ttc_otp_expiry_minutes', [
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 10
]);
// Add the settings fields
add_settings_field(
'ttc_primary_email', // ID
'TTC Primary Email', // Label
'ttc_primary_email_setting_field_html', // Callback
'ttc-settings', // Page
'ttc_email_settings_section' // Section
);
add_settings_field(
'ttc_enable_member_login', // ID
'Enable Member Login', // Label
'ttc_enable_member_login_setting_field_html', // Callback
'ttc-settings', // Page
'ttc_email_settings_section' // Section
);
add_settings_field(
'ttc_enable_partial_payment', // ID
'Enable Partial Payment', // Label
'ttc_enable_partial_payment_setting_field_html', // Callback
'ttc-settings', // Page
'ttc_payment_settings_section' // Section
);
// Add OTP security settings fields
add_settings_field(
'ttc_enable_otp_security', // ID
'Enable OTP Security Features', // Label
'ttc_enable_otp_security_setting_field_html', // Callback
'ttc-settings', // Page
'ttc_payment_settings_section' // Section
);
add_settings_field(
'ttc_otp_request_limit', // ID
'OTP Request Rate Limit', // Label
'ttc_otp_request_limit_setting_field_html', // Callback
'ttc-settings', // Page
'ttc_payment_settings_section' // Section
);
add_settings_field(
'ttc_otp_rate_limit_minutes', // ID
'OTP Rate Limit Time Period (minutes)', // Label
'ttc_otp_rate_limit_minutes_setting_field_html', // Callback
'ttc-settings', // Page
'ttc_payment_settings_section' // Section
);
add_settings_field(
'ttc_otp_resend_limit', // ID
'OTP Resend Rate Limit', // Label
'ttc_otp_resend_limit_setting_field_html', // Callback
'ttc-settings', // Page
'ttc_payment_settings_section' // Section
);
add_settings_field(
'ttc_otp_resend_minutes', // ID
'OTP Resend Time Period (minutes)', // Label
'ttc_otp_resend_minutes_setting_field_html', // Callback
'ttc-settings', // Page
'ttc_payment_settings_section' // Section
);
add_settings_field(
'ttc_otp_max_attempts', // ID
'Maximum OTP Attempts', // Label
'ttc_otp_max_attempts_setting_field_html', // Callback
'ttc-settings', // Page
'ttc_payment_settings_section' // Section
);
add_settings_field(
'ttc_otp_expiry_minutes', // ID
'OTP Expiry Time (minutes)', // Label
'ttc_otp_expiry_minutes_setting_field_html', // Callback
'ttc-settings', // Page
'ttc_payment_settings_section' // Section
);
}
function ttc_email_settings_section_callback() {
echo '<p>Configure email settings for TTC system communications.</p>';
}
function ttc_payment_settings_section_callback() {
echo '<p>Configure payment settings for TTC system functionality.</p>';
}
function ttc_primary_email_setting_field_html() {
$value = get_option('ttc_primary_email', '');
echo '<input type="email" id="ttc_primary_email_setting" name="ttc_primary_email" value="' . esc_attr($value) . '" class="regular-text" />';
echo '<p class="description">This is the primary email for the system to communicate, for example: payment notifications, etc.</p>';
}
function ttc_enable_member_login_setting_field_html() {
$value = get_option('ttc_enable_member_login', false);
echo '<input type="checkbox" id="ttc_enable_member_login_setting" name="ttc_enable_member_login" value="1" ' . checked(1, $value, false) . ' />';
echo '<label for="ttc_enable_member_login_setting">Enable member login functionality.</label>';
echo '<p class="description">If checked, members will be able to log in to the website using their membership number and email.</p>';
}
function ttc_enable_partial_payment_setting_field_html() {
$value = get_option('ttc_enable_partial_payment', true);
echo '<input type="checkbox" id="ttc_enable_partial_payment_setting" name="ttc_enable_partial_payment" value="1" ' . checked(1, $value, false) . ' />';
echo '<label for="ttc_enable_partial_payment_setting">Enable partial payment functionality. If unchecked, only full payments are allowed.</label>';
}
function ttc_enable_otp_security_setting_field_html() {
$value = get_option('ttc_enable_otp_security', true);
echo '<input type="checkbox" id="ttc_enable_otp_security_setting" name="ttc_enable_otp_security" value="1" ' . checked(1, $value, false) . ' />';
echo '<label for="ttc_enable_otp_security_setting">Enable OTP security features. If unchecked, OTP will not be required for payment.</label>';
}
function ttc_otp_request_limit_setting_field_html() {
$value = get_option('ttc_otp_request_limit', 3);
echo '<input type="number" id="ttc_otp_request_limit_setting" name="ttc_otp_request_limit" value="' . esc_attr($value) . '" class="small-text" />';
echo '<p class="description">Enter the maximum number of OTP requests per time period. Default is 3.</p>';
}
function ttc_otp_rate_limit_minutes_setting_field_html() {
$value = get_option('ttc_otp_rate_limit_minutes', 15);
echo '<input type="number" id="ttc_otp_rate_limit_minutes_setting" name="ttc_otp_rate_limit_minutes" value="' . esc_attr($value) . '" class="small-text" />';
echo '<p class="description">Enter the time period in minutes for OTP request rate limiting. Default is 15 minutes.</p>';
}
function ttc_otp_resend_limit_setting_field_html() {
$value = get_option('ttc_otp_resend_limit', 2);
echo '<input type="number" id="ttc_otp_resend_limit_setting" name="ttc_otp_resend_limit" value="' . esc_attr($value) . '" class="small-text" />';
echo '<p class="description">Enter the maximum number of OTP resends per time period. Default is 2.</p>';
}
function ttc_otp_resend_minutes_setting_field_html() {
$value = get_option('ttc_otp_resend_minutes', 10);
echo '<input type="number" id="ttc_otp_resend_minutes_setting" name="ttc_otp_resend_minutes" value="' . esc_attr($value) . '" class="small-text" />';
echo '<p class="description">Enter the time period in minutes for OTP resend rate limiting. Default is 10 minutes.</p>';
}
function ttc_otp_max_attempts_setting_field_html() {
$value = get_option('ttc_otp_max_attempts', 5);
echo '<input type="number" id="ttc_otp_max_attempts_setting" name="ttc_otp_max_attempts" value="' . esc_attr($value) . '" class="small-text" />';
echo '<p class="description">Enter the maximum number of OTP attempts. Default is 5.</p>';
}
function ttc_otp_expiry_minutes_setting_field_html() {
$value = get_option('ttc_otp_expiry_minutes', 10);
echo '<input type="number" id="ttc_otp_expiry_minutes_setting" name="ttc_otp_expiry_minutes" value="' . esc_attr($value) . '" class="small-text" />';
echo '<p class="description">Enter the OTP expiry time in minutes. Default is 10 minutes.</p>';
}
function ttc_settings_page_html() {
// Check user capabilities
if (!current_user_can('manage_options')) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<div class="ttc-settings-container">
<form action="options.php" method="post">
<?php
settings_fields('ttc-settings');
do_settings_sections('ttc-settings');
submit_button('Save Settings');
?>
</form>
</div>
<div class="ttc-settings-info" style="margin-top: 30px; padding: 20px; background: #f9f9f9; border-left: 4px solid #0073aa;">
<h3>About TTC Settings</h3>
<p>This page contains all the configuration settings for the TTC (Table Tennis Club) website functionality.</p>
<p><strong>Email Settings:</strong> Configure email addresses for system communications like payment notifications, member communications, etc.</p>
<p><strong>Member Login Settings:</strong> Control member authentication features including login functionality for registered members.</p>
<p><strong>Payment Settings:</strong> Configure payment options including partial payment functionality and OTP security features for Quick Pay.</p>
<p><strong>OTP Security Features:</strong> Control the security settings for Quick Pay verification including rate limiting, attempt limits, and expiry times. You can disable OTP entirely for testing or less secure environments.</p>
</div>
</div>
<style>
.ttc-settings-container {
max-width: 800px;
margin-top: 20px;
}
.ttc-settings-info {
max-width: 800px;
}
.ttc-settings-info h3 {
margin-top: 0;
color: #0073aa;
}
</style>
<?php
}
// Hook the admin menu and settings registration
add_action('admin_menu', 'ttc_add_settings_page');
add_action('admin_init', 'ttc_register_settings');
/**
* Helper function to check if partial payment is enabled
*/
function ttc_is_partial_payment_enabled() {
return get_option('ttc_enable_partial_payment', true);
}
/**
* Helper function to check if OTP security is enabled
*/
function ttc_is_otp_security_enabled() {
return get_option('ttc_enable_otp_security', true);
}
/**
* Helper function to get OTP request rate limit
*/
function ttc_get_otp_request_limit() {
return get_option('ttc_otp_request_limit', 3);
}
/**
* Helper function to get OTP rate limit time period in minutes
*/
function ttc_get_otp_rate_limit_minutes() {
return get_option('ttc_otp_rate_limit_minutes', 15);
}
/**
* Helper function to get OTP resend rate limit
*/
function ttc_get_otp_resend_limit() {
return get_option('ttc_otp_resend_limit', 2);
}
/**
* Helper function to get OTP resend time period in minutes
*/
function ttc_get_otp_resend_minutes() {
return get_option('ttc_otp_resend_minutes', 10);
}
/**
* Helper function to get maximum OTP attempts
*/
function ttc_get_otp_max_attempts() {
return get_option('ttc_otp_max_attempts', 5);
}
/**
* Helper function to get OTP expiry time in minutes
*/
function ttc_get_otp_expiry_minutes() {
return get_option('ttc_otp_expiry_minutes', 10);
}
/**
* Quick Pay - Find Member Dues by Member ID or Email
*/
function find_member_dues() {
// Verify nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'quick_pay_nonce')) {
wp_send_json_error('Security verification failed. Please refresh the page and try again.');
}
// Get and sanitize member identifier
$member_identifier = isset($_POST['member_identifier']) ? sanitize_text_field($_POST['member_identifier']) : '';
if (empty($member_identifier)) {
wp_send_json_error('Please enter a Member ID or Email address.');
}
// Get and sanitize member identifier
$member_identifier = isset($_POST['member_identifier']) ? sanitize_text_field($_POST['member_identifier']) : '';
// Remove all whitespace from member identifier
$member_identifier = preg_replace('/\s+/', '', $member_identifier);
if (empty($member_identifier)) {
wp_send_json_error('Please enter a Member ID or Email address.');
}
// Determine if input is email or membership number
$is_email = is_email($member_identifier);
// Build query to find member
$meta_query = array();
if ($is_email) {
// Search by email
$meta_query[] = array(
'key' => 'email',
'value' => $member_identifier,
'compare' => '='
);
} else {
// Search by membership number
$meta_query[] = array(
'key' => 'membership_number',
'value' => $member_identifier,
'compare' => '='
);
}
// Query for member
$member_query = new WP_Query(array(
'post_type' => 'member',
'posts_per_page' => 1,
'meta_query' => $meta_query,
'post_status' => 'publish'
));
if (!$member_query->have_posts()) {
$identifier_type = $is_email ? 'email address' : 'Member ID';
wp_send_json_error("No member found with the provided {$identifier_type}. Please check your details and try again.");
}
// Get member details
$member_query->the_post();
$member_id = get_the_ID();
$member_name = get_the_title();
$member_status = get_field('member_status', $member_id);
$membership_number = get_field('membership_number', $member_id);
$email = get_field('email', $member_id);
wp_reset_postdata();
// Check if member is active
if ($member_status !== 'active') {
$status_message = '';
switch ($member_status) {
case 'inactive':
$status_message = 'Your membership is currently inactive. Please contact the club office to activate your membership.';
break;
case 'suspended':
$status_message = 'Your membership has been suspended. Please contact the club office for assistance.';
break;
case 'expired':
$status_message = 'Your membership has expired. Please renew your membership to continue.';
break;
default:
$status_message = 'Your membership status requires attention. Please contact the club office.';
}
wp_send_json_error($status_message);
}
// Get payment dues for this member
global $wpdb;
$table_name = $wpdb->prefix . 'payment_dues';
$payment_dues = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $table_name
WHERE member_id = %d AND status IN ('pending', 'overdue')
ORDER BY due_date ASC",
$member_id
)
);
if (empty($payment_dues)) {
// No dues - continue to show member info and empty dues list
}
// Build HTML response
$html = '<div class="space-y-4">';
// Member info header
$html .= '<div class="bg-gray-50 p-4 rounded-lg mb-6">';
$html .= '<h4 class="text-lg font-semibold text-gray-800 mb-2">Member Information</h4>';
$html .= '<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">';
$html .= '<div><span class="font-medium text-gray-600">Name:</span> ' . esc_html($member_name) . '</div>';
$html .= '<div><span class="font-medium text-gray-600">Membership No.:</span> ' . esc_html($membership_number) . '</div>';
$html .= '<div><span class="font-medium text-gray-600">Email:</span> ' . esc_html($email) . '</div>';
$html .= '</div>';
$html .= '</div>';
// Dues list
$html .= '<h4 class="text-lg font-semibold text-gray-800 mb-4">Pending Dues</h4>';
$html .= '<div class="space-y-4">';
// Check if partial payment is enabled
$partial_payment_enabled = ttc_is_partial_payment_enabled();
if (empty($payment_dues)) {
// No dues - create a dummy entry showing 0 amount
$html .= '<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-6">';
$html .= '<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">';
// Left Section
$html .= '<div class="flex-1">';
// Get the due as of date from options
$due_as_of_date = get_option('ttc_last_bulk_upload_due_as_of_date', '');
if ($due_as_of_date) {
$formatted_due_as_of_date = date('F j, Y', strtotime($due_as_of_date));
$html .= '<h3 class="text-xl font-semibold text-gray-800 mb-2">Dues as of ' . esc_html($formatted_due_as_of_date) . '</h3>';
} else {
$html .= '<h3 class="text-xl font-semibold text-gray-800 mb-2">No Pending Dues</h3>';
}
if ($partial_payment_enabled) {
// Show Original Amount and Remaining Amount when partial payment is enabled
$html .= '<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Original Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹0.00</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Remaining Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹0.00</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Due Date</span>';
$html .= '<span class="text-base font-medium text-gray-800">-</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Status</span>';
$html .= '<span class="text-base font-medium text-green-600">No Dues</span>';
$html .= '</div>';
$html .= '</div>';
} else {
// Show only Amount when partial payment is disabled
$html .= '<div class="grid grid-cols-2 lg:grid-cols-3 gap-4 text-sm">';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹0.00</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Due Date</span>';
$html .= '<span class="text-base font-medium text-gray-800">-</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Status</span>';
$html .= '<span class="text-base font-medium text-green-600">No Dues</span>';
$html .= '</div>';
$html .= '</div>';
}
$html .= '</div>';
// Right Section - Payment Button (enabled for advance payments)
$html .= '<div class="flex-shrink-0">';
$html .= '<button onclick="openPaymentModal(event)" ';
$html .= 'class="inline-flex items-center justify-center px-6 py-3 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 transition-colors duration-200" ';
$html .= 'data-due-id="0" ';
$html .= 'data-due-amount="0.00" ';
$html .= 'data-original-amount="0.00" ';
$html .= 'data-advance-payment="true">';
$html .= 'Make Advance Payment';
$html .= '</button>';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
} else {
// Has dues - show dues list
foreach ($payment_dues as $due) {
$html .= '<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-200">';
$html .= '<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 p-6">';
// Left Section
$html .= '<div class="flex-1">';
// Get the due as of date from options and format the title
$due_as_of_date = get_option('ttc_last_bulk_upload_due_as_of_date', '');
if ($due_as_of_date && $due->is_bulk_uploaded) {
$formatted_due_as_of_date = date('F j, Y', strtotime($due_as_of_date));
$html .= '<h3 class="text-xl font-semibold text-gray-800 mb-2">Due as of ' . esc_html($formatted_due_as_of_date) . '</h3>';
} else {
$html .= '<h3 class="text-xl font-semibold text-gray-800 mb-2">' . esc_html($due->title) . '</h3>';
}
if ($partial_payment_enabled) {
// Show Original Amount and Remaining Amount when partial payment is enabled
$html .= '<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Original Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹' . esc_html(number_format($due->original_amount ?? $due->amount, 2)) . '</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Remaining Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹' . esc_html(number_format($due->remaining_amount ?? $due->amount, 2)) . '</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Due Date</span>';
$html .= '<span class="text-base font-medium text-gray-800">' . esc_html(date('d M Y', strtotime($due->due_date))) . '</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Status</span>';
$html .= '<span class="text-base font-medium ' . ($due->status === 'overdue' ? 'text-red-600' : 'text-yellow-600') . '">' . esc_html(ucfirst($due->status)) . '</span>';
$html .= '</div>';
$html .= '</div>';
} else {
// Show only Amount when partial payment is disabled
$html .= '<div class="grid grid-cols-2 lg:grid-cols-3 gap-4 text-sm">';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹' . esc_html(number_format($due->remaining_amount ?? $due->amount, 2)) . '</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Due Date</span>';
$html .= '<span class="text-base font-medium text-gray-800">' . esc_html(date('d M Y', strtotime($due->due_date))) . '</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Status</span>';
$html .= '<span class="text-base font-medium ' . ($due->status === 'overdue' ? 'text-red-600' : 'text-yellow-600') . '">' . esc_html(ucfirst($due->status)) . '</span>';
$html .= '</div>';
$html .= '</div>';
}
$html .= '</div>';
// Right Section - Payment Button
$html .= '<div class="flex-shrink-0">';
$html .= '<button onclick="openPaymentModal(event)" ';
$html .= 'class="inline-flex items-center justify-center px-6 py-3 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 transition-colors duration-200" ';
$html .= 'data-due-id="' . esc_attr($due->id) . '" ';
$html .= 'data-due-amount="' . esc_attr(number_format($due->remaining_amount ?? $due->amount, 2)) . '" ';
$html .= 'data-original-amount="' . esc_attr(number_format($due->original_amount ?? $due->amount, 2)) . '">';
$html .= 'Make Payment';
$html .= '</button>';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
}
}
$html .= '</div>';
$html .= '</div>';
wp_send_json_success(array('html' => $html));
}
// Register AJAX actions
add_action('wp_ajax_find_member_dues', 'find_member_dues');
add_action('wp_ajax_nopriv_find_member_dues', 'find_member_dues');
/**
* Send OTP for Quick Pay verification
*/
function send_otp_for_quick_pay() {
// Verify nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'quick_pay_nonce')) {
wp_send_json_error('Security verification failed. Please refresh the page and try again.');
}
// Get and sanitize member identifier
$member_identifier = isset($_POST['member_identifier']) ? sanitize_text_field($_POST['member_identifier']) : '';
if (empty($member_identifier)) {
wp_send_json_error('Please enter a Member ID or Email address.');
}
// Get and sanitize member identifier
$member_identifier = isset($_POST['member_identifier']) ? sanitize_text_field($_POST['member_identifier']) : '';
// Remove all whitespace from member identifier
$member_identifier = preg_replace('/\s+/', '', $member_identifier);
if (empty($member_identifier)) {
wp_send_json_error('Please enter a Member ID or Email address.');
}
// Check rate limiting (max 3 attempts per 15 minutes per IP)
$ip_address = $_SERVER['REMOTE_ADDR'];
$rate_limit_key = 'otp_rate_limit_' . md5($ip_address);
$rate_limit_data = get_transient($rate_limit_key);
if ($rate_limit_data && $rate_limit_data['count'] >= 3) {
$remaining_time = $rate_limit_data['expires'] - time();
wp_send_json_error("Too many OTP requests. Please wait {$remaining_time} seconds before trying again.");
}
// Check if OTP security is enabled
if (ttc_is_otp_security_enabled()) {
// Check rate limiting using configurable settings
$ip_address = $_SERVER['REMOTE_ADDR'];
$rate_limit_key = 'otp_rate_limit_' . md5($ip_address);
$rate_limit_data = get_transient($rate_limit_key);
$request_limit = ttc_get_otp_request_limit();
$rate_limit_minutes = ttc_get_otp_rate_limit_minutes();
if ($rate_limit_data && $rate_limit_data['count'] >= $request_limit) {
$remaining_time = $rate_limit_data['expires'] - time();
wp_send_json_error("Too many OTP requests. Please wait {$remaining_time} seconds before trying again.");
}
}
// Determine if input is email or membership number
$is_email = is_email($member_identifier);
// Build query to find member
$meta_query = array();
if ($is_email) {
// Search by email
$meta_query[] = array(
'key' => 'email',
'value' => $member_identifier,
'compare' => '='
);
} else {
// Search by membership number
$meta_query[] = array(
'key' => 'membership_number',
'value' => $member_identifier,
'compare' => '='
);
}
// Query for member
$member_query = new WP_Query(array(
'post_type' => 'member',
'posts_per_page' => 1,
'meta_query' => $meta_query,
'post_status' => 'publish'
));
if (!$member_query->have_posts()) {
$identifier_type = $is_email ? 'email address' : 'Member ID';
wp_send_json_error("No member found with the provided {$identifier_type}. Please check your details and try again.");
}
// Get member details
$member_query->the_post();
$member_id = get_the_ID();
$member_name = get_the_title();
$member_status = get_field('member_status', $member_id);
$membership_number = get_field('membership_number', $member_id);
$email = get_field('email', $member_id);
wp_reset_postdata();
// Check if member is active
if ($member_status !== 'active') {
$status_message = '';
switch ($member_status) {
case 'inactive':
$status_message = 'Your membership is currently inactive. Please contact the club office to activate your membership.';
break;
case 'suspended':
$status_message = 'Your membership has been suspended. Please contact the club office for assistance.';
break;
case 'expired':
$status_message = 'Your membership has expired. Please renew your membership to continue.';
break;
default:
$status_message = 'Your membership status requires attention. Please contact the club office.';
}
wp_send_json_error($status_message);
}
// Generate OTP
$otp = sprintf('%06d', mt_rand(0, 999999));
$otp_expires = time() + (10 * 60); // 10 minutes expiry
// Generate OTP using configurable expiry time
$otp = sprintf('%04d', mt_rand(0, 9999));
$expiry_minutes = ttc_get_otp_expiry_minutes();
$otp_expires = time() + ($expiry_minutes * 60);
// Store OTP in transient with member info
$otp_key = 'quick_pay_otp_' . md5($email . $membership_number);
$otp_data = array(
'otp' => $otp,
'expires' => $otp_expires,
'member_id' => $member_id,
'member_email' => $email,
'member_name' => $member_name,
'membership_number' => $membership_number,
'attempts' => 0
);
set_transient($otp_key, $otp_data, 10 * 60); // 10 minutes
// Update rate limiting
if (!$rate_limit_data) {
$rate_limit_data = array('count' => 1, 'expires' => time() + (15 * 60));
} else {
$rate_limit_data['count']++;
}
set_transient($rate_limit_key, $rate_limit_data, 15 * 60);
// Generate OTP using configurable expiry time
$otp = sprintf('%04d', mt_rand(0, 9999));
$expiry_minutes = ttc_get_otp_expiry_minutes();
$otp_expires = time() + ($expiry_minutes * 60);
// Store OTP in transient with member info
$otp_key = 'quick_pay_otp_' . md5($email . $membership_number);
$otp_data = array(
'otp' => $otp,
'expires' => $otp_expires,
'member_id' => $member_id,
'member_email' => $email,
'member_name' => $member_name,
'membership_number' => $membership_number,
'attempts' => 0
);
set_transient($otp_key, $otp_data, $expiry_minutes * 60);
// Update rate limiting if OTP security is enabled
if (ttc_is_otp_security_enabled()) {
if (!$rate_limit_data) {
$rate_limit_data = array('count' => 1, 'expires' => time() + ($rate_limit_minutes * 60));
} else {
$rate_limit_data['count']++;
}
set_transient($rate_limit_key, $rate_limit_data, $rate_limit_minutes * 60);
}
// Send OTP email
$email_sent = send_otp_email($email, $member_name, $otp);
if (!$email_sent) {
wp_send_json_error('Failed to send verification code. Please try again later.');
}
wp_send_json_success(array(
'member_id' => $member_id,
'member_email' => $email,
'member_name' => $member_name,
'membership_number' => $membership_number
));
}
/**
* Verify OTP and return member dues
*/
function verify_otp() {
// Verify nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'quick_pay_nonce')) {
wp_send_json_error('Security verification failed. Please refresh the page and try again.');
}
$otp_code = isset($_POST['otp_code']) ? sanitize_text_field($_POST['otp_code']) : '';
$member_email = isset($_POST['member_email']) ? sanitize_email($_POST['member_email']) : '';
$membership_number = isset($_POST['membership_number']) ? sanitize_text_field($_POST['membership_number']) : '';
if (empty($otp_code) || empty($member_email) || empty($membership_number)) {
wp_send_json_error('Invalid verification data. Please try again.');
}
// Get stored OTP data
$otp_key = 'quick_pay_otp_' . md5($member_email . $membership_number);
$otp_data = get_transient($otp_key);
if (!$otp_data) {
wp_send_json_error('Verification code has expired. Please request a new code.');
}
// Check if OTP has expired
if (time() > $otp_data['expires']) {
delete_transient($otp_key);
wp_send_json_error('Verification code has expired. Please request a new code.');
}
// Check attempts limit using configurable settings
$max_attempts = ttc_get_otp_max_attempts();
if ($otp_data['attempts'] >= $max_attempts) {
delete_transient($otp_key);
wp_send_json_error('Too many failed attempts. Please request a new verification code.');
}
// Verify OTP
if ($otp_code !== $otp_data['otp']) {
// Increment attempts
$otp_data['attempts']++;
set_transient($otp_key, $otp_data, 10 * 60);
$remaining_attempts = $max_attempts - $otp_data['attempts'];
wp_send_json_error("Invalid verification code. {$remaining_attempts} attempts remaining.");
}
// OTP is valid - delete it and get member dues
delete_transient($otp_key);
// Get payment dues for this member
global $wpdb;
$table_name = $wpdb->prefix . 'payment_dues';
$payment_dues = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM $table_name
WHERE member_id = %d AND status IN ('pending', 'overdue')
ORDER BY due_date ASC",
$otp_data['member_id']
)
);
// Build HTML response
$html = '<div class="space-y-4">';
// Member info header
$html .= '<div class="bg-gray-50 p-4 rounded-lg mb-6">';
$html .= '<h4 class="text-lg font-semibold text-gray-800 mb-2">Member Information</h4>';
$html .= '<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">';
$html .= '<div><span class="font-medium text-gray-600">Name:</span> ' . esc_html($otp_data['member_name']) . '</div>';
$html .= '<div><span class="font-medium text-gray-600">Membership No.:</span> ' . esc_html($otp_data['membership_number']) . '</div>';
$html .= '<div><span class="font-medium text-gray-600">Email:</span> ' . esc_html($otp_data['member_email']) . '</div>';
$html .= '</div>';
$html .= '</div>';
// Dues list
$html .= '<h4 class="text-lg font-semibold text-gray-800 mb-4">Pending Dues</h4>';
$html .= '<div class="space-y-4">';
// Check if partial payment is enabled
$partial_payment_enabled = ttc_is_partial_payment_enabled();
if (empty($payment_dues)) {
// No dues - create a dummy entry showing 0 amount
$html .= '<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-6">';
$html .= '<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">';
// Left Section
$html .= '<div class="flex-1">';
// Get the due as of date from options
$due_as_of_date = get_option('ttc_last_bulk_upload_due_as_of_date', '');
if ($due_as_of_date) {
$formatted_due_as_of_date = date('F j, Y', strtotime($due_as_of_date));
$html .= '<h3 class="text-xl font-semibold text-gray-800 mb-2">Dues as of ' . esc_html($formatted_due_as_of_date) . '</h3>';
} else {
$html .= '<h3 class="text-xl font-semibold text-gray-800 mb-2">No Pending Dues</h3>';
}
if ($partial_payment_enabled) {
// Show Original Amount and Remaining Amount when partial payment is enabled
$html .= '<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Original Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹0.00</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Remaining Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹0.00</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Due Date</span>';
$html .= '<span class="text-base font-medium text-gray-800">-</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Status</span>';
$html .= '<span class="text-base font-medium text-green-600">No Dues</span>';
$html .= '</div>';
$html .= '</div>';
} else {
// Show only Amount when partial payment is disabled
$html .= '<div class="grid grid-cols-2 lg:grid-cols-3 gap-4 text-sm">';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹0.00</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Due Date</span>';
$html .= '<span class="text-base font-medium text-gray-800">-</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Status</span>';
$html .= '<span class="text-base font-medium text-green-600">No Dues</span>';
$html .= '</div>';
$html .= '</div>';
}
$html .= '</div>';
// Right Section - Payment Button (enabled for advance payments)
$html .= '<div class="flex-shrink-0">';
$html .= '<button onclick="openPaymentModal(event)" ';
$html .= 'class="inline-flex items-center justify-center px-6 py-3 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 transition-colors duration-200" ';
$html .= 'data-due-id="0" ';
$html .= 'data-due-amount="0.00" ';
$html .= 'data-original-amount="0.00" ';
$html .= 'data-advance-payment="true">';
$html .= 'Make Advance Payment';
$html .= '</button>';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
} else {
// Has dues - show dues list
foreach ($payment_dues as $due) {
$html .= '<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-200">';
$html .= '<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 p-6">';
// Left Section
$html .= '<div class="flex-1">';
// Get the due as of date from options and format the title
$due_as_of_date = get_option('ttc_last_bulk_upload_due_as_of_date', '');
if ($due_as_of_date && $due->is_bulk_uploaded) {
$formatted_due_as_of_date = date('F j, Y', strtotime($due_as_of_date));
$html .= '<h3 class="text-xl font-semibold text-gray-800 mb-2">Due as of ' . esc_html($formatted_due_as_of_date) . '</h3>';
} else {
$html .= '<h3 class="text-xl font-semibold text-gray-800 mb-2">' . esc_html($due->title) . '</h3>';
}
if ($partial_payment_enabled) {
// Show Original Amount and Remaining Amount when partial payment is enabled
$html .= '<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Original Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹' . esc_html(number_format($due->original_amount ?? $due->amount, 2)) . '</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Remaining Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹' . esc_html(number_format($due->remaining_amount ?? $due->amount, 2)) . '</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Due Date</span>';
$html .= '<span class="text-base font-medium text-gray-800">' . esc_html(date('d M Y', strtotime($due->due_date))) . '</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Status</span>';
$html .= '<span class="text-base font-medium ' . ($due->status === 'overdue' ? 'text-red-600' : 'text-yellow-600') . '">' . esc_html(ucfirst($due->status)) . '</span>';
$html .= '</div>';
$html .= '</div>';
} else {
// Show only Amount when partial payment is disabled
$html .= '<div class="grid grid-cols-2 lg:grid-cols-3 gap-4 text-sm">';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Amount</span>';
$html .= '<span class="text-base font-medium text-gray-800">₹' . esc_html(number_format($due->remaining_amount ?? $due->amount, 2)) . '</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Due Date</span>';
$html .= '<span class="text-base font-medium text-gray-800">' . esc_html(date('d M Y', strtotime($due->due_date))) . '</span>';
$html .= '</div>';
$html .= '<div class="flex flex-col">';
$html .= '<span class="text-gray-500 text-xs">Status</span>';
$html .= '<span class="text-base font-medium ' . ($due->status === 'overdue' ? 'text-red-600' : 'text-yellow-600') . '">' . esc_html(ucfirst($due->status)) . '</span>';
$html .= '</div>';
$html .= '</div>';
}
$html .= '</div>';
// Right Section - Payment Button
$html .= '<div class="flex-shrink-0">';
$html .= '<button onclick="openPaymentModal(event)" ';
$html .= 'class="inline-flex items-center justify-center px-6 py-3 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 transition-colors duration-200" ';
$html .= 'data-due-id="' . esc_attr($due->id) . '" ';
$html .= 'data-due-amount="' . esc_attr(number_format($due->remaining_amount ?? $due->amount, 2)) . '" ';
$html .= 'data-original-amount="' . esc_attr(number_format($due->original_amount ?? $due->amount, 2)) . '">';
$html .= 'Make Payment';
$html .= '</button>';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
}
}
$html .= '</div>';
$html .= '</div>';
wp_send_json_success(array('html' => $html));
}
/**
* Resend OTP for Quick Pay
*/
function resend_otp() {
// Verify nonce
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'quick_pay_nonce')) {
wp_send_json_error('Security verification failed. Please refresh the page and try again.');
}
$member_id = isset($_POST['member_id']) ? intval($_POST['member_id']) : 0;
$member_email = isset($_POST['member_email']) ? sanitize_email($_POST['member_email']) : '';
$member_name = isset($_POST['member_name']) ? sanitize_text_field($_POST['member_name']) : '';
$membership_number = isset($_POST['membership_number']) ? sanitize_text_field($_POST['membership_number']) : '';
if (!$member_id || !$member_email || !$member_name || !$membership_number) {
wp_send_json_error('Invalid member data. Please try again.');
}
// Check rate limiting for resend (max 2 resends per 10 minutes per email)
$resend_rate_limit_key = 'otp_resend_limit_' . md5($member_email);
$resend_rate_limit_data = get_transient($resend_rate_limit_key);
if ($resend_rate_limit_data && $resend_rate_limit_data['count'] >= 2) {
$remaining_time = $resend_rate_limit_data['expires'] - time();
wp_send_json_error("Too many resend requests. Please wait {$remaining_time} seconds before trying again.");
}
// Check rate limiting for resend using configurable settings
if (ttc_is_otp_security_enabled()) {
$resend_rate_limit_key = 'otp_resend_limit_' . md5($member_email);
$resend_rate_limit_data = get_transient($resend_rate_limit_key);
$resend_limit = ttc_get_otp_resend_limit();
$resend_minutes = ttc_get_otp_resend_minutes();
if ($resend_rate_limit_data && $resend_rate_limit_data['count'] >= $resend_limit) {
$remaining_time = $resend_rate_limit_data['expires'] - time();
wp_send_json_error("Too many resend requests. Please wait {$remaining_time} seconds before trying again.");
}
}
// Generate new OTP
$otp = sprintf('%06d', mt_rand(0, 999999));
$otp_expires = time() + (10 * 60); // 10 minutes expiry
// Generate new OTP using configurable expiry time
$otp = sprintf('%04d', mt_rand(0, 9999));
$expiry_minutes = ttc_get_otp_expiry_minutes();
$otp_expires = time() + ($expiry_minutes * 60);
// Store new OTP data
$otp_key = 'quick_pay_otp_' . md5($member_email . $membership_number);
$otp_data = array(
'otp' => $otp,
'expires' => $otp_expires,
'member_id' => $member_id,
'member_email' => $member_email,
'member_name' => $member_name,
'membership_number' => $membership_number,
'attempts' => 0
);
set_transient($otp_key, $otp_data, 10 * 60);
// Update resend rate limiting
if (!$resend_rate_limit_data) {
$resend_rate_limit_data = array('count' => 1, 'expires' => time() + (10 * 60));
} else {
$resend_rate_limit_data['count']++;
}
set_transient($resend_rate_limit_key, $resend_rate_limit_data, 10 * 60);
// Generate new OTP using configurable expiry time
$otp = sprintf('%06d', mt_rand(0, 999999));
$expiry_minutes = ttc_get_otp_expiry_minutes();
$otp_expires = time() + ($expiry_minutes * 60);
// Store new OTP data
$otp_key = 'quick_pay_otp_' . md5($member_email . $membership_number);
$otp_data = array(
'otp' => $otp,
'expires' => $otp_expires,
'member_id' => $member_id,
'member_email' => $member_email,
'member_name' => $member_name,
'membership_number' => $membership_number,
'attempts' => 0
);
set_transient($otp_key, $otp_data, $expiry_minutes * 60);
// Update resend rate limiting if OTP security is enabled
if (ttc_is_otp_security_enabled()) {
if (!$resend_rate_limit_data) {
$resend_rate_limit_data = array('count' => 1, 'expires' => time() + ($resend_minutes * 60));
} else {
$resend_rate_limit_data['count']++;
}
set_transient($resend_rate_limit_key, $resend_rate_limit_data, $resend_minutes * 60);
}
// Send new OTP email
$email_sent = send_otp_email($member_email, $member_name, $otp);
if (!$email_sent) {
wp_send_json_error('Failed to resend verification code. Please try again later.');
}
wp_send_json_success('Verification code resent successfully.');
}
/**
* Send OTP email to member
*/
function send_otp_email($email, $member_name, $otp) {
$subject = 'TTC Quick Pay - Email Verification Code';
// Get expiry minutes for the email template
$expiry_minutes = ttc_get_otp_expiry_minutes();
// Create HTML email template
$message = '
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #f8f9fa; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
.otp-container {
margin: 25px 0;
padding: 20px;
background-color: #f8f9fa;
border-radius: 5px;
text-align: center;
}
.otp-code {
font-size: 32px;
color: #28a745;
font-weight: bold;
letter-spacing: 8px;
display: block;
margin: 15px 0;
font-family: monospace;
}
.footer { margin-top: 20px; font-size: 12px; color: #6c757d; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>TTC Quick Pay Verification</h2>
</div>
<p>Dear ' . esc_html($member_name) . ',</p>
<p>You have requested to view your payment dues through TTC Quick Pay. To proceed, please use the verification code below:</p>
<div class="otp-container">
<span class="otp-code">' . esc_html($otp) . '</span>
<p><strong>This code will expire in ' . esc_html($expiry_minutes) . ' minutes.</strong></p>
</div>
<p>If you did not request this verification code, please ignore this email.</p>
<div class="footer">
<p>This is an automated message from the TTC website. Please do not reply to this email.</p>
</div>
</div>
</body>
</html>';
$headers = [
'Content-Type: text/html; charset=UTF-8',
'From: TTC Website <noreply@ttc.com>'
];
return wp_mail($email, $subject, $message, $headers);
}
// Register AJAX actions for OTP functionality
add_action('wp_ajax_send_otp_for_quick_pay', 'send_otp_for_quick_pay');
add_action('wp_ajax_nopriv_send_otp_for_quick_pay', 'send_otp_for_quick_pay');
add_action('wp_ajax_verify_otp', 'verify_otp');
add_action('wp_ajax_nopriv_verify_otp', 'verify_otp');
add_action('wp_ajax_resend_otp', 'resend_otp');
add_action('wp_ajax_nopriv_resend_otp', 'resend_otp');
?>