init(); // Initialize service providers. $invoice_service_provider = new \EasyInvoice\Providers\InvoiceServiceProvider(); $invoice_service_provider->register(); $invoice_item_service_provider = new \EasyInvoice\Providers\InvoiceItemServiceProvider(); $invoice_item_service_provider->register(); $client_service_provider = new \EasyInvoice\Providers\ClientServiceProvider(); $client_service_provider->register(); $invoice_number_service_provider = new \EasyInvoice\Providers\InvoiceNumberServiceProvider(); $invoice_number_service_provider->register(); $quote_number_service_provider = new \EasyInvoice\Providers\QuoteNumberServiceProvider(); $quote_number_service_provider->register(); $quote_service_provider = new \EasyInvoice\Providers\QuoteServiceProvider(); $quote_service_provider->register(); // Initialize EmailManager $email_manager = \EasyInvoice\Services\EmailManager::getInstance(); // Initialize controllers. $payment_controller = new \EasyInvoice\Controllers\PaymentController(); $payment_controller->init(); $settings_controller = new \EasyInvoice\Controllers\SettingsController(); $settings_controller->init(); $quote_controller = new \EasyInvoice\Controllers\QuoteController(); $quote_controller->init(); // Initialize shortcode manager $shortcode_manager = new \EasyInvoice\Shortcodes\ShortcodeManager(); // Initialize review notice service \EasyInvoice\Services\ReviewNoticeService::init(); // One-time notice explaining that document links now require an access key. // Registered here rather than on activation because WordPress updates a plugin // by replacing files, without firing the activation hook — so an upgrading site // would otherwise get no warning that previously-emailed links stop working. \EasyInvoice\Services\DocumentAccessNoticeService::init(); \EasyInvoice\Services\ProCompatibilityNotice::init(); \EasyInvoice\Services\TaxTreatment::init(); \EasyInvoice\Rest\RestController::init(); \EasyInvoice\Services\PdfDownload::init(); \EasyInvoice\Services\DocumentPage::init(); \EasyInvoice\Services\DocumentViews::init(); \EasyInvoice\Services\DocumentAttachments::init(); \EasyInvoice\Services\InvoiceRetention::init(); \EasyInvoice\Controllers\CreditNoteController::init(); \EasyInvoice\Controllers\VatCheckController::init(); \EasyInvoice\Controllers\StatementController::init(); \EasyInvoice\Controllers\ImportController::init(); // Initialize promotion service \EasyInvoice\Services\PromotionService::init(); // Initialize Addons controller (admin page + AJAX toggle) (new \EasyInvoice\Controllers\AddonsController())->init(); do_action('easy_invoice_loaded'); } add_action( 'init', 'easy_invoice_init' ); // Any plugin option change invalidates the per-request settings cache the // money formatter reads (see SettingsController::getSettings()). foreach ( [ 'updated_option', 'added_option', 'deleted_option' ] as $ei_opt_hook ) { add_action( $ei_opt_hook, static function ( $option ) { if ( is_string( $option ) && 0 === strpos( $option, 'easy_invoice_' ) ) { \EasyInvoice\Controllers\SettingsController::flushSharedCache(); // The builder field set depends on settings (tax fields are only registered // while tax is on), so the models' cached definitions go with it. \EasyInvoice\Models\Invoice::flushFieldDefinitionsCache(); \EasyInvoice\Models\Quote::flushFieldDefinitionsCache(); } } ); } unset( $ei_opt_hook ); // Payment and credit-note writes invalidate the balance totals primed for // dashboard and report screens (see InvoiceBalance::prime()). add_action( 'save_post', static function ( $post_id, $post ) { if ( $post instanceof WP_Post && in_array( $post->post_type, [ 'easy_invoice_payment', 'easy_invoice_credit', 'easy_invoice', 'easy_invoice_quote' ], true ) ) { \EasyInvoice\Services\InvoiceBalance::forget(); \EasyInvoice\Services\InvoiceTotalsCache::bumpVersion(); } }, 10, 2 ); add_action( 'updated_post_meta', static function ( $meta_id, $post_id, $meta_key ) { 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 ) ) { \EasyInvoice\Services\InvoiceBalance::forget(); \EasyInvoice\Services\InvoiceTotalsCache::bumpVersion(); } }, 10, 3 ); add_action( 'added_post_meta', static function ( $meta_id, $post_id, $meta_key ) { 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 ) ) { \EasyInvoice\Services\InvoiceBalance::forget(); \EasyInvoice\Services\InvoiceTotalsCache::bumpVersion(); } }, 10, 3 ); // Persisted invoice totals (InvoiceTotalsCache): written on save, backfilled by cron, // dropped when a total's inputs change. add_action( 'easy_invoice_totals_backfill', [ \EasyInvoice\Services\InvoiceTotalsCache::class, 'cronBackfill' ] ); add_action( \EasyInvoice\Services\QuoteTotals::CRON_HOOK, [ \EasyInvoice\Services\QuoteTotals::class, 'cronBackfill' ] ); foreach ( [ 'updated_post_meta', 'added_post_meta', 'deleted_post_meta' ] as $ei_meta_hook ) { add_action( $ei_meta_hook, static function ( $meta_id, $post_id, $meta_key ) { if ( in_array( $meta_key, \EasyInvoice\Services\InvoiceTotalsCache::DEPENDENT_META, true ) && 'easy_invoice' === get_post_type( $post_id ) ) { \EasyInvoice\Services\InvoiceTotalsCache::invalidate( (int) $post_id ); } if ( in_array( $meta_key, \EasyInvoice\Services\QuoteTotals::DEPENDENT_META, true ) && 'easy_invoice_quote' === get_post_type( $post_id ) ) { \EasyInvoice\Services\QuoteTotals::invalidate( (int) $post_id ); } }, 10, 3 ); } unset( $ei_meta_hook ); foreach ( [ 'trashed_post', 'untrashed_post', 'deleted_post', 'before_delete_post' ] as $ei_post_hook ) { add_action( $ei_post_hook, static function ( $post_id ) { if ( in_array( get_post_type( $post_id ), [ 'easy_invoice', 'easy_invoice_payment', 'easy_invoice_credit', 'easy_invoice_quote' ], true ) ) { \EasyInvoice\Services\InvoiceBalance::forget(); \EasyInvoice\Services\InvoiceTotalsCache::bumpVersion(); } } ); } unset( $ei_post_hook ); foreach ( [ 'updated_option', 'added_option', 'deleted_option' ] as $ei_opt_hook ) { add_action( $ei_opt_hook, static function ( $option ) { if ( in_array( $option, \EasyInvoice\Services\InvoiceTotalsCache::DEPENDENT_OPTIONS, true ) ) { \EasyInvoice\Services\InvoiceTotalsCache::invalidateAll(); } } ); } unset( $ei_opt_hook ); add_action( 'easy_invoice_loaded', static function () { $want = \EasyInvoice\Services\InvoiceTotalsCache::CACHE_VERSION; if ( $want !== get_option( 'easy_invoice_totals_cache_version' ) ) { update_option( 'easy_invoice_totals_cache_version', $want, false ); \EasyInvoice\Services\InvoiceTotalsCache::invalidateAll(); // drops stale totals and queues the backfill } if ( '1' !== get_option( 'easy_invoice_quote_totals_version' ) ) { update_option( 'easy_invoice_quote_totals_version', '1', false ); \EasyInvoice\Services\QuoteTotals::scheduleBackfill(); } // Versioned memo keys from the first 2.4.0 builds left one transient per write behind. if ( '1' !== get_option( 'easy_invoice_stats_memo_keys' ) ) { global $wpdb; $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_ei_stats_%' OR option_name LIKE '_transient_timeout_ei_stats_%'" ); update_option( 'easy_invoice_stats_memo_keys', '1', false ); } } ); /** * Handle payment gateway logging */ function easy_invoice_payment_gateway_log_handler($message, $level, $gateway_name) { $log_message = sprintf('[%s] Easy Invoice %s Gateway: %s', gmdate('Y-m-d H:i:s'), ucfirst($gateway_name), $message ); if (defined('WP_DEBUG') && WP_DEBUG) { error_log($log_message); } } add_action('easy_invoice_payment_gateway_log', 'easy_invoice_payment_gateway_log_handler', 10, 3); // Initialize migration system if ( class_exists( 'EasyInvoice\Migration\MigrationInit' ) ) { EasyInvoice\Migration\MigrationInit::init(); } /** * Register post types early to ensure they're available */ function easy_invoice_register_post_types() { // Initialize the main plugin class and register post types $plugin = \EasyInvoice\EasyInvoice::getInstance(); $plugin->registerPostTypes(); $plugin->registerCustomStatuses(); } add_action( 'init', 'easy_invoice_register_post_types', 0 ); // Priority 0 to run early /** * Load textdomain early to avoid translation loading warnings. * * @since 1.0.0 * @return void */ function easy_invoice_load_textdomain() { load_plugin_textdomain('easy-invoice', false, dirname(plugin_basename(__FILE__)) . '/languages'); } add_action( 'init', 'easy_invoice_load_textdomain', 1 ); /** * Plugin activation hook. * * @since 1.0.0 * @return void */ register_activation_hook( __FILE__, 'easy_invoice_activate' ); function easy_invoice_activate() { // Initialization of post types so rewrite rules can be flushed properly. \EasyInvoice\EasyInvoice::getInstance()->registerPostTypes(); // Initialize default settings $settings_controller = new \EasyInvoice\Controllers\SettingsController(); $settings_controller->initializeSettings(); // Record first install timestamp (do not overwrite if it already exists) if (!get_option('easy_invoice_installed')) { update_option('easy_invoice_installed', current_time('mysql')); } // Check if this is a fresh installation or an upgrade $stored_version = get_option('easy_invoice_version', ''); // If no version is stored, this is a fresh installation if (empty($stored_version)) { update_option('easy_invoice_version', EASY_INVOICE_VERSION); update_option('easy_invoice_migration_completed', true); } else { // This is an upgrade. Do NOT bump version here; migration will set it when completed. } // Flush rewrite rules. flush_rewrite_rules(); } /** * Plugin deactivation hook. * * @since 1.0.0 * @return void */ register_deactivation_hook( __FILE__, 'easy_invoice_deactivate' ); function easy_invoice_deactivate() { // Flush rewrite rules. flush_rewrite_rules(); // Clear scheduled events \EasyInvoice\Services\QuoteExpirationService::clear_schedule(); foreach ( [ 'easy_invoice_payment_reminder', 'easy_invoice_send_payment_reminders', 'easy_invoice_quote_expiration_check' ] as $hook ) { wp_clear_scheduled_hook( $hook ); } } /** * Toast notification helper functions */