# easy-invoice/2.4.1/includes/Services/EmailManager.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.1. 2,153 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.1/code/includes/Services/EmailManager.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.1/raw/includes/Services/EmailManager.php
- Modified: 2026-09-15T12:31:20+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.4.1/code/includes/Services/EmailManager.php#L10-L20`.

```php
<?php
/**
 * Email Manager Service
 *
 * Handles all email functionality for Easy Invoice
 *
 * @package     EasyInvoice
 * @subpackage  Services
 * @since       1.0.0
 */

namespace EasyInvoice\Services;

use EasyInvoice\Models\Invoice;
use EasyInvoice\Models\Quote;
use EasyInvoice\Models\Client;

if (!defined('ABSPATH')) {
    exit;
}

/**
 * EmailManager Class
 *
 * Centralized email management for Easy Invoice
 */
class EmailManager extends BaseService {
    
    /**
     * Singleton instance
     *
     * @var EmailManager|null
     */
    private static $instance = null;
    
    /**
     * Service name
     *
     * @var string
     */
    protected $service_name = 'email_manager';
    
    /**
     * Email templates
     *
     * @var array
     */
    private $templates = [];
    
    /**
     * Email settings
     *
     * @var array
     */
    private $settings = [];
    
    /**
     * Get singleton instance
     *
     * @return EmailManager
     */
    public static function getInstance(): EmailManager {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    /**
     * Constructor
     */
    public function __construct() {
        parent::__construct();
        $this->loadSettings();
        $this->loadTemplates();
        $this->initHooks();
    }
    
    /**
     * Initialize hooks
     */
    private function initHooks(): void {
        // Invoice/quote send-email AJAX is handled exclusively by EasyInvoice\Admin\EasyInvoiceAjax
        // (published-document checks, single handler) to avoid duplicate nopriv callbacks.

        // Add email settings to admin
        add_action('admin_init', [$this, 'registerEmailSettings']);
        add_action('admin_init', [__CLASS__, 'refreshStockTemplates']);
        
        // Refresh settings when they're updated
        add_action('update_option_easy_invoice_email_from_name', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_email_from_address', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_email_reply_to', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_email_reply_to_name', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_enable_email_styling', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_email_logo', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_email_footer_text', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_bcc_admin', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_admin_email', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_email_subject', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_email_body', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_quote_subject', [$this, 'refreshSettings']);
        add_action('update_option_easy_invoice_quote_body', [$this, 'refreshSettings']);
        
        // Add email logs
        add_action('easy_invoice_email_sent', [$this, 'logEmailSent'], 10, 3);
        add_action('easy_invoice_email_failed', [$this, 'logEmailFailed'], 10, 3);
        
        // Listen for payment completion to send admin notifications
        add_action('easy_invoice_payment_completed', [$this, 'handlePaymentCompleted'], 10, 3);
        // An instalment gets a receipt as well; the invoice just is not settled yet.
        add_action('easy_invoice_payment_received', [$this, 'handlePaymentCompleted'], 10, 3);

        // A client has submitted a manual payment (bank transfer, cheque,
        // cash, with or without proof) that now waits for verification. The
        // only listener used to live in an admin class nothing instantiates,
        // so the admin was never told.
        add_action('easy_invoice_manual_payment_submitted', [$this, 'handleManualPaymentSubmitted'], 10, 2);
    }
    
    /**
     * Load email settings
     */
    private function loadSettings(): void {
        $this->settings = [
            'from_name' => get_option('easy_invoice_email_from_name', get_bloginfo('name')),
            'from_email' => get_option('easy_invoice_email_from_address', get_bloginfo('admin_email')),
            'reply_to_email' => get_option('easy_invoice_email_reply_to', ''),
            'reply_to_name' => get_option('easy_invoice_email_reply_to_name', ''),
            'enable_html' => get_option('easy_invoice_enable_email_styling', 'yes'),
            'email_logo' => get_option('easy_invoice_email_logo', ''),
            'footer_text' => get_option('easy_invoice_email_footer_text', ''),
            'bcc_admin' => get_option('easy_invoice_bcc_admin', 'no'),
            'admin_email' => get_option('easy_invoice_admin_email', get_option('admin_email')),
        ];
    }
    
    /**
     * Load email templates
     */
    private function loadTemplates(): void {
        $this->templates = [
            'invoice_new' => [
                'enabled' => get_option('easy_invoice_invoice_email_enabled', 'yes') === 'yes',
                'subject' => get_option('easy_invoice_invoice_email_subject', __('Your Invoice #{{invoice_number}} from {{company_name}}', 'easy-invoice')),
                'body' => get_option('easy_invoice_invoice_email_body', $this->getDefaultInvoiceTemplate()),
                'type' => 'invoice'
            ],
            'invoice_reminder' => [
                'enabled' => get_option('easy_invoice_invoice_email_enabled', 'yes') === 'yes',
                'subject' => get_option('easy_invoice_reminder_subject', 'Payment Reminder - Invoice #{{invoice_number}}'),
                'body' => get_option('easy_invoice_reminder_body', $this->getDefaultReminderTemplate()),
                'type' => 'invoice'
            ],
            'invoice_paid' => [
                'enabled' => get_option('easy_invoice_payment_email_enabled', 'yes') === 'yes',
                'subject' => get_option('easy_invoice_payment_email_subject', __('Payment Received - Invoice #{{invoice_number}}', 'easy-invoice')),
                'body' => get_option('easy_invoice_payment_email_body', $this->getDefaultPaymentTemplate()),
                'type' => 'invoice'
            ],
            'quote_new' => [
                'enabled' => get_option('easy_invoice_quote_email_enabled', 'yes') === 'yes',
                'subject' => get_option('easy_invoice_quote_email_subject', __('Your Quote #{{quote_number}} from {{company_name}}', 'easy-invoice')),
                'body' => get_option('easy_invoice_quote_email_body', $this->getDefaultQuoteTemplate()),
                'type' => 'quote'
            ],
            'quote_accepted' => [
                'enabled' => get_option('easy_invoice_quote_email_enabled', 'yes') === 'yes',
                'subject' => get_option('easy_invoice_quote_accepted_subject', 'Quote Accepted - #{{quote_number}}'),
                'body' => get_option('easy_invoice_quote_accepted_body', $this->getDefaultQuoteAcceptedTemplate()),
                'type' => 'quote'
            ],
            'quote_declined' => [
                'enabled' => get_option('easy_invoice_quote_email_enabled', 'yes') === 'yes',
                'subject' => get_option('easy_invoice_quote_declined_subject', 'Quote Declined - #{{quote_number}}'),
                'body' => get_option('easy_invoice_quote_declined_body', $this->getDefaultQuoteDeclinedTemplate()),
                'type' => 'quote'
            ]
        ];
    }
    
    /**
     * Send invoice email
     *
     * @param Invoice $invoice The invoice
     * @param string $template_type Template type (new, reminder, paid)
     * @param array $additional_data Additional data for template
     * @return array Result array with success status and message
     */
    public function sendInvoiceEmail(Invoice $invoice, string $template_type = 'new', array $additional_data = []): array {
        try {
            // Validate invoice
            if (!$invoice || !$invoice->getId()) {
                return ['success' => false, 'message' => __('Invalid invoice', 'easy-invoice')];
            }
            
            // Get client email
            $client_email = $invoice->getCustomerEmail();
            if (empty($client_email)) {
                return ['success' => false, 'message' => __('Client email is missing', 'easy-invoice')];
            }
            
            // Get template
            $template_key = 'invoice_' . $template_type;
            if (!isset($this->templates[$template_key])) {
                return ['success' => false, 'message' => __('Email template not found', 'easy-invoice')];
            }
            
            $template = $this->templates[$template_key];

            /**
             * Filter an email template before subject and body are built.
             *
             * Runs for invoice, quote and payment emails, so a listener can
             * switch locale for the client or swap the template wholesale.
             * `easy_invoice_email_finished` fires once the send is over.
             *
             * @param array  $template     subject, body, enabled.
             * @param string $template_key invoice_new, quote_reminder, invoice_paid…
             * @param object $document     Invoice or Quote model.
             */
            $template = (array) apply_filters('easy_invoice_email_template_data', $template, $template_key, $invoice);
            
            // Check if email is enabled
            if (!$template['enabled']) {
                return ['success' => false, 'message' => __('Invoice available email is disabled', 'easy-invoice')];
            }
            
            // Prepare email data
            $email_data = $this->prepareInvoiceEmailData($invoice, $template, $additional_data);
            
            // Send email
            // Attach the invoice as a PDF, when the site has asked for it.
            //
            // This is the capability the browser-based renderer could never provide:
            // wp_mail() needs a file on disk, and until PdfRenderer existed the server
            // never held the document. Off by default so an upgrade does not silently
            // change what customers receive.
            $attachments = [];
            $attached_path = '';
            if ($this->shouldAttachInvoicePdf()) {
                $rendered = \EasyInvoice\Services\PdfRenderer::renderToFile($invoice, 'invoice');
                if (is_wp_error($rendered)) {
                    // A failed attachment must never stop the invoice being sent.
                    error_log('Easy Invoice: could not attach invoice PDF — ' . $rendered->get_error_message());
                } else {
                    $attached_path = $rendered;
                    $attachments[] = $rendered;
                }
            }

            /**
             * Filter the files sent with an invoice email.
             *
             * @param array  $attachments Paths.
             * @param object $invoice     Invoice model.
             * @param string $type        'invoice'.
             */
            $attachments = (array) apply_filters( 'easy_invoice_email_attachments', $attachments, $invoice, 'invoice' );
            $email_data['message'] = $this->attachmentWording($email_data['message'], !empty($attachments));

            $sent = $this->sendEmail(
                $email_data['to'],
                $email_data['subject'],
                $email_data['message'],
                $email_data['headers'],
                $attachments
            );

            // The rendered PDF lives in the system temp directory; remove it once
            // wp_mail() has handed it to the transport.
            if ($attached_path !== '' && file_exists($attached_path)) {
                wp_delete_file($attached_path);
            }
            
            /**
             * Fires once an email send has finished, whether or not it went out.
             *
             * @param object $document     Invoice or Quote model.
             * @param string $template_key Template key.
             * @param bool   $sent         Whether wp_mail() accepted it.
             */
            do_action('easy_invoice_email_finished', $invoice, $template_key, (bool) $sent);
            
            if ($sent) {
                // Log success
                do_action('easy_invoice_email_sent', $invoice, $client_email, $template_type);

                // Emailing a draft issues it: from here on it is a document
                // the client holds, so it reads Available, is chased by
                // reminders and is corrected by credit note, not by editing.
                if ('new' === $template_type && 'draft' === strtolower((string) $invoice->getStatus())) {
                    $invoice->setStatus('available');
                    $invoice->save();
                }
                
                return [
                    'success' => true, 
                    'message' => __('Email sent successfully', 'easy-invoice'),
                    'email_data' => $email_data
                ];
            } else {
                // Log failure
                do_action('easy_invoice_email_failed', $invoice, $client_email, $template_type);
                
                return ['success' => false, 'message' => __('Failed to send email', 'easy-invoice')];
            }
            
        } catch (\Exception $e) {
            $this->log('Email sending error: ' . $e->getMessage(), 'error');
            return ['success' => false, 'message' => __('Error sending email: ', 'easy-invoice') . $e->getMessage()];
        }
    }
    
    /**
     * Send quote email
     *
     * @param Quote $quote The quote
     * @param string $template_type Template type (new, accepted, declined)
     * @param array $additional_data Additional data for template
     * @return array Result array with success status and message
     */
    public function sendQuoteEmail(Quote $quote, string $template_type = 'new', array $additional_data = []): array {
        try {
            // Validate quote
            if (!$quote || !$quote->getId()) {
                return ['success' => false, 'message' => __('Invalid quote', 'easy-invoice')];
            }
            
            // Get client email
            $client_email = $quote->getCustomerEmail();
            if (empty($client_email)) {
                return ['success' => false, 'message' => __('Client email is missing', 'easy-invoice')];
            }
            
            // Get template
            $template_key = 'quote_' . $template_type;
            if (!isset($this->templates[$template_key])) {
                return ['success' => false, 'message' => __('Email template not found', 'easy-invoice')];
            }
            
            $template = $this->templates[$template_key];
            /** This filter is documented above in sendInvoiceEmail(). */
            $template = (array) apply_filters('easy_invoice_email_template_data', $template, $template_key, $quote);
            
            // Check if email is enabled
            if (!$template['enabled']) {
                return ['success' => false, 'message' => __('Quote available email is disabled', 'easy-invoice')];
            }
            
            // Prepare email data
            $email_data = $this->prepareQuoteEmailData($quote, $template, $additional_data);

            // Same setting as invoices: a PDF copy goes with the quote when asked for.
            $attachments   = [];
            $attached_path = '';
            if ($this->shouldAttachInvoicePdf()) {
                $rendered = \EasyInvoice\Services\PdfRenderer::renderToFile($quote, 'quote');
                if (is_wp_error($rendered)) {
                    error_log('Easy Invoice: could not attach quote PDF — ' . $rendered->get_error_message());
                } else {
                    $attached_path = $rendered;
                    $attachments[] = $rendered;
                }
            }
            $attachments = (array) apply_filters( 'easy_invoice_email_attachments', $attachments, $quote, 'quote' );
            $email_data['message'] = $this->attachmentWording($email_data['message'], !empty($attachments));
            
            // Send email
            $sent = $this->sendEmail(
                $email_data['to'],
                $email_data['subject'],
                $email_data['message'],
                $email_data['headers'],
                $attachments
            );
            if ($attached_path !== '' && file_exists($attached_path)) {
                wp_delete_file($attached_path);
            }
            
            /**
             * Fires once an email send has finished, whether or not it went out.
             *
             * @param object $document     Invoice or Quote model.
             * @param string $template_key Template key.
             * @param bool   $sent         Whether wp_mail() accepted it.
             */
            do_action('easy_invoice_email_finished', $quote, $template_key, (bool) $sent);
            
            if ($sent) {
                // Log success
                do_action('easy_invoice_quote_email_sent', $quote, $client_email, $template_type);

                // A quote that has been emailed is "sent".
                if ('new' === $template_type && in_array(strtolower((string) $quote->getStatus()), ['draft', 'available'], true)) {
                    $quote->setStatus('sent');
                    $quote->save();
                }
                
                return [
                    'success' => true, 
                    'message' => __('Quote email sent successfully', 'easy-invoice'),
                    'email_data' => $email_data
                ];
            } else {
                // Log failure
                do_action('easy_invoice_quote_email_failed', $quote, $client_email, $template_type);
                
                return ['success' => false, 'message' => __('Failed to send quote email', 'easy-invoice')];
            }
            
        } catch (\Exception $e) {
            $this->log('Quote email sending error: ' . $e->getMessage(), 'error');
            return ['success' => false, 'message' => __('Error sending quote email: ', 'easy-invoice') . $e->getMessage()];
        }
    }
    
    /**
     * Prepare invoice email data
     *
     * @param Invoice $invoice The invoice
     * @param array $template The email template
     * @param array $additional_data Additional data
     * @return array Email data
     */
    private function prepareInvoiceEmailData(Invoice $invoice, array $template, array $additional_data = []): array {
        // Get replacements — the receipt needs the payment placeholders too.
        $replacements = !empty($additional_data['payment_receipt'])
            ? $this->getPaymentReplacements($invoice, $additional_data)
            : $this->getInvoiceReplacements($invoice, $additional_data);
        
        // Process template
        $subject = $this->processTemplate($template['subject'], $replacements);
        $message = $this->processTemplate($template['body'], $replacements);
        $message = do_shortcode($message); // Render shortcodes like [easy_invoice_url ...]
        
        // Add HTML wrapper if enabled
        if ($this->settings['enable_html'] === 'yes') {
            $message = $this->wrapInHtmlTemplate($message);
        }
        
        // Prepare headers
        $headers = $this->prepareEmailHeaders('invoice', $invoice);
        
        // Add BCC to admin if enabled
        if ($this->settings['bcc_admin'] === 'yes' && !empty($this->settings['admin_email'])) {
            $headers[] = 'Bcc: ' . $this->settings['admin_email'];
        }
        
        return [
            'to' => $invoice->getCustomerEmail(),
            'subject' => $subject,
            'message' => $message,
            'headers' => $headers
        ];
    }
    
    /**
     * Prepare quote email data
     *
     * @param Quote $quote The quote
     * @param array $template The email template
     * @param array $additional_data Additional data
     * @return array Email data
     */
    private function prepareQuoteEmailData(Quote $quote, array $template, array $additional_data = []): array {
        // Get replacements
        $replacements = $this->getQuoteReplacements($quote, $additional_data);
        
        // Process template
        $subject = $this->processTemplate($template['subject'], $replacements);
        $message = $this->processTemplate($template['body'], $replacements);
        
        // Add HTML wrapper if enabled
        if ($this->settings['enable_html'] === 'yes') {
            $message = $this->wrapInHtmlTemplate($message);
        }
        
        // Prepare headers
        $headers = $this->prepareEmailHeaders('quote', $quote);
        
        // Add BCC to admin if enabled
        if ($this->settings['bcc_admin'] === 'yes' && !empty($this->settings['admin_email'])) {
            $headers[] = 'Bcc: ' . $this->settings['admin_email'];
        }
        
        return [
            'to' => $quote->getCustomerEmail(),
            'subject' => $subject,
            'message' => $message,
            'headers' => $headers
        ];
    }
    
    /**
     * Prepare payment email data
     *
     * @param Invoice $invoice The invoice
     * @param array $template The email template
     * @param array $payment_data Payment data
     * @return array Email data
     */
    private function preparePaymentEmailData(Invoice $invoice, array $template, array $payment_data = []): array {
        // Get replacements
        $replacements = $this->getPaymentReplacements($invoice, $payment_data);
        
        // Process template
        $subject = $this->processTemplate($template['subject'], $replacements);
        $message = $this->processTemplate($template['body'], $replacements);
        
        // Add HTML wrapper if enabled
        if ($this->settings['enable_html'] === 'yes') {
            $message = $this->wrapInHtmlTemplate($message);
        }
        
        // Prepare headers
        $headers = $this->prepareEmailHeaders('receipt', $invoice);
        
        // Add BCC to admin if enabled
        if ($this->settings['bcc_admin'] === 'yes' && !empty($this->settings['admin_email'])) {
            $headers[] = 'Bcc: ' . $this->settings['admin_email'];
        }
        
        return [
            'to' => $invoice->getCustomerEmail(),
            'subject' => $subject,
            'message' => $message,
            'headers' => $headers
        ];
    }
    
    /**
     * Get invoice replacements
     *
     * @param Invoice $invoice The invoice
     * @param array $additional_data Additional data
     * @return array Replacements
     */
    private function getInvoiceReplacements(Invoice $invoice, array $additional_data = []): array {
        $currency_symbol = get_option('easy_invoice_currency_symbol', '$');
        
        // Secure link support
        $invoice_url = get_permalink($invoice->getId());
        $secure_links_enabled = get_option('easy_invoice_pro_enable_secure_links', 'no') === 'yes';
        if ($secure_links_enabled && class_exists('\EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController')) {
            $secure_url = \EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController::getInvoiceSecureLinkUrl($invoice->getId());
            if ($secure_url) {
                $invoice_url = $secure_url;
            }
        }

        // SECURITY: attach a per-invoice access token to the outbound URL
        // so the legitimate email recipient can submit manual payments
        // without needing to log in. The token is verified server-side in
        // PaymentController::submitManualPayment via
        // InvoiceController::canSubmitPaymentForInvoice. Empty-token
        // guard so a CSPRNG failure doesn't produce malformed `?ik=` URLs.
        $invoice_access_token = \EasyInvoice\Controllers\InvoiceController::invoiceAccessToken((int) $invoice->getId());
        if ($invoice_access_token !== '' && $invoice_url) {
            $invoice_url = add_query_arg('ik', $invoice_access_token, $invoice_url);
            // The recipient now holds a keyed link; the bare one may close (TemplateLoader::isLegacyOpenDocument).
            \EasyInvoice\TemplateLoader::markKeyedLinkSent((int) $invoice->getId());
        }

        // Get client data for additional fields
        $client = null;
        if ($invoice->getClientId()) {
            $client_repository = new \EasyInvoice\Repositories\ClientRepository();
            $client = $client_repository->find($invoice->getClientId());
        }
        
        return array_merge([
            '{{invoice_number}}' => $invoice->getNumber(),
            '{{invoice_title}}' => $invoice->getTitle(),
            '{{client_name}}' => $invoice->getCustomerName(),
            '{{client_email}}' => $invoice->getCustomerEmail(),
            '{{client_address}}' => $invoice->getCustomerAddress(),
            '{{client_first_name}}' => $client ? $client->getFirstName() : '',
            '{{client_last_name}}' => $client ? $client->getLastName() : '',
            '{{company_name}}' => get_option('easy_invoice_company_name') ?: get_bloginfo('name'),
            '{{company_email}}' => $this->settings['from_email'],
            '{{company_phone}}' => get_option('easy_invoice_company_phone', ''),
            '{{company_address}}' => get_option('easy_invoice_company_address', ''),
            '{{company_website}}' => get_option('easy_invoice_company_website', ''),
            '{{total_amount}}' => (new \EasyInvoice\Helpers\InvoiceFormatter($invoice))->format($invoice->getTotal()),
            '{{amount_due}}' => (new \EasyInvoice\Helpers\InvoiceFormatter($invoice))->format(\EasyInvoice\Services\InvoiceBalance::due($invoice)),
            '{{subtotal}}' => (new \EasyInvoice\Helpers\InvoiceFormatter($invoice))->format($invoice->getSubtotal()),
            '{{tax_amount}}' => (new \EasyInvoice\Helpers\InvoiceFormatter($invoice))->format($invoice->getTaxAmount()),
            '{{discount_amount}}' => (new \EasyInvoice\Helpers\InvoiceFormatter($invoice))->format($invoice->getDiscountAmount()),
            '{{due_date}}' => gmdate('F j, Y', strtotime($invoice->getDueDate())),
            '{{issue_date}}' => gmdate('F j, Y', strtotime($invoice->getIssueDate())),
            '{{invoice_url}}' => $invoice_url, // Use correct (possibly secure) link
            '{{payment_url}}' => add_query_arg('payment', '1', get_permalink($invoice->getId())),
            '{{site_url}}' => get_site_url(),
            '{{admin_url}}' => admin_url(),
            '{{payment_terms}}' => get_option('easy_invoice_payment_terms', __('Due on receipt', 'easy-invoice')),
        ], self::placeholderKeysOnly($additional_data));
    }

    /**
     * Keep only entries shaped like placeholders. Callers pass raw payment
     * data ('amount', 'date', 'payment_method') alongside; merged as-is those
     * became replacements of the bare words, turning "{{payment_amount}}"
     * into "{{payment_40}}" and every "date" in the text into a date.
     *
     * @param array $data Mixed data.
     * @return array<string,string>
     */
    private static function placeholderKeysOnly(array $data): array {
        $out = [];
        foreach ($data as $key => $value) {
            if (is_string($key) && 0 === strpos($key, '{{') && is_scalar($value)) {
                $out[$key] = (string) $value;
            }
        }
        return $out;
    }
    
    /**
     * Get quote replacements
     *
     * @param Quote $quote The quote
     * @param array $additional_data Additional data
     * @return array Replacements
     */
    private function getQuoteReplacements(Quote $quote, array $additional_data = []): array {
        $currency_symbol = get_option('easy_invoice_currency_symbol', '$');
        
        $quote_url = get_permalink($quote->getId());
        $secure_links_enabled = get_option('easy_invoice_pro_enable_secure_links', 'no') === 'yes';
        if ($secure_links_enabled && class_exists('\EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController')) {
            $secure_url = \EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController::getQuoteSecureLinkUrl($quote->getId());
            if ($secure_url) {
                $quote_url = $secure_url;
            }
        }

        // SECURITY (CVE-2026-9021): attach the per-quote access token so
        // the emailed recipient lands on a page that renders the
        // Accept/Decline UI and can submit either action without
        // authenticating. Without the token the public single-quote page
        // is read-only (no buttons, no nonce in DOM). Lazily generates
        // the token on first send. The query parameter name is
        // intentionally short ('qk') and opaque — leaking it via referer
        // headers is no worse than leaking the secure-link signature.
        $quote_access_token = \EasyInvoice\Controllers\QuoteController::quoteAccessToken((int) $quote->getId());
        if ($quote_access_token !== '' && $quote_url) {
            $quote_url = add_query_arg('qk', $quote_access_token, $quote_url);
            \EasyInvoice\TemplateLoader::markKeyedLinkSent((int) $quote->getId());
        }
        
        // Get client data for additional fields
        $client = null;
        if ($quote->getClientId()) {
            $client_repository = new \EasyInvoice\Repositories\ClientRepository();
            $client = $client_repository->find($quote->getClientId());
        }
        
        return array_merge([
            '{{quote_number}}' => $quote->getNumber(),
            '{{quote_title}}' => $quote->getTitle(),
            '{{client_name}}' => $quote->getCustomerName(),
            '{{client_email}}' => $quote->getCustomerEmail(),
            '{{client_address}}' => $quote->getCustomerAddress(),
            '{{client_first_name}}' => $client ? $client->getFirstName() : '',
            '{{client_last_name}}' => $client ? $client->getLastName() : '',
            '{{company_name}}' => get_option('easy_invoice_company_name') ?: get_bloginfo('name'),
            '{{company_email}}' => $this->settings['from_email'],
            '{{company_phone}}' => get_option('easy_invoice_company_phone', ''),
            '{{company_address}}' => get_option('easy_invoice_company_address', ''),
            '{{company_website}}' => get_option('easy_invoice_company_website', ''),
            '{{total_amount}}' => (new \EasyInvoice\Helpers\QuoteFormatter($quote))->format($quote->getTotal()),
            '{{subtotal}}' => (new \EasyInvoice\Helpers\QuoteFormatter($quote))->format($quote->getSubtotal()),
            '{{tax_amount}}' => (new \EasyInvoice\Helpers\QuoteFormatter($quote))->format($quote->getTaxAmount()),
            '{{discount_amount}}' => (new \EasyInvoice\Helpers\QuoteFormatter($quote))->format($quote->getDiscountAmount()),
            '{{expiry_date}}' => gmdate('F j, Y', strtotime($quote->getExpiryDate())),
            '{{issue_date}}' => gmdate('F j, Y', strtotime($quote->getIssueDate())),
            '{{quote_url}}' => $quote_url,
            '{{site_url}}' => get_site_url(),
            '{{admin_url}}' => admin_url(),
            '{{payment_terms}}' => get_option('easy_invoice_payment_terms', __('Due on receipt', 'easy-invoice')),
        ], self::placeholderKeysOnly($additional_data));
    }
    
    /**
     * Get payment replacements
     *
     * @param Invoice $invoice The invoice
     * @param array $payment_data Payment data
     * @return array Replacements
     */
    private function getPaymentReplacements(Invoice $invoice, array $payment_data = []): array {
        $replacements = $this->getInvoiceReplacements($invoice, $payment_data);
        
        // Add payment-specific replacements
        $formatter = new \EasyInvoice\Helpers\InvoiceFormatter($invoice);
        $replacements['{{payment_amount}}'] = isset($payment_data['amount']) ? $formatter->format((float) $payment_data['amount']) : $formatter->format($invoice->getTotal());
        $paid_on = !empty($payment_data['date']) ? strtotime((string) $payment_data['date']) : false;
        $replacements['{{payment_date}}'] = date_i18n(get_option('date_format'), $paid_on ?: current_time('timestamp'));
        // Callers pass the gateway id as payment_method (some as method); show its label.
        $method_key = (string) ($payment_data['payment_method'] ?? $payment_data['method'] ?? '');
        $replacements['{{payment_method}}'] = '' !== $method_key ? $this->getPaymentMethodLabel($method_key) : __('Online Payment', 'easy-invoice');
        $replacements['{{transaction_id}}'] = !empty($payment_data['transaction_id']) ? (string) $payment_data['transaction_id'] : __('N/A', 'easy-invoice');
        $replacements['{{acceptance_date}}'] = isset($payment_data['acceptance_date']) ? $payment_data['acceptance_date'] : current_time('Y-m-d');
        $replacements['{{response_date}}'] = isset($payment_data['response_date']) ? $payment_data['response_date'] : current_time('Y-m-d');
        $replacements['{{decline_reason}}'] = isset($payment_data['decline_reason']) ? $payment_data['decline_reason'] : __('No specific reason provided', 'easy-invoice');
        
        return $replacements;
    }
    
    /**
     * Process template with replacements
     *
     * @param string $template The template
     * @param array $replacements The replacements
     * @return string Processed template
     */
    private function processTemplate(string $template, array $replacements): string {
        return easy_invoice_str_replace(array_keys($replacements), array_values($replacements), $template);
    }
    
    /**
     * Prepare email headers
     *
     * @return array Headers
     */
    /**
     * Should outgoing invoice emails carry a PDF copy?
     *
     * Defaults to off. Attaching a document changes what every customer receives and
     * makes messages substantially larger, which some SMTP relays limit — that is the
     * site owner's decision, not something an update should impose.
     *
     * @return bool
     */
    private function shouldAttachInvoicePdf(): bool {
        if (!\EasyInvoice\Services\PdfRenderer::isAvailable()) {
            return false;
        }

        $enabled = get_option('easy_invoice_attach_pdf_to_email', 'no') === 'yes';

        /**
         * Filter whether to attach a PDF to invoice emails.
         *
         * @param bool $enabled Current setting.
         */
        return (bool) apply_filters('easy_invoice_attach_pdf_to_email', $enabled);
    }

    private function prepareEmailHeaders(string $template_name = '', $document = null): array {
        $headers = [
            'Content-Type: text/html; charset=UTF-8',
            'From: ' . $this->settings['from_name'] . ' <' . $this->settings['from_email'] . '>',
        ];
        
        // Add Reply-To if set
        if (!empty($this->settings['reply_to_email'])) {
            $reply_to_name = !empty($this->settings['reply_to_name']) ? $this->settings['reply_to_name'] : $this->settings['from_name'];
            $headers[] = 'Reply-To: ' . $reply_to_name . ' <' . $this->settings['reply_to_email'] . '>';
        }

        /**
         * Filter the headers of an outgoing Easy Invoice email.
         *
         * This is the extension point Easy Invoice Pro's Email Enhancements addon uses
         * to set a per-document-type Reply-To. The addon has always registered against
         * it, but nothing here ever applied it, so that half of the addon did nothing
         * at all — the Reply-To customers saw came only from the free plugin's own
         * Email settings above.
         *
         * @param array  $headers       Headers assembled so far.
         * @param string $template_name Which email this is: invoice, quote, receipt,
         *                              reminder, and so on. Empty when the caller has
         *                              no template context.
         * @param mixed  $document      The Invoice or Quote the email concerns, or null.
         */
        return (array) apply_filters('easy_invoice_email_headers', $headers, $template_name, $document);
    }
    
    /**
     * Wrap message in HTML template
     *
     * @param string $message The message
     * @return string HTML wrapped message
     */
    /**
     * Wrap a message body in the plugin's HTML email layout (logo, styles,
     * footer) — for anything outside this class that sends a branded email.
     *
     * @param string $message Body HTML.
     * @return string
     */
    /**
     * The stock templates mention an attached copy. When nothing is attached
     * (the setting is off by default) that sentence would be untrue, so the
     * exact stock phrases are reworded; a merchant's own text is left alone.
     *
     * @param string $message  Rendered email body.
     * @param bool   $attached Whether a file goes with it.
     * @return string
     */
    private function attachmentWording(string $message, bool $attached): string {
        if ($attached) {
            return $message;
        }
        return str_replace(
            [
                __('The invoice is attached and can also be viewed and paid online:', 'easy-invoice'),
                __('It is attached, and you can review, accept or decline it online:', 'easy-invoice'),
            ],
            [
                __('You can view and pay it online:', 'easy-invoice'),
                __('You can review, accept or decline it online:', 'easy-invoice'),
            ],
            $message
        );
    }

    public function wrapMessage(string $message): string {
        return $this->wrapInHtmlTemplate($message);
    }

    /**
     * Headers for an email sent by something other than this class (Pro's
     * reminders, addons): From and Reply-To from Settings → Email, then the
     * `easy_invoice_email_headers` filter with the template name.
     *
     * @param string $template_name invoice, quote, receipt, reminder…
     * @param mixed  $document      The Invoice or Quote concerned, or null.
     * @return array<int,string>
     */
    public function headers(string $template_name = '', $document = null): array {
        return $this->prepareEmailHeaders($template_name, $document);
    }

    /**
     * Placeholder replacements for an invoice, for a template sent by
     * something other than this class (Pro's reminders, addons).
     *
     * @param object $invoice Invoice model.
     * @return array<string,string>
     */
    public function invoicePlaceholders($invoice): array {
        return $this->getInvoiceReplacements($invoice);
    }

    private function wrapInHtmlTemplate(string $message): string {
        $logo_html = '';
        if (!empty($this->settings['email_logo'])) {
            $logo_html = '<div style="text-align: center; margin-bottom: 40px;"><img src="' . esc_url($this->settings['email_logo']) . '" alt="' . esc_attr($this->settings['from_name']) . '" style="max-width: 200px; height: auto; border-radius: 8px;"></div>';
        }
        
        $footer_html = '';
        if (!empty($this->settings['footer_text'])) {
            $footer_html = '<div style="margin-top: 50px; padding-top: 25px; border-top: 2px solid #f3f4f6; font-size: 14px; color: #6b7280; text-align: center;">' . wpautop($this->settings['footer_text']) . '</div>';
        }

        /**
         * Filter the footer block of every Easy Invoice email.
         *
         * @param string $footer_html The footer markup ('' when no footer text is set).
         * @param array  $settings    Email settings.
         */
        $footer_html = (string) apply_filters('easy_invoice_email_footer_html', $footer_html, $this->settings);

        /**
         * Replace the whole email layout.
         *
         * Return a full HTML document to use it instead of the stock layout.
         * Pro's Email Enhancements addon uses this for a custom branded
         * layout; the placeholders it offers are resolved before this fires.
         *
         * @param string $html        '' — return non-empty markup to take over.
         * @param string $message     The email body (placeholders already replaced), unwrapped.
         * @param string $logo_html   Logo block from Settings → Email, or ''.
         * @param string $footer_html Footer block, after the filter above.
         * @param array  $settings    Email settings.
         */
        $custom = (string) apply_filters('easy_invoice_email_html', '', $message, $logo_html, $footer_html, $this->settings);
        if ('' !== trim($custom)) {
            return $custom;
        }
        
        return '
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>' . esc_html($this->settings['from_name']) . '</title>
            <style>
                body { 
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 
                    line-height: 1.6; 
                    color: #374151; 
                    margin: 0; 
                    padding: 0; 
                    background-color: #f9fafb; 
                }
                .email-container { 
                    max-width: 600px; 
                    margin: 0 auto; 
                    background-color: #ffffff; 
                    border-radius: 12px;
                    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
                    overflow: hidden;
                }
                .email-header { 
                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 
                    padding: 50px 30px; 
                    text-align: center; 
                    position: relative;
                }
                .email-header::before {
                    content: "";
                    position: absolute;
                    top: 0;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    background: url("data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Ccircle cx="30" cy="30" r="2"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
                    opacity: 0.3;
                }
                .email-header h1 { 
                    color: #ffffff; 
                    margin: 0; 
                    font-size: 28px; 
                    font-weight: 700; 
                    position: relative;
                    z-index: 1;
                }
                .email-content { 
                    padding: 50px 40px; 
                    background: #ffffff;
                }
                .email-content p { 
                    margin: 0 0 20px 0; 
                    color: #374151;
                    line-height: 1.7;
                }
                .email-content h2 { 
                    color: #1f2937; 
                    font-size: 28px; 
                    font-weight: 700; 
                    margin: 0 0 30px 0; 
                    text-align: center;
                }
                .email-content h3 { 
                    color: #374151; 
                    font-size: 20px; 
                    font-weight: 600; 
                    margin: 0 0 16px 0; 
                }
                .email-footer { 
                    background-color: #f9fafb; 
                    padding: 40px 30px; 
                    text-align: center; 
                    border-top: 1px solid #e5e7eb; 
                }
                .email-footer p { 
                    margin: 0; 
                    color: #6b7280; 
                    font-size: 14px; 
                }
                .highlight-box { 
                    background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
                    border-left: 4px solid #3b82f6; 
                    padding: 30px; 
                    margin: 30px 0; 
                    border-radius: 0 12px 12px 0; 
                    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
                }
                .highlight-box p { 
                    margin: 0; 
                    font-size: 16px;
                    line-height: 1.6;
                }
                .highlight-box strong {
                    color: #1f2937;
                    font-weight: 600;
                }
                .info-box { 
                    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
                    border: 1px solid #93c5fd; 
                    border-radius: 12px; 
                    padding: 25px; 
                    margin: 30px 0; 
                    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
                }
                .info-box p { 
                    margin: 0; 
                    color: #1e40af; 
                    font-size: 15px;
                    line-height: 1.7;
                }
                .info-box strong {
                    color: #1e3a8a;
                    font-weight: 600;
                }
                .success-box {
                    background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
                    border: 1px solid #6ee7b7;
                    border-radius: 12px;
                    padding: 25px;
                    margin: 30px 0;
                    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
                }
                .success-box p {
                    margin: 0;
                    color: #065f46;
                    font-size: 15px;
                    line-height: 1.7;
                }
                .success-box strong {
                    color: #047857;
                    font-weight: 600;
                }
                .warning-box {
                    background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
                    border: 1px solid #f59e0b;
                    border-radius: 12px;
                    padding: 25px;
                    margin: 30px 0;
                    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
                }
                .warning-box p {
                    margin: 0;
                    color: #92400e;
                    font-size: 15px;
                    line-height: 1.7;
                }
                .warning-box strong {
                    color: #78350f;
                    font-weight: 600;
                }
                .button { 
                    display: inline-block; 
                    background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
                    color: #ffffff; 
                    padding: 14px 28px; 
                    text-decoration: none; 
                    border-radius: 8px; 
                    font-weight: 600; 
                    margin: 20px 0; 
                    box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3);
                    transition: all 0.2s ease;
                }
                .button:hover { 
                    background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
                    transform: translateY(-1px);
                    box-shadow: 0 6px 8px -1px rgba(59, 130, 246, 0.4);
                }
                .divider {
                    height: 1px;
                    background: linear-gradient(90deg, transparent 0%, #e5e7eb 50%, transparent 100%);
                    margin: 40px 0;
                }
                .amount-highlight {
                    font-size: 28px;
                    font-weight: 700;
                    color: #059669;
                    text-align: center;
                    margin: 25px 0;
                    display: block;
                }
                .status-badge {
                    display: inline-block;
                    padding: 6px 12px;
                    border-radius: 20px;
                    font-size: 12px;
                    font-weight: 600;
                    text-transform: uppercase;
                    letter-spacing: 0.5px;
                }
                .status-paid {
                    background: #d1fae5;
                    color: #065f46;
                }
                .status-pending {
                    background: #fef3c7;
                    color: #92400e;
                }
                .status-overdue {
                    background: #fee2e2;
                    color: #991b1b;
                }
                @media only screen and (max-width: 600px) {
                    .email-content { padding: 25px 20px; }
                    .email-header { padding: 35px 20px; }
                    .email-header h1 { font-size: 24px; }
                    .email-content h2 { font-size: 22px; }
                    .highlight-box, .info-box, .success-box, .warning-box { padding: 20px; }
                    .amount-highlight { font-size: 24px; }
                }
            </style>
        </head>
        <body>
            <div class="email-container">
            ' . $logo_html . '
                <div class="email-content">
                ' . wpautop($message) . '
            </div>
            ' . $footer_html . '
            </div>
        </body>
        </html>';
    }
    
    /**
     * Register email settings
     */
    public function registerEmailSettings(): void {
        // Email settings section
        add_settings_section(
            'easy_invoice_email_settings',
            __('Email Configuration', 'easy-invoice'),
            [$this, 'emailSettingsSectionCallback'],
            'easy_invoice_settings'
        );
        
        // Register settings
        $yes_no = static function ($value) {
            return in_array((string) $value, ['yes', '1', 'on', 'true'], true) ? 'yes' : 'no';
        };
        register_setting('easy_invoice_settings', 'easy_invoice_email_from_name', ['sanitize_callback' => 'sanitize_text_field']);
        register_setting('easy_invoice_settings', 'easy_invoice_email_from_address', ['sanitize_callback' => 'sanitize_email']);
        register_setting('easy_invoice_settings', 'easy_invoice_email_reply_to', ['sanitize_callback' => 'sanitize_email']);
        register_setting('easy_invoice_settings', 'easy_invoice_email_reply_to_name', ['sanitize_callback' => 'sanitize_text_field']);
        register_setting('easy_invoice_settings', 'easy_invoice_enable_email_styling', ['sanitize_callback' => $yes_no]);
        register_setting('easy_invoice_settings', 'easy_invoice_email_logo', ['sanitize_callback' => 'esc_url_raw']);
        register_setting('easy_invoice_settings', 'easy_invoice_email_footer_text', ['sanitize_callback' => 'wp_kses_post']);
        register_setting('easy_invoice_settings', 'easy_invoice_bcc_admin', ['sanitize_callback' => $yes_no]);
        register_setting('easy_invoice_settings', 'easy_invoice_admin_email', ['sanitize_callback' => 'sanitize_email']);
        
        // Add settings fields
        add_settings_field(
            'easy_invoice_email_from_name',
            __('From Name', 'easy-invoice'),
            [$this, 'textFieldCallback'],
            'easy_invoice_settings',
            'easy_invoice_email_settings',
            ['label_for' => 'easy_invoice_email_from_name']
        );
        
        add_settings_field(
            'easy_invoice_email_from_address',
            __('From Email Address', 'easy-invoice'),
            [$this, 'emailFieldCallback'],
            'easy_invoice_settings',
            'easy_invoice_email_settings',
            ['label_for' => 'easy_invoice_email_from_address']
        );
        
        add_settings_field(
            'easy_invoice_email_reply_to',
            __('Reply-To Email', 'easy-invoice'),
            [$this, 'emailFieldCallback'],
            'easy_invoice_settings',
            'easy_invoice_email_settings',
            ['label_for' => 'easy_invoice_email_reply_to']
        );
        
        add_settings_field(
            'easy_invoice_enable_email_styling',
            __('Enable HTML Emails', 'easy-invoice'),
            [$this, 'checkboxFieldCallback'],
            'easy_invoice_settings',
            'easy_invoice_email_settings',
            ['label_for' => 'easy_invoice_enable_email_styling']
        );
        
        add_settings_field(
            'easy_invoice_bcc_admin',
            __('BCC Admin on All Emails', 'easy-invoice'),
            [$this, 'checkboxFieldCallback'],
            'easy_invoice_settings',
            'easy_invoice_email_settings',
            ['label_for' => 'easy_invoice_bcc_admin']
        );
    }
    
    
    /**
     * Email settings section callback
     */
    public function emailSettingsSectionCallback(): void {
        echo '<p>' . esc_html__('Configure how emails are sent from Easy Invoice.', 'easy-invoice') . '</p>';
    }
    
    /**
     * Text field callback
     *
     * @param array $args Field arguments
     */
    public function textFieldCallback(array $args): void {
        $field_id = $args['label_for'];
        $value = get_option($field_id, '');
        echo '<input type="text" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_id) . '" value="' . esc_attr($value) . '" class="regular-text">';
    }
    
    /**
     * Email field callback
     *
     * @param array $args Field arguments
     */
    public function emailFieldCallback(array $args): void {
        $field_id = $args['label_for'];
        $value = get_option($field_id, '');
        echo '<input type="email" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_id) . '" value="' . esc_attr($value) . '" class="regular-text">';
    }
    
    /**
     * Checkbox field callback
     *
     * @param array $args Field arguments
     */
    public function checkboxFieldCallback(array $args): void {
        $field_id = $args['label_for'];
        $value = get_option($field_id, '');
        echo '<input type="checkbox" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_id) . '" value="yes"' . checked($value, 'yes', false) . '>';
        echo '<span class="description">' . esc_html__('Enable this option', 'easy-invoice') . '</span>';
    }
    
    /**
     * Log email sent
     *
     * @param Invoice $invoice The invoice
     * @param string $email The email address
     * @param string $type The email type
     */
    public function logEmailSent($invoice, string $email, string $type): void {
        $this->log(sprintf('Email sent to %s for invoice #%s (%s)', $email, $invoice->getNumber(), $type), 'info');
    }
    
    /**
     * Log email failed
     *
     * @param Invoice $invoice The invoice
     * @param string $email The email address
     * @param string $type The email type
     */
    public function logEmailFailed($invoice, string $email, string $type): void {
        $this->log(sprintf('Email failed to %s for invoice #%s (%s)', $email, $invoice->getNumber(), $type), 'error');
    }
    
    /**
     * Get default invoice template
     *
     * @return string Template
     */
    /**
     * Replace the 2.3.x stock email bodies with the 2.4.0 ones — once, and
     * only where the saved body is still the stock text (compared by its
     * words, since the editor re-wraps markup on save). A body the site
     * edited is left alone.
     */
    public static function refreshStockTemplates(): void {
        if ( get_option( 'easy_invoice_email_stock_v240' ) ) {
            return;
        }
        $old = [
            'invoice' => [ '83846ae6875ad4335976cc07140e56bc', 'b40d303fa4194c0da4257495fa9e138f' ],
            'quote'   => [ '7f31d11b8368fce31daddb7cbace9fb1', 'f9b9c4ad911ac729a04e52e35d056968' ],
            'payment' => [ '52c9e647eac3d10ea39cf641e2bfc2b0' ],
        ];
        foreach ( $old as $kind => $fingerprints ) {
            $key    = 'easy_invoice_' . $kind . '_email_body';
            $stored = get_option( $key, null );
            if ( null === $stored || '' === $stored ) {
                continue;
            }
            $words = preg_replace( '/[^A-Za-z0-9{}]/u', '', html_entity_decode( wp_strip_all_tags( stripslashes( (string) $stored ) ) ) );
            if ( in_array( md5( (string) $words ), $fingerprints, true ) ) {
                update_option( $key, self::defaultTemplate( $kind ) );
            }
        }
        update_option( 'easy_invoice_email_stock_v240', 1, false );
    }

    /**
     * The stock body for one of the emails, used wherever a default is needed
     * (settings screen, activation seeding, sending when nothing is saved).
     *
     * @param string $kind invoice | reminder | payment | quote | quote_accepted | quote_declined
     * @return string
     */
    public static function defaultTemplate( string $kind ): string {
        switch ( $kind ) {
            case 'reminder':       return self::getDefaultReminderTemplate();
            case 'payment':        return self::getDefaultPaymentTemplate();
            case 'quote':          return self::getDefaultQuoteTemplate();
            case 'quote_accepted': return self::getDefaultQuoteAcceptedTemplate();
            case 'quote_declined': return self::getDefaultQuoteDeclinedTemplate();
            default:               return self::getDefaultInvoiceTemplate();
        }
    }

    private static function getDefaultInvoiceTemplate(): string {
        return '<h2>Invoice {{invoice_number}}</h2>

<p>Dear {{client_name}},</p>

<p>Please find invoice {{invoice_number}} for <strong>{{total_amount}}</strong>, due on <strong>{{due_date}}</strong>. The invoice is attached and can also be viewed and paid online:</p>

<p style="text-align:center;margin:28px 0;"><a class="button" href="{{invoice_url}}">View and pay invoice</a></p>
<p style="font-size:13px;color:#6b7280;word-break:break-all;">{{invoice_url}}</p>

<div class="info-box">
    <p>Invoice number: {{invoice_number}}<br>
    Amount due: {{amount_due}}<br>
    Due date: {{due_date}}</p>
</div>

<p>If you have any questions about this invoice, just reply to this email.</p>

<div class="divider"></div>

<p>Thank you for your business.</p>

<p>{{company_name}}<br>
{{company_email}}</p>';
    }

    private static function getDefaultReminderTemplate(): string {
        return '<h2>Payment reminder — invoice {{invoice_number}}</h2>

<p>Dear {{client_name}},</p>

<p>A reminder that invoice {{invoice_number}} was due on <strong>{{due_date}}</strong>; <strong>{{amount_due}}</strong> is still outstanding. If you have already paid, please disregard this message.</p>

<p style="text-align:center;margin:28px 0;"><a class="button" href="{{invoice_url}}">View and pay invoice</a></p>
<p style="font-size:13px;color:#6b7280;word-break:break-all;">{{invoice_url}}</p>

<p>If you have a question about the invoice or need to arrange payment, reply to this email and we will sort it out.</p>

<div class="divider"></div>

<p>Thank you.</p>

<p>{{company_name}}<br>
{{company_email}}</p>';
    }
    
    private static function getDefaultPaymentTemplate(): string {
        return '<h2>Payment received — thank you</h2>

<p>Dear {{client_name}},</p>

<p>We have received your payment of <strong>{{payment_amount}}</strong> against invoice {{invoice_number}}.</p>

<div class="info-box">
    <p>Invoice: {{invoice_number}}<br>
    Amount paid: {{payment_amount}}<br>
    Date: {{payment_date}}<br>
    Method: {{payment_method}}<br>
    Reference: {{transaction_id}}<br>
    Balance remaining: {{amount_due}}</p>
</div>

<p>Keep this email as your receipt. If you need anything else, reply to this message.</p>

<div class="divider"></div>

<p>Thank you for your business.</p>

<p>{{company_name}}<br>
{{company_email}}</p>';
    }
    
    private static function getDefaultQuoteTemplate(): string {
        return '<h2>Quote {{quote_number}}</h2>

<p>Dear {{client_name}},</p>

<p>Please find our quote {{quote_number}} for <strong>{{total_amount}}</strong>, valid until <strong>{{expiry_date}}</strong>. It is attached, and you can review, accept or decline it online:</p>

<p style="text-align:center;margin:28px 0;"><a class="button" href="{{quote_url}}">View quote</a></p>
<p style="font-size:13px;color:#6b7280;word-break:break-all;">{{quote_url}}</p>

<div class="info-box">
    <p>Quote number: {{quote_number}}<br>
    Amount: {{total_amount}}<br>
    Valid until: {{expiry_date}}</p>
</div>

<p>If you would like to discuss any part of it, just reply to this email.</p>

<div class="divider"></div>

<p>We look forward to working with you.</p>

<p>{{company_name}}<br>
{{company_email}}</p>';
    }

    private static function getDefaultQuoteAcceptedTemplate(): string {
        return '<h2>Quote {{quote_number}} accepted</h2>

<p>Dear {{client_name}},</p>

<p>Thank you for accepting quote {{quote_number}} for <strong>{{total_amount}}</strong> on {{acceptance_date}}.</p>

<p>We will send the invoice and any next steps shortly. If you have questions in the meantime, reply to this email.</p>

<div class="divider"></div>

<p>Thank you for choosing us.</p>

<p>{{company_name}}<br>
{{company_email}}</p>';
    }
    
    private static function getDefaultQuoteDeclinedTemplate(): string {
        return '<h2>Quote {{quote_number}}</h2>

<p>Dear {{client_name}},</p>

<p>Thank you for letting us know that quote {{quote_number}} is not going ahead ({{response_date}}).</p>

<div class="info-box">
    <p>Reason given: {{decline_reason}}</p>
</div>

<p>If your requirements change, or there is something we could adjust, we would be glad to prepare a revised quote — just reply to this email.</p>

<div class="divider"></div>

<p>{{company_name}}<br>
{{company_email}}</p>';
    }
    
    /**
     * Refresh settings and templates
     * Call this method when settings are updated
     */
    public function refreshSettings(): void {
        $this->loadSettings();
        $this->loadTemplates();
    }
    
    /**
     * Get email templates
     *
     * @return array Templates
     */
    public function getTemplates(): array {
        return $this->templates;
    }
    
    /**
     * Get email settings
     *
     * @return array Settings
     */
    public function getSettings(): array {
        return $this->settings;
    }
    
    /**
     * Test email functionality
     *
     * @param string $to_email Email to send test to
     * @return array Result
     */
    public function testEmail(string $to_email): array {
        $subject = 'Easy Invoice - Email Configuration Test';
        $message = '<h2>🧪 Email Configuration Test</h2>

<p>Hello!</p>

<div class="success-box">
    <p><strong>✅ Test Email Successfully Sent</strong><br>
    Date: <strong>' . current_time('Y-m-d H:i:s') . '</strong><br>
    To: <strong>' . esc_html($to_email) . '</strong></p>
</div>

<p>This is a test email to verify that your Easy Invoice email configuration is working correctly.</p>

<div class="info-box">
    <p><strong>⚙️ Email Settings Verified:</strong><br>
    • From Name: ' . esc_html($this->settings['from_name']) . '<br>
    • From Email: ' . esc_html($this->settings['from_email']) . '<br>
    • Reply-To: ' . esc_html($this->settings['reply_to_email'] ?: 'Not set') . '<br>
    • HTML Emails: ' . ($this->settings['enable_html'] === 'yes' ? 'Enabled' : 'Disabled') . '</p>
</div>

<div class="highlight-box">
    <p><strong>🎉 Congratulations!</strong> If you received this email, your email configuration is working properly and you can now send invoices, quotes, and payment confirmations to your clients.</p>
</div>

<div class="divider"></div>

<p>Thank you for using Easy Invoice!</p>

<p>Best regards,<br>
<strong>' . esc_html($this->settings['from_name']) . '</strong></p>';
        
        if ($this->settings['enable_html'] === 'yes') {
            $message = $this->wrapInHtmlTemplate($message);
        }
        
        $headers = $this->prepareEmailHeaders();
        
        $sent = $this->sendEmail($to_email, $subject, $message, $headers);
        
        if ($sent) {
            return ['success' => true, 'message' => __('Test email sent successfully', 'easy-invoice')];
        } else {
            return ['success' => false, 'message' => __('Failed to send test email', 'easy-invoice')];
        }
    }

    /**
     * Send test template email with custom subject and body
     *
     * @param string $to_email Email to send test to
     * @param string $subject Email subject
     * @param string $body Email body
     * @return array Result
     */
    public function sendTestTemplateEmail(string $to_email, string $subject, string $body): array {
        if ($this->settings['enable_html'] === 'yes') {
            $body = $this->wrapInHtmlTemplate($body);
        }
        
        $headers = $this->prepareEmailHeaders();
        
        $sent = $this->sendEmail($to_email, $subject, $body, $headers);
        
        if ($sent) {
            return ['success' => true, 'message' => __('Template test email sent successfully', 'easy-invoice')];
        } else {
            return ['success' => false, 'message' => __('Failed to send template test email', 'easy-invoice')];
        }
    }

    /**
     * Send payment received email
     *
     * @param Invoice $invoice The invoice
     * @param array $payment_data Payment data
     * @return array Result array with success status and message
     */
    public function sendPaymentEmail(Invoice $invoice, array $payment_data = []): array {
        try {
            // Validate invoice
            if (!$invoice || !$invoice->getId()) {
                return ['success' => false, 'message' => __('Invalid invoice', 'easy-invoice')];
            }
            
            // Get client email
            $client_email = $invoice->getCustomerEmail();
            if (empty($client_email)) {
                return ['success' => false, 'message' => __('Client email is missing', 'easy-invoice')];
            }
            
            // Get template
            $template_key = 'invoice_paid';
            if (!isset($this->templates[$template_key])) {
                return ['success' => false, 'message' => __('Payment email template not found', 'easy-invoice')];
            }
            
            $template = $this->templates[$template_key];
            /** This filter is documented above in sendInvoiceEmail(). */
            $template = (array) apply_filters('easy_invoice_email_template_data', $template, $template_key, $invoice);
            
            // Check if email is enabled
            if (!$template['enabled']) {
                return ['success' => false, 'message' => __('Payment received email is disabled', 'easy-invoice')];
            }
            
            // Prepare email data
            $email_data = $this->preparePaymentEmailData($invoice, $template, $payment_data);
            
            // Send email
            $sent = $this->sendEmail(
                $email_data['to'],
                $email_data['subject'],
                $email_data['message'],
                $email_data['headers']
            );
            
            /**
             * Fires once an email send has finished, whether or not it went out.
             *
             * @param object $document     Invoice or Quote model.
             * @param string $template_key Template key.
             * @param bool   $sent         Whether wp_mail() accepted it.
             */
            do_action('easy_invoice_email_finished', $invoice, $template_key, (bool) $sent);
            
            if ($sent) {
                // Log success
                do_action('easy_invoice_payment_email_sent', $invoice, $client_email, $payment_data);
                
                return [
                    'success' => true, 
                    'message' => __('Payment email sent successfully', 'easy-invoice'),
                    'email_data' => $email_data
                ];
            } else {
                // Log failure
                do_action('easy_invoice_payment_email_failed', $invoice, $client_email, $payment_data);
                
                return ['success' => false, 'message' => __('Failed to send payment email', 'easy-invoice')];
            }
            
        } catch (\Exception $e) {
            $this->log('Payment email sending error: ' . $e->getMessage(), 'error');
            return ['success' => false, 'message' => __('Error sending payment email: ', 'easy-invoice') . $e->getMessage()];
        }
    }
    
    /**
     * Send admin notification when payment is received
     *
     * @param Invoice $invoice The invoice
     * @param array $payment_data Payment data (method, amount, etc.)
     * @return array Result array with success status and message
     */
    public function sendAdminPaymentNotification(Invoice $invoice, array $payment_data = []): array {
        try {
            // Validate invoice
            if (!$invoice || !$invoice->getId()) {
                return ['success' => false, 'message' => __('Invalid invoice', 'easy-invoice')];
            }
            
            // Get admin email
            $admin_email = $this->settings['admin_email'] ?? get_option('admin_email');
            if (empty($admin_email)) {
                return ['success' => false, 'message' => __('Admin email is missing', 'easy-invoice')];
            }
            
            // Get payment method
            $payment_method = $payment_data['payment_method'] ?? $payment_data['method'] ?? 'online';
            $payment_method_label = $this->getPaymentMethodLabel($payment_method);
            
            // Format amount
            $formatter = new \EasyInvoice\Helpers\InvoiceFormatter($invoice);
            // The payment that came in, not the invoice's face value.
            $amount = $formatter->format(isset($payment_data['amount']) && (float) $payment_data['amount'] > 0 ? (float) $payment_data['amount'] : $invoice->getTotal());
            
            // Prepare email subject
            $pending = !empty($payment_data['pending']);
            $subject = sprintf(
                /* translators: %s: document number. */
                $pending ? __('Payment awaiting verification - Invoice #%s', 'easy-invoice') : __('New Payment Received - Invoice #%s', 'easy-invoice'),
                $invoice->getNumber()
            );
            
            // Prepare email message
            $message = $this->prepareAdminPaymentNotificationMessage($invoice, $payment_method_label, $amount, $payment_data);
            
            // Add HTML wrapper if enabled
            if ($this->settings['enable_html'] === 'yes') {
                $message = $this->wrapInHtmlTemplate($message);
            }
            
            // Prepare headers
            $headers = $this->prepareEmailHeaders();
            
            // Send email
            $sent = $this->sendEmail($admin_email, $subject, $message, $headers);
            
            if ($sent) {
                do_action('easy_invoice_admin_payment_notification_sent', $invoice, $admin_email, $payment_data);
                return [
                    'success' => true,
                    'message' => __('Admin notification sent successfully', 'easy-invoice')
                ];
            } else {
                do_action('easy_invoice_admin_payment_notification_failed', $invoice, $admin_email, $payment_data);
                return ['success' => false, 'message' => __('Failed to send admin notification', 'easy-invoice')];
            }
            
        } catch (\Exception $e) {
            $this->log('Admin payment notification error: ' . $e->getMessage(), 'error');
            return ['success' => false, 'message' => __('Error sending admin notification: ', 'easy-invoice') . $e->getMessage()];
        }
    }
    
    /**
     * Tell the admin a manual payment is waiting for verification.
     *
     * @param int    $invoice_id     The invoice paid.
     * @param string $payment_method Gateway or payment type submitted.
     */
    public function handleManualPaymentSubmitted($invoice_id, $payment_method = 'manual'): void {
        $post = get_post((int) $invoice_id);
        if (!$post) {
            return;
        }
        $invoice = new Invoice($post);
        if (!$invoice->getId()) {
            return;
        }
        $payment_data = [
            'payment_method' => (string) $payment_method,
            'pending' => true,
        ];
        $notes = get_post_meta($invoice->getId(), '_manual_payment_notes', true);
        if ($notes) {
            $payment_data['notes'] = $notes;
        }
        $this->sendAdminPaymentNotification($invoice, $payment_data);
    }

    /**
     * Send payment confirmation email to customer
     *
     * @param Invoice $invoice The invoice
     * @param array $payment_data Payment data
     * @return array Result array with success status and message
     */
    public function sendPaymentConfirmationEmail(Invoice $invoice, array $payment_data = []): array {
        try {
            // Validate invoice
            if (!$invoice || !$invoice->getId()) {
                return ['success' => false, 'message' => __('Invalid invoice', 'easy-invoice')];
            }
            
            // Get customer email
            $customer_email = $invoice->getCustomerEmail();
            if (empty($customer_email)) {
                return ['success' => false, 'message' => __('Customer email is missing', 'easy-invoice')];
            }
            
            // Get currency settings
            $settings_controller = new \EasyInvoice\Controllers\SettingsController();
            $settings = $settings_controller->getSettings();
            $currency_code = $settings['easy_invoice_currency_code'] ?? 'USD';
            $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
            
            // Format amount
            $amount = $invoice->getTotal();
            $formatted_amount = $currency_symbol . number_format($amount, 2);
            
            // Prepare email subject
            $site_name = get_bloginfo('name');
            $subject = sprintf(
                /* translators: %1$s: site name; %2$s: document number. */
                __('[%1$s] Payment Confirmed - Invoice #%2$s', 'easy-invoice'),
                $site_name,
                $invoice->getNumber()
            );
            
            // Prepare email message
            $message = $this->preparePaymentConfirmationMessage($invoice, $formatted_amount);
            
            // Add HTML wrapper if enabled
            if ($this->settings['enable_html'] === 'yes') {
                $message = $this->wrapInHtmlTemplate($message);
            }
            
            // Prepare headers
            $headers = $this->prepareEmailHeaders();
            
            // Add BCC to admin if enabled (but skip if this is from payment completion hook to avoid duplicate)
            // The payment completion hook already sends a dedicated admin notification
            $skip_bcc = isset($payment_data['skip_bcc']) && $payment_data['skip_bcc'] === true;
            if (!$skip_bcc && $this->settings['bcc_admin'] === 'yes' && !empty($this->settings['admin_email'])) {
                $headers[] = 'Bcc: ' . $this->settings['admin_email'];
            }
            
            // Send email
            $sent = $this->sendEmail($customer_email, $subject, $message, $headers);
            
            if ($sent) {
                do_action('easy_invoice_payment_confirmation_sent', $invoice, $customer_email, $payment_data);
                return [
                    'success' => true,
                    'message' => __('Payment confirmation email sent successfully', 'easy-invoice')
                ];
            } else {
                do_action('easy_invoice_payment_confirmation_failed', $invoice, $customer_email, $payment_data);
                return ['success' => false, 'message' => __('Failed to send payment confirmation email', 'easy-invoice')];
            }
            
        } catch (\Exception $e) {
            $this->log('Payment confirmation email error: ' . $e->getMessage(), 'error');
            return ['success' => false, 'message' => __('Error sending payment confirmation: ', 'easy-invoice') . $e->getMessage()];
        }
    }
    
    /**
     * Send payment rejection email to customer
     *
     * @param Invoice $invoice The invoice
     * @param string $reason Rejection reason
     * @return array Result array with success status and message
     */
    public function sendPaymentRejectionEmail(Invoice $invoice, string $reason = ''): array {
        try {
            // Validate invoice
            if (!$invoice || !$invoice->getId()) {
                return ['success' => false, 'message' => __('Invalid invoice', 'easy-invoice')];
            }
            
            // Get customer email
            $customer_email = $invoice->getCustomerEmail();
            if (empty($customer_email)) {
                return ['success' => false, 'message' => __('Customer email is missing', 'easy-invoice')];
            }
            
            // Prepare email subject
            $site_name = get_bloginfo('name');
            $subject = sprintf(
                /* translators: %1$s: site name; %2$s: document number. */
                __('[%1$s] Payment Rejected - Invoice #%2$s', 'easy-invoice'),
                $site_name,
                $invoice->getNumber()
            );
            
            // Prepare email message
            $message = $this->preparePaymentRejectionMessage($invoice, $reason);
            
            // Add HTML wrapper if enabled
            if ($this->settings['enable_html'] === 'yes') {
                $message = $this->wrapInHtmlTemplate($message);
            }
            
            // Prepare headers
            $headers = $this->prepareEmailHeaders();
            
            // Add BCC to admin if enabled
            if ($this->settings['bcc_admin'] === 'yes' && !empty($this->settings['admin_email'])) {
                $headers[] = 'Bcc: ' . $this->settings['admin_email'];
            }
            
            // Send email
            $sent = $this->sendEmail($customer_email, $subject, $message, $headers);
            
            if ($sent) {
                do_action('easy_invoice_payment_rejection_sent', $invoice, $customer_email, $reason);
                return [
                    'success' => true,
                    'message' => __('Payment rejection email sent successfully', 'easy-invoice')
                ];
            } else {
                do_action('easy_invoice_payment_rejection_failed', $invoice, $customer_email, $reason);
                return ['success' => false, 'message' => __('Failed to send payment rejection email', 'easy-invoice')];
            }
            
        } catch (\Exception $e) {
            $this->log('Payment rejection email error: ' . $e->getMessage(), 'error');
            return ['success' => false, 'message' => __('Error sending payment rejection: ', 'easy-invoice') . $e->getMessage()];
        }
    }
    
    /**
     * Prepare admin payment notification message
     *
     * @param Invoice $invoice The invoice
     * @param string $payment_method_label Payment method label
     * @param string $amount Formatted amount
     * @param array $payment_data Payment data
     * @return string Email message
     */
    private function prepareAdminPaymentNotificationMessage(Invoice $invoice, string $payment_method_label, string $amount, array $payment_data = []): string {
        $invoice_number = $invoice->getNumber();
        $customer_name = $invoice->getCustomerName();
        $customer_email = $invoice->getCustomerEmail();
        $invoice_id = $invoice->getId();
        
        $pending = !empty($payment_data['pending']);
        $message = sprintf(
            /* translators: %1$s: payment method; %2$s: invoice number. */
            $pending ? __('A %1$s payment has been submitted for invoice #%2$s and is waiting for your verification.', 'easy-invoice') : __('A new %1$s payment has been received for invoice #%2$s.', 'easy-invoice'),
            $payment_method_label,
            $invoice_number
        );
        $message .= "\n\n";
        /* translators: . */
        $message .= __('Invoice Details:', 'easy-invoice');
        $message .= "\n";
        /* translators: %s: amount. */
        $message .= sprintf($pending ? __('- Amount submitted: %s', 'easy-invoice') : __('- Amount received: %s', 'easy-invoice'), $amount);
        $message .= "\n";
        $ei_due = \EasyInvoice\Services\InvoiceBalance::due($invoice);
        /* translators: %s: amount. */
        $message .= sprintf(__('- Still owed: %s', 'easy-invoice'), (new \EasyInvoice\Helpers\InvoiceFormatter($invoice))->format($ei_due));
        $message .= "\n";
        /* translators: %s: customer name. */
        $message .= sprintf(__('- Customer: %s', 'easy-invoice'), $customer_name);
        $message .= "\n";
        /* translators: %s: customer email address. */
        $message .= sprintf(__('- Email: %s', 'easy-invoice'), $customer_email);
        
        if (!empty($payment_data['notes'])) {
            $message .= "\n";
            /* translators: %s: note left by the client. */
            $message .= sprintf(__('- Client note: %s', 'easy-invoice'), $payment_data['notes']);
        }

        // Add transaction ID if available
        if (!empty($payment_data['transaction_id'])) {
            $message .= "\n";
            /* translators: %s: transaction id. */
            $message .= sprintf(__('- Transaction ID: %s', 'easy-invoice'), $payment_data['transaction_id']);
        }
        
        $message .= "\n\n";
        $message .= __('Please review this payment in the admin dashboard:', 'easy-invoice');
        $message .= "\n";
        $message .= admin_url('admin.php?page=easy-invoice-payments&action=verify&invoice_id=' . $invoice_id);
        $message .= "\n\n";
        $message .= __('This is an automated message from Easy Invoice.', 'easy-invoice');
        
        return $message;
    }
    
    /**
     * Prepare payment confirmation message
     *
     * @param Invoice $invoice The invoice
     * @param string $formatted_amount Formatted amount
     * @return string Email message
     */
    private function preparePaymentConfirmationMessage(Invoice $invoice, string $formatted_amount): string {
        $customer_name = $invoice->getCustomerName();
        $invoice_number = $invoice->getNumber();
        $site_name = get_bloginfo('name');
        $company_name = get_option('easy_invoice_company_name', $site_name);
        
        /* translators: %s: customer name. */
        /* translators: %s: customer name. */
        $message = sprintf(__('Dear %s,', 'easy-invoice'), $customer_name);
        $message .= "\n\n";
        $message .= sprintf(
            /* translators: %1$s: amount paid; %2$s: invoice number. */
            __('We are pleased to confirm that your payment of %1$s for Invoice #%2$s has been received and processed successfully.', 'easy-invoice'),
            $formatted_amount,
            $invoice_number
        );
        $message .= "\n\n";
        $message .= __('Thank you for your business.', 'easy-invoice');
        $message .= "\n\n";
        $message .= __('Regards,', 'easy-invoice');
        $message .= "\n";
        $message .= $company_name;
        
        return $message;
    }
    
    /**
     * Prepare payment rejection message
     *
     * @param Invoice $invoice The invoice
     * @param string $reason Rejection reason
     * @return string Email message
     */
    private function preparePaymentRejectionMessage(Invoice $invoice, string $reason = ''): string {
        $customer_name = $invoice->getCustomerName();
        $invoice_number = $invoice->getNumber();
        $site_name = get_bloginfo('name');
        $company_name = get_option('easy_invoice_company_name', $site_name);
        
        /* translators: %s: customer name. */
        /* translators: %s: customer name. */
        $message = sprintf(__('Dear %s,', 'easy-invoice'), $customer_name);
        $message .= "\n\n";
        $message .= sprintf(
            /* translators: %s: invoice number. */
            __('We regret to inform you that your payment for Invoice #%s has been rejected.', 'easy-invoice'),
            $invoice_number
        );
        
        if (!empty($reason)) {
            $message .= "\n\n";
            $message .= __('Reason:', 'easy-invoice');
            $message .= "\n";
            $message .= $reason;
        }
        
        $message .= "\n\n";
        $message .= __('Please contact us if you have any questions or concerns.', 'easy-invoice');
        $message .= "\n\n";
        $message .= __('Regards,', 'easy-invoice');
        $message .= "\n";
        $message .= $company_name;
        
        return $message;
    }
    
    /**
     * Send admin notification for quote acceptance/decline
     *
     * @param Quote $quote The quote
     * @param string $action Action type ('accepted' or 'declined')
     * @return array Result array with success status and message
     */
    public function sendAdminQuoteNotification(Quote $quote, string $action = 'accepted'): array {
        try {
            // Validate quote
            if (!$quote || !$quote->getId()) {
                return ['success' => false, 'message' => __('Invalid quote', 'easy-invoice')];
            }
            
            // Get admin email
            $admin_email = $this->settings['admin_email'] ?? get_option('admin_email');
            if (empty($admin_email)) {
                return ['success' => false, 'message' => __('Admin email is missing', 'easy-invoice')];
            }
            
            // Prepare email subject
            $subject = sprintf(
                /* translators: %1$s: document number; %2$s: value. */
                __('Quote %1$s has been %2$s', 'easy-invoice'),
                $quote->getNumber(),
                $action === 'accepted' ? __('accepted', 'easy-invoice') : __('declined', 'easy-invoice')
            );
            
            // Prepare email message
            $message = $this->prepareAdminQuoteNotificationMessage($quote, $action);
            
            // Add HTML wrapper if enabled
            if ($this->settings['enable_html'] === 'yes') {
                $message = $this->wrapInHtmlTemplate($message);
            }
            
            // Prepare headers
            $headers = $this->prepareEmailHeaders();
            
            // Send email
            $sent = $this->sendEmail($admin_email, $subject, $message, $headers);
            
            if ($sent) {
                do_action('easy_invoice_admin_quote_notification_sent', $quote, $admin_email, $action);
                return [
                    'success' => true,
                    'message' => __('Admin notification sent successfully', 'easy-invoice')
                ];
            } else {
                do_action('easy_invoice_admin_quote_notification_failed', $quote, $admin_email, $action);
                return ['success' => false, 'message' => __('Failed to send admin notification', 'easy-invoice')];
            }
            
        } catch (\Exception $e) {
            $this->log('Admin quote notification error: ' . $e->getMessage(), 'error');
            return ['success' => false, 'message' => __('Error sending admin notification: ', 'easy-invoice') . $e->getMessage()];
        }
    }
    
    /**
     * Prepare admin quote notification message
     *
     * @param Quote $quote The quote
     * @param string $action Action type ('accepted' or 'declined')
     * @return string Email message
     */
    private function prepareAdminQuoteNotificationMessage(Quote $quote, string $action): string {
        $site_name = get_bloginfo('name');
        $quote_number = $quote->getNumber();
        $customer_name = $quote->getCustomerName();
        
        // Format amount
        $formatter = new \EasyInvoice\Helpers\InvoiceFormatter($quote);
        $formatted_amount = $formatter->format($quote->getTotal());
        
        $action_label = $action === 'accepted' ? __('accepted', 'easy-invoice') : __('declined', 'easy-invoice');
        $date_label = $action === 'accepted' ? __('Accepted Date', 'easy-invoice') : __('Declined Date', 'easy-invoice');
        
        /* translators: . */
        $message = __('Hello,', 'easy-invoice');
        $message .= "\n\n";
        $message .= sprintf(
            /* translators: %1$s: quote number; %2$s: quote title; %3$s: accepted or declined. */
            __('The quote %1$s for %2$s has been %3$s by the client.', 'easy-invoice'),
            $quote_number,
            $customer_name,
            $action_label
        );
        $message .= "\n\n";
        /* translators: . */
        $message .= __('Quote Details:', 'easy-invoice');
        $message .= "\n";
        /* translators: %s: quote number. */
        $message .= sprintf(__('- Quote Number: %s', 'easy-invoice'), $quote_number);
        $message .= "\n";
        /* translators: %s: customer name. */
        $message .= sprintf(__('- Client: %s', 'easy-invoice'), $customer_name);
        $message .= "\n";
        /* translators: %s: amount. */
        $message .= sprintf(__('- Total Amount: %s', 'easy-invoice'), $formatted_amount);
        $message .= "\n";
        /* translators: %1$s: label such as "Accepted on"; %2$s: date and time. */
        $message .= sprintf(__('- %1$s: %2$s', 'easy-invoice'), $date_label, date_i18n(get_option('date_format') . ' ' . get_option('time_format')));
        $message .= "\n\n";
        $message .= __('You can view the quote at:', 'easy-invoice');
        $message .= "\n";
        $message .= get_permalink($quote->getId());
        $message .= "\n\n";
        $message .= __('Best regards,', 'easy-invoice');
        $message .= "\n";
        $message .= $site_name;
        
        return $message;
    }
    
    /**
     * Handle payment completed hook
     * Sends admin notification and customer confirmation when payment is completed
     *
     * @param int $invoice_id Invoice ID
     * @param \EasyInvoice\Models\Invoice $invoice Invoice object
     * @param array $payment_data Payment data (method, gateway, transaction_id, amount)
     * @return void
     */
    public function handlePaymentCompleted(int $invoice_id, $invoice, array $payment_data = []): void {
        if (!$invoice || !$invoice->getId()) {
            return;
        }
        
        // Send admin notification
        $this->sendAdminPaymentNotification($invoice, $payment_data);
        
        // Send customer confirmation email using proper template system
        // Check if payment email is enabled first
        if (isset($this->templates['invoice_paid']) && $this->templates['invoice_paid']['enabled']) {
            $this->sendInvoiceEmail($invoice, 'paid', array_merge($payment_data, ['skip_bcc' => true, 'payment_receipt' => true]));
        }
    }
    
    /**
     * Get payment method label
     *
     * @param string $method Payment method
     * @return string Payment method label
     */
    private function getPaymentMethodLabel(string $method): string {
        $labels = [
            'bank' => __('Bank Transfer', 'easy-invoice'),
            'bank_transfer' => __('Bank Transfer', 'easy-invoice'),
            'cash' => __('Cash', 'easy-invoice'),
            'check' => __('Cheque', 'easy-invoice'),
            'cheque' => __('Cheque', 'easy-invoice'),
            'paystack' => __('Paystack', 'easy-invoice'),
            'moneris' => __('Moneris', 'easy-invoice'),
            'other' => __('Other', 'easy-invoice'),
            'paypal' => __('PayPal', 'easy-invoice'),
            'stripe' => __('Stripe', 'easy-invoice'),
            'square' => __('Square', 'easy-invoice'),
            'mollie' => __('Mollie', 'easy-invoice'),
            'authorizenet' => __('Authorize.Net', 'easy-invoice'),
            'manual' => __('Manual Payment', 'easy-invoice'),
            'online' => __('Online Payment', 'easy-invoice'),
        ];
        
        return $labels[$method] ?? ucfirst($method);
    }
    
} 
```
