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

Add support for WooCommerce Deposits when using Apple Pay and Google Pay #7910

Merged
merged 32 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7627f38
Send wc_ prefixed fields for express payments.
peterwilsoncc Dec 14, 2023
52293a3
Merge branch 'develop' into fix/7907-addon-express-pay-fields
gpressutto5 Jan 3, 2024
21cdddf
Reload payment request when the deposit option is changed
gpressutto5 Jan 3, 2024
f55654e
Handling selected deposit option
gpressutto5 Jan 3, 2024
fedb13b
Added changelog entry
gpressutto5 Jan 3, 2024
18af61d
Merge branch 'develop' into fix/7907-addon-express-pay-fields
gpressutto5 Jan 3, 2024
a9b2903
Fixed deposit tests class
gpressutto5 Jan 4, 2024
1f13e7e
Fixed deposit tests class
gpressutto5 Jan 4, 2024
4ce48d4
Merge branch 'develop' into fix/7907-addon-express-pay-fields
gpressutto5 Jan 5, 2024
e37202f
Removed filter + sanitizing POST string
gpressutto5 Jan 8, 2024
9014f3c
Added tests for payment request + deposits
gpressutto5 Jan 8, 2024
0899435
Merge branch 'develop' into fix/7907-addon-express-pay-fields
gpressutto5 Jan 8, 2024
63afd86
Apply suggestions from code review
gpressutto5 Jan 9, 2024
80c03fe
Merge branch 'develop' into fix/7907-addon-express-pay-fields
gpressutto5 Jan 9, 2024
128de05
Fixed existing tests
gpressutto5 Jan 9, 2024
36f9e95
Added deposit plans support
gpressutto5 Jan 9, 2024
8ba894b
JS style fix
gpressutto5 Jan 9, 2024
eff996e
Ensure payment plan is specified.
peterwilsoncc Jan 10, 2024
5f91139
Merge branch 'develop' into fix/7907-addon-express-pay-fields
gpressutto5 Jan 12, 2024
a933580
Merge branch 'develop' into fix/7907-addon-express-pay-fields
peterwilsoncc Jan 31, 2024
980c374
Fixed deposit plans frontend
gpressutto5 Jan 10, 2024
9c54d6b
Fixed itemized amount for deposits and apple pay
gpressutto5 Jan 31, 2024
8cb9622
Merge branch 'develop' into fix/7907-addon-express-pay-fields
gpressutto5 Feb 1, 2024
90a9c25
Checking if plan is available for product
gpressutto5 Feb 5, 2024
4f98928
Merge branch 'develop' into fix/7907-addon-express-pay-fields
gpressutto5 Feb 16, 2024
804ebed
Revert "Fixed itemized amount for deposits and apple pay"
gpressutto5 Feb 19, 2024
369509e
Fixed wcpay_payment_request_hide_itemization filter
gpressutto5 Feb 19, 2024
aa580db
Merge branch 'develop' into fix/7907-addon-express-pay-fields
gpressutto5 Feb 19, 2024
1663d76
Only checking if the plan is valid when using a plan
gpressutto5 Feb 19, 2024
5d8c4ae
Fixed tests
gpressutto5 Feb 20, 2024
fcfe4f5
Merge branch 'develop' into fix/7907-addon-express-pay-fields
gpressutto5 Feb 20, 2024
91ec6c3
Merge branch 'develop' into fix/7907-addon-express-pay-fields
gpressutto5 Feb 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -109,10 +109,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
28 changes: 27 additions & 1 deletion client/payment-request/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ jQuery( ( $ ) => {
// Add addons data to the POST body
const formData = $( 'form.cart' ).serializeArray();
$.each( formData, ( i, field ) => {
if ( /^addon-/.test( field.name ) ) {
if ( /^(addon-|wc_)/.test( field.name ) ) {
if ( /\[\]$/.test( field.name ) ) {
const fieldName = field.name.substring(
0,
Expand Down Expand Up @@ -302,13 +302,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 @@ -429,6 +443,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
40 changes: 32 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,40 @@ 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() );
if ( 'plan' === $deposit_type && 0 === $deposit_plan_id ) {
// Default to first (default) plan if no plan is specified.
$available_plan_ids = WC_Deposits_Plans_Manager::get_plan_ids_for_product( $product->get_id() );
if ( ! empty( $available_plan_ids ) ) {
$deposit_plan_id = $available_plan_ids[0];
}
}

$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 @@ -963,12 +985,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 @@ -996,7 +1020,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 @@ -132,11 +132,19 @@ public function build_display_items( $itemized_display_items = false ) {

$product_name = $cart_item['data']->get_name();

$item_tax = $this->cart_prices_include_tax() ? ( $cart_item['line_subtotal_tax'] ?? 0 ) : 0;
$item_tax = $this->cart_prices_include_tax() ? ( $cart_item['line_subtotal_tax'] ?? 0 ) : 0;
$product_total = $amount + $item_tax;

// Currently deposits and express checkout are not 100% compatible.
// Taxes are not calculated correctly when a deposit is used.
// Check https://github.com/Automattic/woocommerce-payments/pull/7910 for more details.
if ( isset( $cart_item['is_deposit'] ) && $cart_item['is_deposit'] ) {
$product_total = $cart_item['deposit_amount'];
}

$item = [
'label' => $product_name . $quantity_label,
'amount' => WC_Payments_Utils::prepare_amount( $amount + $item_tax, $currency ),
'amount' => WC_Payments_Utils::prepare_amount( $product_total, $currency ),
];

$items[] = $item;
Expand Down
66 changes: 63 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 @@ -11,10 +11,70 @@
* 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;
}
}
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
Loading