Skip to content

Commit

Permalink
Add support for WooCommerce Deposits when using Apple Pay and Google …
Browse files Browse the repository at this point in the history
…Pay (#7910)

Co-authored-by: Guilherme Pressutto <[email protected]>
  • Loading branch information
2 people authored and Jinksi committed Mar 28, 2024
1 parent 76576c7 commit 4dc98c4
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 18 deletions.
4 changes: 4 additions & 0 deletions changelog/7907-support-deposit-express-pay
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fix

Added support for WooCommerce Deposits when using Apple Pay and Google Pay
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,7 @@ const useExpressCheckoutProductHandler = ( api ) => {
const formData = new FormData( addOnForm );

formData.forEach( ( value, name ) => {
if (
/^addon-/.test( name ) ||
/^wc_gc_giftcard_/.test( name )
) {
if ( /^(addon-|wc_)/.test( name ) ) {
if ( /\[\]$/.test( name ) ) {
const fieldName = name.substring( 0, name.length - 2 );

Expand Down
26 changes: 26 additions & 0 deletions client/payment-request/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,27 @@ jQuery( ( $ ) => {
0
);

// WC Deposits Support.
const depositObject = {};
if ( $( 'input[name=wc_deposit_option]' ).length ) {
depositObject.wc_deposit_option = $(
'input[name=wc_deposit_option]:checked'
).val();
}
if ( $( 'input[name=wc_deposit_payment_plan]' ).length ) {
depositObject.wc_deposit_payment_plan = $(
'input[name=wc_deposit_payment_plan]:checked'
).val();
}

const data = {
product_id: productId,
qty: $( '.quantity .qty' ).val(),
attributes: $( '.variations_form' ).length
? wcpayPaymentRequest.getAttributes().data
: [],
addon_value: addonValue,
...depositObject,
};

return api.paymentRequestGetSelectedProductData( data );
Expand Down Expand Up @@ -437,6 +451,18 @@ jQuery( ( $ ) => {
wcpayPaymentRequest.addToCart();
} );

// WooCommerce Deposits support.
// Trigger the "woocommerce_variation_has_changed" event when the deposit option is changed.
$(
'input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]'
).on( 'change', () => {
$( 'form' )
.has(
'input[name=wc_deposit_option],input[name=wc_deposit_payment_plan]'
)
.trigger( 'woocommerce_variation_has_changed' );
} );

$( document.body ).on( 'woocommerce_variation_has_changed', () => {
wcpayPaymentRequest.blockPaymentRequestButton();

Expand Down
41 changes: 33 additions & 8 deletions includes/class-wc-payments-payment-request-button-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,18 +204,41 @@ public function get_button_settings() {
* Gets the product total price.
*
* @param object $product WC_Product_* object.
* @param bool $is_deposit Whether customer is paying a deposit.
* @param int $deposit_plan_id The ID of the deposit plan.
* @return mixed Total price.
*
* @throws Invalid_Price_Exception Whenever a product has no price.
*/
public function get_product_price( $product ) {
public function get_product_price( $product, ?bool $is_deposit = null, int $deposit_plan_id = 0 ) {
// If prices should include tax, using tax inclusive price.
if ( $this->express_checkout_helper->cart_prices_include_tax() ) {
$base_price = wc_get_price_including_tax( $product );
} else {
$base_price = wc_get_price_excluding_tax( $product );
}

// If WooCommerce Deposits is active, we need to get the correct price for the product.
if ( class_exists( 'WC_Deposits_Product_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) {
// If is_deposit is null, we use the default deposit type for the product.
if ( is_null( $is_deposit ) ) {
$is_deposit = 'deposit' === WC_Deposits_Product_Manager::get_deposit_selected_type( $product->get_id() );
}
if ( $is_deposit ) {
$deposit_type = WC_Deposits_Product_Manager::get_deposit_type( $product->get_id() );
$available_plan_ids = WC_Deposits_Plans_Manager::get_plan_ids_for_product( $product->get_id() );
// Default to first (default) plan if no plan is specified.
if ( 'plan' === $deposit_type && 0 === $deposit_plan_id && ! empty( $available_plan_ids ) ) {
$deposit_plan_id = $available_plan_ids[0];
}

// Ensure the selected plan is available for the product.
if ( 0 === $deposit_plan_id || in_array( $deposit_plan_id, $available_plan_ids, true ) ) {
$base_price = WC_Deposits_Product_Manager::get_deposit_amount( $product, $deposit_plan_id, 'display', $base_price );
}
}
}

// Add subscription sign-up fees to product price.
$sign_up_fee = 0;
$subscription_types = [
Expand Down Expand Up @@ -979,12 +1002,14 @@ public function ajax_get_selected_product_data() {
check_ajax_referer( 'wcpay-get-selected-product-data', 'security' );

try {
$product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false;
$qty = ! isset( $_POST['qty'] ) ? 1 : apply_filters( 'woocommerce_add_to_cart_quantity', absint( $_POST['qty'] ), $product_id );
$addon_value = isset( $_POST['addon_value'] ) ? max( (float) $_POST['addon_value'], 0 ) : 0;
$product = wc_get_product( $product_id );
$variation_id = null;
$currency = get_woocommerce_currency();
$product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false;
$qty = ! isset( $_POST['qty'] ) ? 1 : apply_filters( 'woocommerce_add_to_cart_quantity', absint( $_POST['qty'] ), $product_id );
$addon_value = isset( $_POST['addon_value'] ) ? max( (float) $_POST['addon_value'], 0 ) : 0;
$product = wc_get_product( $product_id );
$variation_id = null;
$currency = get_woocommerce_currency();
$is_deposit = isset( $_POST['wc_deposit_option'] ) ? 'yes' === sanitize_text_field( wp_unslash( $_POST['wc_deposit_option'] ) ) : null;
$deposit_plan_id = isset( $_POST['wc_deposit_payment_plan'] ) ? absint( $_POST['wc_deposit_payment_plan'] ) : 0;

if ( ! is_a( $product, 'WC_Product' ) ) {
/* translators: product ID */
Expand Down Expand Up @@ -1012,7 +1037,7 @@ public function ajax_get_selected_product_data() {
throw new Exception( sprintf( __( 'You cannot add that amount of "%1$s"; to the cart because there is not enough stock (%2$s remaining).', 'woocommerce-payments' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ) );
}

$price = $this->get_product_price( $product );
$price = $this->get_product_price( $product, $is_deposit, $deposit_plan_id );
$total = $qty * $price + $addon_value;

$quantity_label = 1 < $qty ? ' (x' . $qty . ')' : '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public function build_display_items( $itemized_display_items = false ) {
$currency = get_woocommerce_currency();

// Default show only subtotal instead of itemization.
if ( ! apply_filters( 'wcpay_payment_request_hide_itemization', true ) || $itemized_display_items ) {
if ( ! apply_filters( 'wcpay_payment_request_hide_itemization', ! $itemized_display_items ) ) {
foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
$amount = $cart_item['line_subtotal'];
$subtotal += $cart_item['line_subtotal'];
Expand Down
95 changes: 92 additions & 3 deletions tests/unit/helpers/class-wc-helper-deposit-product-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,105 @@
* @package WooCommerce\Payments\Tests
*/

// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound

/**
* Class WC_Deposits_Product_Manager.
*
* This helper class should ONLY be used for unit tests!.
*/
class WC_Deposits_Product_Manager {
public static function get_deposit_type( WC_Product_Simple $product ) {
/**
* @param WC_Product_Simple|int $product
* @return mixed
*/
public static function get_deposit_type( $product ) {
$product = self::get_product( $product );
return $product->get_meta( '_wc_deposit_type' );
}
public static function deposits_enabled( WC_Product_Simple $product ) {
return true === $product->get_meta( '_wc_deposits_enabled' );

/**
* @param WC_Product_Simple|int $product
* @return mixed
*/
public static function get_deposit_selected_type( $product ) {
$product = self::get_product( $product );
return $product->get_meta( '_wc_deposit_selected_type' );
}

/**
* @param WC_Product_Simple|int $product
* @return bool
*/
public static function deposits_enabled( $product ) {
$product = self::get_product( $product );
$setting = $product->get_meta( '_wc_deposit_enabled' );
return 'optional' === $setting || 'forced' === $setting;
}

/**
* @param WC_Product_Simple|int $product
* @return mixed
*/
public static function get_deposit_amount( $product, $plan_id = 0, $context = 'display', $product_price = null ) {
$product = self::get_product( $product );
$type = self::get_deposit_type( $product );
$percentage = 'percent' === $type;
$amount = $product->get_meta( '_wc_deposit_amount' );

if ( ! $amount ) {
$amount = get_option( 'wc_deposits_default_amount' );
}

if ( ! $amount ) {
return false;
}

if ( $percentage ) {
$product_price = is_null( $product_price ) ? $product->get_price() : $product_price;
$amount = ( $product_price / 100 ) * $amount;
}

$price = $amount;
return wc_format_decimal( $price );
}

/**
* @param $product
* @return mixed|null
*/
public static function get_product( $product ) {
if ( ! is_object( $product ) ) {
$product = apply_filters( 'test_deposit_get_product', wc_get_product( $product ) );
}

return $product;
}
}

/**
* Class WC_Deposits_Plans_Manager.
*/
class WC_Deposits_Plans_Manager {
/**
* Get plan ids assigned to a product.
*
* @param int $product_id Product ID.
* @return int[]
*/
public static function get_plan_ids_for_product( $product_id ) {
$product = WC_Deposits_Product_Manager::get_product( $product_id );
$map = array_map( 'absint', array_filter( (array) $product->get_meta( '_wc_deposit_payment_plans' ) ) );
if ( count( $map ) <= 0 ) {
$map = self::get_default_plan_ids();
}
return $map;
}

/**
* Get the default plan IDs.
*/
public static function get_default_plan_ids() {
return array_map( 'absint', array_filter( (array) get_option( 'wc_deposits_default_plans', [] ) ) );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function( $input ) {

$amount = 10.00;
$product = WC_Helper_Product::create_simple_product();
$product->add_meta_data( '_wc_deposits_enabled', true );
$product->add_meta_data( '_wc_deposit_enabled', 'optional' );
$product->add_meta_data( '_wc_deposit_type', 'plan' );
$product->save();

Expand All @@ -112,7 +112,7 @@ public function test_maybe_convert_product_prices_for_deposits() {
->willReturn( true );

$product = WC_Helper_Product::create_simple_product();
$product->add_meta_data( '_wc_deposits_enabled', true );
$product->add_meta_data( '_wc_deposit_enabled', 'optional' );
$product->add_meta_data( '_wc_deposit_type', 'plan' );
$product->save();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,75 @@ public function test_get_product_price_returns_simple_price() {
);
}

public function test_get_product_price_returns_deposit_amount() {
$product_price = 10;
$this->simple_product->set_price( $product_price );

$this->assertEquals(
$product_price,
$this->pr->get_product_price( $this->simple_product, false ),
'When deposit is disabled, the regular price should be returned.'
);
$this->assertEquals(
$product_price,
$this->pr->get_product_price( $this->simple_product, true ),
'When deposit is enabled, but the product has no setting for deposit, the regular price should be returned.'
);

$this->simple_product->update_meta_data( '_wc_deposit_enabled', 'optional' );
$this->simple_product->update_meta_data( '_wc_deposit_type', 'percent' );
$this->simple_product->update_meta_data( '_wc_deposit_amount', 50 );
$this->simple_product->save_meta_data();

$this->assertEquals(
$product_price,
$this->pr->get_product_price( $this->simple_product, false ),
'When deposit is disabled, the regular price should be returned.'
);
$this->assertEquals(
$product_price * 0.5,
$this->pr->get_product_price( $this->simple_product, true ),
'When deposit is enabled, the deposit price should be returned.'
);

$this->simple_product->delete_meta_data( '_wc_deposit_amount' );
$this->simple_product->delete_meta_data( '_wc_deposit_type' );
$this->simple_product->delete_meta_data( '_wc_deposit_enabled' );
$this->simple_product->save_meta_data();
}

public function test_get_product_price_returns_deposit_amount_default_values() {
$product_price = 10;
$this->simple_product->set_price( $product_price );

$this->assertEquals(
$product_price,
$this->pr->get_product_price( $this->simple_product ),
'When deposit is disabled by default, the regular price should be returned.'
);

$this->simple_product->update_meta_data( '_wc_deposit_enabled', 'optional' );
$this->simple_product->update_meta_data( '_wc_deposit_type', 'percent' );
$this->simple_product->update_meta_data( '_wc_deposit_amount', 50 );
$this->simple_product->update_meta_data( '_wc_deposit_selected_type', 'full' );
$this->simple_product->save_meta_data();

$this->assertEquals(
$product_price,
$this->pr->get_product_price( $this->simple_product ),
'When deposit is optional and disabled by default, the regular price should be returned.'
);

$this->simple_product->update_meta_data( '_wc_deposit_selected_type', 'deposit' );
$this->simple_product->save_meta_data();

$this->assertEquals(
$product_price * 0.5,
$this->pr->get_product_price( $this->simple_product ),
'When deposit is optional and selected by default, the deposit price should be returned.'
);
}

/**
* @dataProvider provide_get_product_tax_tests
*/
Expand Down Expand Up @@ -440,6 +509,12 @@ public function provide_get_product_tax_tests() {

public function test_get_product_price_includes_subscription_sign_up_fee() {
$mock_product = $this->create_mock_subscription( 'subscription' );
add_filter(
'test_deposit_get_product',
function() use ( $mock_product ) {
return $mock_product;
}
);

// We have a helper because we are not loading subscriptions.
WC_Subscriptions_Product::set_sign_up_fee( 10 );
Expand All @@ -452,6 +527,12 @@ public function test_get_product_price_includes_subscription_sign_up_fee() {

public function test_get_product_price_includes_variable_subscription_sign_up_fee() {
$mock_product = $this->create_mock_subscription( 'subscription_variation' );
add_filter(
'test_deposit_get_product',
function() use ( $mock_product ) {
return $mock_product;
}
);

// We have a helper because we are not loading subscriptions.
WC_Subscriptions_Product::set_sign_up_fee( 10 );
Expand All @@ -477,6 +558,12 @@ public function test_get_product_price_throws_exception_for_products_without_pri

public function test_get_product_price_throws_exception_for_a_non_numeric_signup_fee() {
$mock_product = $this->create_mock_subscription( 'subscription' );
add_filter(
'test_deposit_get_product',
function() use ( $mock_product ) {
return $mock_product;
}
);
WC_Subscriptions_Product::set_sign_up_fee( 'a' );

$this->expectException( WCPay\Exceptions\Invalid_Price_Exception::class );
Expand Down

0 comments on commit 4dc98c4

Please sign in to comment.