feat: Custom email system foundation

##  Step 1-3: Email System Core

### EmailManager.php
-  Disables WooCommerce emails (prevents duplicates)
-  Hooks into all WC order status changes
-  Hooks into customer, product events
-  Checks if events are enabled before sending
-  Sends via wp_mail() (SMTP plugin compatible)

### EmailRenderer.php
-  Renders emails with design templates
-  Variable replacement system
-  Gets recipient email (staff/customer)
-  Loads order/product/customer variables
-  Filter hook: `woonoow_email_template`
-  Supports HTML template designs

### Email Design Templates (3)
**templates/emails/modern.html**
-  Clean, minimalist, Apple-inspired
-  Dark mode support
-  Mobile responsive
-  2024 design trends

**templates/emails/classic.html**
-  Professional, traditional
-  Gradient header
-  Table styling
-  Business-appropriate

**templates/emails/minimal.html**
-  Ultra-clean, monospace font
-  Black & white aesthetic
-  Text-focused
-  Dark mode invert

### Architecture
```
Design Template (HTML) → Content Template (Text) → Final Email
   modern.html        →  order_processing      →  Beautiful HTML
```

---

**Next:** Rich text editor + Content templates 🎨
This commit is contained in:
dwindown
2025-11-12 18:48:55 +07:00
parent c8adb9e924
commit 30384464a1
5 changed files with 1449 additions and 0 deletions

View File

@@ -0,0 +1,382 @@
<?php
/**
* Email Manager
*
* Manages custom email notifications and disables WooCommerce default emails
*
* @package WooNooW\Core\Notifications
*/
namespace WooNooW\Core\Notifications;
class EmailManager {
/**
* Instance
*/
private static $instance = null;
/**
* Get instance
*/
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize hooks
*/
private function init_hooks() {
// Disable WooCommerce emails to prevent duplicates
add_action('woocommerce_email', [$this, 'disable_wc_emails'], 1);
// Hook into WooCommerce order status changes
add_action('woocommerce_order_status_pending_to_processing', [$this, 'send_order_processing_email'], 10, 2);
add_action('woocommerce_order_status_pending_to_completed', [$this, 'send_order_completed_email'], 10, 2);
add_action('woocommerce_order_status_processing_to_completed', [$this, 'send_order_completed_email'], 10, 2);
add_action('woocommerce_order_status_completed', [$this, 'send_order_completed_email'], 10, 2);
add_action('woocommerce_order_status_pending_to_on-hold', [$this, 'send_order_on_hold_email'], 10, 2);
add_action('woocommerce_order_status_failed_to_processing', [$this, 'send_order_processing_email'], 10, 2);
add_action('woocommerce_order_status_cancelled', [$this, 'send_order_cancelled_email'], 10, 2);
add_action('woocommerce_order_status_refunded', [$this, 'send_order_refunded_email'], 10, 2);
add_action('woocommerce_order_fully_refunded', [$this, 'send_order_refunded_email'], 10, 2);
// New order notification for admin
add_action('woocommerce_new_order', [$this, 'send_new_order_admin_email'], 10, 1);
// Customer note
add_action('woocommerce_new_customer_note', [$this, 'send_customer_note_email'], 10, 1);
// New customer account
add_action('woocommerce_created_customer', [$this, 'send_new_customer_email'], 10, 3);
// Low stock / Out of stock
add_action('woocommerce_low_stock', [$this, 'send_low_stock_email'], 10, 1);
add_action('woocommerce_no_stock', [$this, 'send_out_of_stock_email'], 10, 1);
add_action('woocommerce_product_set_stock', [$this, 'check_stock_levels'], 10, 1);
}
/**
* Disable WooCommerce default emails
*
* @param WC_Emails $email_class
*/
public function disable_wc_emails($email_class) {
// Get WooNooW notification settings
$settings = get_option('woonoow_notification_settings', []);
// Check if custom emails are enabled (default: yes)
$use_custom_emails = $settings['use_custom_emails'] ?? true;
if (!$use_custom_emails) {
return; // Keep WC emails if custom emails disabled
}
// Disable all WooCommerce transactional emails
$emails_to_disable = [
'WC_Email_New_Order', // Admin: New order
'WC_Email_Cancelled_Order', // Admin: Cancelled order
'WC_Email_Failed_Order', // Admin: Failed order
'WC_Email_Customer_On_Hold_Order', // Customer: Order on-hold
'WC_Email_Customer_Processing_Order', // Customer: Processing order
'WC_Email_Customer_Completed_Order', // Customer: Completed order
'WC_Email_Customer_Refunded_Order', // Customer: Refunded order
'WC_Email_Customer_Invoice', // Customer: Invoice
'WC_Email_Customer_Note', // Customer: Note added
'WC_Email_Customer_Reset_Password', // Customer: Reset password
'WC_Email_Customer_New_Account', // Customer: New account
];
foreach ($emails_to_disable as $email_id) {
add_filter('woocommerce_email_enabled_' . strtolower(str_replace('WC_Email_', '', $email_id)), '__return_false');
}
}
/**
* Send order processing email
*
* @param int $order_id
* @param WC_Order $order
*/
public function send_order_processing_email($order_id, $order = null) {
if (!$order) {
$order = wc_get_order($order_id);
}
if (!$order) {
return;
}
// Check if event is enabled
if (!$this->is_event_enabled('order_processing', 'email', 'customer')) {
return;
}
// Send email
$this->send_email('order_processing', 'customer', $order);
}
/**
* Send order completed email
*
* @param int $order_id
* @param WC_Order $order
*/
public function send_order_completed_email($order_id, $order = null) {
if (!$order) {
$order = wc_get_order($order_id);
}
if (!$order) {
return;
}
// Check if event is enabled
if (!$this->is_event_enabled('order_completed', 'email', 'customer')) {
return;
}
// Send email
$this->send_email('order_completed', 'customer', $order);
}
/**
* Send order on-hold email
*
* @param int $order_id
* @param WC_Order $order
*/
public function send_order_on_hold_email($order_id, $order = null) {
if (!$order) {
$order = wc_get_order($order_id);
}
if (!$order) {
return;
}
// Check if event is enabled
if (!$this->is_event_enabled('order_processing', 'email', 'customer')) {
return;
}
// Send email (use processing template for on-hold)
$this->send_email('order_processing', 'customer', $order);
}
/**
* Send order cancelled email
*
* @param int $order_id
* @param WC_Order $order
*/
public function send_order_cancelled_email($order_id, $order = null) {
if (!$order) {
$order = wc_get_order($order_id);
}
if (!$order) {
return;
}
// Send to admin
if ($this->is_event_enabled('order_cancelled', 'email', 'staff')) {
$this->send_email('order_cancelled', 'staff', $order);
}
}
/**
* Send order refunded email
*
* @param int $order_id
* @param WC_Order $order
*/
public function send_order_refunded_email($order_id, $order = null) {
if (!$order) {
$order = wc_get_order($order_id);
}
if (!$order) {
return;
}
// Check if event is enabled
if (!$this->is_event_enabled('order_refunded', 'email', 'customer')) {
return;
}
// Send email
$this->send_email('order_refunded', 'customer', $order);
}
/**
* Send new order admin email
*
* @param int $order_id
*/
public function send_new_order_admin_email($order_id) {
$order = wc_get_order($order_id);
if (!$order) {
return;
}
// Check if event is enabled
if (!$this->is_event_enabled('order_placed', 'email', 'staff')) {
return;
}
// Send email
$this->send_email('order_placed', 'staff', $order);
}
/**
* Send customer note email
*
* @param array $args
*/
public function send_customer_note_email($args) {
$order = wc_get_order($args['order_id']);
if (!$order) {
return;
}
// Check if event is enabled
if (!$this->is_event_enabled('customer_note', 'email', 'customer')) {
return;
}
// Send email with note data
$this->send_email('customer_note', 'customer', $order, ['note' => $args['customer_note']]);
}
/**
* Send new customer email
*
* @param int $customer_id
* @param array $new_customer_data
* @param bool $password_generated
*/
public function send_new_customer_email($customer_id, $new_customer_data = [], $password_generated = false) {
// Check if event is enabled
if (!$this->is_event_enabled('new_customer', 'email', 'customer')) {
return;
}
$customer = new \WC_Customer($customer_id);
// Send email
$this->send_email('new_customer', 'customer', $customer, [
'password_generated' => $password_generated,
'user_login' => $new_customer_data['user_login'] ?? '',
'user_pass' => $new_customer_data['user_pass'] ?? '',
]);
}
/**
* Send low stock email
*
* @param WC_Product $product
*/
public function send_low_stock_email($product) {
// Check if event is enabled
if (!$this->is_event_enabled('low_stock', 'email', 'staff')) {
return;
}
// Send email
$this->send_email('low_stock', 'staff', $product);
}
/**
* Send out of stock email
*
* @param WC_Product $product
*/
public function send_out_of_stock_email($product) {
// Check if event is enabled
if (!$this->is_event_enabled('out_of_stock', 'email', 'staff')) {
return;
}
// Send email
$this->send_email('out_of_stock', 'staff', $product);
}
/**
* Check stock levels when product stock is updated
*
* @param WC_Product $product
*/
public function check_stock_levels($product) {
$stock = $product->get_stock_quantity();
$low_stock_threshold = get_option('woocommerce_notify_low_stock_amount', 2);
if ($stock <= 0) {
$this->send_out_of_stock_email($product);
} elseif ($stock <= $low_stock_threshold) {
$this->send_low_stock_email($product);
}
}
/**
* Check if event is enabled
*
* @param string $event_id
* @param string $channel_id
* @param string $recipient_type
* @return bool
*/
private function is_event_enabled($event_id, $channel_id, $recipient_type) {
$settings = get_option('woonoow_notification_settings', []);
// Check if event exists and channel is enabled
if (isset($settings['events'][$event_id]['channels'][$channel_id])) {
return $settings['events'][$event_id]['channels'][$channel_id]['enabled'] ?? false;
}
return false;
}
/**
* Send email
*
* @param string $event_id
* @param string $recipient_type
* @param mixed $data
* @param array $extra_data
*/
private function send_email($event_id, $recipient_type, $data, $extra_data = []) {
// Get email renderer
$renderer = EmailRenderer::instance();
// Render email
$email = $renderer->render($event_id, $recipient_type, $data, $extra_data);
if (!$email) {
return;
}
// Send email via wp_mail
$headers = [
'Content-Type: text/html; charset=UTF-8',
'From: ' . get_bloginfo('name') . ' <' . get_option('admin_email') . '>',
];
wp_mail($email['to'], $email['subject'], $email['body'], $headers);
// Log email sent
do_action('woonoow_email_sent', $event_id, $recipient_type, $email);
}
}

View File

@@ -0,0 +1,274 @@
<?php
/**
* Email Renderer
*
* Renders email templates with content
*
* @package WooNooW\Core\Notifications
*/
namespace WooNooW\Core\Notifications;
class EmailRenderer {
/**
* Instance
*/
private static $instance = null;
/**
* Get instance
*/
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Render email
*
* @param string $event_id Event ID (order_placed, order_processing, etc.)
* @param string $recipient_type Recipient type (staff, customer)
* @param mixed $data Order, Product, or Customer object
* @param array $extra_data Additional data
* @return array|null ['to', 'subject', 'body']
*/
public function render($event_id, $recipient_type, $data, $extra_data = []) {
// Get template settings
$template_settings = $this->get_template_settings($event_id, $recipient_type);
if (!$template_settings) {
return null;
}
// Get recipient email
$to = $this->get_recipient_email($recipient_type, $data);
if (!$to) {
return null;
}
// Get variables
$variables = $this->get_variables($event_id, $data, $extra_data);
// Replace variables in subject and content
$subject = $this->replace_variables($template_settings['subject'], $variables);
$content = $this->replace_variables($template_settings['body'], $variables);
// Get HTML template design
$design_template = $this->get_design_template($template_settings['design'] ?? 'modern');
// Render final HTML
$html = $this->render_html($design_template, $content, $subject, $variables);
return [
'to' => $to,
'subject' => $subject,
'body' => $html,
];
}
/**
* Get template settings
*
* @param string $event_id
* @param string $recipient_type
* @return array|null
*/
private function get_template_settings($event_id, $recipient_type) {
// Get saved template
$template = TemplateProvider::get_template($event_id, 'email');
if (!$template) {
return null;
}
// Get design template preference
$settings = get_option('woonoow_notification_settings', []);
$design = $settings['email_design_template'] ?? 'modern';
return [
'subject' => $template['subject'] ?? '',
'body' => $template['body'] ?? '',
'design' => $design,
];
}
/**
* Get recipient email
*
* @param string $recipient_type
* @param mixed $data
* @return string|null
*/
private function get_recipient_email($recipient_type, $data) {
if ($recipient_type === 'staff') {
return get_option('admin_email');
}
// Customer
if ($data instanceof \WC_Order) {
return $data->get_billing_email();
}
if ($data instanceof \WC_Customer) {
return $data->get_email();
}
return null;
}
/**
* Get variables for template
*
* @param string $event_id
* @param mixed $data
* @param array $extra_data
* @return array
*/
private function get_variables($event_id, $data, $extra_data = []) {
$variables = [
'store_name' => get_bloginfo('name'),
'store_url' => home_url(),
'site_title' => get_bloginfo('name'),
];
// Order variables
if ($data instanceof \WC_Order) {
$variables = array_merge($variables, [
'order_number' => $data->get_order_number(),
'order_id' => $data->get_id(),
'order_date' => $data->get_date_created()->date('F j, Y'),
'order_total' => $data->get_formatted_order_total(),
'order_subtotal' => wc_price($data->get_subtotal()),
'order_tax' => wc_price($data->get_total_tax()),
'order_shipping' => wc_price($data->get_shipping_total()),
'order_discount' => wc_price($data->get_discount_total()),
'order_status' => wc_get_order_status_name($data->get_status()),
'order_url' => $data->get_view_order_url(),
'payment_method' => $data->get_payment_method_title(),
'shipping_method' => $data->get_shipping_method(),
'customer_name' => $data->get_formatted_billing_full_name(),
'customer_first_name' => $data->get_billing_first_name(),
'customer_last_name' => $data->get_billing_last_name(),
'customer_email' => $data->get_billing_email(),
'customer_phone' => $data->get_billing_phone(),
'billing_address' => $data->get_formatted_billing_address(),
'shipping_address' => $data->get_formatted_shipping_address(),
]);
// Order items
$items_html = '';
foreach ($data->get_items() as $item) {
$product = $item->get_product();
$items_html .= sprintf(
'<tr><td>%s × %d</td><td>%s</td></tr>',
$item->get_name(),
$item->get_quantity(),
wc_price($item->get_total())
);
}
$variables['order_items'] = $items_html;
}
// Product variables
if ($data instanceof \WC_Product) {
$variables = array_merge($variables, [
'product_id' => $data->get_id(),
'product_name' => $data->get_name(),
'product_sku' => $data->get_sku(),
'product_price' => wc_price($data->get_price()),
'product_url' => get_permalink($data->get_id()),
'stock_quantity' => $data->get_stock_quantity(),
'stock_status' => $data->get_stock_status(),
]);
}
// Customer variables
if ($data instanceof \WC_Customer) {
$variables = array_merge($variables, [
'customer_id' => $data->get_id(),
'customer_name' => $data->get_display_name(),
'customer_first_name' => $data->get_first_name(),
'customer_last_name' => $data->get_last_name(),
'customer_email' => $data->get_email(),
'customer_username' => $data->get_username(),
]);
}
// Merge extra data
$variables = array_merge($variables, $extra_data);
return apply_filters('woonoow_email_variables', $variables, $event_id, $data);
}
/**
* Replace variables in text
*
* @param string $text
* @param array $variables
* @return string
*/
private function replace_variables($text, $variables) {
foreach ($variables as $key => $value) {
$text = str_replace('{' . $key . '}', $value, $text);
}
return $text;
}
/**
* Get design template path
*
* @param string $design Template name (modern, classic, minimal)
* @return string
*/
private function get_design_template($design) {
$template_path = WOONOOW_PATH . 'templates/emails/' . $design . '.html';
// Allow filtering template path
$template_path = apply_filters('woonoow_email_template', $template_path, $design);
// Fallback to modern if template doesn't exist
if (!file_exists($template_path)) {
$template_path = WOONOOW_PATH . 'templates/emails/modern.html';
}
return $template_path;
}
/**
* Render HTML email
*
* @param string $template_path Path to HTML template
* @param string $content Email content (HTML)
* @param string $subject Email subject
* @param array $variables All variables
* @return string
*/
private function render_html($template_path, $content, $subject, $variables) {
if (!file_exists($template_path)) {
// Fallback to plain HTML
return $content;
}
// Load template
$html = file_get_contents($template_path);
// Replace placeholders
$html = str_replace('{{email_heading}}', $subject, $html);
$html = str_replace('{{email_content}}', $content, $html);
$html = str_replace('{{store_name}}', $variables['store_name'], $html);
$html = str_replace('{{store_url}}', $variables['store_url'], $html);
$html = str_replace('{{current_year}}', date('Y'), $html);
// Replace all other variables
foreach ($variables as $key => $value) {
$html = str_replace('{{' . $key . '}}', $value, $html);
}
return $html;
}
}