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

Fix/issue 2562 - Add Retail Delivery Fee for Colorado #2834

Open
wants to merge 8 commits into
base: trunk
Choose a base branch
from

Conversation

iyut
Copy link
Collaborator

@iyut iyut commented Dec 18, 2024

Description

Adding a function for custom surcharge ( retail delivery fee ) for Colorado state. This changes also introduce a new filter hook that the user can use to disable the custom surcharge addition.

Related issue(s)

Fixes #2562

Steps to reproduce & screenshots/GIFs

  1. Add product to the cart.
  2. Use any Colorado address for the shipping address.
  3. The Retail Delivery Fee will be displayed as a free on the cart.

Checklist

  • unit tests
  • changelog.txt entry added
  • readme.txt entry added

@iyut iyut self-assigned this Dec 18, 2024
@iyut iyut requested a review from bartech December 19, 2024 02:54
@iyut iyut marked this pull request as ready for review December 19, 2024 02:54
@iyut iyut requested a review from Abdalsalaam December 20, 2024 03:38
Copy link
Collaborator

@bartech bartech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to create separate file that have custom fees, maybe even folder to store logic per each case in separate file (function) that will return fee data or false.
In main hooked method check if $custom_fees contain item with $customer->get_shipping_country() . ':" . $customer->get_shipping_state().

$custom_fees = array(
	'US:CO' => 'custom_function_name'
);

If matched include custom fee logic file and run custom_function_name to check if fee should be applied, if should be applied return fee data.

array(
	'value' => 0.27 // can be calculated
	'label' => __( 'Retail Delivery Fee', 'woocommerce_services' ),
);

If fee should not be applied return false.

Comment on lines 1536 to 1538
if ( count( array_intersect( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) {
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should check if all chosen methods wc_get_chosen_shipping_method_ids() are local_pickup, if anyone of them is not fee should be applied.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @bartech , I'm not sure if I understand this. Can you explain a bit more on this?

This code is checking if there is any non "local pickup" method is being chosen or not. If it's a local pickup then the fee should not be applied. That's why it has return there.

Copy link
Collaborator

@bartech bartech Dec 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As wc_get_chosen_shipping_method_ids is an array it can have multiple shipping methods picked (in case there are multiple shipping packages). Now code checks if any method is local pickup and we have to check if all of them are local pickup not just one of them. Even if part of the order is sent via carrier or delivered you apply the flat fee 27c

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh ok, I got it now and have made the changes.

*
* @param WC_Cart $cart WooCommerce Cart object.
*/
public function add_custom_state_surcharge_fee( $cart ) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to check if Colorado state Sales or Use Tax is charged on the items (see if we already have those taxes applied) as usually when items are exempt from state Sales or Use Tax they are exempt from Retail Delivery Fee.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice catch. Thanks! I did try to dig into this and based on this article, and it said this : "If the entire retail sale is exempt from sales tax, the sale is also exempt from the retail delivery fee. If one item in the retail sale is exempt, but another item is subject to sales tax, the retailer should collect the retail delivery fee.". From WooCommerce point of view, it seems we don't need to apply this fee if either the sales tax is zero, or all products is non-taxable.

Copy link
Collaborator

@bartech bartech Dec 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if there is no tax on whole cart level no need to apply the fee.

@iyut iyut requested a review from bartech December 27, 2024 03:28
Comment on lines 1531 to 1533
if ( $this->maybe_all_products_are_virtual( $cart ) ) {
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be part of custom method. There might be a fee or custom tax that should be applied to virtual products.

Comment on lines 1535 to 1538
/** Do not apply the fee if all shipping methods use Local Pickup */
if ( 0 === count( array_diff( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) ) {
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in custom method. It's specific to Colorado fee.


$customer = WC()->customer;

foreach ( $this->get_custom_surcharges() as $key => $custom_info ) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of looping over $this->get_custom_surcharges() better to get $custom_surcharges array and check if key exists. There can be only one item with same index anyway.

$custom_surcharges = $this->get_custom_surcharges();
$key = $customer->get_shipping_country() . ':' . $customer->get_shipping_state();
if ( ! isset( $custom_surcharges[ $key ] )  {
	return;
}

If we want to apply multiple rules (fees) for same location we have to allow item to be array of methods.

'US:CO' => array( 'method_one', 'method_two' ),

IMHO it's better to use : as delimiter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple rules seems like a great idea for scalability.

Comment on lines 1551 to 1556
// Making sure all info is not empty.
if ( ! isset( $custom_info['function'] ) || ! isset( $custom_info['fee'] ) || ! isset( $custom_info['fee_text'] ) || ! is_float( $custom_info['fee'] ) || ! is_callable( $custom_info['function'] ) ) {
continue;
}

$eligible_to_add_fee = call_user_func( $custom_info['function'], $cart );
Copy link
Collaborator

@bartech bartech Dec 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better if custom method returned the fee. This way fee can be defined dynamically by custom method. Return fee as array 'value' => 123, 'label' => 'Fee name' or false if fee should not be applied. And here check if method is callable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree on this. Since we're applying multiple rules, I think we should set the fee in the custom method.

Comment on lines +174 to +175
// Add custom state surcharge.
add_action( 'woocommerce_cart_calculate_fees', array( $this, 'add_custom_state_surcharge_fee' ), 10 );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename to maybe_add_custom_surcharge_fees it doesn't have to be state specific.
No need for comment as method name say it all.

Comment on lines +1531 to +1535
if ( ! is_array( $this->get_custom_surcharges() ) ) {
return;
}

$custom_surcharges = $this->get_custom_surcharges();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the $custom_surcharges = $this->get_custom_surcharges(); above the sanity check, no need to call method twice. Also suggest to rename to just $surcharges

*/
$fee_info = apply_filters( 'wc_services_apply_custom_surcharge_fee', $fee_info, $key, $function_rule, $cart );

if ( isset( $fee_info['fee_text'] ) && isset( $fee_info['fee'] ) && is_float( $fee_info['fee'] ) ) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would check if $fee_info['fee'] is_numeric and cast it to float. Mostly for case when it's an int.

* @param array Custom surcharge function or method.
* @param WC_Cart WooCommerce cart object.
*/
$fee_info = apply_filters( 'wc_services_apply_custom_surcharge_fee', $fee_info, $key, $function_rule, $cart );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can call it just $fee.
Why we pass $function_rule in filter, how is this useful for the developer?

Comment on lines +42 to +49
$has_taxable_product = false;

foreach ( $cart->get_cart() as $cart_item ) {
if ( $cart_item['data']->is_taxable() ) {
$has_taxable_product = true;
break;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we don't have to check if products are taxable. $cart->get_cart_contents_taxes() check is enough, if products are not taxable get_cart_contents_taxes will return empty array. Maybe even check if 0 < $cart->get_cart_contents_tax() will be enough.

Comment on lines +32 to +35
// Do not apply the fee if all products are virtual.
if ( WC_Connect_Utils::is_all_products_are_virtual( $cart ) ) {
return $fee_info;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better replace with $cart->needs_shipping(). It's a loop over items in cart that check if each product $product->needs_shipping() which is ! product->is_virtual()


$content_taxes = $cart->get_cart_contents_taxes();

if ( $has_taxable_product && ! empty( $content_taxes ) ) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to reverse the check and return early.

Comment on lines +18 to +61
/**
* Get US Colorado custom surcharge eligibility.
*
* @param WC_Cart|null $cart Cart object or null.
*
* @return boolean.
*/
public static function get_us_co_eligibility( $cart = null ) {
$fee_info = array();

if ( false === ( $cart instanceof WC_Cart ) ) {
return $fee_info;
}

// Do not apply the fee if all products are virtual.
if ( WC_Connect_Utils::is_all_products_are_virtual( $cart ) ) {
return $fee_info;
}

// Do not apply the fee if all shipping methods use Local Pickup.
if ( 0 === count( array_diff( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) ) {
return $fee_info;
}

$has_taxable_product = false;

foreach ( $cart->get_cart() as $cart_item ) {
if ( $cart_item['data']->is_taxable() ) {
$has_taxable_product = true;
break;
}
}

$content_taxes = $cart->get_cart_contents_taxes();

if ( $has_taxable_product && ! empty( $content_taxes ) ) {
$fee_info = array(
'fee' => 0.27,
'fee_text' => __( 'Retail Delivery Fee', 'woocommerce_services' ),
);
}

return $fee_info;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* Get US Colorado custom surcharge eligibility.
*
* @param WC_Cart|null $cart Cart object or null.
*
* @return boolean.
*/
public static function get_us_co_eligibility( $cart = null ) {
$fee_info = array();
if ( false === ( $cart instanceof WC_Cart ) ) {
return $fee_info;
}
// Do not apply the fee if all products are virtual.
if ( WC_Connect_Utils::is_all_products_are_virtual( $cart ) ) {
return $fee_info;
}
// Do not apply the fee if all shipping methods use Local Pickup.
if ( 0 === count( array_diff( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) ) {
return $fee_info;
}
$has_taxable_product = false;
foreach ( $cart->get_cart() as $cart_item ) {
if ( $cart_item['data']->is_taxable() ) {
$has_taxable_product = true;
break;
}
}
$content_taxes = $cart->get_cart_contents_taxes();
if ( $has_taxable_product && ! empty( $content_taxes ) ) {
$fee_info = array(
'fee' => 0.27,
'fee_text' => __( 'Retail Delivery Fee', 'woocommerce_services' ),
);
}
return $fee_info;
}
/**
* Add US Colorado "Retail Delivery Fee" Tax.
*
* @param WC_Cart|null $cart Cart object or null.
*
* @return array|false.
*/
public static function add_us_co_rdf( $cart = null ) {
if ( false === ( $cart instanceof WC_Cart ) ) {
return false;
}
// Retail Delivery Fee is only applicable if any product in order is taxable.
if ( 0 === $cart->get_cart_contents_tax() ) {
return false;
}
// Retail Delivery Fee is only applicable when order is shipped.
if ( ! $cart->needs_shipping() ) {
return false;
}
// Do not apply Retail Delivery Fee if all selected shipping methods are Local Pickup.
if ( 0 === count( array_diff( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) ) {
return false;
}
return array(
'value' => 0.29,
'label' => __( 'Retail Delivery Fee', 'woocommerce_services' ),
);
}

I renamed the method please adjust callable name in custom surcharges.
Adjusted fee to 29c based on this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Colorado Retail Delivery Fee
2 participants