Files
nolimit-discounts/includes/class-frontend.php

414 lines
9.7 KiB
PHP

<?php
/**
* Frontend / checkout integration.
*
* @package WC_Email_Discount_Manager
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Applies discounts, auto-applies coupons and filters shipping methods on checkout.
*/
class WC_EDM_Frontend {
/**
* Session key holding the tracked guest email.
*/
const SESSION_EMAIL_KEY = 'wc_edm_email';
/**
* Guard so the coupon is only auto-applied once per request.
*
* @var bool
*/
private $coupon_applied = false;
/**
* Hook everything up.
*/
public function __construct() {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'wp_ajax_wc_edm_track_email', array( $this, 'ajax_track_email' ) );
add_action( 'wp_ajax_nopriv_wc_edm_track_email', array( $this, 'ajax_track_email' ) );
add_action( 'woocommerce_cart_calculate_fees', array( $this, 'apply_discount_fees' ), 20 );
add_action( 'woocommerce_before_calculate_totals', array( $this, 'maybe_apply_coupon' ), 20 );
add_filter( 'woocommerce_package_rates', array( $this, 'filter_shipping_rates' ), 20, 2 );
}
/**
* Enqueue the checkout email tracking script.
*/
public function enqueue_scripts() {
if ( ! function_exists( 'is_checkout' ) || ( ! is_checkout() && ! is_cart() ) ) {
return;
}
$params = array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'wc-edm-track-email' ),
);
$script = '
jQuery( function( $ ) {
var wcEdm = ' . wp_json_encode( $params ) . ';
var timer = null;
var lastTracked = "";
function trackEmail( email ) {
if ( ! email || email.indexOf( "@" ) < 1 || email === lastTracked ) {
return;
}
lastTracked = email;
$.post( wcEdm.ajax_url, {
action: "wc_edm_track_email",
nonce: wcEdm.nonce,
email: email
}, function() {
$( document.body ).trigger( "update_checkout" );
} );
}
$( document.body ).on( "keyup change blur", "#billing_email", function() {
var email = $( this ).val();
clearTimeout( timer );
timer = setTimeout( function() {
trackEmail( email );
}, 600 );
} );
// Track a prefilled email on page load.
var initial = $( "#billing_email" ).val();
if ( initial ) {
trackEmail( initial );
}
} );';
wp_add_inline_script( 'jquery', $script );
}
/**
* AJAX: store the checkout email in the WooCommerce session so
* discounts apply when totals are recalculated.
*/
public function ajax_track_email() {
check_ajax_referer( 'wc-edm-track-email', 'nonce' );
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
if ( ! WC()->session ) {
wp_send_json_error( array( 'message' => 'No session.' ) );
}
if ( $email && is_email( $email ) ) {
WC()->session->set( self::SESSION_EMAIL_KEY, $email );
} else {
WC()->session->set( self::SESSION_EMAIL_KEY, null );
}
$this->clear_shipping_cache();
$match = $email ? WC_EDM_Helpers::find_email_match( $email ) : null;
wp_send_json_success(
array(
'matched' => (bool) $match,
)
);
}
/**
* Clear cached shipping rates so shipping rules re-evaluate.
*/
private function clear_shipping_cache() {
if ( ! WC()->cart || ! WC()->session ) {
return;
}
$packages = WC()->cart->get_shipping_packages();
foreach ( array_keys( $packages ) as $index ) {
WC()->session->set( 'shipping_for_package_' . $index, null );
}
}
/**
* Resolve the email address of the current customer.
*
* Logged-in users use their account email; guests use the tracked
* session email or the billing email WooCommerce already knows.
*
* @return string
*/
public function get_customer_email() {
if ( is_user_logged_in() ) {
$user = wp_get_current_user();
if ( $user && $user->user_email ) {
return $user->user_email;
}
}
if ( WC()->session ) {
$session_email = WC()->session->get( self::SESSION_EMAIL_KEY );
if ( $session_email ) {
return $session_email;
}
}
if ( WC()->customer ) {
$billing_email = WC()->customer->get_billing_email();
if ( $billing_email ) {
return $billing_email;
}
}
return '';
}
/**
* Get the current customer's email assignment match, if any.
*
* @return object|null
*/
private function get_current_match() {
$email = $this->get_customer_email();
if ( ! $email ) {
return null;
}
return WC_EDM_Helpers::find_email_match( $email );
}
/**
* Apply group/custom discounts as negative fees.
*
* @param WC_Cart $cart Cart object.
*/
public function apply_discount_fees( $cart ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
return;
}
$match = $this->get_current_match();
if ( ! $match ) {
return;
}
if ( 'group' === $match->assignment_type ) {
$this->apply_group_fee( $cart, $match );
} elseif ( 'custom' === $match->assignment_type ) {
$this->apply_custom_fee( $cart, $match );
}
}
/**
* Group discount: percentage off cart items in the group's categories.
*
* @param WC_Cart $cart Cart object.
* @param object $match Email assignment row.
*/
private function apply_group_fee( $cart, $match ) {
if ( ! $match->group_id ) {
return;
}
$group = WC_EDM_DB::get_group( (int) $match->group_id );
if ( ! $group || (float) $group->discount_percent <= 0 ) {
return;
}
$category_ids = WC_EDM_DB::get_group_category_ids( (int) $group->id );
if ( empty( $category_ids ) ) {
return;
}
$eligible_total = 0;
foreach ( $cart->get_cart() as $cart_item ) {
$product_id = (int) $cart_item['product_id']; // Parent ID for variations.
if ( has_term( $category_ids, 'product_cat', $product_id ) ) {
$eligible_total += (float) $cart_item['line_total'];
}
}
if ( $eligible_total <= 0 ) {
return;
}
$discount = $eligible_total * ( (float) $group->discount_percent / 100 );
$cart->add_fee(
/* translators: %s: discount group name */
sprintf( __( 'Email discount: %s', 'wc-email-discount' ), $group->name ),
-$discount,
false
);
}
/**
* Custom discount: percentage off the entire cart contents.
*
* @param WC_Cart $cart Cart object.
* @param object $match Email assignment row.
*/
private function apply_custom_fee( $cart, $match ) {
$percent = (float) $match->custom_discount;
if ( $percent <= 0 ) {
return;
}
$cart_total = 0;
foreach ( $cart->get_cart() as $cart_item ) {
$cart_total += (float) $cart_item['line_total'];
}
if ( $cart_total <= 0 ) {
return;
}
$discount = $cart_total * ( $percent / 100 );
$cart->add_fee(
/* translators: %s: discount percent */
sprintf( __( 'Email discount (%s%%)', 'wc-email-discount' ), wc_format_localized_decimal( $percent ) ),
-$discount,
false
);
}
/**
* Auto-apply a coupon for coupon-type assignments.
*
* @param WC_Cart $cart Cart object.
*/
public function maybe_apply_coupon( $cart ) {
if ( $this->coupon_applied || ( is_admin() && ! defined( 'DOING_AJAX' ) ) ) {
return;
}
$match = $this->get_current_match();
if ( ! $match || 'coupon' !== $match->assignment_type || empty( $match->coupon_code ) ) {
return;
}
$code = wc_format_coupon_code( $match->coupon_code );
if ( ! wc_get_coupon_id_by_code( $code ) ) {
return;
}
if ( $cart->has_discount( $code ) ) {
return;
}
$this->coupon_applied = true;
$cart->apply_coupon( $code );
}
/**
* Filter shipping rates for the matched email assignment.
*
* Group assignments get their show/hide rules applied; any assignment
* with the free shipping flag gets all remaining rates at zero cost.
*
* @param array $rates Available shipping rates (rate_id => WC_Shipping_Rate).
* @param array $package Shipping package.
* @return array
*/
public function filter_shipping_rates( $rates, $package ) {
$match = $this->get_current_match();
if ( ! $match ) {
return $rates;
}
if ( 'group' === $match->assignment_type && $match->group_id ) {
$rates = $this->apply_group_shipping_rules( $rates, (int) $match->group_id );
}
if ( ! empty( $match->free_shipping ) ) {
foreach ( $rates as $rate ) {
$rate->set_cost( 0 );
$rate->set_taxes( array() );
}
}
return $rates;
}
/**
* Apply a group's show/hide shipping rules.
*
* If the group has no rules, all rates pass through untouched.
* "show" rules act as a whitelist; "hide" rules remove rates.
* Rates are reordered to follow the rules' sort order.
*
* @param array $rates Available shipping rates (rate_id => WC_Shipping_Rate).
* @param int $group_id Discount group ID.
* @return array
*/
private function apply_group_shipping_rules( $rates, $group_id ) {
$rules = WC_EDM_DB::get_shipping_rules( $group_id );
if ( empty( $rules ) ) {
return $rates;
}
$show = array();
$hide = array();
foreach ( $rules as $rule ) {
if ( 'show' === $rule->action ) {
$show[] = $rule->shipping_method_id;
} else {
$hide[] = $rule->shipping_method_id;
}
}
$filtered = array();
foreach ( $rates as $rate_id => $rate ) {
// Whitelist mode: only explicitly shown methods survive.
if ( ! empty( $show ) && ! in_array( $rate_id, $show, true ) ) {
continue;
}
if ( in_array( $rate_id, $hide, true ) ) {
continue;
}
$filtered[ $rate_id ] = $rate;
}
// Never leave the customer without shipping because of misconfigured rules.
if ( empty( $filtered ) ) {
return $rates;
}
// Reorder rates to follow the rules' sort order; unmatched rates keep their position at the end.
$rule_order = wp_list_pluck( $rules, 'shipping_method_id' );
$ordered = array();
foreach ( $rule_order as $method_id ) {
if ( isset( $filtered[ $method_id ] ) ) {
$ordered[ $method_id ] = $filtered[ $method_id ];
unset( $filtered[ $method_id ] );
}
}
return $ordered + $filtered;
}
}