| @@ -2,9 +2,9 @@ | ||
| 2 | 2 | /** |
| 3 | 3 | * Plugin Name: Easy Invoice |
| 4 | 4 | * Plugin URI: https://matrixaddons.com/plugins/easy-invoice |
| 5 | 5 | * Description: A beautiful, full-featured invoicing solution for WordPress. Create professional invoices, quotes, and manage payments with ease. |
| 6 | - * Version: 2.2.0 | |
| 6 | + * Version: 2.4.0 | |
| 7 | 7 | * Author: MatrixAddons |
| 8 | 8 | * Author URI: https://matrixaddons.com |
| 9 | 9 | * License: GPL v2 or later |
| 10 | 10 | * License URI: https://www.gnu.org/licenses/gpl-2.0.html |
| @@ -9,12 +9,11 @@ | ||
| 9 | 9 | * License: GPL v2 or later |
| 10 | 10 | * License URI: https://www.gnu.org/licenses/gpl-2.0.html |
| 11 | 11 | * Text Domain: easy-invoice |
| 12 | 12 | * Domain Path: /languages |
| 13 | - * Requires at least: 5.6 | |
| 14 | - * Tested up to: 6.9 | |
| 13 | + * Requires at least: 6.0 | |
| 14 | + * Tested up to: 7.1 | |
| 15 | 15 | * Requires PHP: 7.4 |
| 16 | - * Network: false | |
| 17 | 16 | * |
| 18 | 17 | * @package EasyInvoice |
| 19 | 18 | */ |
| 20 | 19 | |
| @@ -24,9 +23,9 @@ | ||
| 24 | 23 | } |
| 25 | 24 | |
| 26 | 25 | |
| 27 | 26 | // Define plugin constants. |
| 28 | -define( 'EASY_INVOICE_VERSION', '2.2.0' ); | |
| 27 | +define( 'EASY_INVOICE_VERSION', '2.4.0' ); | |
| 29 | 28 | define( 'EASY_INVOICE_PLUGIN_FILE', __FILE__ ); |
| 30 | 29 | define( 'EASY_INVOICE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); |
| 31 | 30 | define( 'EASY_INVOICE_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); |
| 32 | 31 | |
| @@ -39,15 +38,33 @@ | ||
| 39 | 38 | require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/NotificationHelper.php'; |
| 40 | 39 | require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Admin/Helpers/Utility.php'; |
| 41 | 40 | require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/GeneralHelper.php'; |
| 42 | 41 | require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/ToastHelper.php'; |
| 42 | +require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/CapabilityHelper.php'; | |
| 43 | +require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/AuditHelper.php'; | |
| 44 | +require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/AutoloadOptimizer.php'; | |
| 45 | +require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/BrandHelper.php'; | |
| 43 | 46 | require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/TaxHelper.php'; |
| 44 | 47 | require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/FormattingHelper.php'; |
| 45 | 48 | require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/EmailHelper.php'; |
| 49 | +require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/MoneyHelper.php'; | |
| 46 | 50 | require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/EnqueueHelper.php'; |
| 47 | 51 | require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/PdfHelper.php'; |
| 48 | 52 | require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/QuoteInvoiceHelper.php'; |
| 53 | +require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Helpers/TemplateLocator.php'; | |
| 49 | 54 | |
| 55 | +// Addons system — Free plugin owns the listing UI + license gate; | |
| 56 | +// Pro plugin owns the executable addon code. | |
| 57 | +require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Addons/LicensePlanResolver.php'; | |
| 58 | +require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Addons/AddonRegistry.php'; | |
| 59 | +require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Addons/AddonManager.php'; | |
| 60 | +require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Addons/ProAddonMigration.php'; | |
| 61 | +require_once EASY_INVOICE_PLUGIN_DIR . 'includes/Controllers/AddonsController.php'; | |
| 62 | + | |
| 63 | +// Register the Pro→addon migration. Only fires inside admin (admin_init | |
| 64 | +// hook), only when Pro is active, only once per MIGRATION_VERSION bump. | |
| 65 | +\EasyInvoice\Addons\ProAddonMigration::register(); | |
| 66 | + | |
| 50 | 67 | /** |
| 51 | 68 | * Initialize the plugin. |
| 52 | 69 | * |
| 53 | 70 | * @since 1.0.0 |
| @@ -79,12 +96,8 @@ | ||
| 79 | 96 | |
| 80 | 97 | // Initialize EmailManager |
| 81 | 98 | $email_manager = \EasyInvoice\Services\EmailManager::getInstance(); |
| 82 | 99 | |
| 83 | - // Initialize form manager | |
| 84 | - //$form_manager = \EasyInvoice\Forms\FormManager::getInstance(); | |
| 85 | - // $form_manager->setInvoiceNumberServiceProvider($invoice_number_service_provider); | |
| 86 | - | |
| 87 | 100 | // Initialize controllers. |
| 88 | 101 | $payment_controller = new \EasyInvoice\Controllers\PaymentController(); |
| 89 | 102 | $payment_controller->init(); |
| 90 | 103 | |
| @@ -99,22 +112,129 @@ | ||
| 99 | 112 | |
| 100 | 113 | // Initialize review notice service |
| 101 | 114 | \EasyInvoice\Services\ReviewNoticeService::init(); |
| 102 | 115 | |
| 116 | + // One-time notice explaining that document links now require an access key. | |
| 117 | + // Registered here rather than on activation because WordPress updates a plugin | |
| 118 | + // by replacing files, without firing the activation hook — so an upgrading site | |
| 119 | + // would otherwise get no warning that previously-emailed links stop working. | |
| 120 | + \EasyInvoice\Services\DocumentAccessNoticeService::init(); | |
| 121 | + \EasyInvoice\Services\ProCompatibilityNotice::init(); | |
| 122 | + \EasyInvoice\Services\TaxTreatment::init(); | |
| 123 | + \EasyInvoice\Rest\RestController::init(); | |
| 124 | + \EasyInvoice\Services\PdfDownload::init(); | |
| 125 | + \EasyInvoice\Services\DocumentPage::init(); | |
| 126 | + \EasyInvoice\Services\DocumentViews::init(); | |
| 127 | + \EasyInvoice\Services\DocumentAttachments::init(); | |
| 128 | + \EasyInvoice\Services\InvoiceRetention::init(); | |
| 129 | + \EasyInvoice\Controllers\CreditNoteController::init(); | |
| 130 | + \EasyInvoice\Controllers\VatCheckController::init(); | |
| 131 | + \EasyInvoice\Controllers\StatementController::init(); | |
| 132 | + \EasyInvoice\Controllers\ImportController::init(); | |
| 133 | + | |
| 103 | 134 | // Initialize promotion service |
| 104 | 135 | \EasyInvoice\Services\PromotionService::init(); |
| 105 | 136 | |
| 137 | + // Initialize Addons controller (admin page + AJAX toggle) | |
| 138 | + (new \EasyInvoice\Controllers\AddonsController())->init(); | |
| 139 | + | |
| 106 | 140 | do_action('easy_invoice_loaded'); |
| 107 | 141 | |
| 108 | 142 | } |
| 109 | 143 | add_action( 'init', 'easy_invoice_init' ); |
| 110 | 144 | |
| 145 | +// Any plugin option change invalidates the per-request settings cache the | |
| 146 | +// money formatter reads (see SettingsController::getSettings()). | |
| 147 | +foreach ( [ 'updated_option', 'added_option', 'deleted_option' ] as $ei_opt_hook ) { | |
| 148 | + add_action( $ei_opt_hook, static function ( $option ) { | |
| 149 | + if ( is_string( $option ) && 0 === strpos( $option, 'easy_invoice_' ) ) { | |
| 150 | + \EasyInvoice\Controllers\SettingsController::flushSharedCache(); | |
| 151 | + // The builder field set depends on settings (tax fields are only registered | |
| 152 | + // while tax is on), so the models' cached definitions go with it. | |
| 153 | + \EasyInvoice\Models\Invoice::flushFieldDefinitionsCache(); | |
| 154 | + \EasyInvoice\Models\Quote::flushFieldDefinitionsCache(); | |
| 155 | + } | |
| 156 | + } ); | |
| 157 | +} | |
| 158 | +unset( $ei_opt_hook ); | |
| 159 | + | |
| 160 | +// Payment and credit-note writes invalidate the balance totals primed for | |
| 161 | +// dashboard and report screens (see InvoiceBalance::prime()). | |
| 162 | +add_action( 'save_post', static function ( $post_id, $post ) { | |
| 163 | + if ( $post instanceof WP_Post && in_array( $post->post_type, [ 'easy_invoice_payment', 'easy_invoice_credit', 'easy_invoice', 'easy_invoice_quote' ], true ) ) { | |
| 164 | + \EasyInvoice\Services\InvoiceBalance::forget(); | |
| 165 | + \EasyInvoice\Services\InvoiceTotalsCache::bumpVersion(); | |
| 166 | + } | |
| 167 | +}, 10, 2 ); | |
| 168 | +add_action( 'updated_post_meta', static function ( $meta_id, $post_id, $meta_key ) { | |
| 169 | + if ( in_array( $meta_key, [ '_amount', '_status', '_easy_invoice_total', '_invoice_id', '_easy_invoice_status', '_easy_invoice_due_date', '_easy_invoice_issue_date', '_easy_invoice_client_id', '_easy_invoice_currency_code', '_easy_invoice_quote_status', '_easy_invoice_quote_currency_code' ], true ) ) { | |
| 170 | + \EasyInvoice\Services\InvoiceBalance::forget(); | |
| 171 | + \EasyInvoice\Services\InvoiceTotalsCache::bumpVersion(); | |
| 172 | + } | |
| 173 | +}, 10, 3 ); | |
| 174 | +add_action( 'added_post_meta', static function ( $meta_id, $post_id, $meta_key ) { | |
| 175 | + if ( in_array( $meta_key, [ '_amount', '_status', '_easy_invoice_total', '_invoice_id', '_easy_invoice_status', '_easy_invoice_due_date', '_easy_invoice_issue_date', '_easy_invoice_client_id', '_easy_invoice_currency_code', '_easy_invoice_quote_status', '_easy_invoice_quote_currency_code' ], true ) ) { | |
| 176 | + \EasyInvoice\Services\InvoiceBalance::forget(); | |
| 177 | + \EasyInvoice\Services\InvoiceTotalsCache::bumpVersion(); | |
| 178 | + } | |
| 179 | +}, 10, 3 ); | |
| 180 | + | |
| 181 | +// Persisted invoice totals (InvoiceTotalsCache): written on save, backfilled by cron, | |
| 182 | +// dropped when a total's inputs change. | |
| 183 | +add_action( 'easy_invoice_totals_backfill', [ \EasyInvoice\Services\InvoiceTotalsCache::class, 'cronBackfill' ] ); | |
| 184 | +add_action( \EasyInvoice\Services\QuoteTotals::CRON_HOOK, [ \EasyInvoice\Services\QuoteTotals::class, 'cronBackfill' ] ); | |
| 185 | +foreach ( [ 'updated_post_meta', 'added_post_meta', 'deleted_post_meta' ] as $ei_meta_hook ) { | |
| 186 | + add_action( $ei_meta_hook, static function ( $meta_id, $post_id, $meta_key ) { | |
| 187 | + if ( in_array( $meta_key, \EasyInvoice\Services\InvoiceTotalsCache::DEPENDENT_META, true ) && 'easy_invoice' === get_post_type( $post_id ) ) { | |
| 188 | + \EasyInvoice\Services\InvoiceTotalsCache::invalidate( (int) $post_id ); | |
| 189 | + } | |
| 190 | + if ( in_array( $meta_key, \EasyInvoice\Services\QuoteTotals::DEPENDENT_META, true ) && 'easy_invoice_quote' === get_post_type( $post_id ) ) { | |
| 191 | + \EasyInvoice\Services\QuoteTotals::invalidate( (int) $post_id ); | |
| 192 | + } | |
| 193 | + }, 10, 3 ); | |
| 194 | +} | |
| 195 | +unset( $ei_meta_hook ); | |
| 196 | +foreach ( [ 'trashed_post', 'untrashed_post', 'deleted_post', 'before_delete_post' ] as $ei_post_hook ) { | |
| 197 | + add_action( $ei_post_hook, static function ( $post_id ) { | |
| 198 | + if ( in_array( get_post_type( $post_id ), [ 'easy_invoice', 'easy_invoice_payment', 'easy_invoice_credit', 'easy_invoice_quote' ], true ) ) { | |
| 199 | + \EasyInvoice\Services\InvoiceBalance::forget(); | |
| 200 | + \EasyInvoice\Services\InvoiceTotalsCache::bumpVersion(); | |
| 201 | + } | |
| 202 | + } ); | |
| 203 | +} | |
| 204 | +unset( $ei_post_hook ); | |
| 205 | +foreach ( [ 'updated_option', 'added_option', 'deleted_option' ] as $ei_opt_hook ) { | |
| 206 | + add_action( $ei_opt_hook, static function ( $option ) { | |
| 207 | + if ( in_array( $option, \EasyInvoice\Services\InvoiceTotalsCache::DEPENDENT_OPTIONS, true ) ) { | |
| 208 | + \EasyInvoice\Services\InvoiceTotalsCache::invalidateAll(); | |
| 209 | + } | |
| 210 | + } ); | |
| 211 | +} | |
| 212 | +unset( $ei_opt_hook ); | |
| 213 | +add_action( 'easy_invoice_loaded', static function () { | |
| 214 | + $want = \EasyInvoice\Services\InvoiceTotalsCache::CACHE_VERSION; | |
| 215 | + if ( $want !== get_option( 'easy_invoice_totals_cache_version' ) ) { | |
| 216 | + update_option( 'easy_invoice_totals_cache_version', $want, false ); | |
| 217 | + \EasyInvoice\Services\InvoiceTotalsCache::invalidateAll(); // drops stale totals and queues the backfill | |
| 218 | + } | |
| 219 | + if ( '1' !== get_option( 'easy_invoice_quote_totals_version' ) ) { | |
| 220 | + update_option( 'easy_invoice_quote_totals_version', '1', false ); | |
| 221 | + \EasyInvoice\Services\QuoteTotals::scheduleBackfill(); | |
| 222 | + } | |
| 223 | + // Versioned memo keys from the first 2.4.0 builds left one transient per write behind. | |
| 224 | + if ( '1' !== get_option( 'easy_invoice_stats_memo_keys' ) ) { | |
| 225 | + global $wpdb; | |
| 226 | + $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_ei_stats_%' OR option_name LIKE '_transient_timeout_ei_stats_%'" ); | |
| 227 | + update_option( 'easy_invoice_stats_memo_keys', '1', false ); | |
| 228 | + } | |
| 229 | +} ); | |
| 230 | + | |
| 111 | 231 | /** |
| 112 | 232 | * Handle payment gateway logging |
| 113 | 233 | */ |
| 114 | 234 | function easy_invoice_payment_gateway_log_handler($message, $level, $gateway_name) { |
| 115 | 235 | $log_message = sprintf('[%s] Easy Invoice %s Gateway: %s', |
| 116 | - date('Y-m-d H:i:s'), | |
| 236 | + gmdate('Y-m-d H:i:s'), | |
| 117 | 237 | ucfirst($gateway_name), |
| 118 | 238 | $message |
| 119 | 239 | ); |
| 120 | 240 | |
| @@ -202,8 +322,11 @@ | ||
| 202 | 322 | flush_rewrite_rules(); |
| 203 | 323 | |
| 204 | 324 | // Clear scheduled events |
| 205 | 325 | \EasyInvoice\Services\QuoteExpirationService::clear_schedule(); |
| 326 | + foreach ( [ 'easy_invoice_payment_reminder', 'easy_invoice_send_payment_reminders', 'easy_invoice_quote_expiration_check' ] as $hook ) { | |
| 327 | + wp_clear_scheduled_hook( $hook ); | |
| 328 | + } | |
| 206 | 329 | } |
| 207 | 330 | |
| 208 | 331 | /** |
| 209 | 332 | * Toast notification helper functions |