$order ) ); $placeholders = self::get_order_placeholders( $order ); // Mixed quote orders: show subtotal, shipping and total of the priced items. if ( 'quote' === $template_type ) { $placeholders['{items}'] .= self::format_quote_totals( $order ); } $message = self::replace_placeholders( $template, $placeholders ); $message = self::normalize_message( $message ); $message = self::truncate_message( $message, $order ); /** * Filter the final message before it's sent. * * @since 1.0.0 * @param string $message Final message string. * @param \WC_Order $order Order object. * @param string $template_type Template type. */ return apply_filters( 'ordercourier_final_message', $message, $order, $template_type ); } /** * Build message for a cart. * * @param \WC_Cart $cart Cart object. * @return string */ public static function for_cart( $cart ) { $templates = get_option( 'ordercourier_templates', array() ); $template = isset( $templates['order_template'] ) ? $templates['order_template'] : ''; // Check if cart contains only quote-only items. $has_priced_items = false; foreach ( $cart->get_cart() as $cart_item ) { if ( ! Order_Manager::is_quote_product( $cart_item['data'] ) ) { $has_priced_items = true; break; } } // If quote-only, remove template lines containing price placeholders. if ( ! $has_priced_items ) { $template = self::remove_price_lines( $template ); } $placeholders = self::get_cart_placeholders( $cart ); $message = self::replace_placeholders( $template, $placeholders ); $message = self::normalize_message( $message ); return $message; } /** * Build message for a product. * * @param int $product_id Product ID. * @param array $data Additional data (variation_id, quantity, etc). * @return string */ public static function for_product( $product_id, $data = array() ) { $templates = get_option( 'ordercourier_templates', array() ); $template = isset( $templates['product_template'] ) ? $templates['product_template'] : ''; $placeholders = self::get_product_placeholders( $product_id, $data ); $message = self::replace_placeholders( $template, $placeholders ); $message = self::normalize_message( $message ); return $message; } /** * Get placeholders for an order. * * @param \WC_Order $order Order object. * @return array */ private static function get_order_placeholders( $order ) { $items_text = self::format_order_items( $order ); $coupons_text = self::format_coupons( $order ); $fees_text = self::format_fees( $order ); /** * Filter to modify items text before placeholder replacement. * * @since 1.0.0 * @param string $items_text Formatted items text. * @param \WC_Order $order Order object. */ $items_text = apply_filters( 'ordercourier_format_order_items', $items_text, $order ); // Only format addresses if they have data. $shipping_addr = $order->get_address( 'shipping' ); $billing_addr = $order->get_address( 'billing' ); $address_text = self::format_address( $shipping_addr ); $billing_text = self::format_address( $billing_addr ); // Get customer name, phone, email - only show if available. $customer_name = $order->get_formatted_billing_full_name(); $customer_phone = $order->get_billing_phone(); $customer_email = $order->get_billing_email(); // Get order link (order-received for guests, view-order for logged in). $order_link = $order->get_checkout_order_received_url(); // Calculate total excluding quote items. $order_total = $order->get_total(); $quote_total = 0.0; foreach ( $order->get_items() as $item ) { $product = $item->get_product(); if ( $product && Order_Manager::is_quote_product( $product ) ) { $quote_total += (float) $item->get_total() + (float) $item->get_total_tax(); } } $placeholders = array( '{order_number}' => $order->get_order_number(), '{items}' => $items_text, '{item_meta}' => '', '{coupons}' => $coupons_text, '{discount}' => html_entity_decode( wp_strip_all_tags( wc_price( $order->get_discount_total() ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ), '{fees}' => $fees_text, '{shipping_method}' => $order->get_shipping_method(), '{shipping_total}' => html_entity_decode( wp_strip_all_tags( wc_price( $order->get_shipping_total() + $order->get_shipping_tax() ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ), '{tax}' => html_entity_decode( wp_strip_all_tags( wc_price( self::get_order_tax_excluding_quote_items( $order ) ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ), '{total}' => html_entity_decode( wp_strip_all_tags( wc_price( $order_total - $quote_total ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ), '{customer_name}' => $customer_name, '{phone}' => $customer_phone, '{email}' => $customer_email, '{billing_address}' => $billing_text, '{shipping_address}' => $address_text, '{note}' => $order->get_customer_note() ? __( 'Note:', 'ordercourier' ) . ' ' . $order->get_customer_note() : '', '{checkout_fields}' => self::format_checkout_fields( $order ), '{pay_link}' => $order->get_checkout_payment_url(), '{order_link}' => $order_link, '{site_name}' => get_bloginfo( 'name' ), '{date}' => $order->get_date_created()->date_i18n( wc_date_format() ), ); /** * Filter message placeholders for orders. * * @since 1.0.0 * @param array $placeholders Key => value pairs of placeholders. * @param \WC_Order $order Order object. */ return apply_filters( 'ordercourier_message_placeholders', $placeholders, 'order', $order ); } /** * Get order tax excluding quote-only items. * * @param \WC_Order $order Order object. * @return float Tax amount excluding quote items. */ private static function get_order_tax_excluding_quote_items( $order ) { $total_tax = $order->get_total_tax(); $quote_tax = 0.0; foreach ( $order->get_items() as $item ) { $product = $item->get_product(); if ( $product && Order_Manager::is_quote_product( $product ) ) { $quote_tax += (float) $item->get_total_tax(); } } return $total_tax - $quote_tax; } /** * Get placeholders for a cart. * * @param \WC_Cart $cart Cart object. * @return array */ private static function get_cart_placeholders( $cart ) { $items_text = self::format_cart_items( $cart ); // Check if cart contains only quote-only items. $has_priced_items = false; $has_quote_items = false; foreach ( $cart->get_cart() as $cart_item ) { if ( Order_Manager::is_quote_product( $cart_item['data'] ) ) { $has_quote_items = true; } else { $has_priced_items = true; } } // If cart has ONLY quote items, omit price totals. if ( $has_quote_items && ! $has_priced_items ) { $shipping_method = ''; $shipping_total = ''; $tax = ''; $total = ''; } else { // Calculate shipping before getting method name. if ( ! WC()->cart->is_empty() && WC()->cart->needs_shipping() ) { WC()->cart->calculate_shipping(); } // Get shipping method name from chosen rate. $shipping_method = ''; $chosen_methods = WC()->session ? WC()->session->get( 'chosen_shipping_methods' ) : array(); if ( ! empty( $chosen_methods ) ) { $packages = WC()->shipping()->get_packages(); if ( ! empty( $packages ) && ! empty( $packages[0]['rates'] ) ) { foreach ( $packages[0]['rates'] as $rate ) { if ( $rate->id === $chosen_methods[0] ) { $shipping_method = $rate->get_label(); break; } } } } // For mixed carts, subtract quote items' tax from total tax. $quote_tax = 0.0; if ( $has_quote_items ) { foreach ( $cart->get_cart() as $cart_item ) { if ( Order_Manager::is_quote_product( $cart_item['data'] ) ) { $quote_tax += (float) $cart_item['line_tax']; } } } // For mixed carts, subtract quote items from total. $quote_total = 0.0; if ( $has_quote_items ) { foreach ( $cart->get_cart() as $cart_item ) { if ( Order_Manager::is_quote_product( $cart_item['data'] ) ) { $quote_total += (float) $cart_item['line_total'] + (float) $cart_item['line_tax']; } } } $shipping_total = html_entity_decode( wp_strip_all_tags( wc_price( $cart->get_shipping_total() + $cart->get_shipping_tax() ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $tax = html_entity_decode( wp_strip_all_tags( wc_price( $cart->get_total_tax() - $quote_tax ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $total = html_entity_decode( wp_strip_all_tags( wc_price( $cart->get_total( 'raw' ) - $quote_total ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); } return array( '{order_number}' => '', // No order number for cart (direct mode). '{items}' => $items_text, '{item_meta}' => '', '{coupons}' => self::format_cart_coupons( $cart ), '{discount}' => html_entity_decode( wp_strip_all_tags( wc_price( $cart->get_discount_total() ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ), '{fees}' => self::format_cart_fees( $cart ), '{shipping_method}' => $shipping_method, '{shipping_total}' => $shipping_total, '{tax}' => $tax, '{total}' => $total, '{customer_name}' => '', '{phone}' => '', '{email}' => '', '{billing_address}' => '', '{shipping_address}' => '', '{note}' => '', '{checkout_fields}' => '', '{pay_link}' => '', '{order_link}' => '', // No order link for cart (direct mode). '{site_name}' => get_bloginfo( 'name' ), '{date}' => current_time( wc_date_format() ), ); } /** * Get placeholders for a product. * * @param int $product_id Product ID. * @param array $data Additional data. * @return array */ private static function get_product_placeholders( $product_id, $data ) { $product = wc_get_product( $product_id ); $variation_id = isset( $data['variation_id'] ) ? absint( $data['variation_id'] ) : 0; $quantity = isset( $data['quantity'] ) ? absint( $data['quantity'] ) : 1; if ( $variation_id ) { $product = wc_get_product( $variation_id ); } $item_text = $quantity . ' x ' . $product->get_name(); if ( $product->is_type( 'variation' ) ) { $attributes = $product->get_variation_attributes(); if ( ! empty( $attributes ) ) { $attr_text = array(); foreach ( $attributes as $attr_name => $attr_value ) { $attr_text[] = wc_attribute_label( str_replace( 'attribute_', '', $attr_name ) ) . ': ' . $attr_value; } $item_text .= ' (' . implode( ', ', $attr_text ) . ')'; } } $price = $product->get_price(); if ( Order_Manager::is_quote_product( $product ) ) { $item_text .= ' - ' . __( 'Price on request', 'ordercourier' ); } elseif ( '' !== $price ) { $price_html = wc_price( $price * $quantity ); $price_text = html_entity_decode( wp_strip_all_tags( $price_html ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $item_text .= ' - ' . $price_text; } return array( '{items}' => $item_text, '{site_name}' => get_bloginfo( 'name' ), '{customer_name}' => '', '{phone}' => '', '{email}' => '', '{order_number}' => '', '{shipping_method}' => '', '{shipping_total}' => '', '{tax}' => '', '{total}' => '', '{order_link}' => '', '{pay_link}' => '', '{date}' => current_time( wc_date_format() ), ); } /** * Format order items. * * @param \WC_Order $order Order object. * @return string */ private static function format_order_items( $order ) { $items = array(); $is_quote = Order_Manager::is_quote_order( $order ); foreach ( $order->get_items() as $item ) { $product = $item->get_product(); $item_name = $item->get_name(); $item_text = $item->get_quantity() . ' x ' . $item_name; // Add variation attributes only if not already in item name. if ( $product && $product->is_type( 'variation' ) ) { $formatted_variation = wc_get_formatted_variation( $product, true, false ); if ( $formatted_variation ) { $variation_plain = wp_strip_all_tags( $formatted_variation ); // Only add if the item name doesn't already contain the variation details. if ( false === stripos( $item_name, $variation_plain ) ) { $item_text .= "\n " . $variation_plain; } } } // Add item meta (product add-ons, etc). $meta_data = $item->get_formatted_meta_data( '' ); if ( ! empty( $meta_data ) ) { foreach ( $meta_data as $meta ) { // Skip variation attributes already shown. if ( $product && $product->is_type( 'variation' ) && 0 === strpos( $meta->key, 'pa_' ) ) { continue; } $item_text .= "\n " . wp_strip_all_tags( $meta->display_key ) . ': ' . wp_strip_all_tags( $meta->display_value ); } } // Add price or "Price on request". $item_total = $item->get_total(); $is_quote_item = false; $is_no_price = 0.0 === (float) $item_total; // Check if item is quote-only (check parent for variations). if ( $product ) { $product_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(); $quote_only = get_post_meta( $product_id, '_oc_quote_only', true ); $is_quote_item = 'yes' === $quote_only || '' === $product->get_price(); } if ( $is_quote_item || ( $is_quote && $is_no_price ) ) { $item_text .= ' - ' . __( 'Price on request', 'ordercourier' ); } else { $price_html = wc_price( $item_total ); $price_text = html_entity_decode( wp_strip_all_tags( $price_html ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $item_text .= ' - ' . $price_text; } $items[] = $item_text; } return implode( "\n", $items ); } /** * Format cart items. * * @param \WC_Cart $cart Cart object. * @return string */ private static function format_cart_items( $cart ) { $items = array(); foreach ( $cart->get_cart() as $cart_item ) { $product = $cart_item['data']; $item_name = $product->get_name(); $item_text = $cart_item['quantity'] . ' x ' . $item_name; // Add variation attributes only if not already in item name. if ( $product->is_type( 'variation' ) ) { $formatted_variation = wc_get_formatted_variation( $product, true, false ); if ( $formatted_variation ) { $variation_plain = wp_strip_all_tags( $formatted_variation ); // Only add if the item name doesn't already contain the variation details. if ( false === stripos( $item_name, $variation_plain ) ) { $item_text .= "\n " . $variation_plain; } } } // Add cart item data (product add-ons, etc). if ( ! empty( $cart_item['addons'] ) ) { foreach ( $cart_item['addons'] as $addon ) { $item_text .= "\n " . $addon['name'] . ': ' . $addon['value']; } } // Add price or "Price on request" for quote-only items. $product_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(); $quote_only = get_post_meta( $product_id, '_oc_quote_only', true ); $is_no_price = '' === $product->get_price(); if ( 'yes' === $quote_only || $is_no_price ) { $item_text .= ' - ' . __( 'Price on request', 'ordercourier' ); } else { $price_html = wc_price( $cart_item['line_total'] ); $price_text = html_entity_decode( wp_strip_all_tags( $price_html ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $item_text .= ' - ' . $price_text; } $items[] = $item_text; } return implode( "\n", $items ); } /** * Format subtotal, shipping and total for the priced items of a quote order. * Returns '' when every item is quote-only. * * @param \WC_Order $order Order object. * @return string */ private static function format_quote_totals( $order ) { $subtotal = 0.0; $quote_total = 0.0; $quote_tax = 0.0; $has_priced = false; foreach ( $order->get_items() as $item ) { if ( Order_Manager::is_quote_product( $item->get_product() ) ) { $quote_total += (float) $item->get_total() + (float) $item->get_total_tax(); $quote_tax += (float) $item->get_total_tax(); continue; } $has_priced = true; $subtotal += (float) $item->get_total(); } if ( ! $has_priced ) { return ''; } $plain = function ( $amount ) { return html_entity_decode( wp_strip_all_tags( wc_price( $amount ) ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); }; $text = "\n\n*" . __( 'Subtotal:', 'ordercourier' ) . '* ' . $plain( $subtotal ); if ( $order->get_items( 'shipping' ) ) { $method = $order->get_shipping_method(); $text .= "\n*" . __( 'Shipping:', 'ordercourier' ) . '* ' . ( $method ? $method . ' - ' : '' ) . $plain( (float) $order->get_shipping_total() + (float) $order->get_shipping_tax() ); } $tax = (float) $order->get_total_tax() - $quote_tax; if ( $tax > 0 ) { $text .= "\n*" . __( 'Tax:', 'ordercourier' ) . '* ' . $plain( $tax ); } $text .= "\n*" . __( 'Total:', 'ordercourier' ) . '* ' . $plain( (float) $order->get_total() - $quote_total ); return $text; } /** * Format coupons. * * @param \WC_Order $order Order object. * @return string */ private static function format_coupons( $order ) { $coupons = $order->get_items( 'coupon' ); if ( empty( $coupons ) ) { return ''; } $coupon_texts = array(); foreach ( $coupons as $coupon_item ) { $coupon_code = $coupon_item->get_code(); $discount = $coupon_item->get_discount(); $price_html = wc_price( $discount ); $price_text = html_entity_decode( wp_strip_all_tags( $price_html ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $coupon_texts[] = sprintf( '%s (-%s)', $coupon_code, $price_text ); } $text = '*' . __( 'Coupons:', 'ordercourier' ) . '* ' . implode( ', ', $coupon_texts ) . "\n"; return $text; } /** * Format cart coupons. * * @param \WC_Cart $cart Cart object. * @return string */ private static function format_cart_coupons( $cart ) { $coupons = $cart->get_applied_coupons(); if ( empty( $coupons ) ) { return ''; } $text = '*' . __( 'Coupons:', 'ordercourier' ) . '* ' . implode( ', ', $coupons ) . "\n"; return $text; } /** * Format fees. * * @param \WC_Order $order Order object. * @return string */ private static function format_fees( $order ) { $fees = $order->get_fees(); if ( empty( $fees ) ) { return ''; } $text = ''; foreach ( $fees as $fee ) { $price_html = wc_price( $fee->get_total() ); $price_text = html_entity_decode( wp_strip_all_tags( $price_html ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $text .= '*' . $fee->get_name() . ':* ' . $price_text . "\n"; } return $text; } /** * Format cart fees. * * @param \WC_Cart $cart Cart object. * @return string */ private static function format_cart_fees( $cart ) { $fees = $cart->get_fees(); if ( empty( $fees ) ) { return ''; } $text = ''; foreach ( $fees as $fee ) { $price_html = wc_price( $fee->total ); $price_text = html_entity_decode( wp_strip_all_tags( $price_html ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $text .= '*' . $fee->name . ':* ' . $price_text . "\n"; } return $text; } /** * Format address. * * @param array $address Address data. * @return string */ private static function format_address( $address ) { $parts = array_filter( array( isset( $address['address_1'] ) ? $address['address_1'] : '', isset( $address['address_2'] ) ? $address['address_2'] : '', isset( $address['city'] ) ? $address['city'] : '', isset( $address['state'] ) ? $address['state'] : '', isset( $address['postcode'] ) ? $address['postcode'] : '', isset( $address['country'] ) ? WC()->countries->countries[ $address['country'] ] ?? $address['country'] : '', ) ); return implode( ', ', $parts ); } /** * Remove template lines containing price placeholders for quote-only carts. * * @param string $template Template string. * @return string */ private static function remove_price_lines( $template ) { $price_placeholders = array( '{shipping_method}', '{shipping_total}', '{tax}', '{total}' ); $lines = explode( "\n", $template ); $filtered_lines = array(); foreach ( $lines as $line ) { $has_price_placeholder = false; foreach ( $price_placeholders as $placeholder ) { if ( false !== strpos( $line, $placeholder ) ) { $has_price_placeholder = true; break; } } if ( ! $has_price_placeholder ) { $filtered_lines[] = $line; } } return implode( "\n", $filtered_lines ); } /** * Replace placeholders in template. * * @param string $template Template string. * @param array $placeholders Placeholders array. * @return string */ private static function replace_placeholders( $template, $placeholders ) { // First, remove lines where placeholder is empty (like "View order: {order_link}" when order_link is empty). foreach ( $placeholders as $key => $value ) { if ( '' === $value ) { // Remove lines that contain ONLY this placeholder with optional label prefix. // Pattern: "Label: {placeholder}" or "Label {placeholder}" or just "{placeholder}". $pattern = '/^[^{\n]*' . preg_quote( $key, '/' ) . '\s*$/m'; $template = preg_replace( $pattern, '', $template ); } } return str_replace( array_keys( $placeholders ), array_values( $placeholders ), $template ); } /** * Normalize message. * * @param string $message Message. * @return string */ private static function normalize_message( $message ) { // Strip HTML tags. $message = wp_strip_all_tags( $message ); // Normalize line breaks. $message = str_replace( array( "\r\n", "\r" ), "\n", $message ); // Convert whitespace-only lines to empty lines. $message = preg_replace( '/^[ \t]+$/m', '', $message ); // Remove "*New Order #*" when there's no order number (direct mode). $message = str_replace( '*New Order #*', '', $message ); // Fix shipping line with empty method name: "*Shipping:* - Rs" → "*Shipping:* Rs". $message = preg_replace( '/\*Shipping:\*\s+-\s+/', '*Shipping:* ', $message ); // Remove empty section headings: a line that is only "*Heading:*" and is // followed by a blank line, by another "*Heading:*" line, or by the end of the message. // Other lines, such as "Hi! I'd like to order:" or "Note:", are never touched. $message = preg_replace( '/^\*[^*\n]+:\*[ \t]*(?:\n\n+|\n+(?=\*[^*\n]+:\*))/m', '', $message ); $message = preg_replace( '/(^|\n)\*[^*\n]+:\*\s*\z/', '', $message ); // Remove excessive blank lines. $message = preg_replace( "/\n{3,}/", "\n\n", $message ); // Trim. $message = trim( $message ); return $message; } /** * Truncate message if too long. * * @param string $message Message. * @param \WC_Order $order Order object. * @return string */ private static function truncate_message( $message, $order ) { $encoded_length = strlen( rawurlencode( $message ) ); if ( $encoded_length <= self::MAX_LENGTH ) { return $message; } // Add truncation notice. $truncate_text = "\n\n" . sprintf( /* translators: %s: Order view URL */ __( '...and more. See full details: %s', 'ordercourier' ), $order->get_view_order_url() ); $available_length = self::MAX_LENGTH - strlen( rawurlencode( $truncate_text ) ); // Binary search for the right length. $left = 0; $right = strlen( $message ); while ( $left < $right ) { $mid = intval( ( $left + $right + 1 ) / 2 ); $test = substr( $message, 0, $mid ); $encoded = strlen( rawurlencode( $test ) ); if ( $encoded <= $available_length ) { $left = $mid; } else { $right = $mid - 1; } } $message = substr( $message, 0, $left ) . $truncate_text; return $message; } /** * Build WhatsApp URL. * * @param string $phone Phone number. * @param string $message Message. * @return string */ public static function build_whatsapp_url( $phone, $message ) { $phone = Phone_Validator::normalize( $phone ); $phone = str_replace( '+', '', $phone ); return 'https://wa.me/' . $phone . '?text=' . rawurlencode( $message ); } /** * Format custom checkout fields for an order. * * @param \WC_Order $order Order object. * @return string */ private static function format_checkout_fields( $order ) { $settings = get_option( 'ordercourier_settings', array() ); $fields_config = isset( $settings['custom_checkout_fields'] ) ? $settings['custom_checkout_fields'] : ''; if ( empty( $fields_config ) ) { return ''; } $lines = explode( "\n", $fields_config ); $fields_output = array(); foreach ( $lines as $line ) { $line = trim( $line ); if ( empty( $line ) ) { continue; } $parts = explode( '|', $line ); if ( count( $parts ) !== 2 ) { continue; } $meta_key = trim( $parts[0] ); $label = trim( $parts[1] ); if ( empty( $meta_key ) || empty( $label ) ) { continue; } $value = null; // Check if this is a WooCommerce Blocks additional field (format: namespace/field-id). if ( strpos( $meta_key, '/' ) !== false && class_exists( '\Automattic\WooCommerce\Blocks\Package' ) ) { try { $checkout_fields_service = \Automattic\WooCommerce\Blocks\Package::container()->get( \Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields::class ); $value = $checkout_fields_service->get_field_from_object( $meta_key, $order, 'other' ); } catch ( \Exception $e ) { continue; } } // Fallback to standard order meta if not found. if ( null === $value || '' === $value ) { $value = $order->get_meta( $meta_key, true ); } if ( ! empty( $value ) ) { // Handle array values (checkboxes, multi-select). if ( is_array( $value ) ) { $value = implode( ', ', array_filter( array_map( 'wc_clean', $value ), 'is_scalar' ) ); } elseif ( ! is_scalar( $value ) ) { continue; } $fields_output[] = $label . ': ' . $value; } } return ! empty( $fields_output ) ? implode( "\n", $fields_output ) : ''; } }