# easy-invoice/2.3.3/includes/Controllers/InvoiceController.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.3.3. 1,282 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.3.3/code/includes/Controllers/InvoiceController.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.3.3/raw/includes/Controllers/InvoiceController.php
- Modified: 2026-05-24T14:33:32+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.3.3/code/includes/Controllers/InvoiceController.php#L10-L20`.

```php
<?php
/**
 * Invoice Controller Class
 *
 * @package Easy_Invoice
 * @subpackage Controllers
 */

namespace EasyInvoice\Controllers;

use EasyInvoice\Models\Invoice;
use EasyInvoice\Providers\InvoiceServiceProvider;
use EasyInvoice\Constants\PagesSlugs;
use WP_Query;

/**
 * InvoiceController handles all invoice-related functionality
 */
class InvoiceController extends BaseController {
    /**
     * Initialize the controller
     */
    public function init() {
        // Allow plugins to extend the controller initialization
        do_action('easy_invoice_invoice_controller_before_init', $this);

        // Register AJAX endpoints for invoice management
        add_action('wp_ajax_easy_invoice_trash_invoice', array($this, 'trashInvoice'));
        add_action('wp_ajax_easy_invoice_restore_invoice', array($this, 'restoreInvoice'));
        add_action('wp_ajax_easy_invoice_delete_invoice_permanently', array($this, 'deleteInvoicePermanently'));
        add_action('wp_ajax_easy_invoice_delete_invoice', array($this, 'deleteInvoice')); // Legacy support
        add_action('wp_ajax_easy_invoice_publish_invoice', array($this, 'publishInvoice'));
        add_action('wp_ajax_easy_invoice_draft_invoice', array($this, 'draftInvoice'));

        // Handle bulk actions
        add_action('admin_init', array($this, 'handleBulkActions'));

        // Register additional AJAX handlers
        $this->registerAjaxHandlers();

        // Add meta box for manual payment verification
        add_action('add_meta_boxes_easy-invoice', array($this, 'add_manual_payment_meta_box'));

        // AJAX handler for creating a sample invoice
        add_action('wp_ajax_easy_invoice_create_sample_invoice', array($this, 'ajax_create_sample_invoice'));

        // AJAX handler for creating a new invoice with title
        add_action('wp_ajax_easy_invoice_create_new_invoice', array($this, 'ajax_create_new_invoice'));

        // Allow plugins to extend the controller initialization
        do_action('easy_invoice_invoice_controller_after_init', $this);
    }

    /**
     * Display method implementation
     *
     * @param array $args Display arguments
     */
    public function display(array $args = []) {
        // Allow plugins to modify display arguments
        $args = apply_filters('easy_invoice_invoice_controller_display_args', $args);

        $page = isset($args['page']) ? $args['page'] : '';

        // Allow plugins to modify the page before processing
        $page = apply_filters('easy_invoice_invoice_controller_display_page', $page, $args);

        switch ($page) {
            case PagesSlugs::ALL_INVOICES:
                $this->displayInvoicesPage();
                break;

            case PagesSlugs::INVOICE_NEW:
                // For a new invoice, ensure no ID is passed
                $_GET['id'] = isset($_GET['id']) ? $_GET['id'] : 0;
                $this->displayInvoiceBuilderPage();
                break;

            case PagesSlugs::INVOICE_PREVIEW:
                $this->displayPreviewPage();
                break;

            default:
                $this->displayInvoicesPage();
                break;
        }

        // Allow plugins to perform actions after display
        do_action('easy_invoice_invoice_controller_after_display', $page, $args);
    }

    /**
     * Display all invoices page
     *
     * Uses WordPress's built-in WP_Query and paginate_links() for optimal performance
     * with large datasets (10,000+ invoices). The pagination is handled efficiently
     * by WordPress core functions which are optimized for scalability.
     */
    protected function displayInvoicesPage() {
        // Allow plugins to perform actions before displaying invoices page
        do_action('easy_invoice_invoice_controller_before_display_invoices_page');

        // Get filter parameters
        $status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
        $recurring_filter = isset($_GET['recurring']) ? sanitize_text_field($_GET['recurring']) : '';
        $subscription_filter = isset($_GET['subscription']) ? sanitize_text_field($_GET['subscription']) : '';
        $client_filter = isset($_GET['client_id']) ? absint($_GET['client_id']) : 0;
        $search_query = isset($_GET['search']) ? sanitize_text_field(wp_unslash($_GET['search'])) : '';
        $current_view = isset($_GET['view']) ? sanitize_text_field($_GET['view']) : 'all';
        $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
        $per_page = 20;
        $offset = ($current_page - 1) * $per_page;

        // Build repository query arguments
        $args = [];

        // Set post status based on view
        if ($current_view === 'trash') {
            $args['post_status'] = 'trash';
        } elseif ($current_view === 'draft') {
            $args['post_status'] = 'draft';
        }

        // Build meta query array
        $meta_query = [];

        // Add status filter if provided
        if (!empty($status_filter)) {
            $meta_query[] = [
                'key' => '_easy_invoice_status',
                'value' => $status_filter,
                'compare' => '='
            ];
        }

        // Add client filter if provided.
        //
        // Easy Invoice stores either:
        //   • `_easy_invoice_client_id`  — populated when the user picks a
        //      client from the dropdown in the Invoice Builder, OR
        //   • `_easy_invoice_customer_email` (+ customer_name) — populated when
        //      the biller types ad-hoc customer info inline.
        //
        // To make the filter useful for both flows we match on
        //   client_id == N  OR  customer_email == that client's email.
        if (!empty($client_filter)) {
            $client_email = '';
            try {
                $client_repo = new \EasyInvoice\Repositories\ClientRepository();
                $client_obj  = $client_repo->find($client_filter);
                if ($client_obj) {
                    // The Client model exposes the email via the magic __call → __get fallback.
                    $client_email = (string) $client_obj->getEmail();
                }
            } catch (\Throwable $e) {
                $client_email = '';
            }

            $client_clauses = [
                'relation' => 'OR',
                [
                    'key'     => '_easy_invoice_client_id',
                    'value'   => (string) $client_filter,
                    'compare' => '=',
                ],
            ];
            if ($client_email !== '') {
                $client_clauses[] = [
                    'key'     => '_easy_invoice_customer_email',
                    'value'   => $client_email,
                    'compare' => '=',
                ];
            }
            $meta_query[] = $client_clauses;
        }

        // Add recurring filter if provided
        if (!empty($recurring_filter)) {
            if ($recurring_filter === 'recurring') {
                // Show only recurring invoices
                $meta_query[] = [
                    'key' => '_easy_invoice_recurring_enabled',
                    'value' => '1',
                    'compare' => '='
                ];
            } elseif ($recurring_filter === 'non-recurring') {
                // Show only non-recurring invoices
                $meta_query[] = [
                    'relation' => 'OR',
                    [
                        'key' => '_easy_invoice_recurring_enabled',
                        'compare' => 'NOT EXISTS'
                    ],
                    [
                        'key' => '_easy_invoice_recurring_enabled',
                        'value' => '0',
                        'compare' => '='
                    ]
                ];
            }
        }

        // Add meta query to args if we have any filters
        if (!empty($meta_query)) {
            if (count($meta_query) === 1) {
                $args['meta_query'] = $meta_query[0];
            } else {
                $args['meta_query'] = [
                    'relation' => 'AND',
                    ...$meta_query
                ];
            }
        }

        // Add pagination parameters to args
        $args['posts_per_page'] = $per_page;
        $args['offset'] = $offset;
        $args['orderby'] = 'date';
        $args['order'] = 'DESC';

        // Allow plugins to modify query arguments
        $args = apply_filters('easy_invoice_invoice_controller_query_args', $args, $current_view, $status_filter);

        // Get paginated invoices using WordPress query
        $repository = InvoiceServiceProvider::getInvoiceRepository();

        // Use WordPress WP_Query directly for better pagination handling
        $query_args = array_merge([
            'post_type' => \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE,
            'post_status' => $args['post_status'] ?? 'publish',
            'posts_per_page' => $per_page,
            'paged' => $current_page,
            'orderby' => 'date',
            'order' => 'DESC',
            'no_found_rows' => false, // We need this for pagination
            'update_post_term_cache' => false, // Disable term cache for better performance
            'update_post_meta_cache' => false, // Disable meta cache for better performance
        ], $args);

        // Add search functionality
        if (!empty($search_query)) {
            // For search, we'll use a simpler approach that works better with WordPress
            // First, get all invoices that match the search criteria
            $search_ids = [];

            // Search in post title and content
            $title_search = new WP_Query([
                'post_type' => \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE,
                'post_status' => $args['post_status'] ?? 'publish',
                'posts_per_page' => -1,
                's' => $search_query
            ]);

            if ($title_search->have_posts()) {
                $search_ids = array_merge($search_ids, wp_list_pluck($title_search->posts, 'ID'));
            }

            // Search in meta fields
            $meta_search = new WP_Query([
                'post_type' => \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE,
                'post_status' => $args['post_status'] ?? 'publish',
                'posts_per_page' => -1,
                'meta_query' => [
                    'relation' => 'OR',
                    [
                        'key' => '_easy_invoice_number',
                        'value' => $search_query,
                        'compare' => 'LIKE'
                    ],
                    [
                        'key' => '_easy_invoice_customer_name',
                        'value' => $search_query,
                        'compare' => 'LIKE'
                    ],
                    [
                        'key' => '_easy_invoice_customer_email',
                        'value' => $search_query,
                        'compare' => 'LIKE'
                    ]
                ]
            ]);

            if ($meta_search->have_posts()) {
                $search_ids = array_merge($search_ids, wp_list_pluck($meta_search->posts, 'ID'));
            }

            // Remove duplicates
            $search_ids = array_unique($search_ids);

            if (!empty($search_ids)) {
                // Use post__in to filter by the found IDs
                $query_args['post__in'] = $search_ids;
            } else {
                // If no results found, set post__in to empty array to show no results
                $query_args['post__in'] = [0];
            }
        }

        // Remove offset as we're using paged
        unset($query_args['offset']);

        // Allow plugins to modify the final query arguments
        $query_args = apply_filters('easy_invoice_invoice_controller_final_query_args', $query_args);

        $wp_query = new WP_Query($query_args);
        $invoices = [];

        if ($wp_query->have_posts()) {
            foreach ($wp_query->posts as $post) {
                $invoice = $repository->find($post->ID);
                if ($invoice) {
                    $invoices[] = $invoice;
                }
            }
        }

        // Allow plugins to modify the invoices array
        $invoices = apply_filters('easy_invoice_invoice_controller_invoices_list', $invoices, $wp_query);

        // Get pagination info from WordPress query
        $total_invoices = $wp_query->found_posts;
        $total_pages = $wp_query->max_num_pages;

        // Get trash count for tab display (without pagination)
        $trash_args = ['post_status' => 'trash'];
        $trash_invoices = $repository->all($trash_args);
        $trash_count = count($trash_invoices);

        // Get draft count for tab display (without pagination)
        $draft_args = ['post_status' => 'draft'];
        $draft_invoices = $repository->all($draft_args);
        $draft_count = count($draft_invoices);

        // Build clients list for the listing filter dropdown
        $clients_list = [];
        try {
            $client_repository = new \EasyInvoice\Repositories\ClientRepository();
            foreach ($client_repository->all() as $client) {
                $name = $client->getBusinessClientName() ?: trim($client->getFirstName() . ' ' . $client->getLastName());
                if ($name === '') {
                    continue;
                }
                $clients_list[] = [
                    'id'   => $client->getId(),
                    'name' => $name,
                ];
            }
            usort($clients_list, function ($a, $b) {
                return strcasecmp($a['name'], $b['name']);
            });
        } catch (\Throwable $e) {
            $clients_list = [];
        }

        // Prepare template data
        $template_data = [
            'invoices' => $invoices,
            'current_view' => $current_view,
            'status_filter' => $status_filter,
            'recurring_filter' => $recurring_filter,
            'client_filter' => $client_filter,
            'clients_list' => $clients_list,
            'search_query' => $search_query,
            'trash_count' => $trash_count,
            'draft_count' => $draft_count,
            'repository' => $repository,
            'current_page' => $current_page,
            'per_page' => $per_page,
            'total_invoices' => $total_invoices,
            'total_pages' => $total_pages,
            'wp_query' => $wp_query
        ];

        // Allow plugins to modify template data
        $template_data = apply_filters('easy_invoice_invoice_controller_template_data', $template_data);

        // Display the template
        $this->displayTemplate(
            EASY_INVOICE_PLUGIN_DIR . 'templates/invoices/listing.php',
            $template_data
        );

        // Allow plugins to perform actions after displaying invoices page
        do_action('easy_invoice_invoice_controller_after_display_invoices_page', $template_data);
    }

    /**
     * Display invoice builder page
     */
    protected function displayInvoiceBuilderPage() {
        $invoice_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
        $repository = InvoiceServiceProvider::getInvoiceRepository();

        // Display the template
        $this->displayTemplate(
            EASY_INVOICE_PLUGIN_DIR . 'templates/invoices/builder.php',
            ['invoice_id' => $invoice_id, 'repository' => $repository]
        );
    }

    /**
     * Display invoice preview page
     */
    protected function displayPreviewPage() {
        $this->renderInvoicePreview();
    }

    /**
     * Common helper method to render an invoice preview
     * Used by both preview methods to ensure consistency
     */
    private function renderInvoicePreview() {
        $check = $this->checkCapability();
        if (is_wp_error($check)) {
            wp_die($check->get_error_message());
        }

        $invoice_id = isset($_GET['invoice_id']) ? intval($_GET['invoice_id']) : 0;

        if ($invoice_id <= 0) {
            wp_die(__('Invalid invoice ID', 'easy-invoice'));
        }

        // Get invoice from repository
        $repository = InvoiceServiceProvider::getInvoiceRepository();
        $invoice = $repository->find($invoice_id);

        if (!$invoice) {
            wp_die(__('Invalid invoice ID', 'easy-invoice'));
        }

        // Get common template variables
        $template_vars = $this->getCommonTemplateVars();
        $currency_symbol = $template_vars['currency_symbol'];

        // Enqueue preview styles
        wp_enqueue_style(
            'easy-invoice-preview',
            EASY_INVOICE_PLUGIN_URL . 'assets/css/preview.css',
            array(),
            EASY_INVOICE_VERSION
        );

        // Display the template
        $this->displayTemplate(
            EASY_INVOICE_PLUGIN_DIR . 'templates/invoices/preview.php',
            [
                'invoice' => $invoice,
                'currency_symbol' => $currency_symbol
            ]
        );
    }

    /**
     * Trash an invoice (move to trash)
     */
    public function trashInvoice() {
        if (!$this->handleAjaxSecurity($_POST['nonce'])) {
            return;
        }

        // Check invoice ID
        if (!isset($_POST['invoice_id']) || empty($_POST['invoice_id'])) {
            wp_send_json_error(array('message' => 'Invalid invoice ID'));
        }

        $invoice_id = intval($_POST['invoice_id']);

        // Get the invoice object to update status
        $invoice_repository = InvoiceServiceProvider::getInvoiceRepository();
        $invoice = $invoice_repository->find($invoice_id);
        if ($invoice) {
            // Set status to cancelled before moving to trash
            $invoice->setStatus('cancelled');
            $invoice->save();
        }

        // Move to trash
        $result = wp_trash_post($invoice_id);

        if ($result) {
            wp_send_json_success(array('message' => 'Invoice moved to trash'));
        } else {
            wp_send_json_error(array('message' => 'Error moving invoice to trash'));
        }
    }

    /**
     * Restore an invoice from trash
     */
    public function restoreInvoice() {
        if (!$this->handleAjaxSecurity($_POST['nonce'])) {
            return;
        }

        // Check invoice ID
        if (!isset($_POST['invoice_id']) || empty($_POST['invoice_id'])) {
            wp_send_json_error(array('message' => 'Invalid invoice ID'));
        }

        $invoice_id = intval($_POST['invoice_id']);

        // Restore from trash
        $result = wp_untrash_post($invoice_id);

        if ($result) {
            // WordPress defaults restored posts to 'draft', so we need to explicitly set it to 'publish'
            wp_update_post(array(
                'ID' => $invoice_id,
                'post_status' => 'publish'
            ));

            // Get the invoice object and set status to available
            $invoice_repository = InvoiceServiceProvider::getInvoiceRepository();
            $invoice = $invoice_repository->find($invoice_id);
            if ($invoice) {
                $invoice->setStatus('available');
                $invoice->save();
            }

            wp_send_json_success(array('message' => 'Invoice restored from trash'));
        } else {
            wp_send_json_error(array('message' => 'Error restoring invoice from trash'));
        }
    }

    /**
     * Delete an invoice permanently
     */
    public function deleteInvoicePermanently() {
        if (!$this->handleAjaxSecurity($_POST['nonce'])) {
            return;
        }

        // Check invoice ID
        if (!isset($_POST['invoice_id']) || empty($_POST['invoice_id'])) {
            wp_send_json_error(array('message' => 'Invalid invoice ID'));
        }

        $invoice_id = intval($_POST['invoice_id']);

        // Delete permanently
        $result = wp_delete_post($invoice_id, true);

        if ($result) {
            wp_send_json_success(array('message' => 'Invoice deleted permanently'));
        } else {
            wp_send_json_error(array('message' => 'Error deleting invoice'));
        }
    }

    /**
     * Legacy delete invoice handler (now redirects to trash)
     */
    public function deleteInvoice() {
        // Redirect to trash function for backward compatibility
        $this->trashInvoice();
    }

    /**
     * Publish an invoice (change status from draft to publish)
     */
    public function publishInvoice() {
        if (!$this->handleAjaxSecurity($_POST['nonce'])) {
            return;
        }

        // Check invoice ID
        if (!isset($_POST['invoice_id']) || empty($_POST['invoice_id'])) {
            wp_send_json_error(array('message' => 'Invalid invoice ID'));
        }

        $invoice_id = intval($_POST['invoice_id']);

        // Update post status to published
        $result = wp_update_post(array(
            'ID' => $invoice_id,
            'post_status' => 'publish'
        ));

        if ($result) {
            wp_send_json_success(array('message' => 'Invoice published successfully'));
        } else {
            wp_send_json_error(array('message' => 'Error publishing invoice'));
        }
    }

    /**
     * Set an invoice to draft status
     */
    public function draftInvoice() {
        if (!$this->handleAjaxSecurity($_POST['nonce'])) {
            return;
        }

        // Check invoice ID
        if (!isset($_POST['invoice_id']) || empty($_POST['invoice_id'])) {
            wp_send_json_error(array('message' => 'Invalid invoice ID'));
        }

        $invoice_id = intval($_POST['invoice_id']);

        // Update post status to draft
        $result = wp_update_post(array(
            'ID' => $invoice_id,
            'post_status' => 'draft'
        ));

        if ($result) {
            wp_send_json_success(array('message' => 'Invoice set to draft successfully'));
        } else {
            wp_send_json_error(array('message' => 'Error setting invoice to draft'));
        }
    }

    /**
     * Handle bulk actions
     */
    public function handleBulkActions() {
        // Check if we're processing a bulk action
        if (!isset($_POST['action']) || $_POST['action'] !== 'easy_invoice_bulk_action') {
            return;
        }

        // Check nonce and capability
        $security_check = $this->securityCheck($_POST['easy_invoice_bulk_nonce'], 'easy_invoice_bulk_action');
        if (is_wp_error($security_check)) {
            wp_die($security_check->get_error_message());
        }

        // Check if we have invoice IDs
        if (!isset($_POST['invoice_ids']) || !is_array($_POST['invoice_ids']) || empty($_POST['invoice_ids'])) {
            wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_error=no_selection'));
            exit;
        }

        // Get bulk action and invoice IDs
        $bulk_action = isset($_POST['bulk_action']) ? sanitize_text_field($_POST['bulk_action']) : '';
        $invoice_ids = array_map('intval', $_POST['invoice_ids']);

        // Process based on action
        $processed = 0;

        switch ($bulk_action) {
            case 'trash':
                foreach ($invoice_ids as $id) {
                    if (wp_trash_post($id)) {
                        $processed++;
                    }
                }
                wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_trashed=' . $processed));
                break;

            case 'restore':
                foreach ($invoice_ids as $id) {
                    if (wp_untrash_post($id)) {
                        // Also set status to publish (since WordPress sets it to draft by default)
                        wp_update_post(array(
                            'ID' => $id,
                            'post_status' => 'publish'
                        ));
                        $processed++;
                    }
                }
                wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_restored=' . $processed));
                break;

            case 'delete':
                foreach ($invoice_ids as $id) {
                    if (wp_delete_post($id, true)) {
                        $processed++;
                    }
                }
                wp_redirect(admin_url('admin.php?page=easy-invoice-all&view=trash&bulk_deleted=' . $processed));
                break;

            case 'draft':
                foreach ($invoice_ids as $id) {
                    // Update post status to draft
                    if (wp_update_post(array(
                        'ID' => $id,
                        'post_status' => 'draft'
                    ))) {
                        $processed++;
                    }
                }
                wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_drafted=' . $processed));
                break;

            case 'publish':
                foreach ($invoice_ids as $id) {
                    // Update post status to publish
                    if (wp_update_post(array(
                        'ID' => $id,
                        'post_status' => 'publish'
                    ))) {
                        $processed++;
                    }
                }
                wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_published=' . $processed));
                break;

            default:
                // Includes the `export` action — that's a Pro-only feature handled
                // by the BulkExportSelected extension. When Pro is inactive, the
                // Free-side teaser JS intercepts the submit before the form ever
                // POSTs here. If somehow it does (curl, etc), we redirect cleanly.
                wp_redirect(admin_url('admin.php?page=easy-invoice-all&bulk_error=invalid_action'));
        }

        exit;
    }

    /**
     * Get stats for dashboard
     */
    public function getInvoiceStats() {
        $repository = InvoiceServiceProvider::getInvoiceRepository();
        $total_invoices = count($repository->all());
        $pending_invoices = count($repository->findByStatus('pending'));
        $paid_invoices = count($repository->findByStatus('paid'));

        // Get total revenue by currency from paid invoices
        $revenue_by_currency = [];
        $paid_invoices_list = $repository->findByStatus('paid');

        // First, get all currencies that exist in the system
        $all_invoices = $repository->all();
        $all_currencies = [];

        foreach ($all_invoices as $invoice) {
            $currency_code = $invoice->getCurrencyCode();

            // If currency is empty or "global", get the actual currency that was used
            if (empty($currency_code) || $currency_code === 'global') {
                // Get the actual currency from invoice meta
                $actual_currency = get_post_meta($invoice->getId(), '_easy_invoice_currency_code', true);
                $currency_code = !empty($actual_currency) ? $actual_currency : get_option('easy_invoice_currency_code', 'USD');
            }

            // If currency is still "global", use the global setting
            if ($currency_code === 'global') {
                $currency_code = get_option('easy_invoice_currency_code', 'USD');
            }

            // Normalize currency code to uppercase for consistent grouping
            $currency_code = strtoupper($currency_code);

            if (!empty($currency_code)) {
                $all_currencies[$currency_code] = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
            }
        }

        // Initialize revenue for all currencies found
        foreach ($all_currencies as $currency_code => $currency_symbol) {
            $revenue_by_currency[$currency_code] = [
                'amount' => 0,
                'symbol' => $currency_symbol
            ];
        }

        // Now calculate revenue for paid invoices
        foreach ($paid_invoices_list as $invoice) {
            $invoice_total = $invoice->getTotal();
            if (!is_numeric($invoice_total)) {
                continue;
            }

            // Get the actual currency from the invoice
            $currency_code = $invoice->getCurrencyCode();

            // If currency is empty or "global", get the actual currency that was used
            if (empty($currency_code) || $currency_code === 'global') {
                // Get the actual currency from invoice meta
                $actual_currency = get_post_meta($invoice->getId(), '_easy_invoice_currency_code', true);
                $currency_code = !empty($actual_currency) ? $actual_currency : get_option('easy_invoice_currency_code', 'USD');
            }

            // If currency is still "global", use the global setting
            if ($currency_code === 'global') {
                $currency_code = get_option('easy_invoice_currency_code', 'USD');
            }

            // Normalize currency code to uppercase for consistent grouping
            $currency_code = strtoupper($currency_code);

            if (isset($revenue_by_currency[$currency_code])) {
                $revenue_by_currency[$currency_code]['amount'] += $invoice_total;
            }
        }

        // Calculate total value from ALL invoices (not just paid ones)
        $total_value_by_currency = [];

        // Initialize total value for all currencies found
        foreach ($all_currencies as $currency_code => $currency_symbol) {
            $total_value_by_currency[$currency_code] = [
                'amount' => 0,
                'invoices' => 0,
                'invoice_object' => null // Keep reference for formatting
            ];
        }

        // Calculate total value from all invoices
        foreach ($all_invoices as $invoice) {
            $invoice_total = $invoice->getTotal();
            if (!is_numeric($invoice_total)) {
                continue;
            }

            // Get the actual currency from the invoice
            $currency_code = $invoice->getCurrencyCode();

            // If currency is empty or "global", get the actual currency that was used
            if (empty($currency_code) || $currency_code === 'global') {
                // Get the actual currency from invoice meta
                $actual_currency = get_post_meta($invoice->getId(), '_easy_invoice_currency_code', true);
                $currency_code = !empty($actual_currency) ? $actual_currency : get_option('easy_invoice_currency_code', 'USD');
            }

            // If currency is still "global", use the global setting
            if ($currency_code === 'global') {
                $currency_code = get_option('easy_invoice_currency_code', 'USD');
            }

            // Normalize currency code to uppercase for consistent grouping
            $currency_code = strtoupper($currency_code);

            if (isset($total_value_by_currency[$currency_code])) {
                $total_value_by_currency[$currency_code]['amount'] += $invoice_total;
                $total_value_by_currency[$currency_code]['invoices']++;
                // Keep reference to first invoice for formatting
                if ($total_value_by_currency[$currency_code]['invoice_object'] === null) {
                    $total_value_by_currency[$currency_code]['invoice_object'] = $invoice;
                }
            }
        }

        return [
            'total_invoices' => $total_invoices,
            'pending_invoices' => $pending_invoices,
            'paid_invoices' => $paid_invoices,
            'total_revenue' => $revenue_by_currency,
            'total_value' => $total_value_by_currency
        ];
    }

    /**
     * Register additional AJAX handlers
     */
    public function registerAjaxHandlers() {
        add_action('wp_ajax_easy_invoice_load_template', array($this, 'handleLoadTemplate'));
        add_action('wp_ajax_easy_invoice_create_new_invoice', array($this, 'ajax_create_new_invoice'));
        // The `easy_invoice_search_clients` AJAX is owned by EasyInvoiceAjax.
        // The duplicate registration that used to live here raced with
        // EasyInvoiceAjax::searchClients() — only the first-registered
        // handler ran, and which one won depended on bootstrap order. That
        // intermittently broke the client-search dropdown in the invoice
        // builder. Keep this comment as a tombstone so the registration
        // doesn't get added back.
    }

    /**
     * Handle AJAX request to load invoice template
     */
    public function handleLoadTemplate() {
        // Verify nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'easy_invoice_nonce')) {
            wp_send_json_error(array('message' => __('Security check failed', 'easy-invoice')));
        }

        // Get template name and validate it securely
        $template = isset($_POST['template']) ? sanitize_text_field($_POST['template']) : 'standard';
        $template = $this->validateTemplateName($template, 'invoice');
		$invoice_id = isset($_POST['invoice_id']) ? intval($_POST['invoice_id']) : 0;

        // Get secure template file path
        $template_file = $this->getSecureTemplatePath($template, 'invoice');


        if (!$template_file) {
            wp_send_json_error(array('message' => __('Invalid template', 'easy-invoice')));
        }

        // For new invoices (no ID), just return the template without invoice data
        if ($invoice_id === 0) {
            // Start output buffering
            ob_start();

            // Set up empty variables for new invoices
            $invoice = null;
            $formatter = null;

            include_once $template_file;
            $html = ob_get_clean();

            // Send response
            wp_send_json_success(array('html' => $html));
            return;
        }

        // Get invoice data for existing invoices
        $repository = InvoiceServiceProvider::getInvoiceRepository();
        $invoice = $repository->find($invoice_id);

        if (!$invoice) {
            wp_send_json_error(array('message' => __('Invoice not found', 'easy-invoice')));
        }

        // Initialize formatter for currency formatting
        $formatter = new \EasyInvoice\Helpers\InvoiceFormatter($invoice);

        // Start output buffering
        ob_start();
        include_once $template_file;
        $html = ob_get_clean();

        // Send response
        wp_send_json_success(array('html' => $html));
    }

    /**
     * Validate and sanitize template name to prevent directory traversal attacks
     *
     * @param string $template The template name to validate
     * @param string $type Either 'invoice' or 'quote'
     * @return string Validated template name or 'standard' as fallback
     */
    private function validateTemplateName($template, $type = 'invoice') {
        // Whitelist of allowed template names
        $allowed_templates = array(
            'invoice' => array('classic', 'corporate', 'creative', 'elegant', 'legacy', 'minimal', 'modern', 'professional', 'standard', 'default'),
            'quote' => array('legacy', 'minimal', 'minimalist', 'modern', 'standard', 'default')
        );

        // Strip any directory components using basename
        $template = basename($template);

        // Remove any file extension
        $template = preg_replace('/\.(php|html|htm)$/i', '', $template);

        // Remove any non-alphanumeric characters except hyphens and underscores
        $template = preg_replace('/[^a-z0-9_-]/i', '', $template);

        // Check if template is in whitelist
        if (isset($allowed_templates[$type]) && in_array($template, $allowed_templates[$type], true)) {
            return $template;
        }

        // Return default template if not in whitelist
        return 'standard';
    }

    /**
     * Get secure template file path with directory traversal protection
     *
     * @param string $template The validated template name
     * @param string $type Either 'invoice' or 'quote'
     * @return string|false The secure template file path or false if invalid
     */
    private function getSecureTemplatePath($template, $type = 'invoice') {
        // Define template directories
        $template_dirs = array(
            'invoice' => EASY_INVOICE_PLUGIN_DIR . 'templates/invoice-templates/',
            'quote' => EASY_INVOICE_PLUGIN_DIR . 'templates/quote-templates/'
        );

        if (!isset($template_dirs[$type])) {
            return false;
        }

        $template_dir = $template_dirs[$type];

        // Ensure template directory exists and is a directory
        if (!is_dir($template_dir)) {
            return false;
        }

        // Get the real path of the template directory (resolves any symlinks)
        $real_template_dir = realpath($template_dir);
        if ($real_template_dir === false) {
            return false;
        }

        // Construct the template file path
        $template_file = $real_template_dir . DIRECTORY_SEPARATOR . $template . '.php';

        // Get the real path of the template file (resolves any .. or . components)
        $real_template_file = realpath($template_file);

        // Verify that the resolved path is within the template directory
        // This prevents directory traversal attacks
        if ($real_template_file === false || strpos($real_template_file, $real_template_dir) !== 0) {
            // If template doesn't exist or is outside the directory, use default
            $default_file = $real_template_dir . DIRECTORY_SEPARATOR . 'standard.php';
            $real_default_file = realpath($default_file);

            if ($real_default_file !== false && strpos($real_default_file, $real_template_dir) === 0) {
                return $real_default_file;
            }

            return false;
        }

        // Verify the file exists and is readable
        if (!is_file($real_template_file) || !is_readable($real_template_file)) {
            // Fallback to standard template
            $default_file = $real_template_dir . DIRECTORY_SEPARATOR . 'standard.php';
            $real_default_file = realpath($default_file);

            if ($real_default_file !== false && strpos($real_default_file, $real_template_dir) === 0 && is_file($real_default_file) && is_readable($real_default_file)) {
                return $real_default_file;
            }

            return false;
        }

        return $real_template_file;
    }

    /**
     * Add meta box for manual payment verification to the invoice edit screen.
     */
    public function add_manual_payment_meta_box() {
        add_meta_box(
            'easy_invoice_manual_payment_verification',
            __('Manual Payment Verification', 'easy-invoice'),
            array($this, 'render_manual_payment_meta_box'),
            'easy-invoice', // Post type
            'side',         // Context
            'high'          // Priority
        );
    }

    /**
     * Render the manual payment verification meta box.
     *
     * @param \WP_Post $post The current post object.
     */
    public function render_manual_payment_meta_box(\WP_Post $post) {
        $payment_status = get_post_meta($post->ID, '_payment_status', true);
        $payment_method = get_post_meta($post->ID, '_payment_method', true);

        if (!in_array($payment_status, ['pending-bank', 'pending-cheque'])) {
            echo '<p>' . __('This invoice is not pending manual payment verification.', 'easy-invoice') . '</p>';
            return;
        }

        wp_nonce_field('easy_invoice_mark_paid_' . $post->ID, 'easy_invoice_mark_paid_nonce');

        echo '<h4>' . __('Submitted Payment Proof', 'easy-invoice') . '</h4>';

        if ($payment_method === 'bank') {
            $transaction_id = get_post_meta($post->ID, '_bank_transaction_id', true);
            $notes = get_post_meta($post->ID, '_bank_payment_notes', true);
            $proof_url = get_post_meta($post->ID, '_bank_payment_proof', true);

            echo '<p><strong>' . __('Transaction ID:', 'easy-invoice') . '</strong> ' . esc_html($transaction_id) . '</p>';
            if ($notes) {
                echo '<p><strong>' . __('Notes:', 'easy-invoice') . '</strong></p>';
                echo '<div style="white-space: pre-wrap; background: #f9f9f9; padding: 5px; border: 1px solid #eee;">' . esc_html($notes) . '</div>';
            }
            if ($proof_url) {
                echo '<p><strong>' . __('Proof Document:', 'easy-invoice') . '</strong> <a href="' . esc_url($proof_url) . '" target="_blank" rel="noopener noreferrer">' . __('View Proof', 'easy-invoice') . '</a></p>';
            }
        } elseif ($payment_method === 'cheque') {
            $cheque_number = get_post_meta($post->ID, '_cheque_number', true);
            $bank_name = get_post_meta($post->ID, '_cheque_bank_name', true);
            $cheque_date = get_post_meta($post->ID, '_cheque_date', true);
            $notes = get_post_meta($post->ID, '_cheque_notes', true);
            $image_url = get_post_meta($post->ID, '_cheque_image', true);

            echo '<p><strong>' . __('Cheque Number:', 'easy-invoice') . '</strong> ' . esc_html($cheque_number) . '</p>';
            if ($bank_name) echo '<p><strong>' . __('Bank Name:', 'easy-invoice') . '</strong> ' . esc_html($bank_name) . '</p>';
            if ($cheque_date) echo '<p><strong>' . __('Cheque Date:', 'easy-invoice') . '</strong> ' . esc_html($cheque_date) . '</p>';
            if ($notes) {
                echo '<p><strong>' . __('Notes:', 'easy-invoice') . '</strong></p>';
                echo '<div style="white-space: pre-wrap; background: #f9f9f9; padding: 5px; border: 1px solid #eee;">' . esc_html($notes) . '</div>';
            }
            if ($image_url) {
                echo '<p><strong>' . __('Cheque Image:', 'easy-invoice') . '</strong> <a href="' . esc_url($image_url) . '" target="_blank" rel="noopener noreferrer">' . __('View Image', 'easy-invoice') . '</a></p>';
            }
        }

        echo '<p style="margin-top: 15px;">';
        echo '<button type="button" id="easy-invoice-mark-paid-btn" class="button button-primary" data-invoice-id="' . esc_attr($post->ID) . '">' . __('Mark as Paid', 'easy-invoice') . '</button>';
        echo '</p>';
        echo '<div id="easy-invoice-mark-paid-message" style="margin-top:10px;"></div>';

        // Add a script for the AJAX call
        ?>
        <script type="text/javascript">
            jQuery(document).ready(function($) {
                $('#easy-invoice-mark-paid-btn').on('click', function() {
                    var invoiceId = $(this).data('invoice-id');
                    var nonce = $('#easy_invoice_mark_paid_nonce').val();
                    var button = $(this);
                    var messageDiv = $('#easy-invoice-mark-paid-message');

                    button.prop('disabled', true);
                    messageDiv.html('Processing...');

                    $.ajax({
                        url: ajaxurl, // WordPress AJAX URL
                        type: 'POST',
                        data: {
                            action: 'easy_invoice_mark_paid',
                            invoice_id: invoiceId,
                            nonce: nonce
                        },
                        success: function(response) {
                            if (response.success) {
                                messageDiv.css('color', 'green').html(response.data.message);
                                button.hide();
                                // Optionally, reload the page or update UI elements to reflect paid status
                                // window.location.reload();
                            } else {
                                messageDiv.css('color', 'red').html(response.data.message);
                                button.prop('disabled', false);
                            }
                        },
                        error: function() {
                            messageDiv.css('color', 'red').html('<?php echo esc_js(__("An error occurred. Please try again.", "easy-invoice")); ?>');
                            button.prop('disabled', false);
                        }
                    });
                });
            });
        </script>
        <?php
    }

    /**
     * AJAX handler to create a sample invoice.
     */
    public function ajax_create_sample_invoice() {
        // Security check: verify nonce
        check_ajax_referer('easy_invoice_admin_nonce', 'nonce');

        // Security check: verify user capabilities
        if (!easy_invoice_user_can('ei_create_invoice')) {
            wp_send_json_error([
                'message' => __('You do not have permission to create invoices.', 'easy-invoice')
            ], 403);
            return;
        }

        try {
            $invoice_repository = InvoiceServiceProvider::getInvoiceRepository();

            // Sample Invoice Data
            $sample_invoice_data = [
                'post_title' => 'Sample Invoice - ' . date('Y-m-d H:i'),
                'post_status' => 'draft', // Or 'publish' if you want it live immediately
                // Add other WP_Post fields as needed (e.g., post_author)
            ];

            // Sample Meta Data
            $sample_meta_data = [
                '_easy_invoice_number' => 'SAMPLE-' . time(),
                '_easy_invoice_issue_date' => date('Y-m-d'),
                '_easy_invoice_due_date' => date('Y-m-d', strtotime('+15 days')),
                '_easy_invoice_status' => 'draft',
                '_easy_invoice_customer_name' => 'John Doe (Sample Client)',
                '_easy_invoice_customer_email' => 'customer@example.com',
                '_easy_invoice_customer_address' => "123 Sample Street\nSampleville, ST 12345",
                'currency_code' => 'USD',
                'currency_position' => 'before',
                // Add other meta keys as needed
            ];

            // Sample Line Items
            $sample_items = [];
            for ($i = 1; $i <= 3; $i++) {
                $sample_items[] = [
                    'name' => 'Sample Service ' . $i,
                    'description' => 'Detailed description of sample service ' . $i . '.',
                    'quantity' => rand(1, 5),
                    'price' => rand(50, 200) * 1.00,
                    // 'taxable' => true/false (optional)
                ];
            }
            $sample_meta_data['_easy_invoice_items'] = $sample_items;

            // Create the invoice post
            $invoice_id = wp_insert_post($sample_invoice_data, true); // true for WP_Error on failure

            if (is_wp_error($invoice_id)) {
                throw new \Exception('Failed to create invoice post: ' . $invoice_id->get_error_message());
            }

            // Set invoice meta data
            foreach ($sample_meta_data as $key => $value) {
                update_post_meta($invoice_id, $key, $value);
            }

            // Recalculate totals if your Invoice model or repository has a method for it
            // For example, if you have $invoice->calculateTotals()->save(); or similar.
            wp_send_json_success([
                'message' => __('Sample invoice created successfully!', 'easy-invoice'),
                'invoice_id' => $invoice_id,
                'edit_link' => admin_url('admin.php?page=easy-invoice-builder&id=' . $invoice_id)
            ]);

        } catch (\Exception $e) {
            wp_send_json_error([
                'message' => __('Error creating sample invoice:', 'easy-invoice') . ' ' . $e->getMessage()
            ], 500);
        }
    }

    /**
     * AJAX handler for creating a new invoice with title
     */
    public function ajax_create_new_invoice() {
        // Security check: verify nonce
        check_ajax_referer('easy_invoice_nonce', 'nonce');

        // Security check: verify user capabilities
        if (!easy_invoice_user_can('ei_create_invoice')) {
            wp_send_json_error([
                'message' => __('You do not have permission to create invoices.', 'easy-invoice')
            ], 403);
            return;
        }

        // Get the invoice title
        $title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : '';

        if (empty($title)) {
            wp_send_json_error([
                'message' => __('Invoice title is required.', 'easy-invoice')
            ], 400);
            return;
        }

        try {
            $invoice_repository = InvoiceServiceProvider::getInvoiceRepository();

            // Prepare invoice data for repository
            $invoice_data = [
                'title' => $title,
                'post_status' => 'draft',
                'issue_date' => date('Y-m-d'),
                'due_date' => date('Y-m-d', strtotime('+30 days')),
                'status' => 'draft',
                'invoice_template' => get_option('easy_invoice_last_invoice_template', 'standard')
            ];

			// Create the invoice using repository (this will auto-generate invoice number)
            $invoice = $invoice_repository->create($invoice_data);

            if (!$invoice) {
                throw new \Exception('Failed to create invoice');
            }

            // Debug: Check if invoice number was set
            $invoice_number = $invoice->getNumber();
            if (empty($invoice_number)) {
                // Force set the invoice number if it's empty
                $invoice_number_service = easy_invoice_get_invoice_number_service();
                $generated_number = $invoice_number_service->generateUniqueNumber();
                $invoice->setNumber($generated_number);
                $invoice->save();
            }

            wp_send_json_success([
                'message' => __('Invoice created successfully!', 'easy-invoice'),
                'invoice_id' => $invoice->getId(),
                'invoice_number' => $invoice->getNumber(),
                'redirect_url' => admin_url('admin.php?page=easy-invoice-builder&invoice_id=' . $invoice->getId())
            ]);

        } catch (\Exception $e) {
            wp_send_json_error([
                'message' => __('Error creating invoice:', 'easy-invoice') . ' ' . $e->getMessage()
            ], 500);
        }
    }

}

```
