From 80ee545a531c802a3c3b73c25319881c6c711a77 Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Fri, 25 Jul 2025 14:52:54 +0200 Subject: [PATCH 01/11] Remove X-Frame-Options for login page --- .distignore | 3 + includes/Init.php | 143 +++++++++++++++++++++++++--------------------- 2 files changed, 82 insertions(+), 64 deletions(-) diff --git a/.distignore b/.distignore index 6b029ad..85b6e26 100644 --- a/.distignore +++ b/.distignore @@ -14,3 +14,6 @@ package.json yarn.lock php-scoper generate_autoload.php +turbo.json +pnpm-lock.yaml +pnpm-workspace.yaml \ No newline at end of file diff --git a/includes/Init.php b/includes/Init.php index 2595c1b..900a217 100644 --- a/includes/Init.php +++ b/includes/Init.php @@ -6,22 +6,18 @@ * @author Paul Kilmurray * * @see http://wcpos.com - * @package WooCommercePOS */ namespace WCPOS\WooCommercePOS; -use WCPOS\WooCommercePOS\Services\Settings as SettingsService; +use const DOING_AJAX; use WCPOS\WooCommercePOS\Services\Auth as AuthService; +use WCPOS\WooCommercePOS\Services\Settings as SettingsService; use WP_HTTP_Response; use WP_REST_Request; -use WP_REST_Server; -use const DOING_AJAX; +use WP_REST_Server; -/** - * - */ class Init { /** * Constructor. @@ -39,6 +35,7 @@ public function __construct() { // Headers for API discoverability add_filter( 'rest_pre_serve_request', array( $this, 'rest_pre_serve_request' ), 5, 4 ); add_action( 'send_headers', array( $this, 'send_headers' ), 99, 1 ); + add_action( 'send_headers', array( $this, 'remove_x_frame_options' ), 9999, 1 ); } /** @@ -51,63 +48,6 @@ public function init(): void { $this->init_integrations(); } - /** - * Common initializations - */ - private function init_common() { - // init the Services - SettingsService::instance(); - AuthService::instance(); - - // init other functionality needed by both frontend and admin - new i18n(); - new Gateways(); - new Products(); - new Orders(); - } - - /** - * Frontend specific initializations - */ - private function init_frontend() { - if ( ! is_admin() ) { - new Templates(); - new Form_Handler(); - } - } - - /** - * Admin specific initializations - */ - private function init_admin() { - if ( is_admin() ) { - if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { - new AJAX(); - } else { - new Admin(); - } - } - } - - /** - * Integrations - */ - private function init_integrations() { - // WooCommerce Bookings - http://www.woothemes.com/products/woocommerce-bookings/ - // if ( class_exists( 'WC-Bookings' ) ) { - // new Integrations\Bookings(); - // } - - // Yoast SEO - https://wordpress.org/plugins/wordpress-seo/ - if ( class_exists( 'WPSEO_Options' ) ) { - new Integrations\WPSEO(); - } - - // wePOS alters the WooCommerce REST API, breaking the expected schema - // It's very bad form on their part, but we need to work around it - new Integrations\WePOS(); - } - /** * Loads the POS API and duck punches the WC REST API. */ @@ -184,4 +124,79 @@ public function send_headers(): void { header( 'Access-Control-Expose-Headers: Link' ); } } + + /** + * Some security plugins will set X-Frame-Options: SAMEORIGIN/DENY, which will prevent the POS desktop + * application from opening pages like the login in an iframe. + * + * For pages we need, we will remove the X-Frame-Options header. + * + * @param mixed $wp + * + * @return void + */ + public function remove_x_frame_options( $wp ): void { + if ( woocommerce_pos_request() || isset( $wp->query_vars['wcpos-login'] ) ) { + if ( ! headers_sent() && \function_exists( 'header_remove' ) ) { + header_remove( 'X-Frame-Options' ); + } + } + } + + /** + * Common initializations. + */ + private function init_common(): void { + // init the Services + SettingsService::instance(); + AuthService::instance(); + + // init other functionality needed by both frontend and admin + new i18n(); + new Gateways(); + new Products(); + new Orders(); + } + + /** + * Frontend specific initializations. + */ + private function init_frontend(): void { + if ( ! is_admin() ) { + new Templates(); + new Form_Handler(); + } + } + + /** + * Admin specific initializations. + */ + private function init_admin(): void { + if ( is_admin() ) { + if ( \defined( 'DOING_AJAX' ) && DOING_AJAX ) { + new AJAX(); + } else { + new Admin(); + } + } + } + + /** + * Integrations. + */ + private function init_integrations(): void { + // WooCommerce Bookings - http://www.woothemes.com/products/woocommerce-bookings/ + // if ( class_exists( 'WC-Bookings' ) ) { + // new Integrations\Bookings(); + // } + + // Yoast SEO - https://wordpress.org/plugins/wordpress-seo/ + if ( class_exists( 'WPSEO_Options' ) ) { + new Integrations\WPSEO(); + } + + // wePOS alters the WooCommerce REST API, breaking the expected schema + // It's very bad form on their part, but we need to work around it + new Integrations\WePOS(); + } } From 06b1c7bd3c6dd5eb669fda574af73783f3b321ff Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Fri, 25 Jul 2025 15:41:37 +0200 Subject: [PATCH 02/11] add security check for receipt templates --- includes/API/Orders_Controller.php | 7 +- includes/Templates/Receipt.php | 6 ++ verify-email-separation.php | 118 +++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 verify-email-separation.php diff --git a/includes/API/Orders_Controller.php b/includes/API/Orders_Controller.php index 536a580..27a0407 100644 --- a/includes/API/Orders_Controller.php +++ b/includes/API/Orders_Controller.php @@ -645,7 +645,12 @@ public function wcpos_order_response( WP_REST_Response $response, WC_Abstract_Or $response->add_link( 'payment', $pos_payment_url, array( 'foo' => 'bar' ) ); // Add receipt link to the order. - $pos_receipt_url = get_home_url( null, '/wcpos-checkout/wcpos-receipt/' . $order->get_id() ); + $pos_receipt_url = add_query_arg( + array( + 'key' => method_exists( $order, 'get_order_key' ) ? $order->get_order_key() : '', + ), + get_home_url( null, '/wcpos-checkout/wcpos-receipt/' . $order->get_id() ) + ); $response->add_link( 'receipt', $pos_receipt_url ); // Make sure we parse the meta data before returning the response diff --git a/includes/Templates/Receipt.php b/includes/Templates/Receipt.php index 2ff8561..942868d 100644 --- a/includes/Templates/Receipt.php +++ b/includes/Templates/Receipt.php @@ -56,6 +56,12 @@ public function get_template(): void { wp_die( esc_html__( 'Sorry, this order is invalid.', 'woocommerce-pos' ) ); } + // Validate order key for security. + $order_key = isset( $_GET['key'] ) ? sanitize_text_field( wp_unslash( $_GET['key'] ) ) : ''; + if ( empty( $order_key ) || $order_key !== $order->get_order_key() ) { + wp_die( esc_html__( 'You do not have permission to view this receipt.', 'woocommerce-pos' ) ); + } + /** * Put WC_Order into the global scope so that the template can access it. */ diff --git a/verify-email-separation.php b/verify-email-separation.php new file mode 100644 index 0000000..90878c5 --- /dev/null +++ b/verify-email-separation.php @@ -0,0 +1,118 @@ +set_name('Email Test Product'); + $product->set_regular_price(10.00); + $product->set_manage_stock(false); + $product->set_status('publish'); + $product->save(); + + return $product->get_id(); +} + +// Create test product +$product_id = create_test_product(); + +echo '

Email Control Separation Test

'; +echo '

Test Product ID: ' . $product_id . '

'; + +// Test 1: Create a regular website order +echo '

Test 1: Regular Website Order

'; + +$website_order = wc_create_order(); +$website_order->add_product(wc_get_product($product_id), 1); +$website_order->set_customer_id(1); +$website_order->set_billing_email('website@test.com'); +$website_order->calculate_totals(); +// Website orders typically have 'checkout' or empty created_via +$website_order->set_created_via('checkout'); +$website_order->save(); + +echo '

Website Order ID: ' . $website_order->get_id() . '

'; +echo '

Created Via: ' . $website_order->get_created_via() . '

'; + +// Test the email controls for website order +$admin_email_enabled = apply_filters('woocommerce_email_enabled_new_order', true, $website_order, null); +$customer_email_enabled = apply_filters('woocommerce_email_enabled_customer_completed_order', true, $website_order, null); + +echo '

Admin Email Enabled: ' . ($admin_email_enabled ? 'YES' : 'NO') . ' (should be YES)

'; +echo '

Customer Email Enabled: ' . ($customer_email_enabled ? 'YES' : 'NO') . ' (should be YES)

'; + +// Test 2: Create a POS order +echo '

Test 2: POS Order

'; + +$pos_order = wc_create_order(); +$pos_order->add_product(wc_get_product($product_id), 1); +$pos_order->set_customer_id(1); +$pos_order->set_billing_email('pos@test.com'); +$pos_order->calculate_totals(); +$pos_order->set_created_via('woocommerce-pos'); // This makes it a POS order +$pos_order->save(); + +echo '

POS Order ID: ' . $pos_order->get_id() . '

'; +echo '

Created Via: ' . $pos_order->get_created_via() . '

'; + +// Test the email controls for POS order +$pos_admin_email_enabled = apply_filters('woocommerce_email_enabled_new_order', true, $pos_order, null); +$pos_customer_email_enabled = apply_filters('woocommerce_email_enabled_customer_completed_order', true, $pos_order, null); + +// Get current settings +$settings = woocommerce_pos_get_settings('checkout'); + +echo '

POS Settings - Admin Emails: ' . ($settings['admin_emails'] ? 'ENABLED' : 'DISABLED') . '

'; +echo '

POS Settings - Customer Emails: ' . ($settings['customer_emails'] ? 'ENABLED' : 'DISABLED') . '

'; +echo '

POS Admin Email Enabled: ' . ($pos_admin_email_enabled ? 'YES' : 'NO') . ' (should match POS setting)

'; +echo '

POS Customer Email Enabled: ' . ($pos_customer_email_enabled ? 'YES' : 'NO') . ' (should match POS setting)

'; + +// Summary +echo '
'; +echo '

✅ Test Results Summary

'; + +$website_protected = (true === $admin_email_enabled && true === $customer_email_enabled); +$pos_controlled = ($pos_admin_email_enabled === (bool) $settings['admin_emails'] && + $pos_customer_email_enabled === (bool) $settings['customer_emails']); + +if ($website_protected && $pos_controlled) { + echo '

✅ SUCCESS: Email controls work correctly!

'; + echo '
    '; + echo '
  • ✅ Website orders are NOT affected by POS settings
  • '; + echo '
  • ✅ POS orders ARE controlled by POS settings
  • '; + echo '
'; +} else { + echo '

❌ ISSUE DETECTED:

'; + echo '
    '; + if ( ! $website_protected) { + echo '
  • ❌ Website orders are being affected (should not be)
  • '; + } + if ( ! $pos_controlled) { + echo '
  • ❌ POS orders are not being controlled (should be)
  • '; + } + echo '
'; +} + +// Cleanup +echo '
'; +echo '

Cleanup: Test orders and product created. You may want to delete them manually.

'; +echo '

Website Order: Edit Order #' . $website_order->get_id() . '

'; +echo '

POS Order: Edit Order #' . $pos_order->get_id() . '

'; + +echo '
'; +echo '

Security Warning: Delete this script after testing!

'; From 664ac9aa43ec4b8ce487bcc73351f20e25b872ea Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Fri, 25 Jul 2025 15:41:58 +0200 Subject: [PATCH 03/11] fix: emails for POS orders --- includes/Orders.php | 256 ++++++++++++++++++++++++++++++++++++++------ readme.txt | 5 + 2 files changed, 229 insertions(+), 32 deletions(-) diff --git a/includes/Orders.php b/includes/Orders.php index 666ba9b..7094d69 100644 --- a/includes/Orders.php +++ b/includes/Orders.php @@ -36,28 +36,8 @@ public function __construct() { add_action( 'woocommerce_order_item_after_calculate_taxes', array( $this, 'order_item_after_calculate_taxes' ) ); add_action( 'woocommerce_order_item_shipping_after_calculate_taxes', array( $this, 'order_item_after_calculate_taxes' ) ); - // order emails - $admin_emails = array( - 'new_order', - 'cancelled_order', - 'failed_order', - 'reset_password', - 'new_account', - ); - $customer_emails = array( - 'customer_on_hold_order', - 'customer_processing_order', - 'customer_completed_order', - 'customer_refunded_order', - 'customer_invoice', - 'customer_note', - ); - foreach ( $admin_emails as $email_id ) { - add_filter( "woocommerce_email_enabled_{$email_id}", array( $this, 'manage_admin_emails' ), 10, 3 ); - } - foreach ( $customer_emails as $email_id ) { - add_filter( "woocommerce_email_enabled_{$email_id}", array( $this, 'manage_customer_emails' ), 10, 3 ); - } + // POS email management - higher priority to override other plugins + $this->setup_email_management(); } /** @@ -147,29 +127,95 @@ public function hidden_order_itemmeta( array $meta_keys ): array { } /** - * @param mixed $enabled - * @param mixed $order - * @param mixed $email_class + * Manage admin email sending for POS orders. + * Only affects orders created via WooCommerce POS. + * + * @param bool $enabled Whether the email is enabled. + * @param null|WC_Order $order The order object. + * @param mixed|WC_Email $email_class The email class. + * + * @return bool Whether the email should be sent. */ public function manage_admin_emails( $enabled, $order, $email_class ) { - if ( ! woocommerce_pos_request() ) { + // Only control emails for POS orders + if ( ! $this->is_pos_order( $order ) ) { return $enabled; } - return woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); + // Return the setting value, this will override any other plugin settings + return (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); } /** - * @param mixed $enabled - * @param mixed $order - * @param mixed $email_class + * Manage customer email sending for POS orders. + * Only affects orders created via WooCommerce POS. + * + * @param bool $enabled Whether the email is enabled. + * @param null|WC_Order $order The order object. + * @param mixed|WC_Email $email_class The email class. + * + * @return bool Whether the email should be sent. */ public function manage_customer_emails( $enabled, $order, $email_class ) { - if ( ! woocommerce_pos_request() ) { + // Only control emails for POS orders + if ( ! $this->is_pos_order( $order ) ) { return $enabled; } - return woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); + // Return the setting value, this will override any other plugin settings + return (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); + } + + /** + * Filter admin email recipients for POS orders as a safety net. + * If admin emails are disabled, return empty string to prevent sending. + * + * @param string $recipient The recipient email address. + * @param null|WC_Order $order The order object. + * @param mixed|WC_Email $email_class The email class. + * @param array $args Additional arguments. + * + * @return string The recipient email or empty string to prevent sending. + */ + public function filter_admin_email_recipients( $recipient, $order, $email_class, $args = array() ) { + // Only control emails for POS orders + if ( ! $this->is_pos_order( $order ) ) { + return $recipient; + } + + // If admin emails are disabled, return empty string to prevent sending + $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); + if ( ! $admin_emails_enabled ) { + return ''; + } + + return $recipient; + } + + /** + * Filter customer email recipients for POS orders as a safety net. + * If customer emails are disabled, return empty string to prevent sending. + * + * @param string $recipient The recipient email address. + * @param null|WC_Order $order The order object. + * @param mixed|WC_Email $email_class The email class. + * @param array $args Additional arguments. + * + * @return string The recipient email or empty string to prevent sending. + */ + public function filter_customer_email_recipients( $recipient, $order, $email_class, $args = array() ) { + // Only control emails for POS orders + if ( ! $this->is_pos_order( $order ) ) { + return $recipient; + } + + // If customer emails are disabled, return empty string to prevent sending + $customer_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); + if ( ! $customer_emails_enabled ) { + return ''; + } + + return $recipient; } /** @@ -272,6 +318,152 @@ public function order_item_after_calculate_taxes( $item ): void { } } + /** + * Ultimate failsafe to prevent disabled POS emails from being sent. + * This hooks into wp_mail as the final layer of protection. + * + * @param array $atts The wp_mail arguments. + * + * @return array|false The wp_mail arguments or false to prevent sending. + */ + public function prevent_disabled_pos_emails( $atts ) { + // Check if this email is related to a WooCommerce order + if ( ! isset( $atts['subject'] ) || ! \is_string( $atts['subject'] ) ) { + return $atts; + } + + // Look for WooCommerce order patterns in the subject line + $subject = $atts['subject']; + $is_wc_email = false; + $order_id = null; + + // Common WooCommerce email subject patterns + $patterns = array( + '/Your (.+) order \(#(\d+)\)/', // Customer emails + '/\[(.+)\] New customer order \(#(\d+)\)/', // New order admin email + '/\[(.+)\] Cancelled order \(#(\d+)\)/', // Cancelled order + '/\[(.+)\] Failed order \(#(\d+)\)/', // Failed order + '/Order #(\d+) details/', // Invoice emails + '/Note added to your order #(\d+)/', // Customer note + ); + + foreach ( $patterns as $pattern ) { + if ( preg_match( $pattern, $subject, $matches ) ) { + $is_wc_email = true; + // Extract order ID from the match + $order_id = isset( $matches[2] ) ? (int) $matches[2] : ( isset( $matches[1] ) ? (int) $matches[1] : null ); + + break; + } + } + + // If this doesn't appear to be a WooCommerce email, let it through + if ( ! $is_wc_email || ! $order_id ) { + return $atts; + } + + // Get the order and check if it's a POS order + $order = wc_get_order( $order_id ); + if ( ! $this->is_pos_order( $order ) ) { + return $atts; + } + + // Determine if this is likely an admin or customer email based on recipient and content + $to = $atts['to']; + $admin_email = get_option( 'admin_email' ); + $is_admin_email = ( $to === $admin_email || 0 === strpos( $subject, '[' ) ); + + // Check settings and prevent sending if disabled + if ( $is_admin_email ) { + $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); + if ( ! $admin_emails_enabled ) { + // Log for debugging purposes + Logger::log( 'WCPOS: Prevented admin email for POS order #' . $order_id ); + + return false; // Prevent the email from being sent + } + } else { + $customer_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); + if ( ! $customer_emails_enabled ) { + // Log for debugging purposes + Logger::log( 'WCPOS: Prevented customer email for POS order #' . $order_id ); + + return false; // Prevent the email from being sent + } + } + + return $atts; + } + + /** + * Check if an order was created via WooCommerce POS. + * + * @param null|WC_Order $order The order object. + * + * @return bool True if the order was created via POS, false otherwise. + */ + private function is_pos_order( $order ) { + // Handle various input types and edge cases + if ( ! $order instanceof WC_Order ) { + // Sometimes the order is passed as an ID + if ( is_numeric( $order ) ) { + $order = wc_get_order( $order ); + } + + // If we still don't have a valid order, return false + if ( ! $order instanceof WC_Order ) { + return false; + } + } + + // Check if the order was created via WooCommerce POS + return 'woocommerce-pos' === $order->get_created_via(); + } + + /** + * Setup email management hooks for POS orders. + * Uses high priority (999) to ensure these settings override other plugins. + */ + private function setup_email_management(): void { + // Admin emails - these go to store administrators + $admin_emails = array( + 'new_order', + 'cancelled_order', + 'failed_order', + 'reset_password', + 'new_account', + ); + + // Customer emails - these go to customers + $customer_emails = array( + 'customer_on_hold_order', + 'customer_processing_order', + 'customer_completed_order', + 'customer_refunded_order', + 'customer_invoice', + 'customer_note', + ); + + // Hook into email enabled filters with high priority + foreach ( $admin_emails as $email_id ) { + add_filter( "woocommerce_email_enabled_{$email_id}", array( $this, 'manage_admin_emails' ), 999, 3 ); + } + foreach ( $customer_emails as $email_id ) { + add_filter( "woocommerce_email_enabled_{$email_id}", array( $this, 'manage_customer_emails' ), 999, 3 ); + } + + // Additional safety net - hook into the recipient filters as well to ensure no emails go out when disabled + foreach ( $admin_emails as $email_id ) { + add_filter( "woocommerce_email_recipient_{$email_id}", array( $this, 'filter_admin_email_recipients' ), 999, 4 ); + } + foreach ( $customer_emails as $email_id ) { + add_filter( "woocommerce_email_recipient_{$email_id}", array( $this, 'filter_customer_email_recipients' ), 999, 4 ); + } + + // Ultimate failsafe - use wp_mail filter to prevent sending at the last moment + add_filter( 'wp_mail', array( $this, 'prevent_disabled_pos_emails' ), 999, 1 ); + } + /** * Register the POS order statuses. */ diff --git a/readme.txt b/readme.txt index 6216144..ea3d062 100644 --- a/readme.txt +++ b/readme.txt @@ -88,6 +88,11 @@ There is more information on our website at [https://wcpos.com](https://wcpos.co == Changelog == += 1.7.12 - 2025/07/25 = +* Security Fix: POS receipts should not be publically accessible, NOTE: you may need to re-sync past orders to view the receipt +* Fix: Remove the X-Frame-Options Header for which prevents desktop application users from logging in +* Fix: Checkout email settings have been tested and should now work + = 1.7.11 - 2025/06/18 = * Fix: is_internal_meta_key errors for barcodes as '_global_unique_id' From 57368ff62991cd5310c11896c87d761ed55e46ed Mon Sep 17 00:00:00 2001 From: kilbot Date: Fri, 25 Jul 2025 13:42:24 +0000 Subject: [PATCH 04/11] chore(i18n): update languages/woocommerce-pos.pot --- languages/woocommerce-pos.pot | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/languages/woocommerce-pos.pot b/languages/woocommerce-pos.pot index 3d51ec5..964ea8a 100644 --- a/languages/woocommerce-pos.pot +++ b/languages/woocommerce-pos.pot @@ -2,14 +2,14 @@ # This file is distributed under the GPL-3.0+. msgid "" msgstr "" -"Project-Id-Version: WooCommerce POS 1.7.8\n" +"Project-Id-Version: WooCommerce POS 1.7.11\n" "Report-Msgid-Bugs-To: https://github.com/wcpos/woocommerce-pos/issues\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2025-05-21T10:48:01+00:00\n" +"POT-Creation-Date: 2025-07-25T13:42:20+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: woocommerce-pos\n" @@ -319,6 +319,9 @@ msgstr "" msgid "Sorry, this order is invalid." msgstr "" +msgid "You do not have permission to view this receipt." +msgstr "" + msgid "Point of Sale" msgstr "" From b3b2e0de90e8337fc4a5e70bae68074f60ca3dc6 Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Fri, 25 Jul 2025 16:17:19 +0200 Subject: [PATCH 05/11] Add logging for wcposdev --- includes/Orders.php | 490 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 469 insertions(+), 21 deletions(-) diff --git a/includes/Orders.php b/includes/Orders.php index 7094d69..67038ef 100644 --- a/includes/Orders.php +++ b/includes/Orders.php @@ -137,13 +137,48 @@ public function hidden_order_itemmeta( array $meta_keys ): array { * @return bool Whether the email should be sent. */ public function manage_admin_emails( $enabled, $order, $email_class ) { + // Better email ID detection + $email_id = 'unknown'; + if ( $email_class instanceof WC_Email && isset( $email_class->id ) ) { + $email_id = $email_class->id; + } elseif ( \is_object( $email_class ) && isset( $email_class->id ) ) { + $email_id = $email_class->id; + } elseif ( \is_string( $email_class ) ) { + $email_id = $email_class; + } + + // Get current filter name for additional context + $current_filter = current_filter(); + // Only control emails for POS orders if ( ! $this->is_pos_order( $order ) ) { + Logger::log( \sprintf( + 'WCPOS Admin Email: Order #%s not POS order (created_via: %s), Email ID: %s, Filter: %s - SKIPPING', + $order instanceof WC_Order ? $order->get_id() : 'unknown', + $order instanceof WC_Order ? $order->get_created_via() : 'unknown', + $email_id, + $current_filter + ) ); + return $enabled; } + $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); + $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; + + // Debug logging + Logger::log( \sprintf( + 'WCPOS Admin Email Control: Order #%s, Email ID: %s, Filter: %s, Originally Enabled: %s, POS Setting: %s, Final Result: %s', + $order_id, + $email_id, + $current_filter, + $enabled ? 'YES' : 'NO', + $admin_emails_enabled ? 'ENABLED' : 'DISABLED', + $admin_emails_enabled ? 'ENABLED' : 'DISABLED' + ) ); + // Return the setting value, this will override any other plugin settings - return (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); + return $admin_emails_enabled; } /** @@ -162,8 +197,22 @@ public function manage_customer_emails( $enabled, $order, $email_class ) { return $enabled; } + $customer_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); + $email_id = $email_class instanceof WC_Email ? $email_class->id : 'unknown'; + $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; + + // Debug logging + Logger::log( \sprintf( + 'WCPOS Customer Email Control: Order #%s, Email ID: %s, Originally Enabled: %s, POS Setting: %s, Final Result: %s', + $order_id, + $email_id, + $enabled ? 'YES' : 'NO', + $customer_emails_enabled ? 'ENABLED' : 'DISABLED', + $customer_emails_enabled ? 'ENABLED' : 'DISABLED' + ) ); + // Return the setting value, this will override any other plugin settings - return (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); + return $customer_emails_enabled; } /** @@ -183,8 +232,21 @@ public function filter_admin_email_recipients( $recipient, $order, $email_class, return $recipient; } - // If admin emails are disabled, return empty string to prevent sending $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); + $email_id = $email_class instanceof WC_Email ? $email_class->id : 'unknown'; + $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; + + // Debug logging + Logger::log( \sprintf( + 'WCPOS Admin Recipient Filter: Order #%s, Email ID: %s, Original Recipient: %s, POS Setting: %s, Final Recipient: %s', + $order_id, + $email_id, + $recipient, + $admin_emails_enabled ? 'ENABLED' : 'DISABLED', + $admin_emails_enabled ? $recipient : 'BLOCKED' + ) ); + + // If admin emails are disabled, return empty string to prevent sending if ( ! $admin_emails_enabled ) { return ''; } @@ -209,8 +271,21 @@ public function filter_customer_email_recipients( $recipient, $order, $email_cla return $recipient; } - // If customer emails are disabled, return empty string to prevent sending $customer_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); + $email_id = $email_class instanceof WC_Email ? $email_class->id : 'unknown'; + $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; + + // Debug logging + Logger::log( \sprintf( + 'WCPOS Customer Recipient Filter: Order #%s, Email ID: %s, Original Recipient: %s, POS Setting: %s, Final Recipient: %s', + $order_id, + $email_id, + $recipient, + $customer_emails_enabled ? 'ENABLED' : 'DISABLED', + $customer_emails_enabled ? $recipient : 'BLOCKED' + ) ); + + // If customer emails are disabled, return empty string to prevent sending if ( ! $customer_emails_enabled ) { return ''; } @@ -337,21 +412,27 @@ public function prevent_disabled_pos_emails( $atts ) { $is_wc_email = false; $order_id = null; - // Common WooCommerce email subject patterns + // Common WooCommerce email subject patterns - more comprehensive $patterns = array( - '/Your (.+) order \(#(\d+)\)/', // Customer emails - '/\[(.+)\] New customer order \(#(\d+)\)/', // New order admin email - '/\[(.+)\] Cancelled order \(#(\d+)\)/', // Cancelled order - '/\[(.+)\] Failed order \(#(\d+)\)/', // Failed order - '/Order #(\d+) details/', // Invoice emails - '/Note added to your order #(\d+)/', // Customer note + '/Your (.+) order \(#(\d+)\)/', // Customer emails + '/\[(.+)\] New customer order \(#(\d+)\)/', // New order admin email + '/\[(.+)\] Cancelled order \(#(\d+)\)/', // Cancelled order admin email + '/\[(.+)\] Failed order \(#(\d+)\)/', // Failed order admin email + '/Order #(\d+) details/', // Invoice emails + '/Note added to your order #(\d+)/', // Customer note + '/\[(.+)\] Order #(\d+)/', // Generic admin pattern + '/Order (\d+) \-/', // Alternative order pattern ); foreach ( $patterns as $pattern ) { if ( preg_match( $pattern, $subject, $matches ) ) { $is_wc_email = true; - // Extract order ID from the match - $order_id = isset( $matches[2] ) ? (int) $matches[2] : ( isset( $matches[1] ) ? (int) $matches[1] : null ); + // Extract order ID from the match - try different capture groups + if ( isset( $matches[2] ) && is_numeric( $matches[2] ) ) { + $order_id = (int) $matches[2]; + } elseif ( isset( $matches[1] ) && is_numeric( $matches[1] ) ) { + $order_id = (int) $matches[1]; + } break; } @@ -368,16 +449,22 @@ public function prevent_disabled_pos_emails( $atts ) { return $atts; } - // Determine if this is likely an admin or customer email based on recipient and content - $to = $atts['to']; - $admin_email = get_option( 'admin_email' ); - $is_admin_email = ( $to === $admin_email || 0 === strpos( $subject, '[' ) ); + // More robust admin email detection + $is_admin_email = $this->is_likely_admin_email( $atts, $subject ); + + // Debug logging - helps troubleshoot issues + Logger::log( \sprintf( + 'WCPOS Email Debug: Order #%d, Subject: "%s", To: "%s", Admin Email: %s', + $order_id, + $subject, + $atts['to'], + $is_admin_email ? 'YES' : 'NO' + ) ); // Check settings and prevent sending if disabled if ( $is_admin_email ) { $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); if ( ! $admin_emails_enabled ) { - // Log for debugging purposes Logger::log( 'WCPOS: Prevented admin email for POS order #' . $order_id ); return false; // Prevent the email from being sent @@ -385,7 +472,6 @@ public function prevent_disabled_pos_emails( $atts ) { } else { $customer_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); if ( ! $customer_emails_enabled ) { - // Log for debugging purposes Logger::log( 'WCPOS: Prevented customer email for POS order #' . $order_id ); return false; // Prevent the email from being sent @@ -395,6 +481,348 @@ public function prevent_disabled_pos_emails( $atts ) { return $atts; } + /** + * Debug function to log all email sends for troubleshooting. + * This helps us see exactly what emails are being triggered. + * + * @param string $to Email recipient. + * @param string $subject Email subject. + * @param string $message Email message. + * @param string $headers Email headers. + * @param WC_Email $email_object Email object. + */ + public function debug_email_sending( $to, $subject, $message, $headers, $email_object = null ): void { + if ( ! $email_object instanceof WC_Email ) { + return; + } + + // Check if this email is related to a POS order + $order = null; + if ( isset( $email_object->object ) && $email_object->object instanceof WC_Order ) { + $order = $email_object->object; + } + + if ( $this->is_pos_order( $order ) ) { + Logger::log( \sprintf( + 'WCPOS Email Send Debug: Email ID: %s, Order: #%s, To: %s, Subject: "%s"', + $email_object->id, + $order ? $order->get_id() : 'unknown', + $to, + $subject + ) ); + } + } + + /** + * Debug function to catch all email-related filter calls. + * This helps us see what email hooks are being triggered. + */ + public function debug_all_email_filters(): void { + $hook = current_filter(); + + // Only log WooCommerce email filters + if ( 0 === strpos( $hook, 'woocommerce_email_enabled_' ) || + 0 === strpos( $hook, 'woocommerce_email_recipient_' ) ) { + $args = \func_get_args(); + $order = null; + + // Try to extract order from arguments + foreach ( $args as $arg ) { + if ( $arg instanceof WC_Order ) { + $order = $arg; + + break; + } + } + + // Only log if this is a POS order or if we can't determine the order + if ( null === $order || $this->is_pos_order( $order ) ) { + $email_type = str_replace( array( 'woocommerce_email_enabled_', 'woocommerce_email_recipient_' ), '', $hook ); + $order_id = $order ? $order->get_id() : 'unknown'; + $order_created_via = $order ? $order->get_created_via() : 'unknown'; + + Logger::log( \sprintf( + 'WCPOS Email Filter Debug: Hook: %s, Email Type: %s, Order: #%s, Created Via: %s', + $hook, + $email_type, + $order_id, + $order_created_via + ) ); + } + } + } + + /** + * Handle order status changes for POS orders. + * This bypasses WooCommerce email settings and manually triggers emails based on POS settings. + * + * @param int $order_id Order ID. + * @param WC_Order $order Order object. + */ + public function handle_order_status_change( $order_id, $order = null ): void { + // Get order if not provided + if ( ! $order instanceof WC_Order ) { + $order = wc_get_order( $order_id ); + } + + // Only handle POS orders + if ( ! $this->is_pos_order( $order ) ) { + return; + } + + $current_hook = current_filter(); + Logger::log( \sprintf( + 'WCPOS Order Status Change: Order #%s, Hook: %s, Status: %s', + $order->get_id(), + $current_hook, + $order->get_status() + ) ); + + // Get POS email settings + $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); + $customer_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); + + // Map order status change hooks to email types + $admin_email_triggers = array( + 'woocommerce_order_status_pending_to_processing' => 'new_order', + 'woocommerce_order_status_pending_to_completed' => 'new_order', + 'woocommerce_order_status_pending_to_on-hold' => 'new_order', + 'woocommerce_order_status_failed_to_processing' => 'new_order', + 'woocommerce_order_status_failed_to_completed' => 'new_order', + 'woocommerce_order_status_cancelled_to_processing' => 'new_order', + 'woocommerce_order_status_on-hold_to_processing' => 'new_order', + 'woocommerce_order_status_processing_to_cancelled' => 'cancelled_order', + 'woocommerce_order_status_pending_to_failed' => 'failed_order', + 'woocommerce_order_status_on-hold_to_cancelled' => 'cancelled_order', + 'woocommerce_order_status_on-hold_to_failed' => 'failed_order', + ); + + $customer_email_triggers = array( + 'woocommerce_order_status_pending_to_on-hold' => 'customer_on_hold_order', + 'woocommerce_order_status_pending_to_processing' => 'customer_processing_order', + 'woocommerce_order_status_pending_to_completed' => 'customer_completed_order', + 'woocommerce_order_status_failed_to_processing' => 'customer_processing_order', + 'woocommerce_order_status_failed_to_completed' => 'customer_completed_order', + 'woocommerce_order_status_on-hold_to_processing' => 'customer_processing_order', + ); + + // Handle admin emails + if ( $admin_emails_enabled && isset( $admin_email_triggers[ $current_hook ] ) ) { + $this->force_send_admin_email( $admin_email_triggers[ $current_hook ], $order ); + } elseif ( ! $admin_emails_enabled && isset( $admin_email_triggers[ $current_hook ] ) ) { + // Block default admin emails if POS setting is disabled + $this->block_default_admin_email( $admin_email_triggers[ $current_hook ], $order ); + } + + // Handle customer emails + if ( $customer_emails_enabled && isset( $customer_email_triggers[ $current_hook ] ) ) { + $this->force_send_customer_email( $customer_email_triggers[ $current_hook ], $order ); + } elseif ( ! $customer_emails_enabled && isset( $customer_email_triggers[ $current_hook ] ) ) { + // Block default customer emails if POS setting is disabled + $this->block_default_customer_email( $customer_email_triggers[ $current_hook ], $order ); + } + } + + /** + * Force send an admin email for POS orders, bypassing WooCommerce settings. + * + * @param string $email_type Email type (new_order, cancelled_order, etc.). + * @param WC_Order $order Order object. + */ + private function force_send_admin_email( $email_type, $order ): void { + $emails = WC()->mailer()->get_emails(); + $class_name = 'WC_Email_' . str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $email_type ) ) ); + + if ( ! isset( $emails[ $class_name ] ) ) { + Logger::log( \sprintf( 'WCPOS: Admin email class not found: %s', $class_name ) ); + + return; + } + + $email = $emails[ $class_name ]; + $original_enabled = $email->is_enabled(); + + Logger::log( \sprintf( + 'WCPOS Force Admin Email: Order #%s, Email Type: %s, WC Enabled: %s, Forcing Send', + $order->get_id(), + $email_type, + $original_enabled ? 'YES' : 'NO' + ) ); + + // Temporarily enable the email if it's disabled + if ( ! $original_enabled ) { + $email->enabled = 'yes'; + } + + // Send the email + try { + $email->trigger( $order->get_id(), $order ); + Logger::log( \sprintf( 'WCPOS: Successfully sent admin email %s for order #%s', $email_type, $order->get_id() ) ); + } catch ( Exception $e ) { + Logger::log( \sprintf( 'WCPOS: Failed to send admin email %s for order #%s: %s', $email_type, $order->get_id(), $e->getMessage() ) ); + } + + // Restore original enabled state + $email->enabled = $original_enabled ? 'yes' : 'no'; + } + + /** + * Force send a customer email for POS orders, bypassing WooCommerce settings. + * + * @param string $email_type Email type (customer_processing_order, etc.). + * @param WC_Order $order Order object. + */ + private function force_send_customer_email( $email_type, $order ): void { + $emails = WC()->mailer()->get_emails(); + $class_name = 'WC_Email_' . str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $email_type ) ) ); + + if ( ! isset( $emails[ $class_name ] ) ) { + Logger::log( \sprintf( 'WCPOS: Customer email class not found: %s', $class_name ) ); + + return; + } + + $email = $emails[ $class_name ]; + $original_enabled = $email->is_enabled(); + + Logger::log( \sprintf( + 'WCPOS Force Customer Email: Order #%s, Email Type: %s, WC Enabled: %s, Forcing Send', + $order->get_id(), + $email_type, + $original_enabled ? 'YES' : 'NO' + ) ); + + // Temporarily enable the email if it's disabled + if ( ! $original_enabled ) { + $email->enabled = 'yes'; + } + + // Send the email + try { + $email->trigger( $order->get_id(), $order ); + Logger::log( \sprintf( 'WCPOS: Successfully sent customer email %s for order #%s', $email_type, $order->get_id() ) ); + } catch ( Exception $e ) { + Logger::log( \sprintf( 'WCPOS: Failed to send customer email %s for order #%s: %s', $email_type, $order->get_id(), $e->getMessage() ) ); + } + + // Restore original enabled state + $email->enabled = $original_enabled ? 'yes' : 'no'; + } + + /** + * Block default admin email for POS orders when POS setting is disabled. + * + * @param string $email_type Email type (new_order, cancelled_order, etc.). + * @param WC_Order $order Order object. + */ + private function block_default_admin_email( $email_type, $order ): void { + $emails = WC()->mailer()->get_emails(); + $class_name = 'WC_Email_' . str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $email_type ) ) ); + + if ( ! isset( $emails[ $class_name ] ) ) { + return; + } + + $email = $emails[ $class_name ]; + $original_enabled = $email->is_enabled(); + + Logger::log( \sprintf( + 'WCPOS Block Admin Email: Order #%s, Email Type: %s, WC Enabled: %s, POS Setting: DISABLED - Blocking', + $order->get_id(), + $email_type, + $original_enabled ? 'YES' : 'NO' + ) ); + + // Temporarily disable the email to prevent default sending + $email->enabled = 'no'; + + // Re-enable after a short delay to restore original state + add_action( 'shutdown', function() use ( $email, $original_enabled ): void { + $email->enabled = $original_enabled ? 'yes' : 'no'; + } ); + } + + /** + * Block default customer email for POS orders when POS setting is disabled. + * + * @param string $email_type Email type (customer_processing_order, etc.). + * @param WC_Order $order Order object. + */ + private function block_default_customer_email( $email_type, $order ): void { + $emails = WC()->mailer()->get_emails(); + $class_name = 'WC_Email_' . str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $email_type ) ) ); + + if ( ! isset( $emails[ $class_name ] ) ) { + return; + } + + $email = $emails[ $class_name ]; + $original_enabled = $email->is_enabled(); + + Logger::log( \sprintf( + 'WCPOS Block Customer Email: Order #%s, Email Type: %s, WC Enabled: %s, POS Setting: DISABLED - Blocking', + $order->get_id(), + $email_type, + $original_enabled ? 'YES' : 'NO' + ) ); + + // Temporarily disable the email to prevent default sending + $email->enabled = 'no'; + + // Re-enable after a short delay to restore original state + add_action( 'shutdown', function() use ( $email, $original_enabled ): void { + $email->enabled = $original_enabled ? 'yes' : 'no'; + } ); + } + + /** + * Determine if an email is likely an admin email based on various factors. + * + * @param array $email_args Email arguments from wp_mail. + * @param string $subject Email subject line. + * + * @return bool True if this looks like an admin email. + */ + private function is_likely_admin_email( $email_args, $subject ) { + $to = $email_args['to']; + + // Check if it's going to the main admin email + $admin_email = get_option( 'admin_email' ); + if ( $to === $admin_email ) { + return true; + } + + // Check if it's going to any WooCommerce admin email addresses + $wc_admin_emails = array( + get_option( 'woocommerce_stock_email_recipient' ), + get_option( 'admin_email' ), + ); + + if ( \in_array( $to, $wc_admin_emails, true ) ) { + return true; + } + + // Check subject patterns that indicate admin emails + $admin_subject_patterns = array( + '/^\[.*\]\s+(New|Cancelled|Failed)\s+.*(order|customer)/i', + '/^\[.*\]\s+Order\s+#\d+/i', + ); + + foreach ( $admin_subject_patterns as $pattern ) { + if ( preg_match( $pattern, $subject ) ) { + return true; + } + } + + // Check if subject starts with [site_name] pattern (common for admin emails) + $site_name = get_bloginfo( 'name' ); + if ( $site_name && 0 === strpos( $subject, '[' . $site_name . ']' ) ) { + return true; + } + + return false; + } + /** * Check if an order was created via WooCommerce POS. * @@ -430,8 +858,6 @@ private function setup_email_management(): void { 'new_order', 'cancelled_order', 'failed_order', - 'reset_password', - 'new_account', ); // Customer emails - these go to customers @@ -442,6 +868,8 @@ private function setup_email_management(): void { 'customer_refunded_order', 'customer_invoice', 'customer_note', + 'reset_password', // This is a customer email, not admin + 'new_account', // This is a customer email, not admin ); // Hook into email enabled filters with high priority @@ -460,8 +888,28 @@ private function setup_email_management(): void { add_filter( "woocommerce_email_recipient_{$email_id}", array( $this, 'filter_customer_email_recipients' ), 999, 4 ); } + // CRITICAL: Hook directly into order status changes to bypass WooCommerce email settings + // These hooks fire regardless of whether WooCommerce emails are enabled/disabled + add_action( 'woocommerce_order_status_pending_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pending_to_completed', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pending_to_on-hold', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_failed_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_failed_to_completed', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_cancelled_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_on-hold_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_processing_to_cancelled', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pending_to_failed', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_on-hold_to_cancelled', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_on-hold_to_failed', array( $this, 'handle_order_status_change' ), 5, 2 ); + // Ultimate failsafe - use wp_mail filter to prevent sending at the last moment add_filter( 'wp_mail', array( $this, 'prevent_disabled_pos_emails' ), 999, 1 ); + + // Add action to log all email sends for debugging (can be removed after troubleshooting) + add_action( 'woocommerce_email_send_before', array( $this, 'debug_email_sending' ), 1, 4 ); + + // Add global debugging to catch all email filter calls + add_action( 'all', array( $this, 'debug_all_email_filters' ), 1 ); } /** From 1ae6e237159142e327a20f76d6843720db23c53a Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Fri, 25 Jul 2025 16:57:16 +0200 Subject: [PATCH 06/11] clean up and bump version --- includes/Orders.php | 381 ++++++++++++++++++++---------------- package.json | 2 +- readme.txt | 2 +- verify-email-separation.php | 118 ----------- woocommerce-pos.php | 6 +- 5 files changed, 222 insertions(+), 287 deletions(-) delete mode 100644 verify-email-separation.php diff --git a/includes/Orders.php b/includes/Orders.php index 67038ef..9ca7064 100644 --- a/includes/Orders.php +++ b/includes/Orders.php @@ -152,30 +152,13 @@ public function manage_admin_emails( $enabled, $order, $email_class ) { // Only control emails for POS orders if ( ! $this->is_pos_order( $order ) ) { - Logger::log( \sprintf( - 'WCPOS Admin Email: Order #%s not POS order (created_via: %s), Email ID: %s, Filter: %s - SKIPPING', - $order instanceof WC_Order ? $order->get_id() : 'unknown', - $order instanceof WC_Order ? $order->get_created_via() : 'unknown', - $email_id, - $current_filter - ) ); - return $enabled; } $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; - // Debug logging - Logger::log( \sprintf( - 'WCPOS Admin Email Control: Order #%s, Email ID: %s, Filter: %s, Originally Enabled: %s, POS Setting: %s, Final Result: %s', - $order_id, - $email_id, - $current_filter, - $enabled ? 'YES' : 'NO', - $admin_emails_enabled ? 'ENABLED' : 'DISABLED', - $admin_emails_enabled ? 'ENABLED' : 'DISABLED' - ) ); + // Return the setting value, this will override any other plugin settings return $admin_emails_enabled; @@ -201,15 +184,7 @@ public function manage_customer_emails( $enabled, $order, $email_class ) { $email_id = $email_class instanceof WC_Email ? $email_class->id : 'unknown'; $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; - // Debug logging - Logger::log( \sprintf( - 'WCPOS Customer Email Control: Order #%s, Email ID: %s, Originally Enabled: %s, POS Setting: %s, Final Result: %s', - $order_id, - $email_id, - $enabled ? 'YES' : 'NO', - $customer_emails_enabled ? 'ENABLED' : 'DISABLED', - $customer_emails_enabled ? 'ENABLED' : 'DISABLED' - ) ); + // Return the setting value, this will override any other plugin settings return $customer_emails_enabled; @@ -236,15 +211,7 @@ public function filter_admin_email_recipients( $recipient, $order, $email_class, $email_id = $email_class instanceof WC_Email ? $email_class->id : 'unknown'; $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; - // Debug logging - Logger::log( \sprintf( - 'WCPOS Admin Recipient Filter: Order #%s, Email ID: %s, Original Recipient: %s, POS Setting: %s, Final Recipient: %s', - $order_id, - $email_id, - $recipient, - $admin_emails_enabled ? 'ENABLED' : 'DISABLED', - $admin_emails_enabled ? $recipient : 'BLOCKED' - ) ); + // If admin emails are disabled, return empty string to prevent sending if ( ! $admin_emails_enabled ) { @@ -275,15 +242,7 @@ public function filter_customer_email_recipients( $recipient, $order, $email_cla $email_id = $email_class instanceof WC_Email ? $email_class->id : 'unknown'; $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; - // Debug logging - Logger::log( \sprintf( - 'WCPOS Customer Recipient Filter: Order #%s, Email ID: %s, Original Recipient: %s, POS Setting: %s, Final Recipient: %s', - $order_id, - $email_id, - $recipient, - $customer_emails_enabled ? 'ENABLED' : 'DISABLED', - $customer_emails_enabled ? $recipient : 'BLOCKED' - ) ); + // If customer emails are disabled, return empty string to prevent sending if ( ! $customer_emails_enabled ) { @@ -452,14 +411,8 @@ public function prevent_disabled_pos_emails( $atts ) { // More robust admin email detection $is_admin_email = $this->is_likely_admin_email( $atts, $subject ); - // Debug logging - helps troubleshoot issues - Logger::log( \sprintf( - 'WCPOS Email Debug: Order #%d, Subject: "%s", To: "%s", Admin Email: %s', - $order_id, - $subject, - $atts['to'], - $is_admin_email ? 'YES' : 'NO' - ) ); + + // Check settings and prevent sending if disabled if ( $is_admin_email ) { @@ -481,77 +434,80 @@ public function prevent_disabled_pos_emails( $atts ) { return $atts; } + + + + + + /** - * Debug function to log all email sends for troubleshooting. - * This helps us see exactly what emails are being triggered. - * - * @param string $to Email recipient. - * @param string $subject Email subject. - * @param string $message Email message. - * @param string $headers Email headers. - * @param WC_Email $email_object Email object. + * Handle new order creation - potential trigger for admin emails. + * + * @param int $order_id Order ID. + * @param WC_Order $order Order object. */ - public function debug_email_sending( $to, $subject, $message, $headers, $email_object = null ): void { - if ( ! $email_object instanceof WC_Email ) { + public function handle_new_order( $order_id, $order = null ): void { + if ( ! $order instanceof WC_Order ) { + $order = wc_get_order( $order_id ); + } + + if ( ! $this->is_pos_order( $order ) ) { return; } - // Check if this email is related to a POS order - $order = null; - if ( isset( $email_object->object ) && $email_object->object instanceof WC_Order ) { - $order = $email_object->object; + + + // Check if admin emails are enabled and send new order email + $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); + if ( $admin_emails_enabled ) { + $this->force_send_admin_email( 'new_order', $order ); + } + } + + /** + * Handle completed order status - potential trigger for admin emails. + * + * @param int $order_id Order ID. + * @param WC_Order $order Order object. + */ + public function handle_completed_order( $order_id, $order = null ): void { + if ( ! $order instanceof WC_Order ) { + $order = wc_get_order( $order_id ); } - if ( $this->is_pos_order( $order ) ) { - Logger::log( \sprintf( - 'WCPOS Email Send Debug: Email ID: %s, Order: #%s, To: %s, Subject: "%s"', - $email_object->id, - $order ? $order->get_id() : 'unknown', - $to, - $subject - ) ); + if ( ! $this->is_pos_order( $order ) ) { + return; + } + + + + // Check if admin emails are enabled and send new order email (completed orders should also notify admin) + $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); + if ( $admin_emails_enabled ) { + $this->force_send_admin_email( 'new_order', $order ); } } /** - * Debug function to catch all email-related filter calls. - * This helps us see what email hooks are being triggered. + * Handle thank you page - another potential trigger point. + * + * @param int $order_id Order ID. */ - public function debug_all_email_filters(): void { - $hook = current_filter(); - - // Only log WooCommerce email filters - if ( 0 === strpos( $hook, 'woocommerce_email_enabled_' ) || - 0 === strpos( $hook, 'woocommerce_email_recipient_' ) ) { - $args = \func_get_args(); - $order = null; - - // Try to extract order from arguments - foreach ( $args as $arg ) { - if ( $arg instanceof WC_Order ) { - $order = $arg; + public function handle_thankyou_page( $order_id ): void { + $order = wc_get_order( $order_id ); - break; - } - } - - // Only log if this is a POS order or if we can't determine the order - if ( null === $order || $this->is_pos_order( $order ) ) { - $email_type = str_replace( array( 'woocommerce_email_enabled_', 'woocommerce_email_recipient_' ), '', $hook ); - $order_id = $order ? $order->get_id() : 'unknown'; - $order_created_via = $order ? $order->get_created_via() : 'unknown'; - - Logger::log( \sprintf( - 'WCPOS Email Filter Debug: Hook: %s, Email Type: %s, Order: #%s, Created Via: %s', - $hook, - $email_type, - $order_id, - $order_created_via - ) ); - } + if ( ! $this->is_pos_order( $order ) ) { + return; } + + + + + // but it helps us understand the order flow } + + /** * Handle order status changes for POS orders. * This bypasses WooCommerce email settings and manually triggers emails based on POS settings. @@ -571,12 +527,6 @@ public function handle_order_status_change( $order_id, $order = null ): void { } $current_hook = current_filter(); - Logger::log( \sprintf( - 'WCPOS Order Status Change: Order #%s, Hook: %s, Status: %s', - $order->get_id(), - $current_hook, - $order->get_status() - ) ); // Get POS email settings $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); @@ -584,6 +534,7 @@ public function handle_order_status_change( $order_id, $order = null ): void { // Map order status change hooks to email types $admin_email_triggers = array( + // Regular WooCommerce status changes 'woocommerce_order_status_pending_to_processing' => 'new_order', 'woocommerce_order_status_pending_to_completed' => 'new_order', 'woocommerce_order_status_pending_to_on-hold' => 'new_order', @@ -595,31 +546,90 @@ public function handle_order_status_change( $order_id, $order = null ): void { 'woocommerce_order_status_pending_to_failed' => 'failed_order', 'woocommerce_order_status_on-hold_to_cancelled' => 'cancelled_order', 'woocommerce_order_status_on-hold_to_failed' => 'failed_order', + + // POS-specific status changes + 'woocommerce_order_status_pos-open_to_processing' => 'new_order', + 'woocommerce_order_status_pos-open_to_completed' => 'new_order', + 'woocommerce_order_status_pos-open_to_on-hold' => 'new_order', + 'woocommerce_order_status_pos-partial_to_processing' => 'new_order', + 'woocommerce_order_status_pos-partial_to_completed' => 'new_order', + 'woocommerce_order_status_pos-partial_to_on-hold' => 'new_order', + 'woocommerce_order_status_pos-open_to_cancelled' => 'cancelled_order', + 'woocommerce_order_status_pos-open_to_failed' => 'failed_order', + 'woocommerce_order_status_pos-partial_to_cancelled' => 'cancelled_order', + 'woocommerce_order_status_pos-partial_to_failed' => 'failed_order', ); $customer_email_triggers = array( + // Regular WooCommerce status changes 'woocommerce_order_status_pending_to_on-hold' => 'customer_on_hold_order', 'woocommerce_order_status_pending_to_processing' => 'customer_processing_order', 'woocommerce_order_status_pending_to_completed' => 'customer_completed_order', 'woocommerce_order_status_failed_to_processing' => 'customer_processing_order', 'woocommerce_order_status_failed_to_completed' => 'customer_completed_order', 'woocommerce_order_status_on-hold_to_processing' => 'customer_processing_order', + + // POS-specific status changes + 'woocommerce_order_status_pos-open_to_on-hold' => 'customer_on_hold_order', + 'woocommerce_order_status_pos-open_to_processing' => 'customer_processing_order', + 'woocommerce_order_status_pos-open_to_completed' => 'customer_completed_order', + 'woocommerce_order_status_pos-partial_to_processing' => 'customer_processing_order', + 'woocommerce_order_status_pos-partial_to_completed' => 'customer_completed_order', + 'woocommerce_order_status_pos-partial_to_on-hold' => 'customer_on_hold_order', ); // Handle admin emails - if ( $admin_emails_enabled && isset( $admin_email_triggers[ $current_hook ] ) ) { - $this->force_send_admin_email( $admin_email_triggers[ $current_hook ], $order ); - } elseif ( ! $admin_emails_enabled && isset( $admin_email_triggers[ $current_hook ] ) ) { - // Block default admin emails if POS setting is disabled - $this->block_default_admin_email( $admin_email_triggers[ $current_hook ], $order ); + if ( isset( $admin_email_triggers[ $current_hook ] ) ) { + $email_type = $admin_email_triggers[ $current_hook ]; + + // Get WooCommerce email to check if it's enabled + $mailer = WC()->mailer(); + $emails = $mailer->get_emails(); + $wc_email_enabled = false; + + foreach ( $emails as $email_instance ) { + if ( $email_instance->id === $email_type ) { + $wc_email_enabled = $email_instance->is_enabled(); + + break; + } + } + + if ( $admin_emails_enabled && ! $wc_email_enabled ) { + // POS enabled, WC disabled -> Force send (override WC) + $this->force_send_admin_email( $email_type, $order ); + } elseif ( ! $admin_emails_enabled ) { + // POS disabled -> Block it (regardless of WC setting) + $this->block_default_admin_email( $email_type, $order ); + } + // If POS enabled AND WC enabled -> Let WC handle it normally (no action needed) } // Handle customer emails - if ( $customer_emails_enabled && isset( $customer_email_triggers[ $current_hook ] ) ) { - $this->force_send_customer_email( $customer_email_triggers[ $current_hook ], $order ); - } elseif ( ! $customer_emails_enabled && isset( $customer_email_triggers[ $current_hook ] ) ) { - // Block default customer emails if POS setting is disabled - $this->block_default_customer_email( $customer_email_triggers[ $current_hook ], $order ); + if ( isset( $customer_email_triggers[ $current_hook ] ) ) { + $email_type = $customer_email_triggers[ $current_hook ]; + + // Get WooCommerce email to check if it's enabled + $mailer = WC()->mailer(); + $emails = $mailer->get_emails(); + $wc_email_enabled = false; + + foreach ( $emails as $email_instance ) { + if ( $email_instance->id === $email_type ) { + $wc_email_enabled = $email_instance->is_enabled(); + + break; + } + } + + if ( $customer_emails_enabled && ! $wc_email_enabled ) { + // POS enabled, WC disabled -> Force send (override WC) + $this->force_send_customer_email( $email_type, $order ); + } elseif ( ! $customer_emails_enabled ) { + // POS disabled -> Block it (regardless of WC setting) + $this->block_default_customer_email( $email_type, $order ); + } + // If POS enabled AND WC enabled -> Let WC handle it normally (no action needed) } } @@ -630,24 +640,31 @@ public function handle_order_status_change( $order_id, $order = null ): void { * @param WC_Order $order Order object. */ private function force_send_admin_email( $email_type, $order ): void { - $emails = WC()->mailer()->get_emails(); - $class_name = 'WC_Email_' . str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $email_type ) ) ); + $emails = WC()->mailer()->get_emails(); + $email = null; - if ( ! isset( $emails[ $class_name ] ) ) { - Logger::log( \sprintf( 'WCPOS: Admin email class not found: %s', $class_name ) ); + // Find the email by its ID (not class name) + foreach ( $emails as $email_instance ) { + if ( $email_instance->id === $email_type ) { + $email = $email_instance; - return; + break; + } } - $email = $emails[ $class_name ]; + if ( ! $email ) { + Logger::log( \sprintf( 'WCPOS: Admin email not found: %s', $email_type ) ); + + return; + } $original_enabled = $email->is_enabled(); - Logger::log( \sprintf( - 'WCPOS Force Admin Email: Order #%s, Email Type: %s, WC Enabled: %s, Forcing Send', - $order->get_id(), - $email_type, - $original_enabled ? 'YES' : 'NO' - ) ); + // Logger::log( \sprintf( + // 'WCPOS Force Admin Email: Order #%s, Email Type: %s, WC Enabled: %s, Forcing Send', + // $order->get_id(), + // $email_type, + // $original_enabled ? 'YES' : 'NO' + // ) ); // Temporarily enable the email if it's disabled if ( ! $original_enabled ) { @@ -657,7 +674,7 @@ private function force_send_admin_email( $email_type, $order ): void { // Send the email try { $email->trigger( $order->get_id(), $order ); - Logger::log( \sprintf( 'WCPOS: Successfully sent admin email %s for order #%s', $email_type, $order->get_id() ) ); + // Logger::log( \sprintf( 'WCPOS: Successfully sent admin email %s for order #%s', $email_type, $order->get_id() ) ); } catch ( Exception $e ) { Logger::log( \sprintf( 'WCPOS: Failed to send admin email %s for order #%s: %s', $email_type, $order->get_id(), $e->getMessage() ) ); } @@ -673,24 +690,31 @@ private function force_send_admin_email( $email_type, $order ): void { * @param WC_Order $order Order object. */ private function force_send_customer_email( $email_type, $order ): void { - $emails = WC()->mailer()->get_emails(); - $class_name = 'WC_Email_' . str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $email_type ) ) ); + $emails = WC()->mailer()->get_emails(); + $email = null; - if ( ! isset( $emails[ $class_name ] ) ) { - Logger::log( \sprintf( 'WCPOS: Customer email class not found: %s', $class_name ) ); + // Find the email by its ID (not class name) + foreach ( $emails as $email_instance ) { + if ( $email_instance->id === $email_type ) { + $email = $email_instance; - return; + break; + } } - $email = $emails[ $class_name ]; + if ( ! $email ) { + Logger::log( \sprintf( 'WCPOS: Customer email not found: %s', $email_type ) ); + + return; + } $original_enabled = $email->is_enabled(); - Logger::log( \sprintf( - 'WCPOS Force Customer Email: Order #%s, Email Type: %s, WC Enabled: %s, Forcing Send', - $order->get_id(), - $email_type, - $original_enabled ? 'YES' : 'NO' - ) ); + // Logger::log( \sprintf( + // 'WCPOS Force Customer Email: Order #%s, Email Type: %s, WC Enabled: %s, Forcing Send', + // $order->get_id(), + // $email_type, + // $original_enabled ? 'YES' : 'NO' + // ) ); // Temporarily enable the email if it's disabled if ( ! $original_enabled ) { @@ -700,7 +724,7 @@ private function force_send_customer_email( $email_type, $order ): void { // Send the email try { $email->trigger( $order->get_id(), $order ); - Logger::log( \sprintf( 'WCPOS: Successfully sent customer email %s for order #%s', $email_type, $order->get_id() ) ); + // Logger::log( \sprintf( 'WCPOS: Successfully sent customer email %s for order #%s', $email_type, $order->get_id() ) ); } catch ( Exception $e ) { Logger::log( \sprintf( 'WCPOS: Failed to send customer email %s for order #%s: %s', $email_type, $order->get_id(), $e->getMessage() ) ); } @@ -716,14 +740,21 @@ private function force_send_customer_email( $email_type, $order ): void { * @param WC_Order $order Order object. */ private function block_default_admin_email( $email_type, $order ): void { - $emails = WC()->mailer()->get_emails(); - $class_name = 'WC_Email_' . str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $email_type ) ) ); + $emails = WC()->mailer()->get_emails(); + $email = null; - if ( ! isset( $emails[ $class_name ] ) ) { - return; + // Find the email by its ID (not class name) + foreach ( $emails as $email_instance ) { + if ( $email_instance->id === $email_type ) { + $email = $email_instance; + + break; + } } - $email = $emails[ $class_name ]; + if ( ! $email ) { + return; + } $original_enabled = $email->is_enabled(); Logger::log( \sprintf( @@ -749,14 +780,21 @@ private function block_default_admin_email( $email_type, $order ): void { * @param WC_Order $order Order object. */ private function block_default_customer_email( $email_type, $order ): void { - $emails = WC()->mailer()->get_emails(); - $class_name = 'WC_Email_' . str_replace( ' ', '_', ucwords( str_replace( '_', ' ', $email_type ) ) ); + $emails = WC()->mailer()->get_emails(); + $email = null; - if ( ! isset( $emails[ $class_name ] ) ) { - return; + // Find the email by its ID (not class name) + foreach ( $emails as $email_instance ) { + if ( $email_instance->id === $email_type ) { + $email = $email_instance; + + break; + } } - $email = $emails[ $class_name ]; + if ( ! $email ) { + return; + } $original_enabled = $email->is_enabled(); Logger::log( \sprintf( @@ -890,6 +928,8 @@ private function setup_email_management(): void { // CRITICAL: Hook directly into order status changes to bypass WooCommerce email settings // These hooks fire regardless of whether WooCommerce emails are enabled/disabled + + // Regular WooCommerce status changes (for completeness) add_action( 'woocommerce_order_status_pending_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); add_action( 'woocommerce_order_status_pending_to_completed', array( $this, 'handle_order_status_change' ), 5, 2 ); add_action( 'woocommerce_order_status_pending_to_on-hold', array( $this, 'handle_order_status_change' ), 5, 2 ); @@ -902,14 +942,27 @@ private function setup_email_management(): void { add_action( 'woocommerce_order_status_on-hold_to_cancelled', array( $this, 'handle_order_status_change' ), 5, 2 ); add_action( 'woocommerce_order_status_on-hold_to_failed', array( $this, 'handle_order_status_change' ), 5, 2 ); + // POS-specific status changes + add_action( 'woocommerce_order_status_pos-open_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pos-open_to_completed', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pos-open_to_on-hold', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pos-partial_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pos-partial_to_completed', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pos-partial_to_on-hold', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pos-open_to_cancelled', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pos-open_to_failed', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pos-partial_to_cancelled', array( $this, 'handle_order_status_change' ), 5, 2 ); + add_action( 'woocommerce_order_status_pos-partial_to_failed', array( $this, 'handle_order_status_change' ), 5, 2 ); + // Ultimate failsafe - use wp_mail filter to prevent sending at the last moment add_filter( 'wp_mail', array( $this, 'prevent_disabled_pos_emails' ), 999, 1 ); - // Add action to log all email sends for debugging (can be removed after troubleshooting) - add_action( 'woocommerce_email_send_before', array( $this, 'debug_email_sending' ), 1, 4 ); - // Add global debugging to catch all email filter calls - add_action( 'all', array( $this, 'debug_all_email_filters' ), 1 ); + + // Additional hooks for admin emails - these might catch cases the status change hooks miss + add_action( 'woocommerce_new_order', array( $this, 'handle_new_order' ), 5, 2 ); + add_action( 'woocommerce_order_status_completed', array( $this, 'handle_completed_order' ), 5, 2 ); + add_action( 'woocommerce_thankyou', array( $this, 'handle_thankyou_page' ), 5, 1 ); } /** diff --git a/package.json b/package.json index 6fbdafb..727e27c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@wcpos/woocommerce-pos", - "version": "1.7.11", + "version": "1.7.12", "description": "A simple front-end for taking WooCommerce orders at the Point of Sale.", "main": "index.js", "workspaces": { diff --git a/readme.txt b/readme.txt index ea3d062..fd4314a 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: kilbot Tags: ecommerce, point-of-sale, pos, inventory, woocommerce Requires at least: 5.6 Tested up to: 6.8 -Stable tag: 1.7.11 +Stable tag: 1.7.12 License: GPL-3.0 License URI: http://www.gnu.org/licenses/gpl-3.0.html diff --git a/verify-email-separation.php b/verify-email-separation.php deleted file mode 100644 index 90878c5..0000000 --- a/verify-email-separation.php +++ /dev/null @@ -1,118 +0,0 @@ -set_name('Email Test Product'); - $product->set_regular_price(10.00); - $product->set_manage_stock(false); - $product->set_status('publish'); - $product->save(); - - return $product->get_id(); -} - -// Create test product -$product_id = create_test_product(); - -echo '

Email Control Separation Test

'; -echo '

Test Product ID: ' . $product_id . '

'; - -// Test 1: Create a regular website order -echo '

Test 1: Regular Website Order

'; - -$website_order = wc_create_order(); -$website_order->add_product(wc_get_product($product_id), 1); -$website_order->set_customer_id(1); -$website_order->set_billing_email('website@test.com'); -$website_order->calculate_totals(); -// Website orders typically have 'checkout' or empty created_via -$website_order->set_created_via('checkout'); -$website_order->save(); - -echo '

Website Order ID: ' . $website_order->get_id() . '

'; -echo '

Created Via: ' . $website_order->get_created_via() . '

'; - -// Test the email controls for website order -$admin_email_enabled = apply_filters('woocommerce_email_enabled_new_order', true, $website_order, null); -$customer_email_enabled = apply_filters('woocommerce_email_enabled_customer_completed_order', true, $website_order, null); - -echo '

Admin Email Enabled: ' . ($admin_email_enabled ? 'YES' : 'NO') . ' (should be YES)

'; -echo '

Customer Email Enabled: ' . ($customer_email_enabled ? 'YES' : 'NO') . ' (should be YES)

'; - -// Test 2: Create a POS order -echo '

Test 2: POS Order

'; - -$pos_order = wc_create_order(); -$pos_order->add_product(wc_get_product($product_id), 1); -$pos_order->set_customer_id(1); -$pos_order->set_billing_email('pos@test.com'); -$pos_order->calculate_totals(); -$pos_order->set_created_via('woocommerce-pos'); // This makes it a POS order -$pos_order->save(); - -echo '

POS Order ID: ' . $pos_order->get_id() . '

'; -echo '

Created Via: ' . $pos_order->get_created_via() . '

'; - -// Test the email controls for POS order -$pos_admin_email_enabled = apply_filters('woocommerce_email_enabled_new_order', true, $pos_order, null); -$pos_customer_email_enabled = apply_filters('woocommerce_email_enabled_customer_completed_order', true, $pos_order, null); - -// Get current settings -$settings = woocommerce_pos_get_settings('checkout'); - -echo '

POS Settings - Admin Emails: ' . ($settings['admin_emails'] ? 'ENABLED' : 'DISABLED') . '

'; -echo '

POS Settings - Customer Emails: ' . ($settings['customer_emails'] ? 'ENABLED' : 'DISABLED') . '

'; -echo '

POS Admin Email Enabled: ' . ($pos_admin_email_enabled ? 'YES' : 'NO') . ' (should match POS setting)

'; -echo '

POS Customer Email Enabled: ' . ($pos_customer_email_enabled ? 'YES' : 'NO') . ' (should match POS setting)

'; - -// Summary -echo '
'; -echo '

✅ Test Results Summary

'; - -$website_protected = (true === $admin_email_enabled && true === $customer_email_enabled); -$pos_controlled = ($pos_admin_email_enabled === (bool) $settings['admin_emails'] && - $pos_customer_email_enabled === (bool) $settings['customer_emails']); - -if ($website_protected && $pos_controlled) { - echo '

✅ SUCCESS: Email controls work correctly!

'; - echo '
    '; - echo '
  • ✅ Website orders are NOT affected by POS settings
  • '; - echo '
  • ✅ POS orders ARE controlled by POS settings
  • '; - echo '
'; -} else { - echo '

❌ ISSUE DETECTED:

'; - echo '
    '; - if ( ! $website_protected) { - echo '
  • ❌ Website orders are being affected (should not be)
  • '; - } - if ( ! $pos_controlled) { - echo '
  • ❌ POS orders are not being controlled (should be)
  • '; - } - echo '
'; -} - -// Cleanup -echo '
'; -echo '

Cleanup: Test orders and product created. You may want to delete them manually.

'; -echo '

Website Order: Edit Order #' . $website_order->get_id() . '

'; -echo '

POS Order: Edit Order #' . $pos_order->get_id() . '

'; - -echo '
'; -echo '

Security Warning: Delete this script after testing!

'; diff --git a/woocommerce-pos.php b/woocommerce-pos.php index 81a5ab6..ab33002 100644 --- a/woocommerce-pos.php +++ b/woocommerce-pos.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce POS * Plugin URI: https://wordpress.org/plugins/woocommerce-pos/ * Description: A simple front-end for taking WooCommerce orders at the Point of Sale. Requires WooCommerce. - * Version: 1.7.11 + * Version: 1.7.12 * Author: kilbot * Author URI: http://wcpos.com * Text Domain: woocommerce-pos @@ -14,7 +14,7 @@ * Tested up to: 6.8 * Requires PHP: 7.4 * Requires Plugins: woocommerce - * WC tested up to: 9.9 + * WC tested up to: 10.0 * WC requires at least: 5.3. * * @see http://wcpos.com @@ -23,7 +23,7 @@ namespace WCPOS\WooCommercePOS; // Define plugin constants. -const VERSION = '1.7.11'; +const VERSION = '1.7.12'; const PLUGIN_NAME = 'woocommerce-pos'; const SHORT_NAME = 'wcpos'; \define( __NAMESPACE__ . '\PLUGIN_FILE', plugin_basename( __FILE__ ) ); // 'woocommerce-pos/woocommerce-pos.php' From 2740dce5ff1ee91d7e8ca15afd667bc127129cf0 Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Fri, 25 Jul 2025 17:08:37 +0200 Subject: [PATCH 07/11] fix tests --- tests/includes/API/Test_Order_Taxes.php | 57 ++++++++++++------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/tests/includes/API/Test_Order_Taxes.php b/tests/includes/API/Test_Order_Taxes.php index 25f116d..630b557 100644 --- a/tests/includes/API/Test_Order_Taxes.php +++ b/tests/includes/API/Test_Order_Taxes.php @@ -2,9 +2,9 @@ namespace WCPOS\WooCommercePOS\Tests\API; +use WC_Admin_Settings; use WCPOS\WooCommercePOS\API\Orders_Controller; use WCPOS\WooCommercePOS\Tests\Helpers\TaxHelper; -use WC_Admin_Settings; /** * @internal @@ -21,16 +21,16 @@ public function setup(): void { // Set default address // update_option( 'woocommerce_default_country', 'GB' ); - /** + /* * Init Taxes * * use WooCommerce Tax Dummy Data */ TaxHelper::create_tax_rate( array( - 'country' => 'GB', - 'rate' => '20.000', - 'name' => 'VAT', + 'country' => 'GB', + 'rate' => '20.000', + 'name' => 'VAT', 'priority' => 1, 'compound' => true, 'shipping' => true, @@ -38,9 +38,9 @@ public function setup(): void { ); TaxHelper::create_tax_rate( array( - 'country' => 'GB', - 'rate' => '5.000', - 'name' => 'VAT', + 'country' => 'GB', + 'rate' => '5.000', + 'name' => 'VAT', 'priority' => 1, 'compound' => true, 'shipping' => true, @@ -49,9 +49,9 @@ public function setup(): void { ); TaxHelper::create_tax_rate( array( - 'country' => 'GB', - 'rate' => '0.000', - 'name' => 'VAT', + 'country' => 'GB', + 'rate' => '0.000', + 'name' => 'VAT', 'priority' => 1, 'compound' => true, 'shipping' => true, @@ -60,9 +60,9 @@ public function setup(): void { ); TaxHelper::create_tax_rate( array( - 'country' => 'US', - 'rate' => '10.000', - 'name' => 'US', + 'country' => 'US', + 'rate' => '10.000', + 'name' => 'US', 'priority' => 1, 'compound' => true, 'shipping' => true, @@ -70,11 +70,11 @@ public function setup(): void { ); TaxHelper::create_tax_rate( array( - 'country' => 'US', - 'state' => 'AL', + 'country' => 'US', + 'state' => 'AL', 'postcode' => '12345; 123456', - 'rate' => '2.000', - 'name' => 'US AL', + 'rate' => '2.000', + 'name' => 'US AL', 'priority' => 2, 'compound' => true, 'shipping' => true, @@ -127,7 +127,7 @@ public function test_create_order_with_tax(): void { // line item taxes $this->assertEquals( 1, \count( $data['line_items'] ) ); $this->assertEquals( 1, \count( $data['line_items'][0]['taxes'] ) ); - $this->assertEquals( '1', $data['line_items'][0]['taxes'][0]['total'] ); + $this->assertEquals( '1.000000', $data['line_items'][0]['taxes'][0]['total'] ); // order taxes $this->assertEquals( 1, \count( $data['tax_lines'] ) ); @@ -184,7 +184,7 @@ public function test_create_order_with_customer_billing_address_as_tax_location( $this->assertEquals( 201, $response->get_status() ); // check meta data - $count = 0; + $count = 0; $tax_based_on = ''; // Look for the _woocommerce_pos_uuid key in meta_data @@ -201,7 +201,7 @@ public function test_create_order_with_customer_billing_address_as_tax_location( // line item taxes $this->assertEquals( 1, \count( $data['line_items'] ) ); $this->assertEquals( 1, \count( $data['line_items'][0]['taxes'] ) ); - $this->assertEquals( '2', $data['line_items'][0]['taxes'][0]['total'] ); + $this->assertEquals( '2.000000', $data['line_items'][0]['taxes'][0]['total'] ); // order taxes $this->assertEquals( 1, \count( $data['tax_lines'] ) ); @@ -251,7 +251,7 @@ public function test_create_order_with_customer_shipping_address_as_tax_location $this->assertEquals( 201, $response->get_status() ); // check meta data - $count = 0; + $count = 0; $tax_based_on = ''; // Look for the _woocommerce_pos_uuid key in meta_data @@ -268,8 +268,8 @@ public function test_create_order_with_customer_shipping_address_as_tax_location // line item taxes $this->assertEquals( 1, \count( $data['line_items'] ) ); $this->assertEquals( 2, \count( $data['line_items'][0]['taxes'] ) ); - $this->assertEquals( '1', $data['line_items'][0]['taxes'][0]['total'] ); - $this->assertEquals( '0.22', $data['line_items'][0]['taxes'][1]['total'] ); + $this->assertEquals( '1.000000', $data['line_items'][0]['taxes'][0]['total'] ); + $this->assertEquals( '0.220000', $data['line_items'][0]['taxes'][1]['total'] ); // order taxes $this->assertEquals( 2, \count( $data['tax_lines'] ) ); @@ -286,10 +286,7 @@ public function test_create_order_with_customer_shipping_address_as_tax_location $this->assertEquals( '1.220000', $data['total_tax'] ); } - /** - * - */ - public function test_fee_lines_should_respect_tax_status_when_negative() { + public function test_fee_lines_should_respect_tax_status_when_negative(): void { $this->assertEquals( 'base', WC_Admin_Settings::get_option( 'woocommerce_tax_based_on' ) ); $this->assertEquals( 'US:CA', WC_Admin_Settings::get_option( 'woocommerce_default_country' ) ); @@ -306,8 +303,8 @@ public function test_fee_lines_should_respect_tax_status_when_negative() { ), 'fee_lines' => array( array( - 'name' => 'Fee', - 'total' => '-10', + 'name' => 'Fee', + 'total' => '-10', 'tax_status' => 'none', ), ), From e36ea648d4f6509cecce5b479d56963e933066b1 Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Wed, 6 Aug 2025 18:08:08 +0200 Subject: [PATCH 08/11] Fix: New Order emails to send after order calculations --- includes/Emails.php | 156 ++++++++ includes/Init.php | 1 + includes/Orders.php | 742 ----------------------------------- includes/wcpos-functions.php | 27 +- package.json | 2 +- readme.txt | 5 +- woocommerce-pos.php | 4 +- 7 files changed, 181 insertions(+), 756 deletions(-) create mode 100644 includes/Emails.php diff --git a/includes/Emails.php b/includes/Emails.php new file mode 100644 index 0000000..88db447 --- /dev/null +++ b/includes/Emails.php @@ -0,0 +1,156 @@ + + * + * @see http://wcpos.com + */ + +namespace WCPOS\WooCommercePOS; + +use WC_Email; +use WC_Order; + +/** + * Emails Class + * - manages email sending for POS orders. + */ +class Emails { + /** + * Constructor. + */ + public function __construct() { + // Get filterable email arrays - allow users to customize which emails are affected + $admin_emails = apply_filters( 'woocommerce_pos_admin_emails', array( + 'cancelled_order', + 'failed_order', + ) ); + + $customer_emails = apply_filters( 'woocommerce_pos_customer_emails', array( + 'customer_failed_order', + 'customer_on_hold_order', + 'customer_processing_order', + 'customer_completed_order', + 'customer_refunded_order', + ) ); + + // Hook into email enabled filters - this is the main control mechanism + foreach ( $admin_emails as $email_id ) { + add_filter( "woocommerce_email_enabled_{$email_id}", array( $this, 'manage_admin_emails' ), 999, 3 ); + } + foreach ( $customer_emails as $email_id ) { + add_filter( "woocommerce_email_enabled_{$email_id}", array( $this, 'manage_customer_emails' ), 999, 3 ); + } + + // Manually trigger new_order email for POS status changes + // WooCommerce doesn't automatically trigger new_order for pos-open/pos-partial transitions + add_action( 'woocommerce_order_status_pos-open_to_completed', array( $this, 'trigger_new_order_email' ), 10, 2 ); + add_action( 'woocommerce_order_status_pos-open_to_processing', array( $this, 'trigger_new_order_email' ), 10, 2 ); + add_action( 'woocommerce_order_status_pos-open_to_on-hold', array( $this, 'trigger_new_order_email' ), 10, 2 ); + add_action( 'woocommerce_order_status_pos-partial_to_completed', array( $this, 'trigger_new_order_email' ), 10, 2 ); + add_action( 'woocommerce_order_status_pos-partial_to_processing', array( $this, 'trigger_new_order_email' ), 10, 2 ); + add_action( 'woocommerce_order_status_pos-partial_to_on-hold', array( $this, 'trigger_new_order_email' ), 10, 2 ); + } + + /** + * Manage admin email sending for POS orders. + * Only affects orders created via WooCommerce POS. + * + * @param bool $enabled Whether the email is enabled. + * @param null|WC_Order $order The order object. + * @param mixed|WC_Email $email_class The email class. + * + * @return bool Whether the email should be sent. + */ + public function manage_admin_emails( $enabled, $order, $email_class ) { + // Only control emails for POS orders + if ( ! woocommerce_pos_is_pos_order( $order ) ) { + return $enabled; + } + + // Get email ID for filtering + $email_id = $email_class instanceof WC_Email ? $email_class->id : 'unknown'; + + // Get POS admin email setting + $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); + + + + // Allow final filtering of the email enabled status + return apply_filters( 'woocommerce_pos_admin_email_enabled', $admin_emails_enabled, $email_id, $order, $email_class ); + } + + /** + * Manage customer email sending for POS orders. + * Only affects orders created via WooCommerce POS. + * + * @param bool $enabled Whether the email is enabled. + * @param null|WC_Order $order The order object. + * @param mixed|WC_Email $email_class The email class. + * + * @return bool Whether the email should be sent. + */ + public function manage_customer_emails( $enabled, $order, $email_class ) { + // Only control emails for POS orders + if ( ! woocommerce_pos_is_pos_order( $order ) ) { + return $enabled; + } + + // Get email ID for filtering + $email_id = $email_class instanceof WC_Email ? $email_class->id : 'unknown'; + + // Get POS customer email setting + $customer_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); + + + + // Allow final filtering of the email enabled status + return apply_filters( 'woocommerce_pos_customer_email_enabled', $customer_emails_enabled, $email_id, $order, $email_class ); + } + + /** + * Manually trigger new_order admin email for POS orders. + * This is needed because WooCommerce doesn't automatically trigger new_order + * for pos-open/pos-partial status transitions. + * + * @param int $order_id Order ID. + * @param WC_Order $order Order object. + */ + public function trigger_new_order_email( $order_id, $order = null ): void { + if ( ! $order ) { + $order = wc_get_order( $order_id ); + } + + if ( ! woocommerce_pos_is_pos_order( $order ) ) { + return; + } + + // Check if admin emails are enabled + $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); + if ( ! $admin_emails_enabled ) { + return; + } + + // Get the new_order email by ID, not class name + $mailer = WC()->mailer(); + $emails = $mailer->get_emails(); + + foreach ( $emails as $email ) { + if ( 'new_order' === $email->id ) { + // Temporarily enable the email to ensure it sends + $original_enabled = $email->enabled; + $email->enabled = 'yes'; + + // Trigger the email + $email->trigger( $order_id, $order ); + + // Restore original state + $email->enabled = $original_enabled; + + break; + } + } + } +} diff --git a/includes/Init.php b/includes/Init.php index 900a217..0e91a75 100644 --- a/includes/Init.php +++ b/includes/Init.php @@ -156,6 +156,7 @@ private function init_common(): void { new Gateways(); new Products(); new Orders(); + new Emails(); } /** diff --git a/includes/Orders.php b/includes/Orders.php index 9ca7064..93f22ba 100644 --- a/includes/Orders.php +++ b/includes/Orders.php @@ -35,9 +35,6 @@ public function __construct() { add_filter( 'woocommerce_order_get_tax_location', array( $this, 'get_tax_location' ), 10, 2 ); add_action( 'woocommerce_order_item_after_calculate_taxes', array( $this, 'order_item_after_calculate_taxes' ) ); add_action( 'woocommerce_order_item_shipping_after_calculate_taxes', array( $this, 'order_item_after_calculate_taxes' ) ); - - // POS email management - higher priority to override other plugins - $this->setup_email_management(); } /** @@ -126,132 +123,6 @@ public function hidden_order_itemmeta( array $meta_keys ): array { return array_merge( $meta_keys, array( '_woocommerce_pos_uuid', '_woocommerce_pos_tax_status', '_woocommerce_pos_data' ) ); } - /** - * Manage admin email sending for POS orders. - * Only affects orders created via WooCommerce POS. - * - * @param bool $enabled Whether the email is enabled. - * @param null|WC_Order $order The order object. - * @param mixed|WC_Email $email_class The email class. - * - * @return bool Whether the email should be sent. - */ - public function manage_admin_emails( $enabled, $order, $email_class ) { - // Better email ID detection - $email_id = 'unknown'; - if ( $email_class instanceof WC_Email && isset( $email_class->id ) ) { - $email_id = $email_class->id; - } elseif ( \is_object( $email_class ) && isset( $email_class->id ) ) { - $email_id = $email_class->id; - } elseif ( \is_string( $email_class ) ) { - $email_id = $email_class; - } - - // Get current filter name for additional context - $current_filter = current_filter(); - - // Only control emails for POS orders - if ( ! $this->is_pos_order( $order ) ) { - return $enabled; - } - - $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); - $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; - - - - // Return the setting value, this will override any other plugin settings - return $admin_emails_enabled; - } - - /** - * Manage customer email sending for POS orders. - * Only affects orders created via WooCommerce POS. - * - * @param bool $enabled Whether the email is enabled. - * @param null|WC_Order $order The order object. - * @param mixed|WC_Email $email_class The email class. - * - * @return bool Whether the email should be sent. - */ - public function manage_customer_emails( $enabled, $order, $email_class ) { - // Only control emails for POS orders - if ( ! $this->is_pos_order( $order ) ) { - return $enabled; - } - - $customer_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); - $email_id = $email_class instanceof WC_Email ? $email_class->id : 'unknown'; - $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; - - - - // Return the setting value, this will override any other plugin settings - return $customer_emails_enabled; - } - - /** - * Filter admin email recipients for POS orders as a safety net. - * If admin emails are disabled, return empty string to prevent sending. - * - * @param string $recipient The recipient email address. - * @param null|WC_Order $order The order object. - * @param mixed|WC_Email $email_class The email class. - * @param array $args Additional arguments. - * - * @return string The recipient email or empty string to prevent sending. - */ - public function filter_admin_email_recipients( $recipient, $order, $email_class, $args = array() ) { - // Only control emails for POS orders - if ( ! $this->is_pos_order( $order ) ) { - return $recipient; - } - - $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); - $email_id = $email_class instanceof WC_Email ? $email_class->id : 'unknown'; - $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; - - - - // If admin emails are disabled, return empty string to prevent sending - if ( ! $admin_emails_enabled ) { - return ''; - } - - return $recipient; - } - - /** - * Filter customer email recipients for POS orders as a safety net. - * If customer emails are disabled, return empty string to prevent sending. - * - * @param string $recipient The recipient email address. - * @param null|WC_Order $order The order object. - * @param mixed|WC_Email $email_class The email class. - * @param array $args Additional arguments. - * - * @return string The recipient email or empty string to prevent sending. - */ - public function filter_customer_email_recipients( $recipient, $order, $email_class, $args = array() ) { - // Only control emails for POS orders - if ( ! $this->is_pos_order( $order ) ) { - return $recipient; - } - - $customer_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); - $email_id = $email_class instanceof WC_Email ? $email_class->id : 'unknown'; - $order_id = $order instanceof WC_Order ? $order->get_id() : 'unknown'; - - - - // If customer emails are disabled, return empty string to prevent sending - if ( ! $customer_emails_enabled ) { - return ''; - } - - return $recipient; - } - /** * Filter the product object for an order item. * @@ -352,619 +223,6 @@ public function order_item_after_calculate_taxes( $item ): void { } } - /** - * Ultimate failsafe to prevent disabled POS emails from being sent. - * This hooks into wp_mail as the final layer of protection. - * - * @param array $atts The wp_mail arguments. - * - * @return array|false The wp_mail arguments or false to prevent sending. - */ - public function prevent_disabled_pos_emails( $atts ) { - // Check if this email is related to a WooCommerce order - if ( ! isset( $atts['subject'] ) || ! \is_string( $atts['subject'] ) ) { - return $atts; - } - - // Look for WooCommerce order patterns in the subject line - $subject = $atts['subject']; - $is_wc_email = false; - $order_id = null; - - // Common WooCommerce email subject patterns - more comprehensive - $patterns = array( - '/Your (.+) order \(#(\d+)\)/', // Customer emails - '/\[(.+)\] New customer order \(#(\d+)\)/', // New order admin email - '/\[(.+)\] Cancelled order \(#(\d+)\)/', // Cancelled order admin email - '/\[(.+)\] Failed order \(#(\d+)\)/', // Failed order admin email - '/Order #(\d+) details/', // Invoice emails - '/Note added to your order #(\d+)/', // Customer note - '/\[(.+)\] Order #(\d+)/', // Generic admin pattern - '/Order (\d+) \-/', // Alternative order pattern - ); - - foreach ( $patterns as $pattern ) { - if ( preg_match( $pattern, $subject, $matches ) ) { - $is_wc_email = true; - // Extract order ID from the match - try different capture groups - if ( isset( $matches[2] ) && is_numeric( $matches[2] ) ) { - $order_id = (int) $matches[2]; - } elseif ( isset( $matches[1] ) && is_numeric( $matches[1] ) ) { - $order_id = (int) $matches[1]; - } - - break; - } - } - - // If this doesn't appear to be a WooCommerce email, let it through - if ( ! $is_wc_email || ! $order_id ) { - return $atts; - } - - // Get the order and check if it's a POS order - $order = wc_get_order( $order_id ); - if ( ! $this->is_pos_order( $order ) ) { - return $atts; - } - - // More robust admin email detection - $is_admin_email = $this->is_likely_admin_email( $atts, $subject ); - - - - - // Check settings and prevent sending if disabled - if ( $is_admin_email ) { - $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); - if ( ! $admin_emails_enabled ) { - Logger::log( 'WCPOS: Prevented admin email for POS order #' . $order_id ); - - return false; // Prevent the email from being sent - } - } else { - $customer_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); - if ( ! $customer_emails_enabled ) { - Logger::log( 'WCPOS: Prevented customer email for POS order #' . $order_id ); - - return false; // Prevent the email from being sent - } - } - - return $atts; - } - - - - - - - - /** - * Handle new order creation - potential trigger for admin emails. - * - * @param int $order_id Order ID. - * @param WC_Order $order Order object. - */ - public function handle_new_order( $order_id, $order = null ): void { - if ( ! $order instanceof WC_Order ) { - $order = wc_get_order( $order_id ); - } - - if ( ! $this->is_pos_order( $order ) ) { - return; - } - - - - // Check if admin emails are enabled and send new order email - $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); - if ( $admin_emails_enabled ) { - $this->force_send_admin_email( 'new_order', $order ); - } - } - - /** - * Handle completed order status - potential trigger for admin emails. - * - * @param int $order_id Order ID. - * @param WC_Order $order Order object. - */ - public function handle_completed_order( $order_id, $order = null ): void { - if ( ! $order instanceof WC_Order ) { - $order = wc_get_order( $order_id ); - } - - if ( ! $this->is_pos_order( $order ) ) { - return; - } - - - - // Check if admin emails are enabled and send new order email (completed orders should also notify admin) - $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); - if ( $admin_emails_enabled ) { - $this->force_send_admin_email( 'new_order', $order ); - } - } - - /** - * Handle thank you page - another potential trigger point. - * - * @param int $order_id Order ID. - */ - public function handle_thankyou_page( $order_id ): void { - $order = wc_get_order( $order_id ); - - if ( ! $this->is_pos_order( $order ) ) { - return; - } - - - - - // but it helps us understand the order flow - } - - - - /** - * Handle order status changes for POS orders. - * This bypasses WooCommerce email settings and manually triggers emails based on POS settings. - * - * @param int $order_id Order ID. - * @param WC_Order $order Order object. - */ - public function handle_order_status_change( $order_id, $order = null ): void { - // Get order if not provided - if ( ! $order instanceof WC_Order ) { - $order = wc_get_order( $order_id ); - } - - // Only handle POS orders - if ( ! $this->is_pos_order( $order ) ) { - return; - } - - $current_hook = current_filter(); - - // Get POS email settings - $admin_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'admin_emails' ); - $customer_emails_enabled = (bool) woocommerce_pos_get_settings( 'checkout', 'customer_emails' ); - - // Map order status change hooks to email types - $admin_email_triggers = array( - // Regular WooCommerce status changes - 'woocommerce_order_status_pending_to_processing' => 'new_order', - 'woocommerce_order_status_pending_to_completed' => 'new_order', - 'woocommerce_order_status_pending_to_on-hold' => 'new_order', - 'woocommerce_order_status_failed_to_processing' => 'new_order', - 'woocommerce_order_status_failed_to_completed' => 'new_order', - 'woocommerce_order_status_cancelled_to_processing' => 'new_order', - 'woocommerce_order_status_on-hold_to_processing' => 'new_order', - 'woocommerce_order_status_processing_to_cancelled' => 'cancelled_order', - 'woocommerce_order_status_pending_to_failed' => 'failed_order', - 'woocommerce_order_status_on-hold_to_cancelled' => 'cancelled_order', - 'woocommerce_order_status_on-hold_to_failed' => 'failed_order', - - // POS-specific status changes - 'woocommerce_order_status_pos-open_to_processing' => 'new_order', - 'woocommerce_order_status_pos-open_to_completed' => 'new_order', - 'woocommerce_order_status_pos-open_to_on-hold' => 'new_order', - 'woocommerce_order_status_pos-partial_to_processing' => 'new_order', - 'woocommerce_order_status_pos-partial_to_completed' => 'new_order', - 'woocommerce_order_status_pos-partial_to_on-hold' => 'new_order', - 'woocommerce_order_status_pos-open_to_cancelled' => 'cancelled_order', - 'woocommerce_order_status_pos-open_to_failed' => 'failed_order', - 'woocommerce_order_status_pos-partial_to_cancelled' => 'cancelled_order', - 'woocommerce_order_status_pos-partial_to_failed' => 'failed_order', - ); - - $customer_email_triggers = array( - // Regular WooCommerce status changes - 'woocommerce_order_status_pending_to_on-hold' => 'customer_on_hold_order', - 'woocommerce_order_status_pending_to_processing' => 'customer_processing_order', - 'woocommerce_order_status_pending_to_completed' => 'customer_completed_order', - 'woocommerce_order_status_failed_to_processing' => 'customer_processing_order', - 'woocommerce_order_status_failed_to_completed' => 'customer_completed_order', - 'woocommerce_order_status_on-hold_to_processing' => 'customer_processing_order', - - // POS-specific status changes - 'woocommerce_order_status_pos-open_to_on-hold' => 'customer_on_hold_order', - 'woocommerce_order_status_pos-open_to_processing' => 'customer_processing_order', - 'woocommerce_order_status_pos-open_to_completed' => 'customer_completed_order', - 'woocommerce_order_status_pos-partial_to_processing' => 'customer_processing_order', - 'woocommerce_order_status_pos-partial_to_completed' => 'customer_completed_order', - 'woocommerce_order_status_pos-partial_to_on-hold' => 'customer_on_hold_order', - ); - - // Handle admin emails - if ( isset( $admin_email_triggers[ $current_hook ] ) ) { - $email_type = $admin_email_triggers[ $current_hook ]; - - // Get WooCommerce email to check if it's enabled - $mailer = WC()->mailer(); - $emails = $mailer->get_emails(); - $wc_email_enabled = false; - - foreach ( $emails as $email_instance ) { - if ( $email_instance->id === $email_type ) { - $wc_email_enabled = $email_instance->is_enabled(); - - break; - } - } - - if ( $admin_emails_enabled && ! $wc_email_enabled ) { - // POS enabled, WC disabled -> Force send (override WC) - $this->force_send_admin_email( $email_type, $order ); - } elseif ( ! $admin_emails_enabled ) { - // POS disabled -> Block it (regardless of WC setting) - $this->block_default_admin_email( $email_type, $order ); - } - // If POS enabled AND WC enabled -> Let WC handle it normally (no action needed) - } - - // Handle customer emails - if ( isset( $customer_email_triggers[ $current_hook ] ) ) { - $email_type = $customer_email_triggers[ $current_hook ]; - - // Get WooCommerce email to check if it's enabled - $mailer = WC()->mailer(); - $emails = $mailer->get_emails(); - $wc_email_enabled = false; - - foreach ( $emails as $email_instance ) { - if ( $email_instance->id === $email_type ) { - $wc_email_enabled = $email_instance->is_enabled(); - - break; - } - } - - if ( $customer_emails_enabled && ! $wc_email_enabled ) { - // POS enabled, WC disabled -> Force send (override WC) - $this->force_send_customer_email( $email_type, $order ); - } elseif ( ! $customer_emails_enabled ) { - // POS disabled -> Block it (regardless of WC setting) - $this->block_default_customer_email( $email_type, $order ); - } - // If POS enabled AND WC enabled -> Let WC handle it normally (no action needed) - } - } - - /** - * Force send an admin email for POS orders, bypassing WooCommerce settings. - * - * @param string $email_type Email type (new_order, cancelled_order, etc.). - * @param WC_Order $order Order object. - */ - private function force_send_admin_email( $email_type, $order ): void { - $emails = WC()->mailer()->get_emails(); - $email = null; - - // Find the email by its ID (not class name) - foreach ( $emails as $email_instance ) { - if ( $email_instance->id === $email_type ) { - $email = $email_instance; - - break; - } - } - - if ( ! $email ) { - Logger::log( \sprintf( 'WCPOS: Admin email not found: %s', $email_type ) ); - - return; - } - $original_enabled = $email->is_enabled(); - - // Logger::log( \sprintf( - // 'WCPOS Force Admin Email: Order #%s, Email Type: %s, WC Enabled: %s, Forcing Send', - // $order->get_id(), - // $email_type, - // $original_enabled ? 'YES' : 'NO' - // ) ); - - // Temporarily enable the email if it's disabled - if ( ! $original_enabled ) { - $email->enabled = 'yes'; - } - - // Send the email - try { - $email->trigger( $order->get_id(), $order ); - // Logger::log( \sprintf( 'WCPOS: Successfully sent admin email %s for order #%s', $email_type, $order->get_id() ) ); - } catch ( Exception $e ) { - Logger::log( \sprintf( 'WCPOS: Failed to send admin email %s for order #%s: %s', $email_type, $order->get_id(), $e->getMessage() ) ); - } - - // Restore original enabled state - $email->enabled = $original_enabled ? 'yes' : 'no'; - } - - /** - * Force send a customer email for POS orders, bypassing WooCommerce settings. - * - * @param string $email_type Email type (customer_processing_order, etc.). - * @param WC_Order $order Order object. - */ - private function force_send_customer_email( $email_type, $order ): void { - $emails = WC()->mailer()->get_emails(); - $email = null; - - // Find the email by its ID (not class name) - foreach ( $emails as $email_instance ) { - if ( $email_instance->id === $email_type ) { - $email = $email_instance; - - break; - } - } - - if ( ! $email ) { - Logger::log( \sprintf( 'WCPOS: Customer email not found: %s', $email_type ) ); - - return; - } - $original_enabled = $email->is_enabled(); - - // Logger::log( \sprintf( - // 'WCPOS Force Customer Email: Order #%s, Email Type: %s, WC Enabled: %s, Forcing Send', - // $order->get_id(), - // $email_type, - // $original_enabled ? 'YES' : 'NO' - // ) ); - - // Temporarily enable the email if it's disabled - if ( ! $original_enabled ) { - $email->enabled = 'yes'; - } - - // Send the email - try { - $email->trigger( $order->get_id(), $order ); - // Logger::log( \sprintf( 'WCPOS: Successfully sent customer email %s for order #%s', $email_type, $order->get_id() ) ); - } catch ( Exception $e ) { - Logger::log( \sprintf( 'WCPOS: Failed to send customer email %s for order #%s: %s', $email_type, $order->get_id(), $e->getMessage() ) ); - } - - // Restore original enabled state - $email->enabled = $original_enabled ? 'yes' : 'no'; - } - - /** - * Block default admin email for POS orders when POS setting is disabled. - * - * @param string $email_type Email type (new_order, cancelled_order, etc.). - * @param WC_Order $order Order object. - */ - private function block_default_admin_email( $email_type, $order ): void { - $emails = WC()->mailer()->get_emails(); - $email = null; - - // Find the email by its ID (not class name) - foreach ( $emails as $email_instance ) { - if ( $email_instance->id === $email_type ) { - $email = $email_instance; - - break; - } - } - - if ( ! $email ) { - return; - } - $original_enabled = $email->is_enabled(); - - Logger::log( \sprintf( - 'WCPOS Block Admin Email: Order #%s, Email Type: %s, WC Enabled: %s, POS Setting: DISABLED - Blocking', - $order->get_id(), - $email_type, - $original_enabled ? 'YES' : 'NO' - ) ); - - // Temporarily disable the email to prevent default sending - $email->enabled = 'no'; - - // Re-enable after a short delay to restore original state - add_action( 'shutdown', function() use ( $email, $original_enabled ): void { - $email->enabled = $original_enabled ? 'yes' : 'no'; - } ); - } - - /** - * Block default customer email for POS orders when POS setting is disabled. - * - * @param string $email_type Email type (customer_processing_order, etc.). - * @param WC_Order $order Order object. - */ - private function block_default_customer_email( $email_type, $order ): void { - $emails = WC()->mailer()->get_emails(); - $email = null; - - // Find the email by its ID (not class name) - foreach ( $emails as $email_instance ) { - if ( $email_instance->id === $email_type ) { - $email = $email_instance; - - break; - } - } - - if ( ! $email ) { - return; - } - $original_enabled = $email->is_enabled(); - - Logger::log( \sprintf( - 'WCPOS Block Customer Email: Order #%s, Email Type: %s, WC Enabled: %s, POS Setting: DISABLED - Blocking', - $order->get_id(), - $email_type, - $original_enabled ? 'YES' : 'NO' - ) ); - - // Temporarily disable the email to prevent default sending - $email->enabled = 'no'; - - // Re-enable after a short delay to restore original state - add_action( 'shutdown', function() use ( $email, $original_enabled ): void { - $email->enabled = $original_enabled ? 'yes' : 'no'; - } ); - } - - /** - * Determine if an email is likely an admin email based on various factors. - * - * @param array $email_args Email arguments from wp_mail. - * @param string $subject Email subject line. - * - * @return bool True if this looks like an admin email. - */ - private function is_likely_admin_email( $email_args, $subject ) { - $to = $email_args['to']; - - // Check if it's going to the main admin email - $admin_email = get_option( 'admin_email' ); - if ( $to === $admin_email ) { - return true; - } - - // Check if it's going to any WooCommerce admin email addresses - $wc_admin_emails = array( - get_option( 'woocommerce_stock_email_recipient' ), - get_option( 'admin_email' ), - ); - - if ( \in_array( $to, $wc_admin_emails, true ) ) { - return true; - } - - // Check subject patterns that indicate admin emails - $admin_subject_patterns = array( - '/^\[.*\]\s+(New|Cancelled|Failed)\s+.*(order|customer)/i', - '/^\[.*\]\s+Order\s+#\d+/i', - ); - - foreach ( $admin_subject_patterns as $pattern ) { - if ( preg_match( $pattern, $subject ) ) { - return true; - } - } - - // Check if subject starts with [site_name] pattern (common for admin emails) - $site_name = get_bloginfo( 'name' ); - if ( $site_name && 0 === strpos( $subject, '[' . $site_name . ']' ) ) { - return true; - } - - return false; - } - - /** - * Check if an order was created via WooCommerce POS. - * - * @param null|WC_Order $order The order object. - * - * @return bool True if the order was created via POS, false otherwise. - */ - private function is_pos_order( $order ) { - // Handle various input types and edge cases - if ( ! $order instanceof WC_Order ) { - // Sometimes the order is passed as an ID - if ( is_numeric( $order ) ) { - $order = wc_get_order( $order ); - } - - // If we still don't have a valid order, return false - if ( ! $order instanceof WC_Order ) { - return false; - } - } - - // Check if the order was created via WooCommerce POS - return 'woocommerce-pos' === $order->get_created_via(); - } - - /** - * Setup email management hooks for POS orders. - * Uses high priority (999) to ensure these settings override other plugins. - */ - private function setup_email_management(): void { - // Admin emails - these go to store administrators - $admin_emails = array( - 'new_order', - 'cancelled_order', - 'failed_order', - ); - - // Customer emails - these go to customers - $customer_emails = array( - 'customer_on_hold_order', - 'customer_processing_order', - 'customer_completed_order', - 'customer_refunded_order', - 'customer_invoice', - 'customer_note', - 'reset_password', // This is a customer email, not admin - 'new_account', // This is a customer email, not admin - ); - - // Hook into email enabled filters with high priority - foreach ( $admin_emails as $email_id ) { - add_filter( "woocommerce_email_enabled_{$email_id}", array( $this, 'manage_admin_emails' ), 999, 3 ); - } - foreach ( $customer_emails as $email_id ) { - add_filter( "woocommerce_email_enabled_{$email_id}", array( $this, 'manage_customer_emails' ), 999, 3 ); - } - - // Additional safety net - hook into the recipient filters as well to ensure no emails go out when disabled - foreach ( $admin_emails as $email_id ) { - add_filter( "woocommerce_email_recipient_{$email_id}", array( $this, 'filter_admin_email_recipients' ), 999, 4 ); - } - foreach ( $customer_emails as $email_id ) { - add_filter( "woocommerce_email_recipient_{$email_id}", array( $this, 'filter_customer_email_recipients' ), 999, 4 ); - } - - // CRITICAL: Hook directly into order status changes to bypass WooCommerce email settings - // These hooks fire regardless of whether WooCommerce emails are enabled/disabled - - // Regular WooCommerce status changes (for completeness) - add_action( 'woocommerce_order_status_pending_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pending_to_completed', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pending_to_on-hold', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_failed_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_failed_to_completed', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_cancelled_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_on-hold_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_processing_to_cancelled', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pending_to_failed', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_on-hold_to_cancelled', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_on-hold_to_failed', array( $this, 'handle_order_status_change' ), 5, 2 ); - - // POS-specific status changes - add_action( 'woocommerce_order_status_pos-open_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pos-open_to_completed', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pos-open_to_on-hold', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pos-partial_to_processing', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pos-partial_to_completed', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pos-partial_to_on-hold', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pos-open_to_cancelled', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pos-open_to_failed', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pos-partial_to_cancelled', array( $this, 'handle_order_status_change' ), 5, 2 ); - add_action( 'woocommerce_order_status_pos-partial_to_failed', array( $this, 'handle_order_status_change' ), 5, 2 ); - - // Ultimate failsafe - use wp_mail filter to prevent sending at the last moment - add_filter( 'wp_mail', array( $this, 'prevent_disabled_pos_emails' ), 999, 1 ); - - - - // Additional hooks for admin emails - these might catch cases the status change hooks miss - add_action( 'woocommerce_new_order', array( $this, 'handle_new_order' ), 5, 2 ); - add_action( 'woocommerce_order_status_completed', array( $this, 'handle_completed_order' ), 5, 2 ); - add_action( 'woocommerce_thankyou', array( $this, 'handle_thankyou_page' ), 5, 1 ); - } - /** * Register the POS order statuses. */ diff --git a/includes/wcpos-functions.php b/includes/wcpos-functions.php index 51829de..2057a64 100644 --- a/includes/wcpos-functions.php +++ b/includes/wcpos-functions.php @@ -17,8 +17,8 @@ */ use WCPOS\WooCommercePOS\Admin\Permalink; -use WCPOS\WooCommercePOS\Services\Settings; use const WCPOS\WooCommercePOS\PLUGIN_PATH; +use WCPOS\WooCommercePOS\Services\Settings; use const WCPOS\WooCommercePOS\SHORT_NAME; use const WCPOS\WooCommercePOS\VERSION; @@ -207,7 +207,7 @@ function woocommerce_pos_faq_url( $page ): string { } } -/** +/* * Helper function checks whether order is a POS order * * @param $order WC_Order|int @@ -215,15 +215,22 @@ function woocommerce_pos_faq_url( $page ): string { */ if ( ! \function_exists( 'woocommerce_pos_is_pos_order' ) ) { function woocommerce_pos_is_pos_order( $order ): bool { - $order = is_int( $order ) ? wc_get_order( $order ) : $order; - - if ( $order instanceof WC_Order ) { - $legacy = $order->get_meta( '_pos', true ); - $created_via = $order->get_created_via(); - - return 'woocommerce-pos' === $created_via || '1' === $legacy; + // Handle various input types and edge cases + if ( ! $order instanceof WC_Order ) { + // Sometimes the order is passed as an ID + if ( is_numeric( $order ) ) { + $order = wc_get_order( $order ); + } + + // If we still don't have a valid order, return false + if ( ! $order instanceof WC_Order ) { + return false; + } } - return false; + $legacy = $order->get_meta( '_pos', true ); + $created_via = $order->get_created_via(); + + return 'woocommerce-pos' === $created_via || '1' === $legacy; } } diff --git a/package.json b/package.json index 727e27c..9b3c2bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@wcpos/woocommerce-pos", - "version": "1.7.12", + "version": "1.7.13", "description": "A simple front-end for taking WooCommerce orders at the Point of Sale.", "main": "index.js", "workspaces": { diff --git a/readme.txt b/readme.txt index fd4314a..46cbbaa 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: kilbot Tags: ecommerce, point-of-sale, pos, inventory, woocommerce Requires at least: 5.6 Tested up to: 6.8 -Stable tag: 1.7.12 +Stable tag: 1.7.13 License: GPL-3.0 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -88,6 +88,9 @@ There is more information on our website at [https://wcpos.com](https://wcpos.co == Changelog == += 1.7.13 - 2025/08/06 = +* Fix: email class to trigger New Order email after order calculations + = 1.7.12 - 2025/07/25 = * Security Fix: POS receipts should not be publically accessible, NOTE: you may need to re-sync past orders to view the receipt * Fix: Remove the X-Frame-Options Header for which prevents desktop application users from logging in diff --git a/woocommerce-pos.php b/woocommerce-pos.php index ab33002..53fe90b 100644 --- a/woocommerce-pos.php +++ b/woocommerce-pos.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce POS * Plugin URI: https://wordpress.org/plugins/woocommerce-pos/ * Description: A simple front-end for taking WooCommerce orders at the Point of Sale. Requires WooCommerce. - * Version: 1.7.12 + * Version: 1.7.13 * Author: kilbot * Author URI: http://wcpos.com * Text Domain: woocommerce-pos @@ -23,7 +23,7 @@ namespace WCPOS\WooCommercePOS; // Define plugin constants. -const VERSION = '1.7.12'; +const VERSION = '1.7.13'; const PLUGIN_NAME = 'woocommerce-pos'; const SHORT_NAME = 'wcpos'; \define( __NAMESPACE__ . '\PLUGIN_FILE', plugin_basename( __FILE__ ) ); // 'woocommerce-pos/woocommerce-pos.php' From 30c048120e233811ab9e0d427b25f968f825574c Mon Sep 17 00:00:00 2001 From: Paul Kilmurray Date: Wed, 6 Aug 2025 18:08:47 +0200 Subject: [PATCH 09/11] Update readme.txt --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 46cbbaa..ec99011 100644 --- a/readme.txt +++ b/readme.txt @@ -89,7 +89,7 @@ There is more information on our website at [https://wcpos.com](https://wcpos.co == Changelog == = 1.7.13 - 2025/08/06 = -* Fix: email class to trigger New Order email after order calculations +* Fix: New Order emails to send after order calculations = 1.7.12 - 2025/07/25 = * Security Fix: POS receipts should not be publically accessible, NOTE: you may need to re-sync past orders to view the receipt From efafbeae49de9ed65bc5eeaa183e68fcbd57733e Mon Sep 17 00:00:00 2001 From: DAnn2012 Date: Tue, 23 Sep 2025 23:44:11 +0200 Subject: [PATCH 10/11] Update pos.php --- templates/pos.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/pos.php b/templates/pos.php index 8729e2a..3023a88 100644 --- a/templates/pos.php +++ b/templates/pos.php @@ -11,7 +11,7 @@ - <?php esc_attr_e( 'Point of Sale', 'woocommerce-pos' ); ?> - <?php esc_html( bloginfo( 'name' ) ); ?> + <?php esc_attr_e( 'Point of Sale', 'woocommerce-pos' ); ?> - <?php echo esc_html( bloginfo( 'name' ) ); ?> From 2c5f647d4d919f7936bac3f97f2fafb1cc4825e2 Mon Sep 17 00:00:00 2001 From: DAnn2012 Date: Wed, 24 Sep 2025 12:35:58 +0200 Subject: [PATCH 11/11] Update pos.php --- templates/pos.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/pos.php b/templates/pos.php index 3023a88..0c15bd6 100644 --- a/templates/pos.php +++ b/templates/pos.php @@ -11,7 +11,7 @@ - <?php esc_attr_e( 'Point of Sale', 'woocommerce-pos' ); ?> - <?php echo esc_html( bloginfo( 'name' ) ); ?> + <?php esc_html_e( 'Point of Sale', 'woocommerce-pos' ); ?> - <?php echo esc_html( bloginfo( 'name' ) ); ?>