PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.2
2.4.3 2.4.4 2.4.2 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 All 60 releases
easy-invoice / includes / Controllers / SettingsController.php

SettingsController.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.2, at includes/Controllers/SettingsController.php

2,162 lines 106.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Settings Controller Class
4 *
5 * Handles configuration, display and storage of all plugin settings
6 *
7 * @package Easy_Invoice
8 * @subpackage Controllers
9 * @since 1.0.0
10 */
11
12 namespace EasyInvoice\Controllers;
13
14 use EasyInvoice\Constants\PagesSlugs;
15 use EasyInvoice\Helpers\CurrencyHelper;
16
17 /**
18 * SettingsController handles all settings-related functionality
19 */
20 class SettingsController extends BaseController {
21 /**
22 * Settings cache
23 *
24 * @var array
25 */
26 private $settings_cache = null;
27
28 /**
29 * Settings resolved once per request. Every money formatter used to
30 * construct its own controller and rebuild the whole settings array (the
31 * config tree with every translated label) — around a millisecond per
32 * formatted amount, ten seconds on a 4,500-line statement.
33 *
34 * @var array|null
35 */
36 private static $shared_settings_cache = null;
37
38 /**
39 * Drop the per-request settings cache (any easy_invoice_* option changed).
40 */
41 public static function flushSharedCache(): void {
42 self::$shared_settings_cache = null;
43 }
44
45 /**
46 * Settings configuration cache
47 *
48 * @var array
49 */
50 private $config_cache = null;
51
52 /**
53 * Clear settings cache
54 *
55 * @return void
56 */
57 public function clearCache(): void {
58 $this->config_cache = null;
59 // Also clear any WordPress object cache
60 wp_cache_delete('easy_invoice_settings_config', 'easy_invoice');
61 // Force clear any other caches
62 if (function_exists('wp_cache_flush')) {
63 wp_cache_flush();
64 }
65 }
66
67 /**
68 * Initialize the controller
69 *
70 * @return void
71 */
72 public function init() {
73 // Add AJAX handlers
74 add_action('wp_ajax_easy_invoice_save_settings', [$this, 'saveSettings']);
75 add_action('wp_ajax_easy_invoice_test_email', [$this, 'testEmail']);
76 add_action('wp_ajax_easy_invoice_test_template_email', [$this, 'testTemplateEmail']);
77 add_action('wp_ajax_easy_invoice_test_payment_reminder_email', [$this, 'testPaymentReminderEmail']);
78 add_action('wp_ajax_regenerate_invoice_numbers', [$this, 'ajaxRegenerateInvoiceNumbers']);
79 add_action('wp_ajax_easy_invoice_add_offline_method', [$this, 'ajaxAddOfflineMethod']);
80 add_action('wp_ajax_easy_invoice_remove_offline_method', [$this, 'ajaxRemoveOfflineMethod']);
81 add_action('wp_ajax_regenerate_quote_numbers', [$this, 'ajaxRegenerateQuoteNumbers']);
82
83 // Clear cache on admin init to ensure new fields are recognized
84 add_action('admin_init', [$this, 'clearCache']);
85
86 // Enqueue admin scripts
87 add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']);
88
89 // Initialize settings
90 $this->initializeSettings();
91 }
92
93
94
95 /**
96 * Enqueue admin scripts and styles for the settings page.
97 *
98 * @param string $hook_suffix The current admin page.
99 * @return void
100 */
101 public function enqueueAdminScripts($hook_suffix) {
102 // Check if we are on the Easy Invoice settings page.
103 if (empty($hook_suffix) || strpos($hook_suffix, PagesSlugs::SETTINGS) === false) {
104 return;
105 }
106
107 wp_enqueue_script('jquery-ui-sortable');
108 wp_enqueue_media(); // For the logo uploader
109
110 // Enqueue Select2 for enhancing multiselect fields
111 wp_enqueue_style(
112 'select2',
113 EASY_INVOICE_URL . 'assets/lib/select2/select2.min.css',
114 [],
115 '4.0.13'
116 );
117
118
119 wp_enqueue_script(
120 'select2',
121 EASY_INVOICE_URL . 'assets/lib/select2/select2.min.js',
122 ['jquery'],
123 '4.0.13',
124 true
125 );
126
127 // Enqueue confirmation modal for premium features
128 wp_enqueue_script(
129 'easy-invoice-confirmation-modal',
130 EASY_INVOICE_URL . 'assets/js/confirmation-modal.js',
131 ['jquery'],
132 EASY_INVOICE_VERSION,
133 true
134 );
135
136 // Enqueue toast notification system
137 wp_enqueue_script(
138 'easy-invoice-toast',
139 EASY_INVOICE_URL . 'assets/js/easy-invoice-toast.js',
140 ['jquery'],
141 EASY_INVOICE_VERSION,
142 true
143 );
144
145 $script_handle = 'easy-invoice-settings';
146 wp_enqueue_script(
147 $script_handle,
148 EASY_INVOICE_URL . 'assets/js/settings.js',
149 ['jquery', 'jquery-ui-sortable', 'wp-util', 'select2', 'easy-invoice-confirmation-modal', 'media-views'],
150 EASY_INVOICE_VERSION,
151 true
152 );
153
154 // Enqueue the new settings.css only on the settings page
155 wp_enqueue_style(
156 'easy-invoice-settings',
157 EASY_INVOICE_URL . 'assets/css/settings.css',
158 [],
159 EASY_INVOICE_VERSION
160 );
161
162 $localize_data = apply_filters('easy_invoice_settings_js_data', [
163 'ajaxurl' => admin_url('admin-ajax.php'),
164 'nonce' => wp_create_nonce('easy_invoice_settings'),
165 'saveSuccess' => __('Settings saved successfully', 'easy-invoice'),
166 'saveError' => __('Error saving settings', 'easy-invoice'),
167 ]);
168
169 wp_localize_script($script_handle, 'easyInvoiceSettings', $localize_data);
170
171 do_action('easy_invoice_after_admin_scripts', $hook_suffix);
172 }
173
174 /**
175 * Check if Easy Invoice Pro is active
176 *
177 * @return bool
178 */
179 private function isProActive(): bool {
180 return easy_invoice_has_pro();
181 }
182
183 /**
184 * Get settings fields configuration
185 *
186 * @return array
187 */
188 public function get_settings_fields_config() {
189 // Use cached config if available
190 if (is_array($this->config_cache)) {
191 return $this->config_cache;
192 }
193
194 $config = [
195 'company' => [
196 'title' => __('Company Information', 'easy-invoice'),
197 'description' => __('Set up your company details', 'easy-invoice'),
198 'icon' => 'fas fa-building',
199 'fields' => [
200 'easy_invoice_company_name' => ['label' => __('Company Name', 'easy-invoice'), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-6' ],
201 'easy_invoice_company_email' => ['label' => __('Email', 'easy-invoice'), 'type' => 'email', 'default' => '', 'col_span' => 'sm:col-span-6' ],
202 'easy_invoice_company_phone' => ['label' => __('Phone Number', 'easy-invoice'), 'type' => 'tel', 'default' => '', 'col_span' => 'sm:col-span-6' ],
203 'easy_invoice_company_website' => ['label' => __('Website', 'easy-invoice'), 'type' => 'url', 'default' => '', 'col_span' => 'sm:col-span-6' ],
204 // Both are required by EN 16931 and are what makes an intra-EU
205 // reverse charge determinable. See Services\TaxTreatment.
206 'easy_invoice_company_vat_number' => ['label' => __('VAT number', 'easy-invoice'), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-3', 'description' => __('Your VAT identifier, including the country prefix — for example DE123456789.', 'easy-invoice') ],
207 'easy_invoice_company_country' => ['label' => __('Country', 'easy-invoice'), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-3', 'description' => __('Two-letter country code, for example DE, FR or GB.', 'easy-invoice') ],
208 'easy_invoice_company_address' => ['label' => __('Address', 'easy-invoice'), 'type' => 'textarea', 'default' => '', 'col_span' => 'sm:col-span-6' ],
209 'easy_invoice_tax_number' => ['label' => __('Tax ID / VAT Number', 'easy-invoice'), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-6' ],
210 'easy_invoice_company_logo' => ['label' => __('Company Logo', 'easy-invoice'), 'type' => 'image', 'default' => '', 'col_span' => 'sm:col-span-6' ],
211 ]
212 ],
213 'invoice' => [
214 'title' => __('Invoice Settings', 'easy-invoice'),
215 'description' => __('Configure invoice-specific settings and preferences', 'easy-invoice'),
216 'icon' => 'fas fa-file-invoice',
217 'fields' => [
218 'easy_invoice_invoice_prefix' => [
219 'label' => __('Invoice Prefix', 'easy-invoice'),
220 'type' => 'text',
221 'default' => 'EIIN_',
222 'description' => __('Prefix for invoice numbers (e.g., EIIN_, INV-)', 'easy-invoice'),
223 'col_span' => 'sm:col-span-2'
224 ],
225 'easy_invoice_next_invoice_number' => [
226 'label' => __('Next Invoice Number', 'easy-invoice'),
227 'type' => 'number',
228 'default' => '1',
229 'description' => __('Set the next invoice number to be generated. This will be the number of the next invoice created.', 'easy-invoice'),
230 'col_span' => 'sm:col-span-2',
231 ],
232 'easy_invoice_regenerate_invoice_numbers' => [
233 'label' => __('Regenerate Invoice Numbers', 'easy-invoice'),
234 'type' => 'button',
235 'button_text' => __('Regenerate All Invoice Numbers', 'easy-invoice'),
236 'button_class' => 'button button-secondary',
237 'description' => __('Click to regenerate all invoice numbers starting from the Last Invoice Number. This will update all existing invoices with new sequential numbers.', 'easy-invoice'),
238 'col_span' => 'sm:col-span-2',
239 'ajax_action' => 'regenerate_invoice_numbers'
240 ],
241 'easy_invoice_invoice_show_adjust_field' => [
242 'label' => __('Show/Hide Adjust Field', 'easy-invoice'),
243 'type' => 'checkbox',
244 'default' => 'yes',
245 'description' => __('Enable/Disable Adjust field. Tick this to show adjust field', 'easy-invoice'),
246 'col_span' => 'sm:col-span-6'
247 ],
248 'easy_invoice_invoice_terms_conditions' => [
249 'label' => __('Terms & Conditions', 'easy-invoice'),
250 'type' => 'wp_editor',
251 'default' => __('Payment is due within 30 days from date of invoice', 'easy-invoice'),
252 'description' => __('Terms and conditions that will be displayed on your invoice', 'easy-invoice'),
253 'col_span' => 'sm:col-span-6'
254 ],
255 'easy_invoice_invoice_footer_text' => [
256 'label' => __('Footer Text', 'easy-invoice'),
257 'type' => 'wp_editor',
258 'default' => '',
259 'description' => __('You can modify your invoice footer text from here. HTML tags supports: a, br, em, strong, hr, p, h1 to h4', 'easy-invoice'),
260 'col_span' => 'sm:col-span-6'
261 ],
262 ],
263 ],
264 'quote' => [
265 'title' => __('Quote Settings', 'easy-invoice'),
266 'description' => __('Configure quote-specific settings and preferences', 'easy-invoice'),
267 'icon' => 'fas fa-file-contract',
268 'fields' => [
269 'easy_invoice_quote_prefix' => [
270 'label' => __('Quote Prefix', 'easy-invoice'),
271 'type' => 'text',
272 'default' => 'EIQN_',
273 'description' => __('Prefix for quote numbers (e.g., EIQN_, QT-)', 'easy-invoice'),
274 'col_span' => 'sm:col-span-2'
275 ],
276 'easy_invoice_next_quote_number' => [
277 'label' => __('Next Quote Number', 'easy-invoice'),
278 'type' => 'number',
279 'default' => '1',
280 'description' => __('Set the next quote number to be generated. This will be the number of the next quote created.', 'easy-invoice'),
281 'col_span' => 'sm:col-span-2',
282 ],
283 'easy_invoice_regenerate_quote_numbers' => [
284 'label' => __('Regenerate Quote Numbers', 'easy-invoice'),
285 'type' => 'button',
286 'button_text' => __('Regenerate All Quote Numbers', 'easy-invoice'),
287 'button_class' => 'button button-secondary',
288 'description' => __('Click to regenerate all quote numbers starting from the Next Quote Number. This will update all existing quotes with new sequential numbers.', 'easy-invoice'),
289 'col_span' => 'sm:col-span-2',
290 'ajax_action' => 'regenerate_quote_numbers'
291 ],
292 'easy_invoice_quote_show_adjust_field' => [
293 'label' => __('Show/Hide Adjust Field', 'easy-invoice'),
294 'type' => 'checkbox',
295 'default' => 'yes',
296 'description' => __('Enable/Disable Adjust field. Tick this to show adjust field', 'easy-invoice'),
297 'col_span' => 'sm:col-span-6'
298 ],
299 'easy_invoice_quote_terms_conditions' => [
300 'label' => __('Terms & Conditions', 'easy-invoice'),
301 'type' => 'wp_editor',
302 'default' => __('This quote has a fixed price. Upon acceptance, we kindly ask for a 25% deposit prior to initiating the work.', 'easy-invoice'),
303 'description' => __('Terms and conditions that will be displayed on your quote!', 'easy-invoice'),
304 'col_span' => 'sm:col-span-6'
305 ],
306 'easy_invoice_quote_footer_text' => [
307 'label' => __('Footer Text', 'easy-invoice'),
308 'type' => 'wp_editor',
309 'default' => __('Thank you for your business.', 'easy-invoice'),
310 'description' => __('You can modify your quote footer text from here. HTML tags supports: a, br, em, strong, hr, p, h1 to h4', 'easy-invoice'),
311 'col_span' => 'sm:col-span-6'
312 ],
313 'easy_invoice_quote_accept_button' => [
314 'label' => __('Accept quote button', 'easy-invoice'),
315 'type' => 'checkbox',
316 'default' => 'yes',
317 'description' => __('Show/hide accept quote button on quotes.', 'easy-invoice'),
318 'col_span' => 'sm:col-span-6'
319 ],
320 'easy_invoice_quote_accept_action' => [
321 'label' => __('Accept quote button action', 'easy-invoice'),
322 'type' => 'select',
323 'default' => 'convert',
324 'options' => [
325 'convert' => __('Convert quote to invoice - Draft', 'easy-invoice'),
326 'convert_available' => __('Convert quote to invoice - Available', 'easy-invoice'),
327 'convert_send' => __('Convert quote to invoice and send to client - Available', 'easy-invoice'),
328 'duplicate' => __('Create new invoice, keep quote as-is - Draft', 'easy-invoice'),
329 'duplicate_send' => __('Create new invoice and send to client, keep quote as-is - Available', 'easy-invoice'),
330 'do_nothing' => __('Do nothing', 'easy-invoice'),
331 ],
332 'description' => __('Upon the client clicking the "accept quote" button, the subsequent action will be activated.', 'easy-invoice'),
333 'col_span' => 'sm:col-span-6'
334 ],
335 'easy_invoice_quote_accept_text' => [
336 'label' => __('Accept Quote Text', 'easy-invoice'),
337 'type' => 'wp_editor',
338 'default' => __('Important: When you accept this Quote, an Invoice will be created automatically. This will form a legally binding contract.', 'easy-invoice'),
339 'description' => __('This information tells your client what happens once they accept the Quote', 'easy-invoice'),
340 'col_span' => 'sm:col-span-6'
341 ],
342 'easy_invoice_quote_accepted_message' => [
343 'label' => __('Accepted Quote Message', 'easy-invoice'),
344 'type' => 'wp_editor',
345 'default' => __('You\'ve confirmed the Quote.<br>We\'ll get in touch with you shortly.', 'easy-invoice'),
346 'description' => __('If the client accepts the Quote, display this message.', 'easy-invoice'),
347 'col_span' => 'sm:col-span-6'
348 ],
349 'easy_invoice_quote_decline_reason_required' => [
350 'label' => __('Decline Reason Required', 'easy-invoice'),
351 'type' => 'checkbox',
352 'default' => 'no',
353 'description' => __('Make the \'Reason for declining\' field mandatory when rejecting.', 'easy-invoice'),
354 'col_span' => 'sm:col-span-6'
355 ],
356 'easy_invoice_quote_declined_message' => [
357 'label' => __('Declined Quote Message', 'easy-invoice'),
358 'type' => 'wp_editor',
359 'default' => '',
360 'description' => __('Message to display if client declines the Quote', 'easy-invoice'),
361 'col_span' => 'sm:col-span-6'
362 ],
363 ],
364 ],
365 'currency' => [
366 'title' => __('Currency Settings', 'easy-invoice'),
367 'description' => __('Configure currency display preferences', 'easy-invoice'),
368 'icon' => 'fas fa-dollar-sign',
369 'fields' => [
370 'easy_invoice_currency_code' => [
371 'label' => __('Currency & Symbol', 'easy-invoice'),
372 'type' => 'select',
373 'default' => 'USD',
374 'options' => CurrencyHelper::getCurrencyOptions(),
375 'col_span' => 'sm:col-span-6',
376 'description' => __('Select your preferred currency. The symbol will be automatically set.', 'easy-invoice')
377 ],
378 'easy_invoice_currency_position' => [
379 'label' => __('Symbol Position', 'easy-invoice'),
380 'type' => 'select',
381 'default' => 'left',
382 'options' => [
383 'left' => __('Left', 'easy-invoice'),
384 'right' => __('Right', 'easy-invoice'),
385 'left_space' => __('Left with space', 'easy-invoice'),
386 'right_space' => __('Right with space', 'easy-invoice'),
387 ],
388 'col_span' => 'sm:col-span-3'
389 ],
390 'easy_invoice_currency_symbol_type' => [
391 'label' => __('Currency Symbol Type', 'easy-invoice'),
392 'type' => 'select',
393 'default' => 'symbol',
394 'options' => [
395 'code' => __('Currency Code', 'easy-invoice'),
396 'symbol' => __('Currency Symbol', 'easy-invoice'),
397 ],
398 'description' => __('Choose whether to display currency code (USD) or currency symbol ($)', 'easy-invoice'),
399 'col_span' => 'sm:col-span-3'
400 ],
401 'easy_invoice_thousands_separator' => ['label' => __('Thousands Separator', 'easy-invoice'), 'type' => 'text', 'default' => ',', 'col_span' => 'sm:col-span-3' ],
402 'easy_invoice_decimal_separator' => ['label' => __('Decimal Separator', 'easy-invoice'), 'type' => 'text', 'default' => '.', 'col_span' => 'sm:col-span-3' ],
403 'easy_invoice_decimal_precision' => ['label' => __('Decimal Places', 'easy-invoice'), 'type' => 'number', 'default' => '2', 'min' => 0, 'max' => 4, 'col_span' => 'sm:col-span-3' ],
404 ]
405 ],
406 'tax' => [
407 'title' => __('Tax Settings', 'easy-invoice'),
408 'description' => __('Configure tax rates and preferences', 'easy-invoice'),
409 'icon' => 'fas fa-percent',
410 'fields' => [
411 'easy_invoice_tax_enabled' => ['label' => __('Enable Tax', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'no', 'col_span' => 'sm:col-span-6' ],
412 'easy_invoice_tax_entry_method' => [
413 'label' => __('How do you enter tax?', 'easy-invoice'),
414 'type' => 'select',
415 'default' => 'exclusive',
416 'options' => [
417 'inclusive' => __('I will enter price inclusive of tax', 'easy-invoice'),
418 'exclusive' => __('I will enter price exclusive of tax', 'easy-invoice'),
419 ],
420 'description' => __('Choose how you want to enter prices - with or without tax included', 'easy-invoice'),
421 'col_span' => 'sm:col-span-6'
422 ],
423 'easy_invoice_tax_rate' => ['label' => __('Default Tax Rate (%)', 'easy-invoice'), 'type' => 'number', 'default' => '0', 'step' => '0.01', 'min' => '0', 'max' => '100', 'col_span' => 'sm:col-span-3' ],
424 'easy_invoice_tax_name' => ['label' => __('Tax Name', 'easy-invoice'), 'type' => 'text', 'default' => __('Tax', 'easy-invoice'), 'col_span' => 'sm:col-span-3' ],
425 ]
426 ],
427 'payment' => [
428 'title' => __('Payment Methods', 'easy-invoice'),
429 'description' => __('Configure and order available payment methods.', 'easy-invoice'),
430 'icon' => 'fas fa-credit-card',
431 'is_special_section' => true,
432 'gateways' => $this->getGatewaySettingsConfigs(),
433 ],
434 'email' => [
435 'title' => __('Email Settings', 'easy-invoice'),
436 'description' => __('Configure email sending options and templates', 'easy-invoice'),
437 'icon' => 'fas fa-envelope',
438 'subsections' => [
439 'general' => [
440 'title' => __('General Email Settings', 'easy-invoice'),
441 'description' => __('Configure general email sending options', 'easy-invoice'),
442 'fields' => [
443 'easy_invoice_email_from_name' => ['label' => __('From Name', 'easy-invoice'), 'type' => 'text', 'default' => get_bloginfo('name') ?: 'Easy Invoice', 'col_span' => 'sm:col-span-3'],
444 'easy_invoice_email_from_address' => ['label' => __('From Email Address', 'easy-invoice'), 'type' => 'email', 'default' => get_bloginfo('admin_email') ?: get_option('admin_email', ''), 'col_span' => 'sm:col-span-3'],
445 'easy_invoice_email_reply_to' => ['label' => __('Reply-To Email', 'easy-invoice'), 'type' => 'email', 'default' => '', 'col_span' => 'sm:col-span-3'],
446 'easy_invoice_email_reply_to_name' => ['label' => __('Reply-To Name', 'easy-invoice'), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-3'],
447 'easy_invoice_enable_email_styling' => ['label' => __('Enable HTML Emails', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'yes', 'col_span' => 'sm:col-span-6'],
448 'easy_invoice_bcc_admin' => ['label' => __('BCC Admin on All Emails', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'no', 'col_span' => 'sm:col-span-6'],
449 // Off by default: attaching a document changes what every
450 // customer receives and makes messages much larger, which
451 // some SMTP relays reject. See EmailManager::shouldAttachInvoicePdf().
452 'easy_invoice_attach_pdf_to_email' => ['label' => __('Attach a PDF copy to invoice and quote emails', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'no', 'col_span' => 'sm:col-span-6', 'description' => __('Generates the document on the server and attaches it. The PDF contains real, selectable text rather than an image.', 'easy-invoice')],
453 'easy_invoice_pdf_paper_size' => ['label' => __('PDF paper size', 'easy-invoice'), 'type' => 'select', 'default' => 'a4', 'options' => ['a4' => 'A4', 'letter' => 'Letter', 'legal' => 'Legal'], 'col_span' => 'sm:col-span-3'],
454 'easy_invoice_pdf_download_method' => ['label' => __('"Download as PDF" button', 'easy-invoice'), 'type' => 'select', 'default' => 'browser', 'options' => ['browser' => __('Exactly as shown on screen (rendered in the browser)', 'easy-invoice'), 'server' => __('Generated on the server (real text, smaller file)', 'easy-invoice')], 'col_span' => 'sm:col-span-3', 'description' => __('The browser capture reproduces the page pixel for pixel — every design, custom template and watermark. The server file is selectable text and a fraction of the size. Emails, the API and e-invoicing always use the server.', 'easy-invoice')],
455 'easy_invoice_pdf_layout' => ['label' => __('Server-generated PDF layout', 'easy-invoice'), 'type' => 'select', 'default' => 'design', 'options' => ['design' => __('Match the selected invoice / quote design', 'easy-invoice'), 'plain' => __('Plain print layout', 'easy-invoice')], 'col_span' => 'sm:col-span-3', 'description' => __('Applies to PDFs the server produces: email attachments, the API, e-invoicing, and the download button when set to "server". The design is the template chosen on the document; the plain layout is a neutral table-based page.', 'easy-invoice')],
456 'easy_invoice_admin_email' => ['label' => __('Admin Email for BCC', 'easy-invoice'), 'type' => 'email', 'default' => get_option('admin_email', ''), 'col_span' => 'sm:col-span-6'],
457 'easy_invoice_email_logo' => ['label' => __('Email Logo URL', 'easy-invoice'), 'type' => 'url', 'default' => '', 'col_span' => 'sm:col-span-6'],
458 'easy_invoice_email_footer_text' => ['label' => __('Email Footer Text', 'easy-invoice'), 'type' => 'textarea', 'default' => '', 'col_span' => 'sm:col-span-6'],
459 'easy_invoice_test_email' => [
460 'label' => __('Test Email', 'easy-invoice'),
461 'type' => 'test_email',
462 'default' => '',
463 'col_span' => 'sm:col-span-6',
464 'description' => __('Send a test email to verify your email configuration is working correctly.', 'easy-invoice')
465 ],
466 ]
467 ],
468 'invoice_available' => [
469 'title' => __('Invoice Available Email', 'easy-invoice'),
470 'description' => __('Configure email sent when an invoice is available for the client', 'easy-invoice'),
471 'fields' => [
472 'easy_invoice_invoice_email_enabled' => ['label' => __('Enable Invoice Available Email', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'yes', 'col_span' => 'sm:col-span-6'],
473 'easy_invoice_invoice_email_subject' => ['label' => __('Subject', 'easy-invoice'), 'type' => 'text', 'default' => __('Your Invoice #{{invoice_number}} from {{company_name}}', 'easy-invoice'), 'col_span' => 'sm:col-span-6'],
474 'easy_invoice_invoice_email_body' => ['label' => __('Email Body', 'easy-invoice'), 'type' => 'wp_editor', 'default' => \EasyInvoice\Services\EmailManager::defaultTemplate('invoice'), 'col_span' => 'sm:col-span-6'],
475 'easy_invoice_invoice_test_template' => [
476 'label' => __('Test Invoice Email', 'easy-invoice'),
477 'type' => 'test_template_email',
478 'default' => '',
479 'col_span' => 'sm:col-span-6',
480 'description' => __('Send a test invoice email to verify the template and configuration.', 'easy-invoice'),
481 'template_type' => 'invoice_available'
482 ],
483 ]
484 ],
485 'quote_available' => [
486 'title' => __('Quote Available Email', 'easy-invoice'),
487 'description' => __('Configure email sent when a quote is available for the client', 'easy-invoice'),
488 'fields' => [
489 'easy_invoice_quote_email_enabled' => ['label' => __('Enable Quote Available Email', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'yes', 'col_span' => 'sm:col-span-6'],
490 'easy_invoice_quote_email_subject' => ['label' => __('Subject', 'easy-invoice'), 'type' => 'text', 'default' => __('Your Quote #{{quote_number}} from {{company_name}}', 'easy-invoice'), 'col_span' => 'sm:col-span-6'],
491 'easy_invoice_quote_email_body' => ['label' => __('Email Body', 'easy-invoice'), 'type' => 'wp_editor', 'default' => \EasyInvoice\Services\EmailManager::defaultTemplate('quote'), 'col_span' => 'sm:col-span-6'],
492 'easy_invoice_quote_test_template' => [
493 'label' => __('Test Quote Email', 'easy-invoice'),
494 'type' => 'test_template_email',
495 'default' => '',
496 'col_span' => 'sm:col-span-6',
497 'description' => __('Send a test quote email to verify the template and configuration.', 'easy-invoice'),
498 'template_type' => 'quote_available'
499 ],
500 ]
501 ],
502 'payment_received' => [
503 'title' => __('Payment Received Email', 'easy-invoice'),
504 'description' => __('Configure email sent when a payment is received', 'easy-invoice'),
505 'fields' => [
506 'easy_invoice_payment_email_enabled' => ['label' => __('Enable Payment Received Email', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'yes', 'col_span' => 'sm:col-span-6'],
507 'easy_invoice_payment_email_subject' => ['label' => __('Subject', 'easy-invoice'), 'type' => 'text', 'default' => __('Payment Received - Invoice #{{invoice_number}}', 'easy-invoice'), 'col_span' => 'sm:col-span-6'],
508 'easy_invoice_payment_email_body' => ['label' => __('Email Body', 'easy-invoice'), 'type' => 'wp_editor', 'default' => \EasyInvoice\Services\EmailManager::defaultTemplate('payment'), 'col_span' => 'sm:col-span-6'],
509 'easy_invoice_payment_test_template' => [
510 'label' => __('Test Payment Email', 'easy-invoice'),
511 'type' => 'test_template_email',
512 'default' => '',
513 'col_span' => 'sm:col-span-6',
514 'description' => __('Send a test payment email to verify the template and configuration.', 'easy-invoice'),
515 'template_type' => 'payment_received'
516 ],
517 ]
518 ],
519 ]
520 ],
521 'text_settings' => [
522 'title' => __('Text Settings', 'easy-invoice'),
523 'description' => __('Customize text labels for invoices and quotes. Perfect for multi-language support.', 'easy-invoice'),
524 'icon' => '<svg class="text-indigo-600 text-xl h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
525 <path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
526 <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
527 <path d="M8 11H16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
528 <path d="M8 15H12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
529 <path d="M8 19H14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
530 </svg>',
531 'fields' => [
532 // Invoice Text Settings
533 'easy_invoice_text_invoice' => ['label' => __('Invoice', 'easy-invoice'), 'type' => 'text', 'default' => __('Invoice', 'easy-invoice'), 'col_span' => '', 'description' => __('Label for single invoice', 'easy-invoice')],
534 'easy_invoice_text_invoices' => ['label' => __('Invoices', 'easy-invoice'), 'type' => 'text', 'default' => __('Invoices', 'easy-invoice'), 'col_span' => '', 'description' => __('Label for multiple invoices', 'easy-invoice')],
535 'easy_invoice_text_to' => ['label' => __('To', 'easy-invoice'), 'type' => 'text', 'default' => __('To', 'easy-invoice'), 'col_span' => '', 'description' => __('To label on invoice/quote', 'easy-invoice')],
536 'easy_invoice_text_invoice_number' => ['label' => __('Invoice Number', 'easy-invoice'), 'type' => 'text', 'default' => __('Invoice Number', 'easy-invoice'), 'col_span' => '', 'description' => __('Invoice number label', 'easy-invoice')],
537 'easy_invoice_text_invoice_date' => ['label' => __('Invoice Date', 'easy-invoice'), 'type' => 'text', 'default' => __('Invoice Date', 'easy-invoice'), 'col_span' => '', 'description' => __('Invoice date label', 'easy-invoice')],
538 'easy_invoice_text_due_date' => ['label' => __('Due Date', 'easy-invoice'), 'type' => 'text', 'default' => __('Due Date', 'easy-invoice'), 'col_span' => '', 'description' => __('Due date label', 'easy-invoice')],
539 'easy_invoice_text_total_due' => ['label' => __('Total Due', 'easy-invoice'), 'type' => 'text', 'default' => __('Total Due', 'easy-invoice'), 'col_span' => '', 'description' => __('Total due label', 'easy-invoice')],
540 'easy_invoice_text_qty' => ['label' => __('Qty', 'easy-invoice'), 'type' => 'text', 'default' => __('Qty', 'easy-invoice'), 'col_span' => '', 'description' => __('Quantity label', 'easy-invoice')],
541 'easy_invoice_text_service' => ['label' => __('Service', 'easy-invoice'), 'type' => 'text', 'default' => __('Service', 'easy-invoice'), 'col_span' => '', 'description' => __('Service label', 'easy-invoice')],
542 'easy_invoice_text_rate_price' => ['label' => __('Rate', 'easy-invoice'), 'type' => 'text', 'default' => __('Rate', 'easy-invoice'), 'col_span' => '', 'description' => __('Rate or price label', 'easy-invoice')],
543 'easy_invoice_text_adjust' => ['label' => __('Adjust', 'easy-invoice'), 'type' => 'text', 'default' => __('Adjust', 'easy-invoice'), 'col_span' => '', 'description' => __('Adjust label', 'easy-invoice')],
544 'easy_invoice_text_sub_total' => ['label' => __('Sub Total', 'easy-invoice'), 'type' => 'text', 'default' => __('Sub Total', 'easy-invoice'), 'col_span' => '', 'description' => __('Sub total label', 'easy-invoice')],
545 'easy_invoice_text_total' => ['label' => __('Total', 'easy-invoice'), 'type' => 'text', 'default' => __('Total', 'easy-invoice'), 'col_span' => '', 'description' => __('Total label', 'easy-invoice')],
546 'easy_invoice_text_line_total' => ['label' => __('Line total', 'easy-invoice'), 'type' => 'text', 'default' => __('Total', 'easy-invoice'), 'col_span' => '', 'description' => __('Heading of the line-item amount column', 'easy-invoice')],
547 'easy_invoice_text_tax' => ['label' => __('Tax', 'easy-invoice'), 'type' => 'text', 'default' => __('Tax', 'easy-invoice'), 'col_span' => '', 'description' => __('Tax label', 'easy-invoice')],
548 'easy_invoice_text_discount' => ['label' => __('Discount', 'easy-invoice'), 'type' => 'text', 'default' => __('Discount', 'easy-invoice'), 'col_span' => '', 'description' => __('Discount label', 'easy-invoice')],
549 'easy_invoice_text_print' => ['label' => __('Print', 'easy-invoice'), 'type' => 'text', 'default' => __('Print', 'easy-invoice'), 'col_span' => '', 'description' => __('Print button text', 'easy-invoice')],
550 'easy_invoice_text_download_pdf' => ['label' => __('Download as PDF', 'easy-invoice'), 'type' => 'text', 'default' => __('Download as PDF', 'easy-invoice'), 'col_span' => '', 'description' => __('Download PDF button text', 'easy-invoice')],
551 'easy_invoice_text_send_email' => ['label' => __('Send Email', 'easy-invoice'), 'type' => 'text', 'default' => __('Send Email', 'easy-invoice'), 'col_span' => '', 'description' => __('Send email button text', 'easy-invoice')],
552 'easy_invoice_text_pay_now' => ['label' => __('Pay Now', 'easy-invoice'), 'type' => 'text', 'default' => __('Pay Now', 'easy-invoice'), 'col_span' => '', 'description' => __('Pay now button text', 'easy-invoice')],
553
554 // Quote Text Settings
555 'easy_invoice_text_quote' => ['label' => __('Quote', 'easy-invoice'), 'type' => 'text', 'default' => __('Quote', 'easy-invoice'), 'col_span' => '', 'description' => __('Label for single quote', 'easy-invoice')],
556 'easy_invoice_text_quote_number' => ['label' => __('Quote Number', 'easy-invoice'), 'type' => 'text', 'default' => __('Quote Number', 'easy-invoice'), 'col_span' => '', 'description' => __('Quote number label', 'easy-invoice')],
557 'easy_invoice_text_accept_quote' => ['label' => __('Accept Quote', 'easy-invoice'), 'type' => 'text', 'default' => __('Accept Quote', 'easy-invoice'), 'col_span' => '', 'description' => __('Accept quote button text', 'easy-invoice')],
558 'easy_invoice_text_decline_quote' => ['label' => __('Decline Quote', 'easy-invoice'), 'type' => 'text', 'default' => __('Decline Quote', 'easy-invoice'), 'col_span' => '', 'description' => __('Decline quote button text', 'easy-invoice')],
559 'easy_invoice_text_decline_reason' => ['label' => __('Reason for declining', 'easy-invoice'), 'type' => 'text', 'default' => __('Reason for declining', 'easy-invoice'), 'col_span' => '', 'description' => __('Decline reason label', 'easy-invoice')],
560 'easy_invoice_text_valid_until' => ['label' => __('Valid Until Date', 'easy-invoice'), 'type' => 'text', 'default' => __('Valid Until Date', 'easy-invoice'), 'col_span' => '', 'description' => __('Valid until date label', 'easy-invoice')],
561 'easy_invoice_text_quote_date' => ['label' => __('Quote Date', 'easy-invoice'), 'type' => 'text', 'default' => __('Quote Date', 'easy-invoice'), 'col_span' => '', 'description' => __('Quote date label', 'easy-invoice')],
562 ]
563 ],
564 'advanced' => [
565 'title' => __('Advanced Settings', 'easy-invoice'),
566 'description' => __('Configure system preferences', 'easy-invoice'),
567 'icon' => 'fas fa-cog',
568 'fields' => [
569 'easy_invoice_date_format' => [
570 'label' => __('Date Format', 'easy-invoice'),
571 'type' => 'select',
572 'default' => 'us',
573 'options' => [
574 'us' => __('MM/DD/YYYY (US) - 01/15/2024', 'easy-invoice'),
575 'uk' => __('DD/MM/YYYY (UK) - 15/01/2024', 'easy-invoice'),
576 'iso' => __('YYYY-MM-DD (ISO) - 2024-01-15', 'easy-invoice'),
577 ],
578 'col_span' => 'sm:col-span-3'
579 ],
580
581 'easy_invoice_invoice_numbering' => ['label' => __('Auto-increment invoice numbers', 'easy-invoice'), 'type' => 'checkbox', 'description' => __('Automatically increment invoice numbers for new invoices', 'easy-invoice'), 'default' => 'yes', 'col_span' => 'sm:col-span-6' ],
582 'easy_invoice_payment_reminder_days' => ['label' => __('Payment reminder (days after due date)', 'easy-invoice'), 'type' => 'number', 'default' => 3, 'min' => 0, 'col_span' => 'sm:col-span-3', 'description' => __('Emails the Reminder template once to each invoice still owed this many days after its due date. Leave empty to switch reminders off. Easy Invoice Pro replaces this with its own schedule.', 'easy-invoice')],
583 'easy_invoice_remove_data_on_uninstall' => ['label' => __('Remove all data when the plugin is deleted', 'easy-invoice'), 'type' => 'checkbox', 'default' => 'no', 'col_span' => 'sm:col-span-6', 'description' => __('Off, deleting the plugin from the Plugins screen keeps every invoice, quote, payment, client record and setting, so reinstalling picks up where you left off. On, all of it is removed — invoices are tax records, so leave this off unless you are certain.', 'easy-invoice')],
584 ]
585 ],
586 ];
587
588 // Cache and filter the config
589 $this->config_cache = apply_filters('easy_invoice_settings_fields_config', $config);
590
591 return $this->config_cache;
592 }
593
594 /**
595 * Get all gateway settings configurations
596 *
597 * @return array Gateway settings configs
598 */
599 private function getGatewaySettingsConfigs(): array {
600 $gateway_configs = [];
601
602 try {
603 // Get gateway manager from the main plugin instance
604 $gateway_manager = \EasyInvoice\EasyInvoice::getInstance()->getGatewayManager();
605 $gateways = $gateway_manager->getGateways();
606
607 // Collect settings from each gateway
608 foreach ($gateways as $gateway_id => $gateway) {
609 try {
610 $gateway_configs[$gateway_id] = $gateway->getSettingsConfig();
611 } catch (\Exception $e) {
612 // Log the error but continue with other gateways
613 $gateway_configs[$gateway_id] = [];
614 }
615 }
616 } catch (\Exception $e) {
617 }
618
619 return $gateway_configs;
620 }
621
622 /**
623 * Prepare settings for display with proper filtering
624 *
625 * @param array $settings The settings array
626 * @return array The filtered settings
627 */
628 public function prepareSettingsForDisplay($settings) {
629 return apply_filters('easy_invoice_settings_for_display', $settings);
630 }
631
632 /**
633 * Display method implementation
634 *
635 * @param array $args Display arguments
636 * @return void
637 */
638 public function display(array $args = []) {
639 $page = isset($args['page']) ? $args['page'] : '';
640 $subsection = isset($args['subsection']) ? $args['subsection'] : '';
641
642 // Clear cache to ensure new fields are recognized
643 $this->clearCache();
644
645 switch ($page) {
646 case PagesSlugs::SETTINGS:
647 if (!empty($subsection)) {
648 // Handle email settings subsections
649 $this->displayEmailSettingsPage($subsection);
650 } else {
651 $this->displaySettingsPage();
652 }
653 break;
654
655 default:
656 $this->displaySettingsPage();
657 break;
658 }
659 }
660
661 /**
662 * Display email settings page for specific subsection
663 *
664 * @param string $subsection The subsection to display
665 * @return void
666 */
667 protected function displayEmailSettingsPage($subsection) {
668 // Get settings configuration
669 $settings_config = $this->get_settings_fields_config();
670 $settings = $this->getSettings();
671
672 // Filter to show only email settings
673 $email_config = $settings_config['email'] ?? [];
674
675 // Map subsection to the appropriate subsection in email config
676 $subsection_mapping = [
677 PagesSlugs::EMAIL_SETTINGS_GENERAL => 'general',
678 PagesSlugs::EMAIL_SETTINGS_INVOICE => 'invoice_available',
679 PagesSlugs::EMAIL_SETTINGS_QUOTE => 'quote_available',
680 PagesSlugs::EMAIL_SETTINGS_PAYMENT => 'payment_received',
681 PagesSlugs::EMAIL_SETTINGS_PAYMENT_REMINDER => 'payment_reminder',
682 ];
683
684 $target_subsection = $subsection_mapping[$subsection] ?? 'general';
685
686 // Create a modified config with only the target subsection
687 $filtered_config = [
688 'email' => [
689 'title' => __('Email Settings', 'easy-invoice'),
690 'description' => __('Configure email sending options and templates', 'easy-invoice'),
691 'icon' => 'fas fa-envelope',
692 'subsections' => [
693 $target_subsection => $email_config['subsections'][$target_subsection] ?? []
694 ]
695 ]
696 ];
697
698 // Include the settings page template with filtered config
699 include EASY_INVOICE_PLUGIN_DIR . 'templates/settings-page.php';
700 }
701
702 /**
703 * Display settings page
704 *
705 * @return void
706 */
707 protected function displaySettingsPage() {
708 $settings = $this->getSettings();
709
710 // Allow developers to modify settings before display
711 $settings = apply_filters('easy_invoice_before_display_settings', $settings);
712
713 // Ensure payment methods are set (this is critical for gateway checkboxes)
714 if (!isset($settings['easy_invoice_payment_methods']) || !is_array($settings['easy_invoice_payment_methods'])) {
715 $settings['easy_invoice_payment_methods'] = get_option('easy_invoice_payment_methods', []);
716 }
717
718 // Ensure gateway order is set
719 if (!isset($settings['easy_invoice_payment_gateway_order']) || !is_array($settings['easy_invoice_payment_gateway_order'])) {
720 $settings['easy_invoice_payment_gateway_order'] = get_option('easy_invoice_payment_gateway_order', []);
721 }
722
723 $gateway_manager = \EasyInvoice\EasyInvoice::getInstance()->getGatewayManager();
724 $all_gateways_unsorted = $gateway_manager->getGateways();
725 $gateway_order_option = $settings['easy_invoice_payment_gateway_order'];
726 $payment_methods_enabled = $settings['easy_invoice_payment_methods'];
727
728 $all_gateways_sorted = [];
729 if (!empty($gateway_order_option)) {
730 foreach ($gateway_order_option as $gateway_id) {
731 if (isset($all_gateways_unsorted[$gateway_id])) {
732 $all_gateways_sorted[$gateway_id] = $all_gateways_unsorted[$gateway_id];
733 unset($all_gateways_unsorted[$gateway_id]);
734 }
735 }
736 }
737 $all_gateways_sorted = array_merge($all_gateways_sorted, $all_gateways_unsorted);
738
739 $settings_config = $this->get_settings_fields_config();
740
741 // Prepare template variables
742 $template_args = apply_filters('easy_invoice_settings_template_args', [
743 'settings' => $settings,
744 'settings_config' => $settings_config,
745 'all_gateways_sorted' => $all_gateways_sorted,
746 'payment_methods_enabled' => $payment_methods_enabled
747 ]);
748
749 // Display the template with template args
750 $this->displayTemplate(
751 apply_filters('easy_invoice_settings_template_path', EASY_INVOICE_PLUGIN_DIR . 'templates/settings-page.php'),
752 $template_args
753 );
754
755 // Action for plugins to add their own content after the settings page
756 do_action('easy_invoice_after_settings_page');
757 }
758
759 /**
760 * Save payment settings
761 *
762 * @param array $posted_settings Posted settings
763 * @param array $response Response array
764 * @return void
765 */
766 private function savePaymentSettings($posted_settings, &$response) {
767 // Handle payment methods
768 if (isset($posted_settings['easy_invoice_payment_methods']) && is_array($posted_settings['easy_invoice_payment_methods'])) {
769 $sanitized_methods = array_map('sanitize_key', $posted_settings['easy_invoice_payment_methods']);
770 update_option('easy_invoice_payment_methods', $sanitized_methods);
771 $response['saved_options']['easy_invoice_payment_methods'] = $sanitized_methods;
772 } else {
773 update_option('easy_invoice_payment_methods', []);
774 $response['saved_options']['easy_invoice_payment_methods'] = [];
775 }
776
777 // Handle gateway order
778 if (isset($posted_settings['easy_invoice_gateway_order']) && is_array($posted_settings['easy_invoice_gateway_order'])) {
779 $gateway_order = array_map('sanitize_key', $posted_settings['easy_invoice_gateway_order']);
780 update_option('easy_invoice_payment_gateway_order', $gateway_order);
781 $response['saved_options']['easy_invoice_gateway_order'] = $gateway_order;
782 } else {
783 update_option('easy_invoice_payment_gateway_order', []);
784 $response['saved_options']['easy_invoice_gateway_order'] = [];
785 }
786
787 // Handle gateway display names
788 $gateway_manager = \EasyInvoice\EasyInvoice::getInstance()->getGatewayManager();
789 $all_gateways = $gateway_manager->getGateways();
790
791 foreach ($all_gateways as $gateway_id => $gateway) {
792 $display_name_key = 'easy_invoice_gateway_display_name_' . $gateway_id;
793 if (isset($posted_settings[$display_name_key])) {
794 $display_name = wp_strip_all_tags(wp_unslash($posted_settings[$display_name_key]));
795 update_option($display_name_key, $display_name);
796 $response['saved_options'][$display_name_key] = $display_name;
797 }
798 }
799
800 // Handle gateway-specific settings
801 try {
802 $gateway_configs = $this->getGatewaySettingsConfigs();
803 foreach ($gateway_configs as $gateway_id => $gateway_config) {
804 if (isset($gateway_config['fields']) && is_array($gateway_config['fields'])) {
805 foreach ($gateway_config['fields'] as $option_key => $field_config) {
806 if (isset($posted_settings[$option_key])) {
807 $this->sanitizeAndSaveSetting($option_key, $field_config, $posted_settings[$option_key], $response);
808 } elseif ($field_config['type'] === 'checkbox') {
809 // Checkboxes not in POST are considered unchecked
810 update_option($option_key, 'no');
811 $response['saved_options'][$option_key] = 'no';
812 }
813 }
814 }
815 }
816 } catch (\Exception $e) {
817 throw $e; // Re-throw to be caught by the main try-catch
818 }
819 }
820
821 /**
822 * Settings → Payment Methods → "Add an offline method": registers a new
823 * offline payment method (ACH, wire transfer, mobile money, …) by name.
824 */
825 public function ajaxAddOfflineMethod(): void {
826 check_ajax_referer('easy_invoice_settings', 'nonce');
827 if (!current_user_can('manage_options')) {
828 wp_send_json_error(['message' => __('You do not have permission to change payment methods.', 'easy-invoice')]);
829 }
830 $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
831 $id = \EasyInvoice\Services\OfflinePayments::addCustom($title);
832 if (is_wp_error($id)) {
833 wp_send_json_error(['message' => $id->get_error_message()]);
834 }
835 wp_send_json_success([
836 'id' => $id,
837 /* translators: %s: method name. */
838 'message' => sprintf(__('"%s" added. Fill in its instructions below and save.', 'easy-invoice'), $title),
839 ]);
840 }
841
842 /**
843 * Remove a merchant-added offline method. Recorded payments keep the method name.
844 */
845 public function ajaxRemoveOfflineMethod(): void {
846 check_ajax_referer('easy_invoice_settings', 'nonce');
847 if (!current_user_can('manage_options')) {
848 wp_send_json_error(['message' => __('You do not have permission to change payment methods.', 'easy-invoice')]);
849 }
850 $id = isset($_POST['id']) ? sanitize_key(wp_unslash($_POST['id'])) : '';
851 if ('' === $id || !\EasyInvoice\Services\OfflinePayments::removeCustom($id)) {
852 wp_send_json_error(['message' => __('That payment method cannot be removed.', 'easy-invoice')]);
853 }
854 wp_send_json_success(['message' => __('Payment method removed.', 'easy-invoice')]);
855 }
856
857 /**
858 * Save settings
859 *
860 * @return void
861 */
862 public function saveSettings() {
863 // Verify nonce. $_POST['easy_invoice_settings_nonce'] was read unguarded, so a
864 // request without it raised "Undefined array key" before the check could run.
865 $nonce = isset($_POST['easy_invoice_settings_nonce'])
866 ? sanitize_text_field(wp_unslash($_POST['easy_invoice_settings_nonce']))
867 : '';
868 if (!wp_verify_nonce($nonce, 'easy_invoice_settings')) {
869 wp_die(esc_html__('Security check failed', 'easy-invoice'));
870 }
871
872
873 // Check permissions
874 if (!current_user_can('manage_options')) {
875 wp_die(esc_html__('You do not have permission to perform this action', 'easy-invoice'));
876 }
877
878 $posted_settings = $_POST['settings'] ?? [];
879
880 $settings_config = $this->get_settings_fields_config();
881 $response = [
882 'success' => true,
883 'message' => __('Settings saved successfully', 'easy-invoice'),
884 'saved_options' => []
885 ];
886
887 try {
888 // Process each section
889 foreach ($settings_config as $section_id => $section_data) {
890 if (isset($section_data['fields']) && is_array($section_data['fields'])) {
891 // Handle regular fields
892 foreach ($section_data['fields'] as $option_key => $field_config) {
893 if (isset($posted_settings[$option_key])) {
894 $this->sanitizeAndSaveSetting($option_key, $field_config, $posted_settings[$option_key], $response);
895 } elseif ($field_config['type'] === 'checkbox') {
896 // Checkboxes not in POST are considered unchecked
897 update_option($option_key, 'no');
898 $response['saved_options'][$option_key] = 'no';
899 }
900 }
901 } else if (isset($section_data['subsections']) && is_array($section_data['subsections'])) {
902 // Handle sections with subsections (like invoice, quote, email, etc.)
903 foreach ($section_data['subsections'] as $subsection_id => $subsection_data) {
904 if (isset($subsection_data['fields']) && is_array($subsection_data['fields'])) {
905 foreach ($subsection_data['fields'] as $option_key => $field_config) {
906 if (isset($posted_settings[$option_key])) {
907 $this->sanitizeAndSaveSetting($option_key, $field_config, $posted_settings[$option_key], $response);
908 } elseif ($field_config['type'] === 'checkbox') {
909 // Checkboxes not in POST are considered unchecked
910 update_option($option_key, 'no');
911 $response['saved_options'][$option_key] = 'no';
912 }
913 }
914 }
915 }
916 }
917 }
918
919 // Fields other code adds to the Tax section for display only
920 // (`easy_invoice_tax_section_additional_fields`) are saved with the
921 // same rules — the reverse-charge switch lived only there and could
922 // never be turned on from the screen.
923 $extra_tax_fields = apply_filters('easy_invoice_tax_section_additional_fields', [], []);
924 $known = [];
925 foreach ($settings_config as $section_data) {
926 foreach ((array) ($section_data['fields'] ?? []) as $k => $unused) { $known[$k] = true; }
927 foreach ((array) ($section_data['subsections'] ?? []) as $sub) { foreach ((array) ($sub['fields'] ?? []) as $k => $unused) { $known[$k] = true; } }
928 }
929 foreach ((array) $extra_tax_fields as $option_key => $field_config) {
930 if (!is_string($option_key) || isset($known[$option_key]) || !is_array($field_config)) {
931 continue;
932 }
933 if (isset($posted_settings[$option_key])) {
934 $this->sanitizeAndSaveSetting($option_key, $field_config, $posted_settings[$option_key], $response);
935 } elseif (($field_config['type'] ?? '') === 'checkbox') {
936 update_option($option_key, 'no');
937 $response['saved_options'][$option_key] = 'no';
938 }
939 }
940
941 // Handle special sections like payment methods
942 if (isset($settings_config['payment']) && isset($settings_config['payment']['is_special_section'])) {
943 $this->savePaymentSettings($posted_settings, $response);
944 }
945
946 // Clear any caches
947 wp_cache_flush();
948
949 // Clear settings cache
950 $this->settings_cache = null;
951 self::$shared_settings_cache = null;
952
953 // Log the save action
954 $this->log('Settings saved successfully by user: ' . get_current_user_id());
955
956 } catch (\Exception $e) {
957 $response['success'] = false;
958 $response['message'] = __('Error saving settings: ', 'easy-invoice') . $e->getMessage();
959 $this->log('Error saving settings: ' . $e->getMessage());
960
961 }
962
963 // Clear settings cache to ensure new fields are recognized
964 $this->clearCache();
965
966 // Send JSON response
967 wp_send_json($response);
968 }
969
970 /**
971 * Sanitize and save a single setting
972 *
973 * @param string $option_key The option key
974 * @param array $field_config The field configuration
975 * @param mixed $value The value to sanitize and save
976 * @param array &$response The response array to update
977 * @return void
978 */
979 private function sanitizeAndSaveSetting($option_key, $field_config, $value, &$response) {
980 if (in_array($option_key, [
981 'easy_invoice_quote_accept_text',
982 'easy_invoice_quote_accepted_message',
983 'easy_invoice_quote_declined_message'
984 ])) {
985 }
986
987 // Unslash the value to prevent double-escaping
988 $value = wp_unslash($value);
989
990 // Allow pre-sanitization filters
991 $value = apply_filters('easy_invoice_pre_sanitize_option', $value, $option_key, $field_config);
992
993 // Sanitize based on field type
994 switch ($field_config['type']) {
995 case 'email':
996 $value = sanitize_email($value);
997 break;
998 case 'url':
999 $value = esc_url_raw($value);
1000 break;
1001 case 'textarea':
1002 // Use wp_kses_post for textarea to allow safe HTML while preventing double-escaping
1003 $value = wp_kses_post($value);
1004 break;
1005 case 'wp_editor':
1006 // For wp_editor content, use WordPress's built-in sanitization
1007 if (is_string($value)) {
1008 // Use wp_kses_post which expects unslashed data
1009 $value = wp_kses_post($value);
1010 }
1011 break;
1012 case 'number':
1013 if (isset($field_config['step']) && strpos((string)$field_config['step'], '.') !== false) {
1014 $value = floatval(wp_strip_all_tags(easy_invoice_str_replace(',', '.', $value)));
1015 } elseif (isset($field_config['min']) && intval($field_config['min']) < 0) {
1016 $value = intval($value);
1017 } else {
1018 $value = absint($value);
1019 }
1020 break;
1021 case 'checkbox':
1022 // For checkboxes, we expect 'yes' or 'no' values
1023 $value = ($value === 'yes' || $value === '1' || $value === true) ? 'yes' : 'no';
1024 break;
1025 case 'select':
1026 $value = sanitize_key($value);
1027 // Ensure currency codes are always uppercase
1028 if ($option_key === 'easy_invoice_currency_code') {
1029 $value = strtoupper($value);
1030 }
1031 break;
1032 case 'multiselect':
1033 // For multiselect, ensure it's an array and sanitize each value
1034 if (!is_array($value)) {
1035 $value = [];
1036 } else {
1037 $value = array_map('sanitize_key', $value);
1038 }
1039 break;
1040 default:
1041 // For regular text fields, use wp_strip_all_tags to prevent double-escaping
1042 $value = wp_strip_all_tags($value);
1043 break;
1044 }
1045
1046 // Allow post-sanitization value modification
1047 $value = apply_filters("easy_invoice_sanitize_option_{$option_key}", $value, $field_config);
1048 $value = apply_filters('easy_invoice_sanitize_option', $value, $option_key, $field_config);
1049
1050 // Save the setting normally
1051 update_option($option_key, $value);
1052 $response['saved_options'][$option_key] = $value;
1053 }
1054
1055 /**
1056 * Get all settings for the plugin
1057 *
1058 * @param bool $use_cache Whether to use cached settings
1059 * @return array The settings array
1060 */
1061 public function getSettings($use_cache = true): array {
1062 // Return cached settings if available
1063 if ($use_cache && is_array($this->settings_cache)) {
1064 return $this->settings_cache;
1065 }
1066 if ($use_cache && is_array(self::$shared_settings_cache)) {
1067 return $this->settings_cache = self::$shared_settings_cache;
1068 }
1069
1070 $settings = [];
1071 $config = $this->get_settings_fields_config();
1072
1073 foreach ($config as $section_id => $section_data) {
1074 if ($section_id === 'payment' && isset($section_data['gateways'])) {
1075 // Handle gateway fields specifically
1076 foreach ($section_data['gateways'] as $gateway_id => $gateway_config) {
1077 // The per-gateway display name is saved by savePaymentSettings()
1078 // but was never loaded back, so the field always showed the default.
1079 $display_key = 'easy_invoice_gateway_display_name_' . $gateway_id;
1080 $display_val = get_option($display_key, '');
1081 if ('' !== $display_val) {
1082 $settings[$display_key] = $display_val;
1083 }
1084 if (isset($gateway_config['fields']) && is_array($gateway_config['fields'])) {
1085 foreach ($gateway_config['fields'] as $option_key => $field_data) {
1086 $default_value = $field_data['default'] ?? '';
1087 $settings[$option_key] = get_option($option_key, $default_value);
1088 }
1089 }
1090 }
1091 }
1092 else if (isset($section_data['subsections']) && is_array($section_data['subsections'])) {
1093 // Handle sections with subsections (like email, invoice, quote)
1094 foreach ($section_data['subsections'] as $subsection_id => $subsection_data) {
1095 if (isset($subsection_data['fields']) && is_array($subsection_data['fields'])) {
1096 foreach ($subsection_data['fields'] as $option_key => $field_data) {
1097 $default_value = $field_data['default'] ?? '';
1098
1099 // Handle special defaults
1100 if ($option_key === 'easy_invoice_email_from_name' && $default_value === get_bloginfo('name')) {
1101 $default_value = get_bloginfo('name');
1102 }
1103 if ($option_key === 'easy_invoice_email_from_address' && $default_value === get_bloginfo('admin_email')) {
1104 $default_value = get_bloginfo('admin_email');
1105 }
1106
1107 // Check if field has a value_callback function
1108 if (isset($field_data['value_callback']) && is_callable($field_data['value_callback'])) {
1109 $settings[$option_key] = $field_data['value_callback']();
1110 } else {
1111 // Get option value or default
1112 $settings[$option_key] = get_option($option_key, $default_value);
1113 }
1114 }
1115 }
1116 }
1117 }
1118 else if (isset($section_data['fields']) && is_array($section_data['fields'])) {
1119 foreach ($section_data['fields'] as $option_key => $field_data) {
1120 $default_value = $field_data['default'] ?? '';
1121
1122 // Handle special defaults
1123 if ($option_key === 'easy_invoice_email_from_name' && $default_value === get_bloginfo('name')) {
1124 $default_value = get_bloginfo('name');
1125 }
1126 if ($option_key === 'easy_invoice_email_from_address' && $default_value === get_bloginfo('admin_email')) {
1127 $default_value = get_bloginfo('admin_email');
1128 }
1129
1130 // Check if field has a value_callback function
1131 if (isset($field_data['value_callback']) && is_callable($field_data['value_callback'])) {
1132 $settings[$option_key] = $field_data['value_callback']();
1133 } else {
1134 // Get option value or default
1135 $settings[$option_key] = get_option($option_key, $default_value);
1136 }
1137 }
1138 }
1139 }
1140
1141 // Add special array settings
1142 $settings['easy_invoice_payment_methods'] = get_option('easy_invoice_payment_methods', []);
1143 $settings['easy_invoice_payment_gateway_order'] = get_option('easy_invoice_payment_gateway_order', []);
1144
1145 // Cache settings
1146 $this->settings_cache = $settings;
1147 self::$shared_settings_cache = $settings;
1148
1149 // Allow third party to add/modify settings
1150 return apply_filters('easy_invoice_settings', $settings);
1151 }
1152
1153 /**
1154 * Log debug information
1155 *
1156 * @param string $message The message to log
1157 * @return void
1158 */
1159 protected function log(string $message): void {
1160
1161 }
1162
1163 /**
1164 * Initialize default settings
1165 */
1166 public function initializeSettings() {
1167 // One-time migration (2.1.22): strip the leftover
1168 // "<strong>Shortcode:</strong> <code>[easy_(invoice|quote)_url …]</code>"
1169 // reference line from saved email bodies. Originally intended as admin-side
1170 // documentation, but it lived inside the email template and got emailed to
1171 // clients verbatim. Guarded by a flag option so each install runs at most once.
1172 //
1173 // The regex tolerates the optional surrounding <p>...</p> because WP's
1174 // wp_kses_post may normalise the wrapping when the option is saved.
1175 if (!get_option('easy_invoice_email_shortcode_hint_removed_v2')) {
1176 foreach (['easy_invoice_invoice_email_body', 'easy_invoice_quote_email_body'] as $opt_key) {
1177 $body = get_option($opt_key, '');
1178 if (!is_string($body) || $body === '') {
1179 continue;
1180 }
1181 $cleaned = preg_replace(
1182 '#\s*(?:<p>\s*)?<strong>\s*Shortcode\s*:\s*</strong>\s*<code>\s*\[easy_(?:invoice|quote)_url[^\]]*\]\s*</code>(?:\s*</p>)?\s*#i',
1183 '',
1184 $body
1185 );
1186 if ($cleaned !== null && $cleaned !== $body) {
1187 update_option($opt_key, $cleaned);
1188 }
1189 }
1190 update_option('easy_invoice_email_shortcode_hint_removed_v2', 1);
1191 }
1192
1193 // Initialize invoice number settings if not already set
1194 if (!get_option('easy_invoice_invoice_prefix')) {
1195 update_option('easy_invoice_invoice_prefix', 'INV-');
1196 }
1197
1198 if (!get_option('easy_invoice_next_invoice_number')) {
1199 update_option('easy_invoice_next_invoice_number', 1);
1200 }
1201
1202 // Initialize other default settings
1203 if (!get_option('easy_invoice_invoice_due_days')) {
1204 update_option('easy_invoice_invoice_due_days', 30);
1205 }
1206
1207 if (!get_option('easy_invoice_currency_code')) {
1208 update_option('easy_invoice_currency_code', 'USD');
1209 }
1210
1211 if (!get_option('easy_invoice_currency_symbol')) {
1212 update_option('easy_invoice_currency_symbol', '$');
1213 }
1214
1215 if (!get_option('easy_invoice_currency_position')) {
1216 update_option('easy_invoice_currency_position', 'left');
1217 }
1218
1219 if (!get_option('easy_invoice_currency_symbol_type')) {
1220 update_option('easy_invoice_currency_symbol_type', 'symbol');
1221 }
1222
1223 if (!get_option('easy_invoice_decimal_separator')) {
1224 update_option('easy_invoice_decimal_separator', '.');
1225 }
1226
1227 if (!get_option('easy_invoice_thousands_separator')) {
1228 update_option('easy_invoice_thousands_separator', ',');
1229 }
1230
1231 if (!get_option('easy_invoice_decimal_precision')) {
1232 update_option('easy_invoice_decimal_precision', 2);
1233 }
1234
1235 // Initialize date format setting
1236 if (!get_option('easy_invoice_date_format')) {
1237 update_option('easy_invoice_date_format', 'us');
1238 }
1239
1240 // Fix legacy date format values
1241 $current_date_format = get_option('easy_invoice_date_format');
1242 if ($current_date_format === 'mdy' || $current_date_format === 'm/d/Y') {
1243 update_option('easy_invoice_date_format', 'us');
1244 } elseif ($current_date_format === 'd/m/Y') {
1245 update_option('easy_invoice_date_format', 'uk');
1246 } elseif ($current_date_format === 'Y-m-d') {
1247 update_option('easy_invoice_date_format', 'iso');
1248 }
1249
1250 // Initialize email settings
1251 if (!get_option('easy_invoice_email_from_name')) {
1252 update_option('easy_invoice_email_from_name', get_bloginfo('name'));
1253 }
1254
1255 if (!get_option('easy_invoice_email_from_address')) {
1256 update_option('easy_invoice_email_from_address', get_bloginfo('admin_email'));
1257 }
1258
1259 if (!get_option('easy_invoice_enable_email_styling')) {
1260 update_option('easy_invoice_enable_email_styling', 'yes');
1261 }
1262
1263 if (!get_option('easy_invoice_bcc_admin')) {
1264 update_option('easy_invoice_bcc_admin', 'no');
1265 }
1266
1267 if (!get_option('easy_invoice_admin_email')) {
1268 update_option('easy_invoice_admin_email', get_option('admin_email'));
1269 }
1270
1271 // Initialize Text Settings
1272 $text_settings = [
1273 'easy_invoice_text_invoice' => __('Invoice', 'easy-invoice'),
1274 'easy_invoice_text_invoices' => __('Invoices', 'easy-invoice'),
1275 'easy_invoice_text_from' => __('From', 'easy-invoice'),
1276 'easy_invoice_text_to' => __('To', 'easy-invoice'),
1277 'easy_invoice_text_invoice_number' => __('Invoice Number', 'easy-invoice'),
1278 'easy_invoice_text_order_number' => __('Order Number', 'easy-invoice'),
1279 'easy_invoice_text_invoice_date' => __('Invoice Date', 'easy-invoice'),
1280 'easy_invoice_text_due_date' => __('Due Date', 'easy-invoice'),
1281 'easy_invoice_text_total_due' => __('Total Due', 'easy-invoice'),
1282 'easy_invoice_text_qty' => __('Qty', 'easy-invoice'),
1283 'easy_invoice_text_service' => __('Service', 'easy-invoice'),
1284 'easy_invoice_text_rate_price' => __('Rate', 'easy-invoice'),
1285 'easy_invoice_text_adjust' => __('Adjust', 'easy-invoice'),
1286 'easy_invoice_text_sub_total' => __('Sub Total', 'easy-invoice'),
1287 'easy_invoice_text_total' => __('Total', 'easy-invoice'),
1288 'easy_invoice_text_line_total' => __('Total', 'easy-invoice'),
1289 'easy_invoice_text_tax' => __('Tax', 'easy-invoice'),
1290 'easy_invoice_text_discount' => __('Discount', 'easy-invoice'),
1291 'easy_invoice_text_page' => __('Page', 'easy-invoice'),
1292 'easy_invoice_text_print' => __('Print', 'easy-invoice'),
1293 'easy_invoice_text_download_pdf' => __('Download as PDF', 'easy-invoice'),
1294 'easy_invoice_text_send_email' => __('Send Email', 'easy-invoice'),
1295 'easy_invoice_text_pay_now' => __('Pay Now', 'easy-invoice'),
1296 'easy_invoice_text_proceed_payment' => __('Proceed to payment', 'easy-invoice'),
1297 'easy_invoice_text_payment_gateway' => __('Invoice Payment Gateway', 'easy-invoice'),
1298 'easy_invoice_text_quote' => __('Quote', 'easy-invoice'),
1299 'easy_invoice_text_quotes' => __('Quotes', 'easy-invoice'),
1300 'easy_invoice_text_quote_number' => __('Quote Number', 'easy-invoice'),
1301 'easy_invoice_text_accept_quote' => __('Accept Quote', 'easy-invoice'),
1302 'easy_invoice_text_decline_quote' => __('Decline Quote', 'easy-invoice'),
1303 'easy_invoice_text_decline_reason' => __('Reason for declining', 'easy-invoice'),
1304 'easy_invoice_text_quote_amount' => __('Quote Amount', 'easy-invoice'),
1305 'easy_invoice_text_valid_until' => __('Valid Until Date', 'easy-invoice'),
1306 'easy_invoice_text_quote_date' => __('Quote Date', 'easy-invoice'),
1307 'easy_invoice_text_available' => __('Available', 'easy-invoice'),
1308 'easy_invoice_text_draft' => __('Draft', 'easy-invoice'),
1309 'easy_invoice_text_overdue' => __('Overdue', 'easy-invoice'),
1310 'easy_invoice_text_paid' => __('Paid', 'easy-invoice'),
1311 'easy_invoice_text_unpaid' => __('Unpaid', 'easy-invoice'),
1312 'easy_invoice_text_cancelled' => __('Cancelled', 'easy-invoice'),
1313 ];
1314
1315 foreach ($text_settings as $option_key => $default_value) {
1316 if (!get_option($option_key)) {
1317 update_option($option_key, $default_value);
1318 }
1319 }
1320
1321 if (!get_option('easy_invoice_invoice_email_enabled')) {
1322 update_option('easy_invoice_invoice_email_enabled', 'yes');
1323 }
1324
1325 if (!get_option('easy_invoice_invoice_email_subject')) {
1326 update_option('easy_invoice_invoice_email_subject', __('Your Invoice #{{invoice_number}} from {{company_name}}', 'easy-invoice'));
1327 }
1328
1329 if (!get_option('easy_invoice_invoice_email_body')) {
1330 update_option('easy_invoice_invoice_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('invoice'));
1331 }
1332
1333 if (!get_option('easy_invoice_quote_email_enabled')) {
1334 update_option('easy_invoice_quote_email_enabled', 'yes');
1335 }
1336
1337 if (!get_option('easy_invoice_quote_email_subject')) {
1338 update_option('easy_invoice_quote_email_subject', __('Your Quote #{{quote_number}} from {{company_name}}', 'easy-invoice'));
1339 }
1340
1341 if (!get_option('easy_invoice_quote_email_body')) {
1342 update_option('easy_invoice_quote_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('quote'));
1343 }
1344
1345 if (!get_option('easy_invoice_payment_email_enabled')) {
1346 update_option('easy_invoice_payment_email_enabled', 'yes');
1347 }
1348
1349 if (!get_option('easy_invoice_payment_email_subject')) {
1350 update_option('easy_invoice_payment_email_subject', __('Payment Received - Invoice #{{invoice_number}}', 'easy-invoice'));
1351 }
1352
1353 if (!get_option('easy_invoice_payment_email_body')) {
1354 update_option('easy_invoice_payment_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('payment'));
1355 }
1356 }
1357
1358 /**
1359 * Test email functionality
1360 */
1361 public function testEmail(): void {
1362 // Verify nonce
1363 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_settings')) {
1364 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
1365 }
1366
1367 // Check permissions
1368 if (!current_user_can('manage_options')) {
1369 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
1370 }
1371
1372 // Get test email address
1373 $test_email = sanitize_email($_POST['test_email'] ?? '');
1374 if (empty($test_email)) {
1375 wp_send_json_error(['message' => __('Please provide a valid email address', 'easy-invoice')]);
1376 }
1377
1378 // Test email sending
1379 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1380 $result = $email_manager->testEmail($test_email);
1381
1382 if ($result['success']) {
1383 wp_send_json_success($result);
1384 } else {
1385 wp_send_json_error($result);
1386 }
1387 }
1388
1389 /**
1390 * Test payment reminder email functionality
1391 */
1392 public function testPaymentReminderEmail(): void {
1393 // Verify nonce
1394 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_settings')) {
1395 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
1396 }
1397
1398 // Check permissions
1399 if (!current_user_can('manage_options')) {
1400 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
1401 }
1402
1403 // Get test email address
1404 $test_email = sanitize_email($_POST['test_email'] ?? '');
1405 if (empty($test_email)) {
1406 wp_send_json_error(['message' => __('Please provide a valid email address', 'easy-invoice')]);
1407 }
1408
1409 // Get payment reminder settings
1410 $subject = get_option('easy_invoice_pro_payment_reminder_subject', __('A friendly reminder - Invoice #{{invoice_number}}', 'easy-invoice'));
1411 $body = get_option('easy_invoice_pro_payment_reminder_body', '');
1412
1413 if (empty($body)) {
1414 wp_send_json_error(['message' => __('Payment reminder email template is empty. Please configure the email message first.', 'easy-invoice')]);
1415 }
1416
1417 // Create sample data for testing
1418 $sample_data = [
1419 'invoice_number' => 'TEST-001',
1420 'client_name' => 'Test Client',
1421 'company_name' => get_option('easy_invoice_company_name', get_bloginfo('name') ?: 'Easy Invoice'),
1422 'company_email' => get_option('easy_invoice_company_email', get_option('admin_email', '')),
1423 'total_amount' => '$1,000.00',
1424 'amount_due' => '$1,000.00',
1425 'due_date' => wp_date('Y-m-d', strtotime('+7 days')),
1426 ];
1427
1428 // Replace placeholders
1429 foreach ($sample_data as $placeholder => $value) {
1430 $subject = easy_invoice_str_replace('{{' . $placeholder . '}}', $value, $subject);
1431 $body = easy_invoice_str_replace('{{' . $placeholder . '}}', $value, $body);
1432 }
1433
1434 // Test email sending
1435 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1436 $result = $email_manager->sendTestTemplateEmail($test_email, $subject, $body);
1437
1438 if ($result['success']) {
1439 wp_send_json_success($result);
1440 } else {
1441 wp_send_json_error($result);
1442 }
1443 }
1444
1445 /**
1446 * Test template email functionality
1447 */
1448 public function testTemplateEmail(): void {
1449 // Verify nonce
1450 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'easy_invoice_settings')) {
1451 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
1452 }
1453
1454 // Check permissions
1455 if (!current_user_can('manage_options')) {
1456 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
1457 }
1458
1459 // Get test email address and template type
1460 $test_email = sanitize_email($_POST['test_email'] ?? '');
1461 $template_type = sanitize_text_field($_POST['template_type'] ?? '');
1462
1463 if (empty($test_email)) {
1464 wp_send_json_error(['message' => __('Please provide a valid email address', 'easy-invoice')]);
1465 }
1466
1467 if (empty($template_type)) {
1468 wp_send_json_error(['message' => __('Template type is required', 'easy-invoice')]);
1469 }
1470
1471 // Get the actual configured email settings
1472 $subject = '';
1473 $body = '';
1474
1475 switch ($template_type) {
1476 case 'invoice_available':
1477 $subject = get_option('easy_invoice_invoice_email_subject', __('Your Invoice #{{invoice_number}} from {{company_name}}', 'easy-invoice'));
1478 $body = get_option('easy_invoice_invoice_email_body', self::getInvoiceEmailBody());
1479 break;
1480 case 'quote_available':
1481 $subject = get_option('easy_invoice_quote_email_subject', __('Your Quote #{{quote_number}} from {{company_name}}', 'easy-invoice'));
1482 $body = get_option('easy_invoice_quote_email_body', self::getQuoteEmailBody());
1483 break;
1484 case 'payment_received':
1485 $subject = get_option('easy_invoice_payment_email_subject', __('Payment Received - Invoice #{{invoice_number}}', 'easy-invoice'));
1486 $body = get_option('easy_invoice_payment_email_body', self::getPaymentEmailBody());
1487 break;
1488 default:
1489 wp_send_json_error(['message' => __('Invalid template type', 'easy-invoice')]);
1490 }
1491
1492 // Ensure we have valid strings
1493 $subject = is_string($subject) ? $subject : '';
1494 $body = is_string($body) ? $body : '';
1495
1496 if (empty($subject) || empty($body)) {
1497 wp_send_json_error(['message' => __('Email template is empty or invalid', 'easy-invoice')]);
1498 }
1499
1500 // Create sample data for testing
1501 $sample_data = [
1502 'invoice_number' => 'TEST-001',
1503 'quote_number' => 'TEST-Q-001',
1504 'client_name' => 'Test Client',
1505 'company_name' => get_option('easy_invoice_company_name', get_bloginfo('name') ?: 'Easy Invoice'),
1506 'company_email' => get_option('easy_invoice_company_email', get_option('admin_email', '')),
1507 'total_amount' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(1000) : '$1,000.00',
1508 'payment_amount' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(1000) : '$1,000.00',
1509 'due_date' => wp_date('Y-m-d', strtotime('+30 days')),
1510 'expiry_date' => wp_date('Y-m-d', strtotime('+30 days')),
1511 'payment_date' => current_time('Y-m-d'),
1512 'payment_method' => 'Credit Card',
1513 'transaction_id' => 'TXN-' . uniqid(),
1514 'payment_terms' => get_option('easy_invoice_payment_terms', __('Due on receipt', 'easy-invoice')),
1515 // Everything the stock templates can mention, so a test never shows a raw tag.
1516 'amount_due' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(1000) : '$1,000.00',
1517 'subtotal' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(1000) : '$1,000.00',
1518 'tax_amount' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(0) : '$0.00',
1519 'discount_amount' => function_exists('easy_invoice_format_money') ? easy_invoice_format_money(0) : '$0.00',
1520 'issue_date' => current_time('Y-m-d'),
1521 'invoice_title' => __('Sample invoice', 'easy-invoice'),
1522 'quote_title' => __('Sample quote', 'easy-invoice'),
1523 'invoice_url' => home_url('/'),
1524 'quote_url' => home_url('/'),
1525 'payment_url' => home_url('/'),
1526 'site_url' => home_url('/'),
1527 'admin_url' => admin_url(),
1528 'client_email' => '[email protected]',
1529 'client_first_name' => 'Test',
1530 'client_last_name' => 'Client',
1531 'client_address' => '1 Example Street',
1532 'company_phone' => get_option('easy_invoice_company_phone', ''),
1533 'company_address' => get_option('easy_invoice_company_address', ''),
1534 'company_website' => get_option('easy_invoice_company_website', ''),
1535 ];
1536
1537 // Ensure all sample data values are strings
1538 foreach ($sample_data as $key => $value) {
1539 $sample_data[$key] = is_string($value) ? $value : (string) $value;
1540 }
1541
1542 // Replace placeholders in subject and body
1543 foreach ($sample_data as $placeholder => $value) {
1544 $subject = easy_invoice_str_replace('{{' . $placeholder . '}}', $value, $subject);
1545 $body = easy_invoice_str_replace('{{' . $placeholder . '}}', $value, $body);
1546 }
1547
1548 // Test email sending
1549 $email_manager = \EasyInvoice\Services\EmailManager::getInstance();
1550 $result = $email_manager->sendTestTemplateEmail($test_email, $subject, $body);
1551
1552 if ($result['success']) {
1553 wp_send_json_success($result);
1554 } else {
1555 wp_send_json_error($result);
1556 }
1557 }
1558
1559 /**
1560 * Check if quote adjust field should be shown
1561 *
1562 * @return bool True if adjust field should be shown
1563 */
1564 public static function shouldShowQuoteAdjustField(): bool {
1565 return get_option('easy_invoice_quote_show_adjust_field', 'yes') === 'yes';
1566 }
1567
1568 /**
1569 * Check if invoice adjust field should be shown
1570 *
1571 * @return bool True if adjust field should be shown
1572 */
1573 public static function shouldShowInvoiceAdjustField(): bool {
1574 return get_option('easy_invoice_invoice_show_adjust_field', 'yes') === 'yes';
1575 }
1576
1577 /**
1578 * Get invoice prefix
1579 *
1580 * @return string Invoice prefix
1581 */
1582 public static function getInvoicePrefix(): string {
1583 return get_option('easy_invoice_invoice_prefix', 'EIN_');
1584 }
1585
1586 /**
1587 * Get invoice starting number
1588 *
1589 * @return int Invoice starting number
1590 */
1591 public static function getInvoiceStartingNumber(): int {
1592 return (int) get_option('easy_invoice_invoice_starting_number', 0);
1593 }
1594
1595 /**
1596 * Get invoice terms and conditions
1597 *
1598 * @return string Invoice terms and conditions
1599 */
1600 public static function getInvoiceTermsConditions(): string {
1601 return get_option('easy_invoice_invoice_terms_conditions', __('Payment is due within 30 days from date of invoice', 'easy-invoice'));
1602 }
1603
1604 /**
1605 * Get invoice footer text
1606 *
1607 * @return string Invoice footer text
1608 */
1609 public static function getInvoiceFooterText(): string {
1610 return get_option('easy_invoice_invoice_footer_text', '');
1611 }
1612
1613 /**
1614 * Get quote prefix
1615 *
1616 * @return string Quote prefix
1617 */
1618 public static function getQuotePrefix(): string {
1619 return get_option('easy_invoice_quote_prefix', 'EIQN_');
1620 }
1621
1622 /**
1623 * Get quote starting number
1624 *
1625 * @return int Quote starting number
1626 */
1627 public static function getQuoteStartingNumber(): int {
1628 return (int) get_option('easy_invoice_quote_starting_number', 1);
1629 }
1630
1631 /**
1632 * Get quote terms and conditions
1633 *
1634 * @return string Quote terms and conditions
1635 */
1636 public static function getQuoteTermsConditions(): string {
1637 return get_option('easy_invoice_quote_terms_conditions', __('This quote has a fixed price. Upon acceptance, we kindly ask for a 25% deposit prior to initiating the work.', 'easy-invoice'));
1638 }
1639
1640 /**
1641 * Get quote footer text
1642 *
1643 * @return string Quote footer text
1644 */
1645 public static function getQuoteFooterText(): string {
1646 return get_option('easy_invoice_quote_footer_text', __('Thank you for your business.', 'easy-invoice'));
1647 }
1648
1649 /**
1650 * Check if quote accept button should be shown
1651 *
1652 * @return bool True if accept button should be shown
1653 */
1654 public static function shouldShowQuoteAcceptButton(): bool {
1655 return get_option('easy_invoice_quote_accept_button', 'yes') === 'yes';
1656 }
1657
1658 /**
1659 * Get quote accept action
1660 *
1661 * @return string Quote accept action
1662 */
1663 public static function getQuoteAcceptAction(): string {
1664 return get_option('easy_invoice_quote_accept_action', 'convert');
1665 }
1666
1667 /**
1668 * Get quote accept text
1669 *
1670 * @return string Quote accept text
1671 */
1672 public static function getQuoteAcceptText(): string {
1673 return get_option('easy_invoice_quote_accept_text', __('Important: When you accept this Quote, an Invoice will be created automatically. This will form a legally binding contract.', 'easy-invoice'));
1674 }
1675
1676 /**
1677 * Get accepted quote message
1678 *
1679 * @return string Accepted quote message
1680 */
1681 public static function getAcceptedQuoteMessage(): string {
1682 return get_option('easy_invoice_quote_accepted_message', __('You\'ve confirmed the Quote.<br>We\'ll get in touch with you shortly.', 'easy-invoice'));
1683 }
1684
1685 /**
1686 * Check if decline reason is required
1687 *
1688 * @return bool True if decline reason is required
1689 */
1690 public static function isDeclineReasonRequired(): bool {
1691 return get_option('easy_invoice_quote_decline_reason_required', 'no') === 'yes';
1692 }
1693
1694 /**
1695 * Get declined quote message
1696 *
1697 * @return string Declined quote message
1698 */
1699 public static function getDeclinedQuoteMessage(): string {
1700 return get_option('easy_invoice_quote_declined_message', '');
1701 }
1702
1703 // Email Settings Helper Functions
1704
1705 /**
1706 * Check if invoice available email is enabled
1707 *
1708 * @return bool True if invoice email is enabled
1709 */
1710 public static function isInvoiceEmailEnabled(): bool {
1711 return get_option('easy_invoice_invoice_email_enabled', 'yes') === 'yes';
1712 }
1713
1714 /**
1715 * Get invoice email subject
1716 *
1717 * @return string Invoice email subject
1718 */
1719 public static function getInvoiceEmailSubject(): string {
1720 return get_option('easy_invoice_invoice_email_subject', __('Your Invoice #{{invoice_number}} from {{company_name}}', 'easy-invoice'));
1721 }
1722
1723 /**
1724 * Get invoice email body
1725 *
1726 * @return string Invoice email body
1727 */
1728 public static function getInvoiceEmailBody(): string {
1729 return get_option('easy_invoice_invoice_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('invoice'));
1730 }
1731
1732 /**
1733 * Check if quote available email is enabled
1734 *
1735 * @return bool True if quote email is enabled
1736 */
1737 public static function isQuoteEmailEnabled(): bool {
1738 return get_option('easy_invoice_quote_email_enabled', 'yes') === 'yes';
1739 }
1740
1741 /**
1742 * Get quote email subject
1743 *
1744 * @return string Quote email subject
1745 */
1746 public static function getQuoteEmailSubject(): string {
1747 return get_option('easy_invoice_quote_email_subject', __('Your Quote #{{quote_number}} from {{company_name}}', 'easy-invoice'));
1748 }
1749
1750 /**
1751 * Get quote email body
1752 *
1753 * @return string Quote email body
1754 */
1755 public static function getQuoteEmailBody(): string {
1756 return get_option('easy_invoice_quote_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('quote'));
1757 }
1758
1759 /**
1760 * Check if payment received email is enabled
1761 *
1762 * @return bool True if payment email is enabled
1763 */
1764 public static function isPaymentEmailEnabled(): bool {
1765 return get_option('easy_invoice_payment_email_enabled', 'yes') === 'yes';
1766 }
1767
1768 /**
1769 * Get payment email subject
1770 *
1771 * @return string Payment email subject
1772 */
1773 public static function getPaymentEmailSubject(): string {
1774 return get_option('easy_invoice_payment_email_subject', __('Payment Received - Invoice #{{invoice_number}}', 'easy-invoice'));
1775 }
1776
1777 /**
1778 * Get payment email body
1779 *
1780 * @return string Payment email body
1781 */
1782 public static function getPaymentEmailBody(): string {
1783 return get_option('easy_invoice_payment_email_body', \EasyInvoice\Services\EmailManager::defaultTemplate('payment'));
1784 }
1785
1786 // ========================================
1787 // TEXT SETTINGS HELPER FUNCTIONS
1788 // ========================================
1789 //
1790 // USAGE IN TEMPLATES:
1791 // Instead of: echo __('Invoice', 'easy-invoice');
1792 // Use: echo \EasyInvoice\Controllers\SettingsController::getTextInvoice();
1793 //
1794 // Example:
1795 // <h1>echo \EasyInvoice\Controllers\SettingsController::getTextInvoice();</h1>
1796 // <p>echo \EasyInvoice\Controllers\SettingsController::getTextFrom(); : echo $company_name;</p>
1797 // <p>echo \EasyInvoice\Controllers\SettingsController::getTextTo(); : echo $client_name;</p>
1798 //
1799
1800 /**
1801 * Get custom text setting with fallback to default
1802 *
1803 * @param string $key Text setting key
1804 * @param string $default Default text
1805 * @return string Custom text or default
1806 */
1807 /**
1808 * Every document label the site can override, with its saved value.
1809 *
1810 * The getText*() methods below are what templates call; this list exists
1811 * for anything that needs the whole set at once -- the Pro Translations
1812 * page offers each saved label for translation.
1813 *
1814 * @return array<string,array{label:string,default:string,value:string}> keyed by setting key.
1815 */
1816 public static function getTextLabels(): array {
1817 $labels = [
1818 'invoice' => __('Invoice', 'easy-invoice'),
1819 'invoices' => __('Invoices', 'easy-invoice'),
1820 'to' => __('To', 'easy-invoice'),
1821 'from' => __('From', 'easy-invoice'),
1822 'invoice_number' => __('Invoice Number', 'easy-invoice'),
1823 'invoice_date' => __('Invoice Date', 'easy-invoice'),
1824 'due_date' => __('Due Date', 'easy-invoice'),
1825 'total_due' => __('Total Due', 'easy-invoice'),
1826 'qty' => __('Qty', 'easy-invoice'),
1827 'service' => __('Service', 'easy-invoice'),
1828 'rate_price' => __('Rate', 'easy-invoice'),
1829 'adjust' => __('Adjust', 'easy-invoice'),
1830 'sub_total' => __('Sub Total', 'easy-invoice'),
1831 'total' => __('Total', 'easy-invoice'),
1832 'tax' => __('Tax', 'easy-invoice'),
1833 'discount' => __('Discount', 'easy-invoice'),
1834 'print' => __('Print', 'easy-invoice'),
1835 'download_pdf' => __('Download as PDF', 'easy-invoice'),
1836 'send_email' => __('Send Email', 'easy-invoice'),
1837 'pay_now' => __('Pay Now', 'easy-invoice'),
1838 'quote' => __('Quote', 'easy-invoice'),
1839 'quote_number' => __('Quote Number', 'easy-invoice'),
1840 'quote_date' => __('Quote Date', 'easy-invoice'),
1841 'valid_until' => __('Valid Until Date', 'easy-invoice'),
1842 'accept_quote' => __('Accept Quote', 'easy-invoice'),
1843 'decline_quote' => __('Decline Quote', 'easy-invoice'),
1844 'decline_reason' => __('Reason for declining', 'easy-invoice'),
1845 ];
1846 $out = [];
1847 foreach ($labels as $key => $default) {
1848 $out[$key] = [
1849 'label' => ucfirst(str_replace('_', ' ', $key)),
1850 'default' => $default,
1851 'value' => (string) get_option('easy_invoice_text_' . $key, $default),
1852 ];
1853 }
1854 return $out;
1855 }
1856
1857 public static function getTextSetting(string $key, string $default): string {
1858 $option_key = 'easy_invoice_text_' . $key;
1859 $value = (string) get_option($option_key, $default);
1860
1861 /**
1862 * Filter a document label after the saved override is applied.
1863 *
1864 * Saved labels are literal text and skip gettext, so anything that
1865 * renders a document in another language (Pro's Client Language) uses
1866 * this to run them through the active locale.
1867 *
1868 * @param string $value Label to show.
1869 * @param string $key Setting key, e.g. 'total'.
1870 * @param string $default Translated default.
1871 */
1872 return (string) apply_filters('easy_invoice_text_setting', $value, $key, $default);
1873 }
1874
1875 // Invoice Text Settings
1876 public static function getTextInvoice(): string {
1877 return self::getTextSetting('invoice', __('Invoice', 'easy-invoice'));
1878 }
1879
1880 public static function getTextInvoices(): string {
1881 return self::getTextSetting('invoices', __('Invoices', 'easy-invoice'));
1882 }
1883
1884 public static function getTextTo(): string {
1885 return self::getTextSetting('to', __('To', 'easy-invoice'));
1886 }
1887
1888 public static function getTextInvoiceNumber(): string {
1889 return self::getTextSetting('invoice_number', __('Invoice Number', 'easy-invoice'));
1890 }
1891
1892 public static function getTextInvoiceDate(): string {
1893 return self::getTextSetting('invoice_date', __('Invoice Date', 'easy-invoice'));
1894 }
1895
1896 public static function getTextDueDate(): string {
1897 return self::getTextSetting('due_date', __('Due Date', 'easy-invoice'));
1898 }
1899
1900 public static function getTextTotalDue(): string {
1901 return self::getTextSetting('total_due', __('Total Due', 'easy-invoice'));
1902 }
1903
1904 public static function getTextQty(): string {
1905 return self::getTextSetting('qty', __('Qty', 'easy-invoice'));
1906 }
1907
1908 public static function getTextService(): string {
1909 return self::getTextSetting('service', __('Service', 'easy-invoice'));
1910 }
1911
1912 public static function getTextRatePrice(): string {
1913 return self::getTextSetting('rate_price', __('Rate', 'easy-invoice'));
1914 }
1915
1916 public static function getTextAdjust(): string {
1917 return self::getTextSetting('adjust', __('Adjust', 'easy-invoice'));
1918 }
1919
1920 public static function getTextSubTotal(): string {
1921 return self::getTextSetting('sub_total', __('Sub Total', 'easy-invoice'));
1922 }
1923
1924 public static function getTextTotal(): string {
1925 return self::getTextSetting('total', __('Total', 'easy-invoice'));
1926 }
1927
1928 /** Heading of the line-item amount column, separate from the grand total. */
1929 public static function getTextLineTotal(): string {
1930 return self::getTextSetting('line_total', __('Total', 'easy-invoice'));
1931 }
1932
1933 public static function getTextTax(): string {
1934 return self::getTextSetting('tax', __('Tax', 'easy-invoice'));
1935 }
1936
1937 public static function getTextDiscount(): string {
1938 return self::getTextSetting('discount', __('Discount', 'easy-invoice'));
1939 }
1940
1941 public static function getTextPrint(): string {
1942 return self::getTextSetting('print', __('Print', 'easy-invoice'));
1943 }
1944
1945 public static function getTextDownloadPdf(): string {
1946 return self::getTextSetting('download_pdf', __('Download as PDF', 'easy-invoice'));
1947 }
1948
1949 public static function getTextSendEmail(): string {
1950 return self::getTextSetting('send_email', __('Send Email', 'easy-invoice'));
1951 }
1952
1953 public static function getTextPayNow(): string {
1954 return self::getTextSetting('pay_now', __('Pay Now', 'easy-invoice'));
1955 }
1956
1957 // Quote Text Settings
1958 public static function getTextQuote(): string {
1959 return self::getTextSetting('quote', __('Quote', 'easy-invoice'));
1960 }
1961
1962 public static function getTextQuoteNumber(): string {
1963 return self::getTextSetting('quote_number', __('Quote Number', 'easy-invoice'));
1964 }
1965
1966 public static function getTextAcceptQuote(): string {
1967 return self::getTextSetting('accept_quote', __('Accept Quote', 'easy-invoice'));
1968 }
1969
1970 public static function getTextDeclineQuote(): string {
1971 return self::getTextSetting('decline_quote', __('Decline Quote', 'easy-invoice'));
1972 }
1973
1974 public static function getTextDeclineReason(): string {
1975 return self::getTextSetting('decline_reason', __('Reason for declining', 'easy-invoice'));
1976 }
1977
1978 public static function getTextValidUntil(): string {
1979 return self::getTextSetting('valid_until', __('Valid Until Date', 'easy-invoice'));
1980 }
1981
1982 public static function getTextQuoteDate(): string {
1983 return self::getTextSetting('quote_date', __('Quote Date', 'easy-invoice'));
1984 }
1985
1986 public static function getTextFrom(): string {
1987 return self::getTextSetting('from', __('From', 'easy-invoice'));
1988 }
1989
1990 /**
1991 * Get the actual date format string from the stored format identifier
1992 *
1993 * @param string $format_identifier The format identifier (us, uk, iso)
1994 * @return string The actual date format string
1995 */
1996 public static function getDateFormatString($format_identifier = null): string {
1997 if ($format_identifier === null) {
1998 $format_identifier = get_option('easy_invoice_date_format', 'us');
1999 }
2000
2001 switch ($format_identifier) {
2002 case 'uk':
2003 return 'd/m/Y';
2004 case 'iso':
2005 return 'Y-m-d';
2006 case 'us':
2007 default:
2008 return 'm/d/Y';
2009 }
2010 }
2011
2012 /**
2013 * Get the current date format string
2014 *
2015 * @return string The current date format string
2016 */
2017 public static function getCurrentDateFormat(): string {
2018 return self::getDateFormatString();
2019 }
2020
2021 /**
2022 * Format a date using the Easy Invoice date format setting
2023 *
2024 * @param string|int $date The date to format (timestamp or date string)
2025 * @return string The formatted date
2026 */
2027 public static function formatDate($date): string {
2028 $format = self::getCurrentDateFormat();
2029 $timestamp = is_numeric($date) ? $date : strtotime($date);
2030 return date_i18n($format, $timestamp);
2031 }
2032
2033 /**
2034 * AJAX handler for regenerating invoice numbers
2035 */
2036 public function ajaxRegenerateInvoiceNumbers() {
2037
2038 // Verify nonce
2039 $nonce = $_POST['nonce'] ?? $_POST['easy_invoice_settings_nonce'] ?? '';
2040 if (!wp_verify_nonce($nonce, 'easy_invoice_settings')) {
2041 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
2042 }
2043
2044 // Check permissions
2045 if (!current_user_can('manage_options')) {
2046 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
2047 }
2048
2049 try {
2050 // Get the current next invoice number
2051 $current_next_number = intval(get_option('easy_invoice_next_invoice_number', 1));
2052 $prefix = get_option('easy_invoice_invoice_prefix', 'EIIN_');
2053
2054 // Start regeneration from the current next number
2055 $starting_number = $current_next_number;
2056
2057 // Get all invoices ordered by creation date
2058 $invoices = get_posts([
2059 'post_type' => 'easy_invoice',
2060 'post_status' => 'publish',
2061 'numberposts' => -1,
2062 'orderby' => 'date',
2063 'order' => 'ASC'
2064 ]);
2065
2066 if (empty($invoices)) {
2067 wp_send_json_success([
2068 'message' => __('No invoices found to regenerate', 'easy-invoice'),
2069 'regenerated_count' => 0
2070 ]);
2071 }
2072
2073 $regenerated_count = 0;
2074 $current_number = $starting_number;
2075
2076 foreach ($invoices as $invoice) {
2077 // Generate new invoice number
2078 $new_invoice_number = $prefix . str_pad($current_number, 6, '0', STR_PAD_LEFT);
2079
2080 // Update the invoice number
2081 update_post_meta($invoice->ID, '_easy_invoice_number', $new_invoice_number);
2082
2083 $regenerated_count++;
2084 $current_number++;
2085 }
2086
2087 // Update the next invoice number counter to continue from the last regenerated number + 1
2088 update_option('easy_invoice_next_invoice_number', $current_number);
2089
2090 wp_send_json_success([
2091 /* translators: %d: number regenerated. */
2092 'message' => sprintf(__('Successfully regenerated %d invoice numbers', 'easy-invoice'), $regenerated_count),
2093 'regenerated_count' => $regenerated_count,
2094 'next_number' => $current_number
2095 ]);
2096
2097 } catch (Exception $e) {
2098 wp_send_json_error(['message' => __('Failed to regenerate invoice numbers', 'easy-invoice')]);
2099 }
2100 }
2101
2102 /**
2103 * AJAX handler for regenerating quote numbers
2104 */
2105 public function ajaxRegenerateQuoteNumbers() {
2106
2107 // Verify nonce
2108 $nonce = $_POST['nonce'] ?? $_POST['easy_invoice_settings_nonce'] ?? '';
2109 if (!wp_verify_nonce($nonce, 'easy_invoice_settings')) {
2110 wp_send_json_error(['message' => __('Security check failed', 'easy-invoice')]);
2111 }
2112
2113 // Check permissions
2114 if (!current_user_can('manage_options')) {
2115 wp_send_json_error(['message' => __('You do not have permission to perform this action', 'easy-invoice')]);
2116 }
2117
2118 try {
2119 $current_next_number = intval(get_option('easy_invoice_next_quote_number', 1));
2120 $prefix = get_option('easy_invoice_quote_prefix', 'QT-');
2121
2122 $starting_number = $current_next_number;
2123
2124 $quotes = get_posts([
2125 'post_type' => 'easy_invoice_quote',
2126 'post_status' => 'publish',
2127 'numberposts' => -1,
2128 'orderby' => 'date',
2129 'order' => 'ASC',
2130 'meta_query' => [
2131 [
2132 'key' => '_easy_invoice_quote_number',
2133 'compare' => 'EXISTS'
2134 ]
2135 ]
2136 ]);
2137
2138 $regenerated_count = 0;
2139 $current_number = $starting_number;
2140
2141 foreach ($quotes as $quote_post) {
2142 $new_quote_number = $prefix . str_pad($current_number, 6, '0', STR_PAD_LEFT);
2143 update_post_meta($quote_post->ID, '_easy_invoice_quote_number', $new_quote_number);
2144 $regenerated_count++;
2145 $current_number++;
2146 }
2147
2148 update_option('easy_invoice_next_quote_number', $current_number);
2149
2150 wp_send_json_success([
2151 /* translators: %d: number regenerated. */
2152 'message' => sprintf(__('Successfully regenerated %d quote numbers', 'easy-invoice'), $regenerated_count),
2153 'regenerated_count' => $regenerated_count,
2154 'next_number' => $current_number
2155 ]);
2156
2157 } catch (Exception $e) {
2158 wp_send_json_error(['message' => __('Failed to regenerate quote numbers', 'easy-invoice')]);
2159 }
2160 }
2161 }
2162