Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate and present custom error for not in allowed emails coupons #43872

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
071b7ff
Removed deprecated WC_COUPON::is_valid() method usage from CartContro…
wavvves Jan 22, 2024
8a90561
Reverted wrongly changed line.
wavvves Jan 22, 2024
532cf13
Added validate_coupon_allowed_emails() to WC_Discounts
wavvves Jan 22, 2024
a967ef4
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Jan 30, 2024
bda4870
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Jan 30, 2024
f87d1bc
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Jan 30, 2024
943b9b9
Added soft validation for allowed emails coupons, with custom notice …
wavvves Jan 31, 2024
a4a129a
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Jan 31, 2024
a145f69
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Feb 1, 2024
0a41dc8
Fixed log warning
wavvves Feb 1, 2024
0b03e76
Refactored add_coupon_message()
wavvves Feb 1, 2024
c083b2c
Prevent duplicate coupon notices.
wavvves Feb 1, 2024
91d4c43
Changed coupon soft validation notice type.
wavvves Feb 1, 2024
12766ff
Tweaks
wavvves Feb 1, 2024
9b40263
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Feb 2, 2024
99079d0
Run coupon soft validations only on cart validation.
wavvves Feb 2, 2024
3b5cc3f
Reverted soft validation, and added email information for coupon vali…
wavvves Feb 2, 2024
2d97f09
Removed unused coupon message
wavvves Feb 2, 2024
b0735c5
PHP lint fixes.
wavvves Feb 2, 2024
131bad8
Added changelog.
wavvves Feb 2, 2024
9cd04fb
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Feb 2, 2024
1e62459
PHP lint fix
wavvves Feb 2, 2024
647548a
Updated allowed coupon validation error message
wavvves Feb 2, 2024
461a659
Updated PW tests
wavvves Feb 2, 2024
c04edc1
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Feb 2, 2024
964941e
Updated PW tests
wavvves Feb 2, 2024
09b7c1b
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Feb 6, 2024
97edce8
Updated email restricted coupon message.
wavvves Feb 6, 2024
45f0afb
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Feb 6, 2024
8ddcc27
Small change for readability.
wavvves Feb 8, 2024
6b7fe11
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Feb 8, 2024
ff43dbe
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Feb 13, 2024
c978738
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Feb 16, 2024
2b92a18
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Feb 29, 2024
1f42776
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 1, 2024
eb9349f
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 1, 2024
3c17b2a
Different error messages for shortcode cart and shortcode checkout
wavvves Mar 1, 2024
35d96f0
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 6, 2024
43f185b
Simplified CartApplyCoupon::get_post_route_response()
wavvves Mar 6, 2024
582c357
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 6, 2024
640a0a1
Revert "Simplified CartApplyCoupon::get_post_route_response()"
wavvves Mar 6, 2024
509bc3a
Expose additional error data in error API response
wavvves Mar 6, 2024
382a41b
Simplified AbstractCartRoute::get_route_error_response()
wavvves Mar 6, 2024
5cbbc49
Linting
wavvves Mar 6, 2024
811659e
Restored comment deleted by mistake.
wavvves Mar 6, 2024
989401c
Introduced API context based coupon errors
wavvves Mar 6, 2024
44d9b74
Fixed Doc Block
wavvves Mar 6, 2024
44373fb
Linting
wavvves Mar 6, 2024
50743ec
Reverted deprecated method removal
wavvves Mar 6, 2024
96ec3f1
Reverted deprecated method removal
wavvves Mar 6, 2024
1afc3b6
WIP
alexflorisca Mar 6, 2024
04702de
Merge branch 'enhancement/26289-coupons-allowed-emails-dont-work-as-e…
alexflorisca Mar 6, 2024
beed7cd
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 6, 2024
4c621a7
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 8, 2024
3124405
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 8, 2024
cde903a
Display context based errors on cart and checkout for allowed emails …
wavvves Mar 11, 2024
42f9bd3
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 11, 2024
4e93cad
Small code fixes.
wavvves Mar 11, 2024
96d0e15
Removed coupon_error_code from api response.
wavvves Mar 11, 2024
d501c65
Tweaks and used 'details' on the API response
wavvves Mar 11, 2024
e3dbc70
Fixed indent.
wavvves Mar 11, 2024
e9f59de
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 11, 2024
0ed3ff8
Set coupon errors using the validation store rather than local state
alexflorisca Mar 12, 2024
897d8b7
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 12, 2024
ec52660
Revert import to original state.
wavvves Mar 12, 2024
7a91439
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 12, 2024
10d864e
Updated tests.
wavvves Mar 12, 2024
24e2c79
Updated tests.
wavvves Mar 13, 2024
804d113
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 13, 2024
0766730
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 14, 2024
8f599f4
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 18, 2024
aadd10f
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 18, 2024
3b0c849
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 18, 2024
71aa495
Simplified comments
wavvves Mar 18, 2024
13de6d3
Added testing for Cart page
wavvves Mar 18, 2024
b480cf9
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 18, 2024
ce6cf8b
Lint fixes
wavvves Mar 19, 2024
018c790
Merge branch 'trunk' into enhancement/26289-coupons-allowed-emails-do…
wavvves Mar 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -3,9 +3,13 @@
*/
import { __, sprintf } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data';
import {
CART_STORE_KEY,
VALIDATION_STORE_KEY,
CHECKOUT_STORE_KEY,
} from '@woocommerce/block-data';
import { decodeEntities } from '@wordpress/html-entities';
import type { StoreCartCoupon } from '@woocommerce/types';
import type { StoreCartCoupon, ApiErrorResponse } from '@woocommerce/types';
import { applyCheckoutFilter } from '@woocommerce/blocks-checkout';

/**
Expand Down Expand Up @@ -41,6 +45,19 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
);

const { applyCoupon, removeCoupon } = useDispatch( CART_STORE_KEY );
const orderId = useSelect( ( select ) =>
select( CHECKOUT_STORE_KEY ).getOrderId()
);

// Return cart, checkout or generic error message.
const getCouponErrorMessage = ( error: ApiErrorResponse ) => {
if ( orderId && orderId > 0 && error?.data?.details?.checkout ) {
return error.data.details.checkout;
} else if ( error?.data?.details?.cart ) {
return error.data.details.cart;
}
return error.message;
};

const applyCouponWithNotices = ( couponCode: string ) => {
return applyCoupon( couponCode )
Expand Down Expand Up @@ -72,9 +89,10 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
return Promise.resolve( true );
} )
.catch( ( error ) => {
const errorMessage = getCouponErrorMessage( error );
setValidationErrors( {
coupon: {
message: decodeEntities( error.message ),
message: decodeEntities( errorMessage ), // TODO fix the circular loop with ApiErrorResponseData and ApiErrorResponseDataDetails
hidden: false,
},
} );
Expand Down
@@ -0,0 +1,4 @@
Significance: minor
Type: enhancement

Validate coupons with email restrictions upfront and change user's feedback when a coupon is not valid for the user.
5 changes: 3 additions & 2 deletions plugins/woocommerce/client/legacy/js/frontend/checkout.js
Expand Up @@ -617,8 +617,9 @@ jQuery( function( $ ) {
});

var data = {
security: wc_checkout_params.apply_coupon_nonce,
coupon_code: $form.find( 'input[name="coupon_code"]' ).val()
security: wc_checkout_params.apply_coupon_nonce,
coupon_code: $form.find('input[name="coupon_code"]').val(),
billing_email: wc_checkout_form.$checkout_form.find('input[name="billing_email"]').val()
};

$.ajax({
Expand Down
8 changes: 7 additions & 1 deletion plugins/woocommerce/includes/class-wc-ajax.php
Expand Up @@ -245,7 +245,13 @@ public static function apply_coupon() {

check_ajax_referer( 'apply-coupon', 'security' );

$coupon_code = ArrayUtil::get_value_or_default( $_POST, 'coupon_code' );
$coupon_code = ArrayUtil::get_value_or_default( $_POST, 'coupon_code' );
$billing_email = ArrayUtil::get_value_or_default( $_POST, 'billing_email' );

if ( is_email( $billing_email ) ) {
wc()->customer->set_billing_email( $billing_email );
}

if ( ! StringUtil::is_null_or_whitespace( $coupon_code ) ) {
WC()->cart->add_discount( wc_format_coupon_code( wp_unslash( $coupon_code ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
} else {
Expand Down
56 changes: 46 additions & 10 deletions plugins/woocommerce/includes/class-wc-coupon.php
Expand Up @@ -944,20 +944,27 @@ public function is_valid_for_product( $product, $values = array() ) {
* Converts one of the WC_Coupon message/error codes to a message string and.
* displays the message/error.
*
* @param int $msg_code Message/error code.
* @param int $msg_code Message/error code.
* @param string $notice_type Notice type.
*/
public function add_coupon_message( $msg_code ) {
$msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code );
public function add_coupon_message( $msg_code, $notice_type = 'success' ) {
if ( $msg_code < 200 ) {
$msg = $this->get_coupon_error( $msg_code );
$notice_type = 'error';
} else {
$msg = $this->get_coupon_message( $msg_code );
}
nielslange marked this conversation as resolved.
Show resolved Hide resolved

if ( ! $msg ) {
if ( empty( $msg ) ) {
return;
}

if ( $msg_code < 200 ) {
wc_add_notice( $msg, 'error' );
} else {
wc_add_notice( $msg );
// Since coupon validation is done multiple times (e.g. to ensure a valid cart), we need to check for dupes.
if ( wc_has_notice( $msg, $notice_type ) ) {
return;
}

wc_add_notice( $msg, $notice_type );
}

/**
Expand Down Expand Up @@ -1001,8 +1008,15 @@ public function get_coupon_error( $err_code ) {
$err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) );
break;
case self::E_WC_COUPON_NOT_YOURS_REMOVED:
/* translators: %s: coupon code */
$err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) );
// We check for supplied billing email. On shortcode, this will be present for checkout requests.
$billing_email = \Automattic\WooCommerce\Utilities\ArrayUtil::get_value_or_default( $_POST, 'billing_email' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( ! is_null( $billing_email ) ) {
/* translators: %s: coupon code */
$err = sprintf( __( 'Please enter a valid email to use coupon code "%s".', 'woocommerce' ), esc_html( $this->get_code() ) );
} else {
/* translators: %s: coupon code */
$err = sprintf( __( 'Please enter a valid email at checkout to use coupon code "%s".', 'woocommerce' ), esc_html( $this->get_code() ) );
}
break;
case self::E_WC_COUPON_ALREADY_APPLIED:
$err = __( 'Coupon code already applied!', 'woocommerce' );
Expand Down Expand Up @@ -1152,4 +1166,26 @@ public function set_short_info( string $info ) {
$this->set_amount( $info[3] ?? 0 );
$this->set_free_shipping( $info[4] ?? false );
}

/**
* Returns alternate error messages based on context (eg. Cart and Checkout).
*
* @param int $err_code Message/error code.
*
* @return array Context based alternate error messages.
*/
public function get_context_based_coupon_errors( $err_code = null ) {

switch ( $err_code ) {
case self::E_WC_COUPON_NOT_YOURS_REMOVED:
return array(
/* translators: %s: coupon code */
'cart' => sprintf( __( 'Please enter a valid email at checkout to use coupon code "%s".', 'woocommerce' ), esc_html( $this->get_code() ) ),
/* translators: %s: coupon code */
'checkout' => sprintf( __( 'Please enter a valid email to use coupon code "%s".', 'woocommerce' ), esc_html( $this->get_code() ) ),
);
wavvves marked this conversation as resolved.
Show resolved Hide resolved
default:
return array();
}
}
}
69 changes: 62 additions & 7 deletions plugins/woocommerce/includes/class-wc-discounts.php
Expand Up @@ -939,6 +939,50 @@ protected function validate_coupon_excluded_product_categories( $coupon ) {
return true;
}

/**
* Ensure coupon is valid for allowed emails or throw exception.
*
* @since 8.6.0
* @throws Exception Error message.
* @param WC_Coupon $coupon Coupon data.
* @return bool
*/
protected function validate_coupon_allowed_emails( $coupon ) {

$restrictions = $coupon->get_email_restrictions();

if ( ! is_array( $restrictions ) || empty( $restrictions ) ) {
return true;
}

$user = wp_get_current_user();
$check_emails = array( $user->get_billing_email(), $user->get_email() );

if ( $this->object instanceof WC_Cart ) {
$check_emails[] = $this->object->get_customer()->get_billing_email();
} elseif ( $this->object instanceof WC_Order ) {
$check_emails[] = $this->object->get_billing_email();
}

$check_emails = array_unique(
array_filter(
array_map(
'strtolower',
array_map(
'sanitize_email',
$check_emails
)
)
)
);

if ( ! WC()->cart->is_coupon_emails_allowed( $check_emails, $restrictions ) ) {
throw new Exception( $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ), WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}

return true;
}

/**
* Get the object subtotal
*
Expand Down Expand Up @@ -981,10 +1025,11 @@ protected function get_object_subtotal() {
* - 113: Excluded products.
* - 114: Excluded categories.
*
* @since 3.2.0
* @throws Exception Error message.
* @param WC_Coupon $coupon Coupon data.
* @param WC_Coupon $coupon Coupon data.
*
* @return bool|WP_Error
* @throws Exception Error message.
* @since 3.2.0
*/
public function is_coupon_valid( $coupon ) {
try {
Expand All @@ -998,9 +1043,10 @@ public function is_coupon_valid( $coupon ) {
$this->validate_coupon_product_categories( $coupon );
$this->validate_coupon_excluded_items( $coupon );
$this->validate_coupon_eligible_items( $coupon );
$this->validate_coupon_allowed_emails( $coupon );

if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $coupon, $this ) ) {
throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), 100 );
throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), WC_Coupon::E_WC_COUPON_INVALID_FILTERED );
}
} catch ( Exception $e ) {
/**
Expand All @@ -1012,14 +1058,23 @@ public function is_coupon_valid( $coupon ) {
*/
$message = apply_filters( 'woocommerce_coupon_error', is_numeric( $e->getMessage() ) ? $coupon->get_coupon_error( $e->getMessage() ) : $e->getMessage(), $e->getCode(), $coupon );

$additional_data = array(
'status' => 400,
);

$context_coupon_errors = $coupon->get_context_based_coupon_errors( $e->getCode() );

if ( ! empty( $context_coupon_errors ) ) {
$additional_data['details'] = $context_coupon_errors;
}

return new WP_Error(
'invalid_coupon',
$message,
array(
'status' => 400,
)
$additional_data,
);
}

return true;
}
}
25 changes: 8 additions & 17 deletions plugins/woocommerce/src/StoreApi/Routes/V1/AbstractCartRoute.php
Expand Up @@ -332,24 +332,15 @@ protected function check_nonce( \WP_REST_Request $request ) {
* @return \WP_Error WP Error object.
*/
protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500, $additional_data = [] ) {
switch ( $http_status_code ) {
case 409:
// If there was a conflict, return the cart so the client can resolve it.
$cart = $this->cart_controller->get_cart_instance();

return new \WP_Error(
$error_code,
$error_message,
array_merge(
$additional_data,
[
'status' => $http_status_code,
'cart' => $this->cart_schema->get_item_response( $cart ),
]
)
);

$additional_data['status'] = $http_status_code;

// If there was a conflict, return the cart so the client can resolve it.
if ( 409 === $http_status_code ) {
$cart = $this->cart_controller->get_cart_instance();
$additional_data['cart'] = $this->cart_schema->get_item_response( $cart );
}

return new \WP_Error( $error_code, $error_message, [ 'status' => $http_status_code ] );
return new \WP_Error( $error_code, $error_message, $additional_data );
}
}
Expand Up @@ -58,7 +58,6 @@ protected function get_route_post_response( \WP_REST_Request $request ) {
throw new RouteException( 'woocommerce_rest_cart_coupon_disabled', __( 'Coupons are disabled.', 'woocommerce' ), 404 );
}

$cart = $this->cart_controller->get_cart_instance();
$coupon_code = wc_format_coupon_code( wp_unslash( $request['code'] ) );

try {
Expand All @@ -67,6 +66,7 @@ protected function get_route_post_response( \WP_REST_Request $request ) {
throw new RouteException( $e->getErrorCode(), $e->getMessage(), $e->getCode() );
}

$cart = $this->cart_controller->get_cart_instance();
return rest_ensure_response( $this->schema->get_item_response( $cart ) );
}
}
14 changes: 9 additions & 5 deletions plugins/woocommerce/src/StoreApi/Utilities/CartController.php
Expand Up @@ -928,11 +928,15 @@ public function apply_coupon( $coupon_code ) {
);
}

if ( ! $coupon->is_valid() ) {
$discounts = new \WC_Discounts( $this->get_cart_instance() );
$valid = $discounts->is_coupon_valid( $coupon );

if ( is_wp_error( $valid ) ) {
throw new RouteException(
'woocommerce_rest_cart_coupon_error',
wp_strip_all_tags( $coupon->get_error_message() ),
400
esc_html( wp_strip_all_tags( $valid->get_error_message() ) ),
400,
$valid->get_error_data() // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
);
}

Expand Down Expand Up @@ -1013,9 +1017,9 @@ function( $code ) {
/**
* Validates an existing cart coupon and returns any errors.
*
* @throws RouteException Exception if invalid data is detected.
*
* @param \WC_Coupon $coupon Coupon object applied to the cart.
*
* @throws RouteException Exception if invalid data is detected.
*/
protected function validate_cart_coupon( \WC_Coupon $coupon ) {
if ( ! $coupon->is_valid() ) {
Expand Down