PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.7
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.7
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Services / EmailManager.php

EmailManager.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.7, at includes/Services/EmailManager.php

1,913 lines 76.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Email Manager Service
4 *
5 * Handles all email functionality for Easy Invoice
6 *
7 * @package EasyInvoice
8 * @subpackage Services
9 * @since 1.0.0
10 */
11
12 namespace EasyInvoice\Services;
13
14 use EasyInvoice\Models\Invoice;
15 use EasyInvoice\Models\Quote;
16 use EasyInvoice\Models\Client;
17
18 if (!defined('ABSPATH')) {
19 exit;
20 }
21
22 /**
23 * EmailManager Class
24 *
25 * Centralized email management for Easy Invoice
26 */
27 class EmailManager extends BaseService {
28
29 /**
30 * Singleton instance
31 *
32 * @var EmailManager|null
33 */
34 private static $instance = null;
35
36 /**
37 * Service name
38 *
39 * @var string
40 */
41 protected $service_name = 'email_manager';
42
43 /**
44 * Email templates
45 *
46 * @var array
47 */
48 private $templates = [];
49
50 /**
51 * Email settings
52 *
53 * @var array
54 */
55 private $settings = [];
56
57 /**
58 * Get singleton instance
59 *
60 * @return EmailManager
61 */
62 public static function getInstance(): EmailManager {
63 if (self::$instance === null) {
64 self::$instance = new self();
65 }
66 return self::$instance;
67 }
68
69 /**
70 * Constructor
71 */
72 public function __construct() {
73 parent::__construct();
74 $this->loadSettings();
75 $this->loadTemplates();
76 $this->initHooks();
77 }
78
79 /**
80 * Initialize hooks
81 */
82 private function initHooks(): void {
83 // Invoice/quote send-email AJAX is handled exclusively by EasyInvoice\Admin\EasyInvoiceAjax
84 // (published-document checks, single handler) to avoid duplicate nopriv callbacks.
85
86 // Add email settings to admin
87 add_action('admin_init', [$this, 'registerEmailSettings']);
88 add_filter('easy_invoice_settings_sections', [$this, 'addEmailSettingsSection']);
89
90 // Refresh settings when they're updated
91 add_action('update_option_easy_invoice_email_from_name', [$this, 'refreshSettings']);
92 add_action('update_option_easy_invoice_email_from_address', [$this, 'refreshSettings']);
93 add_action('update_option_easy_invoice_email_reply_to', [$this, 'refreshSettings']);
94 add_action('update_option_easy_invoice_email_reply_to_name', [$this, 'refreshSettings']);
95 add_action('update_option_easy_invoice_enable_email_styling', [$this, 'refreshSettings']);
96 add_action('update_option_easy_invoice_email_logo', [$this, 'refreshSettings']);
97 add_action('update_option_easy_invoice_email_footer_text', [$this, 'refreshSettings']);
98 add_action('update_option_easy_invoice_bcc_admin', [$this, 'refreshSettings']);
99 add_action('update_option_easy_invoice_admin_email', [$this, 'refreshSettings']);
100 add_action('update_option_easy_invoice_email_subject', [$this, 'refreshSettings']);
101 add_action('update_option_easy_invoice_email_body', [$this, 'refreshSettings']);
102 add_action('update_option_easy_invoice_quote_subject', [$this, 'refreshSettings']);
103 add_action('update_option_easy_invoice_quote_body', [$this, 'refreshSettings']);
104
105 // Add email logs
106 add_action('easy_invoice_email_sent', [$this, 'logEmailSent'], 10, 3);
107 add_action('easy_invoice_email_failed', [$this, 'logEmailFailed'], 10, 3);
108
109 // Listen for payment completion to send admin notifications
110 add_action('easy_invoice_payment_completed', [$this, 'handlePaymentCompleted'], 10, 3);
111 }
112
113 /**
114 * Load email settings
115 */
116 private function loadSettings(): void {
117 $this->settings = [
118 'from_name' => get_option('easy_invoice_email_from_name', get_bloginfo('name')),
119 'from_email' => get_option('easy_invoice_email_from_address', get_bloginfo('admin_email')),
120 'reply_to_email' => get_option('easy_invoice_email_reply_to', ''),
121 'reply_to_name' => get_option('easy_invoice_email_reply_to_name', ''),
122 'enable_html' => get_option('easy_invoice_enable_email_styling', 'yes'),
123 'email_logo' => get_option('easy_invoice_email_logo', ''),
124 'footer_text' => get_option('easy_invoice_email_footer_text', ''),
125 'bcc_admin' => get_option('easy_invoice_bcc_admin', 'no'),
126 'admin_email' => get_option('easy_invoice_admin_email', get_option('admin_email')),
127 ];
128 }
129
130 /**
131 * Load email templates
132 */
133 private function loadTemplates(): void {
134 $this->templates = [
135 'invoice_new' => [
136 'enabled' => get_option('easy_invoice_invoice_email_enabled', 'yes') === 'yes',
137 'subject' => get_option('easy_invoice_invoice_email_subject', __('Your Invoice #{{invoice_number}} from {{company_name}}', 'easy-invoice')),
138 'body' => get_option('easy_invoice_invoice_email_body', $this->getDefaultInvoiceTemplate()),
139 'type' => 'invoice'
140 ],
141 'invoice_reminder' => [
142 'enabled' => get_option('easy_invoice_invoice_email_enabled', 'yes') === 'yes',
143 'subject' => get_option('easy_invoice_reminder_subject', 'Payment Reminder - Invoice #{{invoice_number}}'),
144 'body' => get_option('easy_invoice_reminder_body', $this->getDefaultReminderTemplate()),
145 'type' => 'invoice'
146 ],
147 'invoice_paid' => [
148 'enabled' => get_option('easy_invoice_payment_email_enabled', 'yes') === 'yes',
149 'subject' => get_option('easy_invoice_payment_email_subject', __('Payment Received - Invoice #{{invoice_number}}', 'easy-invoice')),
150 'body' => get_option('easy_invoice_payment_email_body', $this->getDefaultPaymentTemplate()),
151 'type' => 'invoice'
152 ],
153 'quote_new' => [
154 'enabled' => get_option('easy_invoice_quote_email_enabled', 'yes') === 'yes',
155 'subject' => get_option('easy_invoice_quote_email_subject', __('Your Quote #{{quote_number}} from {{company_name}}', 'easy-invoice')),
156 'body' => get_option('easy_invoice_quote_email_body', $this->getDefaultQuoteTemplate()),
157 'type' => 'quote'
158 ],
159 'quote_accepted' => [
160 'enabled' => get_option('easy_invoice_quote_email_enabled', 'yes') === 'yes',
161 'subject' => get_option('easy_invoice_quote_accepted_subject', 'Quote Accepted - #{{quote_number}}'),
162 'body' => get_option('easy_invoice_quote_accepted_body', $this->getDefaultQuoteAcceptedTemplate()),
163 'type' => 'quote'
164 ],
165 'quote_declined' => [
166 'enabled' => get_option('easy_invoice_quote_email_enabled', 'yes') === 'yes',
167 'subject' => get_option('easy_invoice_quote_declined_subject', 'Quote Declined - #{{quote_number}}'),
168 'body' => get_option('easy_invoice_quote_declined_body', $this->getDefaultQuoteDeclinedTemplate()),
169 'type' => 'quote'
170 ]
171 ];
172 }
173
174 /**
175 * Send invoice email
176 *
177 * @param Invoice $invoice The invoice
178 * @param string $template_type Template type (new, reminder, paid)
179 * @param array $additional_data Additional data for template
180 * @return array Result array with success status and message
181 */
182 public function sendInvoiceEmail(Invoice $invoice, string $template_type = 'new', array $additional_data = []): array {
183 try {
184 // Validate invoice
185 if (!$invoice || !$invoice->getId()) {
186 return ['success' => false, 'message' => __('Invalid invoice', 'easy-invoice')];
187 }
188
189 // Get client email
190 $client_email = $invoice->getCustomerEmail();
191 if (empty($client_email)) {
192 return ['success' => false, 'message' => __('Client email is missing', 'easy-invoice')];
193 }
194
195 // Get template
196 $template_key = 'invoice_' . $template_type;
197 if (!isset($this->templates[$template_key])) {
198 return ['success' => false, 'message' => __('Email template not found', 'easy-invoice')];
199 }
200
201 $template = $this->templates[$template_key];
202
203 // Check if email is enabled
204 if (!$template['enabled']) {
205 return ['success' => false, 'message' => __('Invoice available email is disabled', 'easy-invoice')];
206 }
207
208 // Prepare email data
209 $email_data = $this->prepareInvoiceEmailData($invoice, $template, $additional_data);
210
211 // Send email
212 $sent = $this->sendEmail(
213 $email_data['to'],
214 $email_data['subject'],
215 $email_data['message'],
216 $email_data['headers']
217 );
218
219 if ($sent) {
220 // Log success
221 do_action('easy_invoice_email_sent', $invoice, $client_email, $template_type);
222
223 return [
224 'success' => true,
225 'message' => __('Email sent successfully', 'easy-invoice'),
226 'email_data' => $email_data
227 ];
228 } else {
229 // Log failure
230 do_action('easy_invoice_email_failed', $invoice, $client_email, $template_type);
231
232 return ['success' => false, 'message' => __('Failed to send email', 'easy-invoice')];
233 }
234
235 } catch (\Exception $e) {
236 $this->log('Email sending error: ' . $e->getMessage(), 'error');
237 return ['success' => false, 'message' => __('Error sending email: ', 'easy-invoice') . $e->getMessage()];
238 }
239 }
240
241 /**
242 * Send quote email
243 *
244 * @param Quote $quote The quote
245 * @param string $template_type Template type (new, accepted, declined)
246 * @param array $additional_data Additional data for template
247 * @return array Result array with success status and message
248 */
249 public function sendQuoteEmail(Quote $quote, string $template_type = 'new', array $additional_data = []): array {
250 try {
251 // Validate quote
252 if (!$quote || !$quote->getId()) {
253 return ['success' => false, 'message' => __('Invalid quote', 'easy-invoice')];
254 }
255
256 // Get client email
257 $client_email = $quote->getCustomerEmail();
258 if (empty($client_email)) {
259 return ['success' => false, 'message' => __('Client email is missing', 'easy-invoice')];
260 }
261
262 // Get template
263 $template_key = 'quote_' . $template_type;
264 if (!isset($this->templates[$template_key])) {
265 return ['success' => false, 'message' => __('Email template not found', 'easy-invoice')];
266 }
267
268 $template = $this->templates[$template_key];
269
270 // Check if email is enabled
271 if (!$template['enabled']) {
272 return ['success' => false, 'message' => __('Quote available email is disabled', 'easy-invoice')];
273 }
274
275 // Prepare email data
276 $email_data = $this->prepareQuoteEmailData($quote, $template, $additional_data);
277
278 // Send email
279 $sent = $this->sendEmail(
280 $email_data['to'],
281 $email_data['subject'],
282 $email_data['message'],
283 $email_data['headers']
284 );
285
286 if ($sent) {
287 // Log success
288 do_action('easy_invoice_quote_email_sent', $quote, $client_email, $template_type);
289
290 return [
291 'success' => true,
292 'message' => __('Quote email sent successfully', 'easy-invoice'),
293 'email_data' => $email_data
294 ];
295 } else {
296 // Log failure
297 do_action('easy_invoice_quote_email_failed', $quote, $client_email, $template_type);
298
299 return ['success' => false, 'message' => __('Failed to send quote email', 'easy-invoice')];
300 }
301
302 } catch (\Exception $e) {
303 $this->log('Quote email sending error: ' . $e->getMessage(), 'error');
304 return ['success' => false, 'message' => __('Error sending quote email: ', 'easy-invoice') . $e->getMessage()];
305 }
306 }
307
308 /**
309 * Prepare invoice email data
310 *
311 * @param Invoice $invoice The invoice
312 * @param array $template The email template
313 * @param array $additional_data Additional data
314 * @return array Email data
315 */
316 private function prepareInvoiceEmailData(Invoice $invoice, array $template, array $additional_data = []): array {
317 // Get replacements
318 $replacements = $this->getInvoiceReplacements($invoice, $additional_data);
319
320 // Process template
321 $subject = $this->processTemplate($template['subject'], $replacements);
322 $message = $this->processTemplate($template['body'], $replacements);
323 $message = do_shortcode($message); // Render shortcodes like [easy_invoice_url ...]
324
325 // Add HTML wrapper if enabled
326 if ($this->settings['enable_html'] === 'yes') {
327 $message = $this->wrapInHtmlTemplate($message);
328 }
329
330 // Prepare headers
331 $headers = $this->prepareEmailHeaders();
332
333 // Add BCC to admin if enabled
334 if ($this->settings['bcc_admin'] === 'yes' && !empty($this->settings['admin_email'])) {
335 $headers[] = 'Bcc: ' . $this->settings['admin_email'];
336 }
337
338 return [
339 'to' => $invoice->getCustomerEmail(),
340 'subject' => $subject,
341 'message' => $message,
342 'headers' => $headers
343 ];
344 }
345
346 /**
347 * Prepare quote email data
348 *
349 * @param Quote $quote The quote
350 * @param array $template The email template
351 * @param array $additional_data Additional data
352 * @return array Email data
353 */
354 private function prepareQuoteEmailData(Quote $quote, array $template, array $additional_data = []): array {
355 // Get replacements
356 $replacements = $this->getQuoteReplacements($quote, $additional_data);
357
358 // Process template
359 $subject = $this->processTemplate($template['subject'], $replacements);
360 $message = $this->processTemplate($template['body'], $replacements);
361
362 // Add HTML wrapper if enabled
363 if ($this->settings['enable_html'] === 'yes') {
364 $message = $this->wrapInHtmlTemplate($message);
365 }
366
367 // Prepare headers
368 $headers = $this->prepareEmailHeaders();
369
370 // Add BCC to admin if enabled
371 if ($this->settings['bcc_admin'] === 'yes' && !empty($this->settings['admin_email'])) {
372 $headers[] = 'Bcc: ' . $this->settings['admin_email'];
373 }
374
375 return [
376 'to' => $quote->getCustomerEmail(),
377 'subject' => $subject,
378 'message' => $message,
379 'headers' => $headers
380 ];
381 }
382
383 /**
384 * Prepare payment email data
385 *
386 * @param Invoice $invoice The invoice
387 * @param array $template The email template
388 * @param array $payment_data Payment data
389 * @return array Email data
390 */
391 private function preparePaymentEmailData(Invoice $invoice, array $template, array $payment_data = []): array {
392 // Get replacements
393 $replacements = $this->getPaymentReplacements($invoice, $payment_data);
394
395 // Process template
396 $subject = $this->processTemplate($template['subject'], $replacements);
397 $message = $this->processTemplate($template['body'], $replacements);
398
399 // Add HTML wrapper if enabled
400 if ($this->settings['enable_html'] === 'yes') {
401 $message = $this->wrapInHtmlTemplate($message);
402 }
403
404 // Prepare headers
405 $headers = $this->prepareEmailHeaders();
406
407 // Add BCC to admin if enabled
408 if ($this->settings['bcc_admin'] === 'yes' && !empty($this->settings['admin_email'])) {
409 $headers[] = 'Bcc: ' . $this->settings['admin_email'];
410 }
411
412 return [
413 'to' => $invoice->getCustomerEmail(),
414 'subject' => $subject,
415 'message' => $message,
416 'headers' => $headers
417 ];
418 }
419
420 /**
421 * Get invoice replacements
422 *
423 * @param Invoice $invoice The invoice
424 * @param array $additional_data Additional data
425 * @return array Replacements
426 */
427 private function getInvoiceReplacements(Invoice $invoice, array $additional_data = []): array {
428 $currency_symbol = get_option('easy_invoice_currency_symbol', '$');
429
430 // Secure link support
431 $invoice_url = get_permalink($invoice->getId());
432 $secure_links_enabled = get_option('easy_invoice_pro_enable_secure_links', 'no') === 'yes';
433 if ($secure_links_enabled && class_exists('\EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController')) {
434 $secure_url = \EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController::getInvoiceSecureLinkUrl($invoice->getId());
435 if ($secure_url) {
436 $invoice_url = $secure_url;
437 }
438 }
439
440 // SECURITY: attach a per-invoice access token to the outbound URL
441 // so the legitimate email recipient can submit manual payments
442 // without needing to log in. The token is verified server-side in
443 // PaymentController::submitManualPayment via
444 // InvoiceController::canSubmitPaymentForInvoice. Empty-token
445 // guard so a CSPRNG failure doesn't produce malformed `?ik=` URLs.
446 $invoice_access_token = \EasyInvoice\Controllers\InvoiceController::invoiceAccessToken((int) $invoice->getId());
447 if ($invoice_access_token !== '' && $invoice_url) {
448 $invoice_url = add_query_arg('ik', $invoice_access_token, $invoice_url);
449 }
450
451 // Get client data for additional fields
452 $client = null;
453 if ($invoice->getClientId()) {
454 $client_repository = new \EasyInvoice\Repositories\ClientRepository();
455 $client = $client_repository->find($invoice->getClientId());
456 }
457
458 return array_merge([
459 '{{invoice_number}}' => $invoice->getNumber(),
460 '{{invoice_title}}' => $invoice->getTitle(),
461 '{{client_name}}' => $invoice->getCustomerName(),
462 '{{client_email}}' => $invoice->getCustomerEmail(),
463 '{{client_address}}' => $invoice->getCustomerAddress(),
464 '{{client_first_name}}' => $client ? $client->getFirstName() : '',
465 '{{client_last_name}}' => $client ? $client->getLastName() : '',
466 '{{company_name}}' => get_bloginfo('name'),
467 '{{company_email}}' => $this->settings['from_email'],
468 '{{company_phone}}' => get_option('easy_invoice_company_phone', ''),
469 '{{company_address}}' => get_option('easy_invoice_company_address', ''),
470 '{{company_website}}' => get_option('easy_invoice_company_website', ''),
471 '{{total_amount}}' => (new \EasyInvoice\Helpers\InvoiceFormatter($invoice))->format($invoice->getTotal()),
472 '{{subtotal}}' => (new \EasyInvoice\Helpers\InvoiceFormatter($invoice))->format($invoice->getSubtotal()),
473 '{{tax_amount}}' => (new \EasyInvoice\Helpers\InvoiceFormatter($invoice))->format($invoice->getTaxAmount()),
474 '{{discount_amount}}' => (new \EasyInvoice\Helpers\InvoiceFormatter($invoice))->format($invoice->getDiscountAmount()),
475 '{{due_date}}' => date('F j, Y', strtotime($invoice->getDueDate())),
476 '{{issue_date}}' => date('F j, Y', strtotime($invoice->getIssueDate())),
477 '{{invoice_url}}' => $invoice_url, // Use correct (possibly secure) link
478 '{{payment_url}}' => add_query_arg('payment', '1', get_permalink($invoice->getId())),
479 '{{site_url}}' => get_site_url(),
480 '{{admin_url}}' => admin_url(),
481 '{{payment_terms}}' => get_option('easy_invoice_payment_terms', __('Due on receipt', 'easy-invoice')),
482 ], $additional_data);
483 }
484
485 /**
486 * Get quote replacements
487 *
488 * @param Quote $quote The quote
489 * @param array $additional_data Additional data
490 * @return array Replacements
491 */
492 private function getQuoteReplacements(Quote $quote, array $additional_data = []): array {
493 $currency_symbol = get_option('easy_invoice_currency_symbol', '$');
494
495 $quote_url = get_permalink($quote->getId());
496 $secure_links_enabled = get_option('easy_invoice_pro_enable_secure_links', 'no') === 'yes';
497 if ($secure_links_enabled && class_exists('\EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController')) {
498 $secure_url = \EasyInvoicePro\Addons\SecureLinks\Controllers\PermalinkController::getQuoteSecureLinkUrl($quote->getId());
499 if ($secure_url) {
500 $quote_url = $secure_url;
501 }
502 }
503
504 // SECURITY (CVE-2026-9021): attach the per-quote access token so
505 // the emailed recipient lands on a page that renders the
506 // Accept/Decline UI and can submit either action without
507 // authenticating. Without the token the public single-quote page
508 // is read-only (no buttons, no nonce in DOM). Lazily generates
509 // the token on first send. The query parameter name is
510 // intentionally short ('qk') and opaque — leaking it via referer
511 // headers is no worse than leaking the secure-link signature.
512 $quote_access_token = \EasyInvoice\Controllers\QuoteController::quoteAccessToken((int) $quote->getId());
513 if ($quote_access_token !== '' && $quote_url) {
514 $quote_url = add_query_arg('qk', $quote_access_token, $quote_url);
515 }
516
517 // Get client data for additional fields
518 $client = null;
519 if ($quote->getClientId()) {
520 $client_repository = new \EasyInvoice\Repositories\ClientRepository();
521 $client = $client_repository->find($quote->getClientId());
522 }
523
524 return array_merge([
525 '{{quote_number}}' => $quote->getNumber(),
526 '{{quote_title}}' => $quote->getTitle(),
527 '{{client_name}}' => $quote->getCustomerName(),
528 '{{client_email}}' => $quote->getCustomerEmail(),
529 '{{client_address}}' => $quote->getCustomerAddress(),
530 '{{client_first_name}}' => $client ? $client->getFirstName() : '',
531 '{{client_last_name}}' => $client ? $client->getLastName() : '',
532 '{{company_name}}' => get_bloginfo('name'),
533 '{{company_email}}' => $this->settings['from_email'],
534 '{{company_phone}}' => get_option('easy_invoice_company_phone', ''),
535 '{{company_address}}' => get_option('easy_invoice_company_address', ''),
536 '{{company_website}}' => get_option('easy_invoice_company_website', ''),
537 '{{total_amount}}' => (new \EasyInvoice\Helpers\QuoteFormatter($quote))->format($quote->getTotal()),
538 '{{subtotal}}' => (new \EasyInvoice\Helpers\QuoteFormatter($quote))->format($quote->getSubtotal()),
539 '{{tax_amount}}' => (new \EasyInvoice\Helpers\QuoteFormatter($quote))->format($quote->getTaxAmount()),
540 '{{discount_amount}}' => (new \EasyInvoice\Helpers\QuoteFormatter($quote))->format($quote->getDiscountAmount()),
541 '{{expiry_date}}' => date('F j, Y', strtotime($quote->getExpiryDate())),
542 '{{issue_date}}' => date('F j, Y', strtotime($quote->getIssueDate())),
543 '{{quote_url}}' => $quote_url,
544 '{{site_url}}' => get_site_url(),
545 '{{admin_url}}' => admin_url(),
546 '{{payment_terms}}' => get_option('easy_invoice_payment_terms', __('Due on receipt', 'easy-invoice')),
547 ], $additional_data);
548 }
549
550 /**
551 * Get payment replacements
552 *
553 * @param Invoice $invoice The invoice
554 * @param array $payment_data Payment data
555 * @return array Replacements
556 */
557 private function getPaymentReplacements(Invoice $invoice, array $payment_data = []): array {
558 $replacements = $this->getInvoiceReplacements($invoice, $payment_data);
559
560 // Add payment-specific replacements
561 $replacements['{{payment_amount}}'] = isset($payment_data['amount']) ? $this->formatCurrency($payment_data['amount']) : $this->formatCurrency($invoice->getTotal());
562 $replacements['{{payment_date}}'] = isset($payment_data['date']) ? $payment_data['date'] : current_time('Y-m-d');
563 $replacements['{{payment_method}}'] = isset($payment_data['method']) ? $payment_data['method'] : __('Online Payment', 'easy-invoice');
564 $replacements['{{transaction_id}}'] = isset($payment_data['transaction_id']) ? $payment_data['transaction_id'] : __('N/A', 'easy-invoice');
565 $replacements['{{acceptance_date}}'] = isset($payment_data['acceptance_date']) ? $payment_data['acceptance_date'] : current_time('Y-m-d');
566 $replacements['{{response_date}}'] = isset($payment_data['response_date']) ? $payment_data['response_date'] : current_time('Y-m-d');
567 $replacements['{{decline_reason}}'] = isset($payment_data['decline_reason']) ? $payment_data['decline_reason'] : __('No specific reason provided', 'easy-invoice');
568
569 return $replacements;
570 }
571
572 /**
573 * Process template with replacements
574 *
575 * @param string $template The template
576 * @param array $replacements The replacements
577 * @return string Processed template
578 */
579 private function processTemplate(string $template, array $replacements): string {
580 return easy_invoice_str_replace(array_keys($replacements), array_values($replacements), $template);
581 }
582
583 /**
584 * Prepare email headers
585 *
586 * @return array Headers
587 */
588 private function prepareEmailHeaders(): array {
589 $headers = [
590 'Content-Type: text/html; charset=UTF-8',
591 'From: ' . $this->settings['from_name'] . ' <' . $this->settings['from_email'] . '>',
592 ];
593
594 // Add Reply-To if set
595 if (!empty($this->settings['reply_to_email'])) {
596 $reply_to_name = !empty($this->settings['reply_to_name']) ? $this->settings['reply_to_name'] : $this->settings['from_name'];
597 $headers[] = 'Reply-To: ' . $reply_to_name . ' <' . $this->settings['reply_to_email'] . '>';
598 }
599
600 return $headers;
601 }
602
603 /**
604 * Wrap message in HTML template
605 *
606 * @param string $message The message
607 * @return string HTML wrapped message
608 */
609 private function wrapInHtmlTemplate(string $message): string {
610 $logo_html = '';
611 if (!empty($this->settings['email_logo'])) {
612 $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>';
613 }
614
615 $footer_html = '';
616 if (!empty($this->settings['footer_text'])) {
617 $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>';
618 }
619
620 return '
621 <!DOCTYPE html>
622 <html>
623 <head>
624 <meta charset="UTF-8">
625 <meta name="viewport" content="width=device-width, initial-scale=1.0">
626 <title>' . esc_html($this->settings['from_name']) . '</title>
627 <style>
628 body {
629 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
630 line-height: 1.6;
631 color: #374151;
632 margin: 0;
633 padding: 0;
634 background-color: #f9fafb;
635 }
636 .email-container {
637 max-width: 600px;
638 margin: 0 auto;
639 background-color: #ffffff;
640 border-radius: 12px;
641 box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
642 overflow: hidden;
643 }
644 .email-header {
645 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
646 padding: 50px 30px;
647 text-align: center;
648 position: relative;
649 }
650 .email-header::before {
651 content: "";
652 position: absolute;
653 top: 0;
654 left: 0;
655 right: 0;
656 bottom: 0;
657 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");
658 opacity: 0.3;
659 }
660 .email-header h1 {
661 color: #ffffff;
662 margin: 0;
663 font-size: 28px;
664 font-weight: 700;
665 position: relative;
666 z-index: 1;
667 }
668 .email-content {
669 padding: 50px 40px;
670 background: #ffffff;
671 }
672 .email-content p {
673 margin: 0 0 20px 0;
674 color: #374151;
675 line-height: 1.7;
676 }
677 .email-content h2 {
678 color: #1f2937;
679 font-size: 28px;
680 font-weight: 700;
681 margin: 0 0 30px 0;
682 text-align: center;
683 }
684 .email-content h3 {
685 color: #374151;
686 font-size: 20px;
687 font-weight: 600;
688 margin: 0 0 16px 0;
689 }
690 .email-footer {
691 background-color: #f9fafb;
692 padding: 40px 30px;
693 text-align: center;
694 border-top: 1px solid #e5e7eb;
695 }
696 .email-footer p {
697 margin: 0;
698 color: #6b7280;
699 font-size: 14px;
700 }
701 .highlight-box {
702 background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
703 border-left: 4px solid #3b82f6;
704 padding: 30px;
705 margin: 30px 0;
706 border-radius: 0 12px 12px 0;
707 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
708 }
709 .highlight-box p {
710 margin: 0;
711 font-size: 16px;
712 line-height: 1.6;
713 }
714 .highlight-box strong {
715 color: #1f2937;
716 font-weight: 600;
717 }
718 .info-box {
719 background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
720 border: 1px solid #93c5fd;
721 border-radius: 12px;
722 padding: 25px;
723 margin: 30px 0;
724 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
725 }
726 .info-box p {
727 margin: 0;
728 color: #1e40af;
729 font-size: 15px;
730 line-height: 1.7;
731 }
732 .info-box strong {
733 color: #1e3a8a;
734 font-weight: 600;
735 }
736 .success-box {
737 background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
738 border: 1px solid #6ee7b7;
739 border-radius: 12px;
740 padding: 25px;
741 margin: 30px 0;
742 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
743 }
744 .success-box p {
745 margin: 0;
746 color: #065f46;
747 font-size: 15px;
748 line-height: 1.7;
749 }
750 .success-box strong {
751 color: #047857;
752 font-weight: 600;
753 }
754 .warning-box {
755 background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
756 border: 1px solid #f59e0b;
757 border-radius: 12px;
758 padding: 25px;
759 margin: 30px 0;
760 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
761 }
762 .warning-box p {
763 margin: 0;
764 color: #92400e;
765 font-size: 15px;
766 line-height: 1.7;
767 }
768 .warning-box strong {
769 color: #78350f;
770 font-weight: 600;
771 }
772 .button {
773 display: inline-block;
774 background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
775 color: #ffffff;
776 padding: 14px 28px;
777 text-decoration: none;
778 border-radius: 8px;
779 font-weight: 600;
780 margin: 20px 0;
781 box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3);
782 transition: all 0.2s ease;
783 }
784 .button:hover {
785 background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
786 transform: translateY(-1px);
787 box-shadow: 0 6px 8px -1px rgba(59, 130, 246, 0.4);
788 }
789 .divider {
790 height: 1px;
791 background: linear-gradient(90deg, transparent 0%, #e5e7eb 50%, transparent 100%);
792 margin: 40px 0;
793 }
794 .amount-highlight {
795 font-size: 28px;
796 font-weight: 700;
797 color: #059669;
798 text-align: center;
799 margin: 25px 0;
800 display: block;
801 }
802 .status-badge {
803 display: inline-block;
804 padding: 6px 12px;
805 border-radius: 20px;
806 font-size: 12px;
807 font-weight: 600;
808 text-transform: uppercase;
809 letter-spacing: 0.5px;
810 }
811 .status-paid {
812 background: #d1fae5;
813 color: #065f46;
814 }
815 .status-pending {
816 background: #fef3c7;
817 color: #92400e;
818 }
819 .status-overdue {
820 background: #fee2e2;
821 color: #991b1b;
822 }
823 @media only screen and (max-width: 600px) {
824 .email-content { padding: 25px 20px; }
825 .email-header { padding: 35px 20px; }
826 .email-header h1 { font-size: 24px; }
827 .email-content h2 { font-size: 22px; }
828 .highlight-box, .info-box, .success-box, .warning-box { padding: 20px; }
829 .amount-highlight { font-size: 24px; }
830 }
831 </style>
832 </head>
833 <body>
834 <div class="email-container">
835 ' . $logo_html . '
836 <div class="email-content">
837 ' . wpautop($message) . '
838 </div>
839 ' . $footer_html . '
840 </div>
841 </body>
842 </html>';
843 }
844
845 /**
846 * Register email settings
847 */
848 public function registerEmailSettings(): void {
849 // Email settings section
850 add_settings_section(
851 'easy_invoice_email_settings',
852 __('Email Configuration', 'easy-invoice'),
853 [$this, 'emailSettingsSectionCallback'],
854 'easy_invoice_settings'
855 );
856
857 // Register settings
858 register_setting('easy_invoice_settings', 'easy_invoice_email_from_name');
859 register_setting('easy_invoice_settings', 'easy_invoice_email_from_address');
860 register_setting('easy_invoice_settings', 'easy_invoice_email_reply_to');
861 register_setting('easy_invoice_settings', 'easy_invoice_email_reply_to_name');
862 register_setting('easy_invoice_settings', 'easy_invoice_enable_email_styling');
863 register_setting('easy_invoice_settings', 'easy_invoice_email_logo');
864 register_setting('easy_invoice_settings', 'easy_invoice_email_footer_text');
865 register_setting('easy_invoice_settings', 'easy_invoice_bcc_admin');
866 register_setting('easy_invoice_settings', 'easy_invoice_admin_email');
867
868 // Add settings fields
869 add_settings_field(
870 'easy_invoice_email_from_name',
871 __('From Name', 'easy-invoice'),
872 [$this, 'textFieldCallback'],
873 'easy_invoice_settings',
874 'easy_invoice_email_settings',
875 ['label_for' => 'easy_invoice_email_from_name']
876 );
877
878 add_settings_field(
879 'easy_invoice_email_from_address',
880 __('From Email Address', 'easy-invoice'),
881 [$this, 'emailFieldCallback'],
882 'easy_invoice_settings',
883 'easy_invoice_email_settings',
884 ['label_for' => 'easy_invoice_email_from_address']
885 );
886
887 add_settings_field(
888 'easy_invoice_email_reply_to',
889 __('Reply-To Email', 'easy-invoice'),
890 [$this, 'emailFieldCallback'],
891 'easy_invoice_settings',
892 'easy_invoice_email_settings',
893 ['label_for' => 'easy_invoice_email_reply_to']
894 );
895
896 add_settings_field(
897 'easy_invoice_enable_email_styling',
898 __('Enable HTML Emails', 'easy-invoice'),
899 [$this, 'checkboxFieldCallback'],
900 'easy_invoice_settings',
901 'easy_invoice_email_settings',
902 ['label_for' => 'easy_invoice_enable_email_styling']
903 );
904
905 add_settings_field(
906 'easy_invoice_bcc_admin',
907 __('BCC Admin on All Emails', 'easy-invoice'),
908 [$this, 'checkboxFieldCallback'],
909 'easy_invoice_settings',
910 'easy_invoice_email_settings',
911 ['label_for' => 'easy_invoice_bcc_admin']
912 );
913 }
914
915 /**
916 * Add email settings section
917 *
918 * @param array $sections Settings sections
919 * @return array Modified sections
920 */
921 public function addEmailSettingsSection(array $sections): array {
922 $sections['email'] = [
923 'title' => __('Email Settings', 'easy-invoice'),
924 'description' => __('Configure email sending options and templates', 'easy-invoice'),
925 'icon' => 'fas fa-envelope',
926 'fields' => [
927 'easy_invoice_email_from_name' => [
928 'label' => __('From Name', 'easy-invoice'),
929 'type' => 'text',
930 'default' => get_bloginfo('name'),
931 'col_span' => 'sm:col-span-3'
932 ],
933 'easy_invoice_email_from_address' => [
934 'label' => __('From Email Address', 'easy-invoice'),
935 'type' => 'email',
936 'default' => get_bloginfo('admin_email'),
937 'col_span' => 'sm:col-span-3'
938 ],
939 'easy_invoice_email_reply_to' => [
940 'label' => __('Reply-To Email', 'easy-invoice'),
941 'type' => 'email',
942 'default' => '',
943 'col_span' => 'sm:col-span-3'
944 ],
945 'easy_invoice_enable_email_styling' => [
946 'label' => __('Enable HTML Emails', 'easy-invoice'),
947 'type' => 'checkbox',
948 'default' => 'yes',
949 'col_span' => 'sm:col-span-3'
950 ],
951 'easy_invoice_bcc_admin' => [
952 'label' => __('BCC Admin on All Emails', 'easy-invoice'),
953 'type' => 'checkbox',
954 'default' => 'no',
955 'col_span' => 'sm:col-span-3'
956 ],
957 'easy_invoice_email_logo' => [
958 'label' => __('Email Logo URL', 'easy-invoice'),
959 'type' => 'url',
960 'default' => '',
961 'col_span' => 'sm:col-span-6'
962 ],
963 'easy_invoice_email_footer_text' => [
964 'label' => __('Email Footer Text', 'easy-invoice'),
965 'type' => 'textarea',
966 'default' => '',
967 'col_span' => 'sm:col-span-6'
968 ],
969 ]
970 ];
971
972 return $sections;
973 }
974
975 /**
976 * Email settings section callback
977 */
978 public function emailSettingsSectionCallback(): void {
979 echo '<p>' . __('Configure how emails are sent from Easy Invoice.', 'easy-invoice') . '</p>';
980 }
981
982 /**
983 * Text field callback
984 *
985 * @param array $args Field arguments
986 */
987 public function textFieldCallback(array $args): void {
988 $field_id = $args['label_for'];
989 $value = get_option($field_id, '');
990 echo '<input type="text" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_id) . '" value="' . esc_attr($value) . '" class="regular-text">';
991 }
992
993 /**
994 * Email field callback
995 *
996 * @param array $args Field arguments
997 */
998 public function emailFieldCallback(array $args): void {
999 $field_id = $args['label_for'];
1000 $value = get_option($field_id, '');
1001 echo '<input type="email" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_id) . '" value="' . esc_attr($value) . '" class="regular-text">';
1002 }
1003
1004 /**
1005 * Checkbox field callback
1006 *
1007 * @param array $args Field arguments
1008 */
1009 public function checkboxFieldCallback(array $args): void {
1010 $field_id = $args['label_for'];
1011 $value = get_option($field_id, '');
1012 echo '<input type="checkbox" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_id) . '" value="yes"' . checked($value, 'yes', false) . '>';
1013 echo '<span class="description">' . __('Enable this option', 'easy-invoice') . '</span>';
1014 }
1015
1016 /**
1017 * Log email sent
1018 *
1019 * @param Invoice $invoice The invoice
1020 * @param string $email The email address
1021 * @param string $type The email type
1022 */
1023 public function logEmailSent($invoice, string $email, string $type): void {
1024 $this->log(sprintf('Email sent to %s for invoice #%s (%s)', $email, $invoice->getNumber(), $type), 'info');
1025 }
1026
1027 /**
1028 * Log email failed
1029 *
1030 * @param Invoice $invoice The invoice
1031 * @param string $email The email address
1032 * @param string $type The email type
1033 */
1034 public function logEmailFailed($invoice, string $email, string $type): void {
1035 $this->log(sprintf('Email failed to %s for invoice #%s (%s)', $email, $invoice->getNumber(), $type), 'error');
1036 }
1037
1038 /**
1039 * Get default invoice template
1040 *
1041 * @return string Template
1042 */
1043 private function getDefaultInvoiceTemplate(): string {
1044 return '<h2>📄 Your Invoice is Ready</h2>
1045
1046 <p>Dear {{client_name}},</p>
1047
1048 <div class="highlight-box">
1049 <p><strong>Invoice #{{invoice_number}}</strong><br>
1050 <span class="amount-highlight">{{total_amount}}</span><br>
1051 Due Date: <strong>{{due_date}}</strong></p>
1052 </div>
1053
1054 <p>Your invoice has been prepared and is ready for payment. You can view and download the complete invoice from the attachment or visit the link below.</p>
1055
1056 <div class="info-box">
1057 <p><strong>📋 Payment Details:</strong><br>
1058 • Invoice Number: {{invoice_number}}<br>
1059 • Total Amount: {{total_amount}}<br>
1060 • Due Date: {{due_date}}<br>
1061 • Payment Terms: {{payment_terms}}</p>
1062 </div>
1063
1064 <div class="highlight-box">
1065 <p><strong>🔗 View Invoice Online:</strong><br>
1066 <a href="{{invoice_url}}" style="color: #3b82f6; text-decoration: underline;">{{invoice_url}}</a></p>
1067 <p><strong>Shortcode:</strong> <code>[easy_invoice_url number="{{invoice_number}}" text="View Invoice"]</code></p>
1068 </div>
1069
1070 <div class="warning-box">
1071 <p><strong>⚠️ Important:</strong> Please ensure payment is received by the due date to avoid any late fees or service interruptions.</p>
1072 </div>
1073
1074 <p>If you have any questions about this invoice, please do not hesitate to contact us.</p>
1075
1076 <div class="divider"></div>
1077
1078 <p>Thank you for your business!</p>
1079
1080 <p>Best regards,<br>
1081 <strong>{{company_name}}</strong><br>
1082 {{company_email}}</p>';
1083 }
1084
1085 private function getDefaultReminderTemplate(): string {
1086 return '<h2>⏰ Payment Reminder</h2>
1087
1088 <p>Dear {{client_name}},</p>
1089
1090 <div class="warning-box">
1091 <p><strong>Invoice #{{invoice_number}}</strong><br>
1092 <span class="amount-highlight">{{total_amount}}</span><br>
1093 Due Date: <strong>{{due_date}}</strong></p>
1094 </div>
1095
1096 <p>This is a friendly reminder that payment for the above invoice is now due. If you have already made the payment, please disregard this message.</p>
1097
1098 <div class="info-box">
1099 <p><strong>💳 Payment Options:</strong><br>
1100 • Online payment through our secure portal<br>
1101 • Bank transfer to the details provided<br>
1102 • Check or money order</p>
1103 </div>
1104
1105 <div class="highlight-box">
1106 <p><strong>🔗 View Invoice Online:</strong><br>
1107 <a href="{{invoice_url}}" style="color: #3b82f6; text-decoration: underline;">{{invoice_url}}</a></p>
1108 <p><strong>Shortcode:</strong> <code>[easy_invoice_url number="{{invoice_number}}" text="View Invoice"]</code></p>
1109 </div>
1110
1111 <div class="highlight-box">
1112 <p><strong>📞 Need Help?</strong> If you have any questions or need to discuss payment arrangements, please contact us immediately.</p>
1113 </div>
1114
1115 <p>Thank you for your prompt attention to this matter.</p>
1116
1117 <div class="divider"></div>
1118
1119 <p>Best regards,<br>
1120 <strong>{{company_name}}</strong><br>
1121 {{company_email}}</p>';
1122 }
1123
1124 private function getDefaultPaymentTemplate(): string {
1125 return '<h2>�
1126 Payment Received - Thank You!</h2>
1127
1128 <p>Dear {{client_name}},</p>
1129
1130 <div class="success-box">
1131 <p><strong>Payment Confirmation</strong><br>
1132 Invoice #{{invoice_number}}<br>
1133 <span class="amount-highlight">{{payment_amount}}</span><br>
1134 Payment Date: <strong>{{payment_date}}</strong><br>
1135 Payment Method: <strong>{{payment_method}}</strong></p>
1136 </div>
1137
1138 <p>We have successfully received your payment. Thank you for your prompt payment!</p>
1139
1140 <div class="info-box">
1141 <p><strong>📊 Payment Details:</strong><br>
1142 Invoice Number: {{invoice_number}}<br>
1143 Amount Paid: {{payment_amount}}<br>
1144 Payment Date: {{payment_date}}<br>
1145 Payment Method: {{payment_method}}<br>
1146 Transaction ID: {{transaction_id}}</p>
1147 </div>
1148
1149 <div class="highlight-box">
1150 <p><strong>🎉 Status: PAID</strong><br>
1151 Your payment has been processed and your account is now up to date. We appreciate your business!</p>
1152 </div>
1153
1154 <p>If you have any questions about this payment or need a receipt, please don\'t hesitate to contact us.</p>
1155
1156 <div class="divider"></div>
1157
1158 <p>Thank you for choosing our services!</p>
1159
1160 <p>Best regards,<br>
1161 <strong>{{company_name}}</strong><br>
1162 {{company_email}}</p>';
1163 }
1164
1165 private function getDefaultQuoteTemplate(): string {
1166 return '<h2>📋 Your Quote is Ready</h2>
1167
1168 <p>Dear {{client_name}},</p>
1169
1170 <div class="highlight-box">
1171 <p><strong>Quote #{{quote_number}}</strong><br>
1172 <span class="amount-highlight">{{total_amount}}</span><br>
1173 Valid Until: <strong>{{expiry_date}}</strong></p>
1174 </div>
1175
1176 <p>We have prepared a detailed quote for your project. You can view and download the complete quote from the attachment or visit the link below.</p>
1177
1178 <div class="info-box">
1179 <p><strong>📋 Quote Summary:</strong><br>
1180 • Quote Number: {{quote_number}}<br>
1181 • Total Amount: {{total_amount}}<br>
1182 • Valid Until: {{expiry_date}}<br>
1183 • Terms: {{payment_terms}}</p>
1184 </div>
1185
1186 <div class="highlight-box">
1187 <p><strong>🔗 View Quote Online:</strong><br>
1188 <a href="{{quote_url}}" style="color: #3b82f6; text-decoration: underline;">{{quote_url}}</a></p>
1189 <p><strong>Shortcode:</strong> <code>[easy_quote_url number="{{quote_number}}" text="View Quote"]</code></p>
1190 </div>
1191
1192 <div class="warning-box">
1193 <p><strong>⏰ Time Sensitive:</strong> This quote is valid until {{expiry_date}}. Please review and respond within this timeframe.</p>
1194 </div>
1195
1196 <p>If you have any questions or would like to discuss any aspects of this quote, please contact us.</p>
1197
1198 <div class="divider"></div>
1199
1200 <p>We look forward to working with you!</p>
1201
1202 <p>Best regards,<br>
1203 <strong>{{company_name}}</strong><br>
1204 {{company_email}}</p>';
1205 }
1206
1207 private function getDefaultQuoteAcceptedTemplate(): string {
1208 return '<h2>🎉 Quote Accepted - Project Confirmed!</h2>
1209
1210 <p>Dear {{client_name}},</p>
1211
1212 <div class="success-box">
1213 <p><strong>Quote #{{quote_number}} - ACCEPTED</strong><br>
1214 <span class="amount-highlight">{{total_amount}}</span><br>
1215 Acceptance Date: <strong>{{acceptance_date}}</strong></p>
1216 </div>
1217
1218 <p>Thank you for accepting our quote! We\'re excited to begin working on your project.</p>
1219
1220 <div class="info-box">
1221 <p><strong>🚀 Next Steps:</strong><br>
1222 • We will create an invoice for the accepted quote<br>
1223 • You will receive payment instructions<br>
1224 • Project work will begin as scheduled</p>
1225 </div>
1226
1227 <div class="highlight-box">
1228 <p><strong>📞 What\'s Next?</strong> Our team will be in touch shortly with the next steps and any additional information you may need.</p>
1229 </div>
1230
1231 <div class="divider"></div>
1232
1233 <p>Thank you for choosing our services!</p>
1234
1235 <p>Best regards,<br>
1236 <strong>{{company_name}}</strong><br>
1237 {{company_email}}</p>';
1238 }
1239
1240 private function getDefaultQuoteDeclinedTemplate(): string {
1241 return '<h2>📝 Quote Response Received</h2>
1242
1243 <p>Dear {{client_name}},</p>
1244
1245 <div class="warning-box">
1246 <p><strong>Quote #{{quote_number}} - DECLINED</strong><br>
1247 Response Date: <strong>{{response_date}}</strong></p>
1248 </div>
1249
1250 <p>We have received your response regarding our quote. We understand that this quote may not have met your current needs.</p>
1251
1252 <div class="info-box">
1253 <p><strong>📋 Feedback:</strong><br>
1254 • Reason: {{decline_reason}}<br>
1255 • Response Date: {{response_date}}</p>
1256 </div>
1257
1258 <div class="highlight-box">
1259 <p><strong>🤝 Future Opportunities:</strong> We appreciate you taking the time to review our proposal. If your requirements change in the future, we would be happy to discuss new opportunities.</p>
1260 </div>
1261
1262 <div class="divider"></div>
1263
1264 <p>Thank you for considering our services!</p>
1265
1266 <p>Best regards,<br>
1267 <strong>{{company_name}}</strong><br>
1268 {{company_email}}</p>';
1269 }
1270
1271 /**
1272 * Refresh settings and templates
1273 * Call this method when settings are updated
1274 */
1275 public function refreshSettings(): void {
1276 $this->loadSettings();
1277 $this->loadTemplates();
1278 }
1279
1280 /**
1281 * Get email templates
1282 *
1283 * @return array Templates
1284 */
1285 public function getTemplates(): array {
1286 return $this->templates;
1287 }
1288
1289 /**
1290 * Get email settings
1291 *
1292 * @return array Settings
1293 */
1294 public function getSettings(): array {
1295 return $this->settings;
1296 }
1297
1298 /**
1299 * Test email functionality
1300 *
1301 * @param string $to_email Email to send test to
1302 * @return array Result
1303 */
1304 public function testEmail(string $to_email): array {
1305 $subject = 'Easy Invoice - Email Configuration Test';
1306 $message = '<h2>🧪 Email Configuration Test</h2>
1307
1308 <p>Hello!</p>
1309
1310 <div class="success-box">
1311 <p><strong>�
1312 Test Email Successfully Sent</strong><br>
1313 Date: <strong>' . current_time('Y-m-d H:i:s') . '</strong><br>
1314 To: <strong>' . esc_html($to_email) . '</strong></p>
1315 </div>
1316
1317 <p>This is a test email to verify that your Easy Invoice email configuration is working correctly.</p>
1318
1319 <div class="info-box">
1320 <p><strong>⚙️ Email Settings Verified:</strong><br>
1321 • From Name: ' . esc_html($this->settings['from_name']) . '<br>
1322 • From Email: ' . esc_html($this->settings['from_email']) . '<br>
1323 • Reply-To: ' . esc_html($this->settings['reply_to_email'] ?: 'Not set') . '<br>
1324 • HTML Emails: ' . ($this->settings['enable_html'] === 'yes' ? 'Enabled' : 'Disabled') . '</p>
1325 </div>
1326
1327 <div class="highlight-box">
1328 <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>
1329 </div>
1330
1331 <div class="divider"></div>
1332
1333 <p>Thank you for using Easy Invoice!</p>
1334
1335 <p>Best regards,<br>
1336 <strong>' . esc_html($this->settings['from_name']) . '</strong></p>';
1337
1338 if ($this->settings['enable_html'] === 'yes') {
1339 $message = $this->wrapInHtmlTemplate($message);
1340 }
1341
1342 $headers = $this->prepareEmailHeaders();
1343
1344 $sent = $this->sendEmail($to_email, $subject, $message, $headers);
1345
1346 if ($sent) {
1347 return ['success' => true, 'message' => __('Test email sent successfully', 'easy-invoice')];
1348 } else {
1349 return ['success' => false, 'message' => __('Failed to send test email', 'easy-invoice')];
1350 }
1351 }
1352
1353 /**
1354 * Send test template email with custom subject and body
1355 *
1356 * @param string $to_email Email to send test to
1357 * @param string $subject Email subject
1358 * @param string $body Email body
1359 * @return array Result
1360 */
1361 public function sendTestTemplateEmail(string $to_email, string $subject, string $body): array {
1362 if ($this->settings['enable_html'] === 'yes') {
1363 $body = $this->wrapInHtmlTemplate($body);
1364 }
1365
1366 $headers = $this->prepareEmailHeaders();
1367
1368 $sent = $this->sendEmail($to_email, $subject, $body, $headers);
1369
1370 if ($sent) {
1371 return ['success' => true, 'message' => __('Template test email sent successfully', 'easy-invoice')];
1372 } else {
1373 return ['success' => false, 'message' => __('Failed to send template test email', 'easy-invoice')];
1374 }
1375 }
1376
1377 /**
1378 * Send payment received email
1379 *
1380 * @param Invoice $invoice The invoice
1381 * @param array $payment_data Payment data
1382 * @return array Result array with success status and message
1383 */
1384 public function sendPaymentEmail(Invoice $invoice, array $payment_data = []): array {
1385 try {
1386 // Validate invoice
1387 if (!$invoice || !$invoice->getId()) {
1388 return ['success' => false, 'message' => __('Invalid invoice', 'easy-invoice')];
1389 }
1390
1391 // Get client email
1392 $client_email = $invoice->getCustomerEmail();
1393 if (empty($client_email)) {
1394 return ['success' => false, 'message' => __('Client email is missing', 'easy-invoice')];
1395 }
1396
1397 // Get template
1398 $template_key = 'invoice_paid';
1399 if (!isset($this->templates[$template_key])) {
1400 return ['success' => false, 'message' => __('Payment email template not found', 'easy-invoice')];
1401 }
1402
1403 $template = $this->templates[$template_key];
1404
1405 // Check if email is enabled
1406 if (!$template['enabled']) {
1407 return ['success' => false, 'message' => __('Payment received email is disabled', 'easy-invoice')];
1408 }
1409
1410 // Prepare email data
1411 $email_data = $this->preparePaymentEmailData($invoice, $template, $payment_data);
1412
1413 // Send email
1414 $sent = $this->sendEmail(
1415 $email_data['to'],
1416 $email_data['subject'],
1417 $email_data['message'],
1418 $email_data['headers']
1419 );
1420
1421 if ($sent) {
1422 // Log success
1423 do_action('easy_invoice_payment_email_sent', $invoice, $client_email, $payment_data);
1424
1425 return [
1426 'success' => true,
1427 'message' => __('Payment email sent successfully', 'easy-invoice'),
1428 'email_data' => $email_data
1429 ];
1430 } else {
1431 // Log failure
1432 do_action('easy_invoice_payment_email_failed', $invoice, $client_email, $payment_data);
1433
1434 return ['success' => false, 'message' => __('Failed to send payment email', 'easy-invoice')];
1435 }
1436
1437 } catch (\Exception $e) {
1438 $this->log('Payment email sending error: ' . $e->getMessage(), 'error');
1439 return ['success' => false, 'message' => __('Error sending payment email: ', 'easy-invoice') . $e->getMessage()];
1440 }
1441 }
1442
1443 /**
1444 * Send admin notification when payment is received
1445 *
1446 * @param Invoice $invoice The invoice
1447 * @param array $payment_data Payment data (method, amount, etc.)
1448 * @return array Result array with success status and message
1449 */
1450 public function sendAdminPaymentNotification(Invoice $invoice, array $payment_data = []): array {
1451 try {
1452 // Validate invoice
1453 if (!$invoice || !$invoice->getId()) {
1454 return ['success' => false, 'message' => __('Invalid invoice', 'easy-invoice')];
1455 }
1456
1457 // Get admin email
1458 $admin_email = $this->settings['admin_email'] ?? get_option('admin_email');
1459 if (empty($admin_email)) {
1460 return ['success' => false, 'message' => __('Admin email is missing', 'easy-invoice')];
1461 }
1462
1463 // Get payment method
1464 $payment_method = $payment_data['payment_method'] ?? $payment_data['method'] ?? 'online';
1465 $payment_method_label = $this->getPaymentMethodLabel($payment_method);
1466
1467 // Format amount
1468 $formatter = new \EasyInvoice\Helpers\InvoiceFormatter($invoice);
1469 $amount = $formatter->format($invoice->getTotal());
1470
1471 // Prepare email subject
1472 $subject = sprintf(
1473 __('New Payment Received - Invoice #%s', 'easy-invoice'),
1474 $invoice->getNumber()
1475 );
1476
1477 // Prepare email message
1478 $message = $this->prepareAdminPaymentNotificationMessage($invoice, $payment_method_label, $amount, $payment_data);
1479
1480 // Add HTML wrapper if enabled
1481 if ($this->settings['enable_html'] === 'yes') {
1482 $message = $this->wrapInHtmlTemplate($message);
1483 }
1484
1485 // Prepare headers
1486 $headers = $this->prepareEmailHeaders();
1487
1488 // Send email
1489 $sent = $this->sendEmail($admin_email, $subject, $message, $headers);
1490
1491 if ($sent) {
1492 do_action('easy_invoice_admin_payment_notification_sent', $invoice, $admin_email, $payment_data);
1493 return [
1494 'success' => true,
1495 'message' => __('Admin notification sent successfully', 'easy-invoice')
1496 ];
1497 } else {
1498 do_action('easy_invoice_admin_payment_notification_failed', $invoice, $admin_email, $payment_data);
1499 return ['success' => false, 'message' => __('Failed to send admin notification', 'easy-invoice')];
1500 }
1501
1502 } catch (\Exception $e) {
1503 $this->log('Admin payment notification error: ' . $e->getMessage(), 'error');
1504 return ['success' => false, 'message' => __('Error sending admin notification: ', 'easy-invoice') . $e->getMessage()];
1505 }
1506 }
1507
1508 /**
1509 * Send payment confirmation email to customer
1510 *
1511 * @param Invoice $invoice The invoice
1512 * @param array $payment_data Payment data
1513 * @return array Result array with success status and message
1514 */
1515 public function sendPaymentConfirmationEmail(Invoice $invoice, array $payment_data = []): array {
1516 try {
1517 // Validate invoice
1518 if (!$invoice || !$invoice->getId()) {
1519 return ['success' => false, 'message' => __('Invalid invoice', 'easy-invoice')];
1520 }
1521
1522 // Get customer email
1523 $customer_email = $invoice->getCustomerEmail();
1524 if (empty($customer_email)) {
1525 return ['success' => false, 'message' => __('Customer email is missing', 'easy-invoice')];
1526 }
1527
1528 // Get currency settings
1529 $settings_controller = new \EasyInvoice\Controllers\SettingsController();
1530 $settings = $settings_controller->getSettings();
1531 $currency_code = $settings['easy_invoice_currency_code'] ?? 'USD';
1532 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
1533
1534 // Format amount
1535 $amount = $invoice->getTotal();
1536 $formatted_amount = $currency_symbol . number_format($amount, 2);
1537
1538 // Prepare email subject
1539 $site_name = get_bloginfo('name');
1540 $subject = sprintf(
1541 __('[%s] Payment Confirmed - Invoice #%s', 'easy-invoice'),
1542 $site_name,
1543 $invoice->getNumber()
1544 );
1545
1546 // Prepare email message
1547 $message = $this->preparePaymentConfirmationMessage($invoice, $formatted_amount);
1548
1549 // Add HTML wrapper if enabled
1550 if ($this->settings['enable_html'] === 'yes') {
1551 $message = $this->wrapInHtmlTemplate($message);
1552 }
1553
1554 // Prepare headers
1555 $headers = $this->prepareEmailHeaders();
1556
1557 // Add BCC to admin if enabled (but skip if this is from payment completion hook to avoid duplicate)
1558 // The payment completion hook already sends a dedicated admin notification
1559 $skip_bcc = isset($payment_data['skip_bcc']) && $payment_data['skip_bcc'] === true;
1560 if (!$skip_bcc && $this->settings['bcc_admin'] === 'yes' && !empty($this->settings['admin_email'])) {
1561 $headers[] = 'Bcc: ' . $this->settings['admin_email'];
1562 }
1563
1564 // Send email
1565 $sent = $this->sendEmail($customer_email, $subject, $message, $headers);
1566
1567 if ($sent) {
1568 do_action('easy_invoice_payment_confirmation_sent', $invoice, $customer_email, $payment_data);
1569 return [
1570 'success' => true,
1571 'message' => __('Payment confirmation email sent successfully', 'easy-invoice')
1572 ];
1573 } else {
1574 do_action('easy_invoice_payment_confirmation_failed', $invoice, $customer_email, $payment_data);
1575 return ['success' => false, 'message' => __('Failed to send payment confirmation email', 'easy-invoice')];
1576 }
1577
1578 } catch (\Exception $e) {
1579 $this->log('Payment confirmation email error: ' . $e->getMessage(), 'error');
1580 return ['success' => false, 'message' => __('Error sending payment confirmation: ', 'easy-invoice') . $e->getMessage()];
1581 }
1582 }
1583
1584 /**
1585 * Send payment rejection email to customer
1586 *
1587 * @param Invoice $invoice The invoice
1588 * @param string $reason Rejection reason
1589 * @return array Result array with success status and message
1590 */
1591 public function sendPaymentRejectionEmail(Invoice $invoice, string $reason = ''): array {
1592 try {
1593 // Validate invoice
1594 if (!$invoice || !$invoice->getId()) {
1595 return ['success' => false, 'message' => __('Invalid invoice', 'easy-invoice')];
1596 }
1597
1598 // Get customer email
1599 $customer_email = $invoice->getCustomerEmail();
1600 if (empty($customer_email)) {
1601 return ['success' => false, 'message' => __('Customer email is missing', 'easy-invoice')];
1602 }
1603
1604 // Prepare email subject
1605 $site_name = get_bloginfo('name');
1606 $subject = sprintf(
1607 __('[%s] Payment Rejected - Invoice #%s', 'easy-invoice'),
1608 $site_name,
1609 $invoice->getNumber()
1610 );
1611
1612 // Prepare email message
1613 $message = $this->preparePaymentRejectionMessage($invoice, $reason);
1614
1615 // Add HTML wrapper if enabled
1616 if ($this->settings['enable_html'] === 'yes') {
1617 $message = $this->wrapInHtmlTemplate($message);
1618 }
1619
1620 // Prepare headers
1621 $headers = $this->prepareEmailHeaders();
1622
1623 // Add BCC to admin if enabled
1624 if ($this->settings['bcc_admin'] === 'yes' && !empty($this->settings['admin_email'])) {
1625 $headers[] = 'Bcc: ' . $this->settings['admin_email'];
1626 }
1627
1628 // Send email
1629 $sent = $this->sendEmail($customer_email, $subject, $message, $headers);
1630
1631 if ($sent) {
1632 do_action('easy_invoice_payment_rejection_sent', $invoice, $customer_email, $reason);
1633 return [
1634 'success' => true,
1635 'message' => __('Payment rejection email sent successfully', 'easy-invoice')
1636 ];
1637 } else {
1638 do_action('easy_invoice_payment_rejection_failed', $invoice, $customer_email, $reason);
1639 return ['success' => false, 'message' => __('Failed to send payment rejection email', 'easy-invoice')];
1640 }
1641
1642 } catch (\Exception $e) {
1643 $this->log('Payment rejection email error: ' . $e->getMessage(), 'error');
1644 return ['success' => false, 'message' => __('Error sending payment rejection: ', 'easy-invoice') . $e->getMessage()];
1645 }
1646 }
1647
1648 /**
1649 * Prepare admin payment notification message
1650 *
1651 * @param Invoice $invoice The invoice
1652 * @param string $payment_method_label Payment method label
1653 * @param string $amount Formatted amount
1654 * @param array $payment_data Payment data
1655 * @return string Email message
1656 */
1657 private function prepareAdminPaymentNotificationMessage(Invoice $invoice, string $payment_method_label, string $amount, array $payment_data = []): string {
1658 $invoice_number = $invoice->getNumber();
1659 $customer_name = $invoice->getCustomerName();
1660 $customer_email = $invoice->getCustomerEmail();
1661 $invoice_id = $invoice->getId();
1662
1663 $message = sprintf(
1664 __('A new %s payment has been received for invoice #%s.', 'easy-invoice'),
1665 $payment_method_label,
1666 $invoice_number
1667 );
1668 $message .= "\n\n";
1669 $message .= __('Invoice Details:', 'easy-invoice');
1670 $message .= "\n";
1671 $message .= sprintf(__('- Amount: %s', 'easy-invoice'), $amount);
1672 $message .= "\n";
1673 $message .= sprintf(__('- Customer: %s', 'easy-invoice'), $customer_name);
1674 $message .= "\n";
1675 $message .= sprintf(__('- Email: %s', 'easy-invoice'), $customer_email);
1676
1677 // Add transaction ID if available
1678 if (!empty($payment_data['transaction_id'])) {
1679 $message .= "\n";
1680 $message .= sprintf(__('- Transaction ID: %s', 'easy-invoice'), $payment_data['transaction_id']);
1681 }
1682
1683 $message .= "\n\n";
1684 $message .= __('Please review this payment in the admin dashboard:', 'easy-invoice');
1685 $message .= "\n";
1686 $message .= admin_url('admin.php?page=easy-invoice-payments&action=verify&invoice_id=' . $invoice_id);
1687 $message .= "\n\n";
1688 $message .= __('This is an automated message from Easy Invoice.', 'easy-invoice');
1689
1690 return $message;
1691 }
1692
1693 /**
1694 * Prepare payment confirmation message
1695 *
1696 * @param Invoice $invoice The invoice
1697 * @param string $formatted_amount Formatted amount
1698 * @return string Email message
1699 */
1700 private function preparePaymentConfirmationMessage(Invoice $invoice, string $formatted_amount): string {
1701 $customer_name = $invoice->getCustomerName();
1702 $invoice_number = $invoice->getNumber();
1703 $site_name = get_bloginfo('name');
1704 $company_name = get_option('easy_invoice_company_name', $site_name);
1705
1706 $message = sprintf(__('Dear %s,', 'easy-invoice'), $customer_name);
1707 $message .= "\n\n";
1708 $message .= sprintf(
1709 __('We are pleased to confirm that your payment of %s for Invoice #%s has been received and processed successfully.', 'easy-invoice'),
1710 $formatted_amount,
1711 $invoice_number
1712 );
1713 $message .= "\n\n";
1714 $message .= __('Thank you for your business.', 'easy-invoice');
1715 $message .= "\n\n";
1716 $message .= __('Regards,', 'easy-invoice');
1717 $message .= "\n";
1718 $message .= $company_name;
1719
1720 return $message;
1721 }
1722
1723 /**
1724 * Prepare payment rejection message
1725 *
1726 * @param Invoice $invoice The invoice
1727 * @param string $reason Rejection reason
1728 * @return string Email message
1729 */
1730 private function preparePaymentRejectionMessage(Invoice $invoice, string $reason = ''): string {
1731 $customer_name = $invoice->getCustomerName();
1732 $invoice_number = $invoice->getNumber();
1733 $site_name = get_bloginfo('name');
1734 $company_name = get_option('easy_invoice_company_name', $site_name);
1735
1736 $message = sprintf(__('Dear %s,', 'easy-invoice'), $customer_name);
1737 $message .= "\n\n";
1738 $message .= sprintf(
1739 __('We regret to inform you that your payment for Invoice #%s has been rejected.', 'easy-invoice'),
1740 $invoice_number
1741 );
1742
1743 if (!empty($reason)) {
1744 $message .= "\n\n";
1745 $message .= __('Reason:', 'easy-invoice');
1746 $message .= "\n";
1747 $message .= $reason;
1748 }
1749
1750 $message .= "\n\n";
1751 $message .= __('Please contact us if you have any questions or concerns.', 'easy-invoice');
1752 $message .= "\n\n";
1753 $message .= __('Regards,', 'easy-invoice');
1754 $message .= "\n";
1755 $message .= $company_name;
1756
1757 return $message;
1758 }
1759
1760 /**
1761 * Send admin notification for quote acceptance/decline
1762 *
1763 * @param Quote $quote The quote
1764 * @param string $action Action type ('accepted' or 'declined')
1765 * @return array Result array with success status and message
1766 */
1767 public function sendAdminQuoteNotification(Quote $quote, string $action = 'accepted'): array {
1768 try {
1769 // Validate quote
1770 if (!$quote || !$quote->getId()) {
1771 return ['success' => false, 'message' => __('Invalid quote', 'easy-invoice')];
1772 }
1773
1774 // Get admin email
1775 $admin_email = $this->settings['admin_email'] ?? get_option('admin_email');
1776 if (empty($admin_email)) {
1777 return ['success' => false, 'message' => __('Admin email is missing', 'easy-invoice')];
1778 }
1779
1780 // Prepare email subject
1781 $subject = sprintf(
1782 __('Quote %s has been %s', 'easy-invoice'),
1783 $quote->getNumber(),
1784 $action === 'accepted' ? __('accepted', 'easy-invoice') : __('declined', 'easy-invoice')
1785 );
1786
1787 // Prepare email message
1788 $message = $this->prepareAdminQuoteNotificationMessage($quote, $action);
1789
1790 // Add HTML wrapper if enabled
1791 if ($this->settings['enable_html'] === 'yes') {
1792 $message = $this->wrapInHtmlTemplate($message);
1793 }
1794
1795 // Prepare headers
1796 $headers = $this->prepareEmailHeaders();
1797
1798 // Send email
1799 $sent = $this->sendEmail($admin_email, $subject, $message, $headers);
1800
1801 if ($sent) {
1802 do_action('easy_invoice_admin_quote_notification_sent', $quote, $admin_email, $action);
1803 return [
1804 'success' => true,
1805 'message' => __('Admin notification sent successfully', 'easy-invoice')
1806 ];
1807 } else {
1808 do_action('easy_invoice_admin_quote_notification_failed', $quote, $admin_email, $action);
1809 return ['success' => false, 'message' => __('Failed to send admin notification', 'easy-invoice')];
1810 }
1811
1812 } catch (\Exception $e) {
1813 $this->log('Admin quote notification error: ' . $e->getMessage(), 'error');
1814 return ['success' => false, 'message' => __('Error sending admin notification: ', 'easy-invoice') . $e->getMessage()];
1815 }
1816 }
1817
1818 /**
1819 * Prepare admin quote notification message
1820 *
1821 * @param Quote $quote The quote
1822 * @param string $action Action type ('accepted' or 'declined')
1823 * @return string Email message
1824 */
1825 private function prepareAdminQuoteNotificationMessage(Quote $quote, string $action): string {
1826 $site_name = get_bloginfo('name');
1827 $quote_number = $quote->getNumber();
1828 $customer_name = $quote->getCustomerName();
1829
1830 // Format amount
1831 $formatter = new \EasyInvoice\Helpers\InvoiceFormatter($quote);
1832 $formatted_amount = $formatter->format($quote->getTotal());
1833
1834 $action_label = $action === 'accepted' ? __('accepted', 'easy-invoice') : __('declined', 'easy-invoice');
1835 $date_label = $action === 'accepted' ? __('Accepted Date', 'easy-invoice') : __('Declined Date', 'easy-invoice');
1836
1837 $message = __('Hello,', 'easy-invoice');
1838 $message .= "\n\n";
1839 $message .= sprintf(
1840 __('The quote %s for %s has been %s by the client.', 'easy-invoice'),
1841 $quote_number,
1842 $customer_name,
1843 $action_label
1844 );
1845 $message .= "\n\n";
1846 $message .= __('Quote Details:', 'easy-invoice');
1847 $message .= "\n";
1848 $message .= sprintf(__('- Quote Number: %s', 'easy-invoice'), $quote_number);
1849 $message .= "\n";
1850 $message .= sprintf(__('- Client: %s', 'easy-invoice'), $customer_name);
1851 $message .= "\n";
1852 $message .= sprintf(__('- Total Amount: %s', 'easy-invoice'), $formatted_amount);
1853 $message .= "\n";
1854 $message .= sprintf(__('- %s: %s', 'easy-invoice'), $date_label, date_i18n(get_option('date_format') . ' ' . get_option('time_format')));
1855 $message .= "\n\n";
1856 $message .= __('You can view the quote at:', 'easy-invoice');
1857 $message .= "\n";
1858 $message .= get_permalink($quote->getId());
1859 $message .= "\n\n";
1860 $message .= __('Best regards,', 'easy-invoice');
1861 $message .= "\n";
1862 $message .= $site_name;
1863
1864 return $message;
1865 }
1866
1867 /**
1868 * Handle payment completed hook
1869 * Sends admin notification and customer confirmation when payment is completed
1870 *
1871 * @param int $invoice_id Invoice ID
1872 * @param \EasyInvoice\Models\Invoice $invoice Invoice object
1873 * @param array $payment_data Payment data (method, gateway, transaction_id, amount)
1874 * @return void
1875 */
1876 public function handlePaymentCompleted(int $invoice_id, $invoice, array $payment_data = []): void {
1877 if (!$invoice || !$invoice->getId()) {
1878 return;
1879 }
1880
1881 // Send admin notification
1882 $this->sendAdminPaymentNotification($invoice, $payment_data);
1883
1884 // Send customer confirmation email using proper template system
1885 // Check if payment email is enabled first
1886 if (isset($this->templates['invoice_paid']) && $this->templates['invoice_paid']['enabled']) {
1887 $this->sendInvoiceEmail($invoice, 'paid', array_merge($payment_data, ['skip_bcc' => true]));
1888 }
1889 }
1890
1891 /**
1892 * Get payment method label
1893 *
1894 * @param string $method Payment method
1895 * @return string Payment method label
1896 */
1897 private function getPaymentMethodLabel(string $method): string {
1898 $labels = [
1899 'bank' => __('Bank Transfer', 'easy-invoice'),
1900 'cheque' => __('Cheque', 'easy-invoice'),
1901 'paypal' => __('PayPal', 'easy-invoice'),
1902 'stripe' => __('Stripe', 'easy-invoice'),
1903 'square' => __('Square', 'easy-invoice'),
1904 'mollie' => __('Mollie', 'easy-invoice'),
1905 'authorizenet' => __('Authorize.Net', 'easy-invoice'),
1906 'manual' => __('Manual Payment', 'easy-invoice'),
1907 'online' => __('Online Payment', 'easy-invoice'),
1908 ];
1909
1910 return $labels[$method] ?? ucfirst($method);
1911 }
1912
1913 }