Merge remote-tracking branch 'origin/trunk' into PCP-915-create-pay-later-tab

# Conflicts:
#	modules/ppcp-button/services.php
This commit is contained in:
Narek Zakarian 2022-11-09 19:16:59 +04:00
commit e189325766
46 changed files with 1029 additions and 207 deletions

View file

@ -904,6 +904,513 @@ class WC_Subscriptions_Admin
public static $option_prefix = 'woocommerce_subscriptions';
}
/**
* Allow for payment dates to be synchronised to a specific day of the week, month or year.
*/
class WC_Subscriptions_Synchroniser {
public static $setting_id;
public static $setting_id_proration;
public static $setting_id_days_no_fee;
public static $post_meta_key = '_subscription_payment_sync_date';
public static $post_meta_key_day = '_subscription_payment_sync_date_day';
public static $post_meta_key_month = '_subscription_payment_sync_date_month';
public static $sync_field_label;
public static $sync_description;
public static $sync_description_year;
public static $billing_period_ranges;
/**
* Bootstraps the class and hooks required actions & filters.
*/
public static function init()
{
}
/**
* Set default value of 'no' for our options.
*
* This only sets the default
*
* @param mixed $default The default value for the option.
* @param string $option The option name.
* @param bool $passed_default Whether get_option() was passed a default value.
*
* @return mixed The default option value.
*/
public static function option_default( $default, $option, $passed_default = null )
{
}
/**
* Sanitize our options when they are saved in the admin area.
*
* @param mixed $value The value being saved.
* @param array $option The option data array.
*
* @return mixed The sanitized option value.
*/
public static function sanitize_option( $value, $option )
{
}
/**
* Check if payment syncing is enabled on the store.
*
* @since 1.5
*/
public static function is_syncing_enabled()
{
}
/**
* Check if payments can be prorated on the store.
*
* @since 1.5
*/
public static function is_sync_proration_enabled()
{
}
/**
* Add sync settings to the Subscription's settings page.
*
* @since 1.5
*/
public static function add_settings( $settings )
{
}
/**
* Add the sync setting fields to the Edit Product screen
*
* @since 1.5
*/
public static function subscription_product_fields()
{
}
/**
* Add the sync setting fields to the variation section of the Edit Product screen
*
* @since 1.5
*/
public static function variable_subscription_product_fields( $loop, $variation_data, $variation )
{
}
/**
* Save sync options when a subscription product is saved
*
* @since 1.5
*/
public static function save_subscription_meta( $post_id )
{
}
/**
* Save sync options when a variable subscription product is saved
*
* @since 1.5
*/
public static function process_product_meta_variable_subscription( $post_id )
{
}
/**
* Save sync options when a variable subscription product is saved
*
* @since 1.5
*/
public static function save_product_variation( $variation_id, $index )
{
}
/**
* Add translated syncing options for our client side script
*
* @since 1.5
*/
public static function admin_script_parameters( $script_parameters )
{
}
/**
* Determine whether a product, specified with $product, needs to have its first payment processed on a
* specific day (instead of at the time of sign-up).
*
* @return (bool) True is the product's first payment will be synced to a certain day.
* @since 1.5
*/
public static function is_product_synced( $product )
{
}
/**
* Determine whether a product, specified with $product, should have its first payment processed on a
* at the time of sign-up but prorated to the sync day.
*
* @since 1.5.10
*
* @param WC_Product $product
*
* @return bool
*/
public static function is_product_prorated( $product )
{
}
/**
* Determine whether the payment for a subscription should be the full price upfront.
*
* This method is particularly concerned with synchronized subscriptions. It will only return
* true when the following conditions are met:
*
* - There is no free trial
* - The subscription is synchronized
* - The store owner has determined that new subscribers need to pay for their subscription upfront.
*
* Additionally, if the store owner sets a number of days prior to the synchronization day that do not
* require an upfront payment, this method will check to see whether the current date falls within that
* period for the given product.
*
* @param WC_Product $product The product to check.
* @param string $from_date Optional. A MySQL formatted date/time string from which to calculate from. The default is an empty string which is today's date/time.
*
* @return bool Whether an upfront payment is required for the product.
*/
public static function is_payment_upfront( $product, $from_date = '' )
{
}
/**
* Get the day of the week, month or year on which a subscription's payments should be
* synchronised to.
*
* @return int The day the products payments should be processed, or 0 if the payments should not be sync'd to a specific day.
* @since 1.5
*/
public static function get_products_payment_day( $product )
{
}
/**
* Calculate the first payment date for a synced subscription.
*
* The date is calculated in UTC timezone.
*
* @param WC_Product $product A subscription product.
* @param string $type (optional) The format to return the first payment date in, either 'mysql' or 'timestamp'. Default 'mysql'.
* @param string $from_date (optional) The date to calculate the first payment from in GMT/UTC timzeone. If not set, it will use the current date. This should not include any trial period on the product.
* @since 1.5
*/
public static function calculate_first_payment_date( $product, $type = 'mysql', $from_date = '' )
{
}
/**
* Return an i18n'ified associative array of sync options for 'year' as billing period
*
* @since 3.0.0
*/
public static function get_year_sync_options()
{
}
/**
* Return an i18n'ified associative array of all possible subscription periods.
*
* @since 1.5
*/
public static function get_billing_period_ranges( $billing_period = '' )
{
}
/**
* Add the first payment date to a products summary section
*
* @since 1.5
*/
public static function products_first_payment_date( $echo = false )
{
}
/**
* Return a string explaining when the first payment will be completed for the subscription.
*
* @since 1.5
*/
public static function get_products_first_payment_date( $product )
{
}
/**
* If a product is synchronised to a date in the future, make sure that is set as the product's first payment date
*
* @since 2.0
*/
public static function products_first_renewal_payment_time( $first_renewal_timestamp, $product_id, $from_date, $timezone )
{
}
/**
* Make sure a synchronised subscription's price includes a free trial, unless it's first payment is today.
*
* @since 1.5
*/
public static function maybe_set_free_trial( $total = '' )
{
}
/**
* Make sure a synchronised subscription's price includes a free trial, unless it's first payment is today.
*
* @since 1.5
*/
public static function maybe_unset_free_trial( $total = '' )
{
}
/**
* Check if the cart includes a subscription that needs to be synced.
*
* @return bool Returns true if any item in the cart is a subscription sync request, otherwise, false.
* @since 1.5
*/
public static function cart_contains_synced_subscription( $cart = null )
{
}
/**
* Maybe set the time of a product's trial expiration to be the same as the synced first payment date for products where the first
* renewal payment date falls on the same day as the trial expiration date, but the trial expiration time is later in the day.
*
* When making sure the first payment is after the trial expiration in @see self::calculate_first_payment_date() we only check
* whether the first payment day comes after the trial expiration day, because we don't want to pushing the first payment date
* a month or year in the future because of a few hours difference between it and the trial expiration. However, this means we
* could still end up with a trial end time after the first payment time, even though they are both on the same day because the
* trial end time is normally calculated from the start time, which can be any time of day, but the first renewal time is always
* set to be 3am in the site's timezone. For example, the first payment date might be calculate to be 3:00 on the 21st April 2017,
* while the trial end date is on the same day at 3:01 (or any time after that on the same day). So we need to check both the time and day. We also don't want to make the first payment date/time skip a year because of a few hours difference. That means we need to either modify the trial end time to be 3:00am or make the first payment time occur at the same time as the trial end time. The former is pretty hard to change, but the later will sync'd payments will be at a different times if there is a free trial ending on the same day, which could be confusing. o_0
*
* Fixes #1328
*
* @param mixed $trial_expiration_date MySQL formatted date on which the subscription's trial will end, or 0 if it has no trial
* @param mixed $product_id The product object or post ID of the subscription product
* @return mixed MySQL formatted date on which the subscription's trial is set to end, or 0 if it has no trial
* @since 2.0.13
*/
public static function recalculate_product_trial_expiration_date( $trial_expiration_date, $product_id )
{
}
/**
* Make sure the expiration date is calculated from the synced start date for products where the start date
* will be synced.
*
* @param string $expiration_date MySQL formatted date on which the subscription is set to expire
* @param mixed $product_id The product/post ID of the subscription
* @param mixed $from_date A MySQL formatted date/time string from which to calculate the expiration date, or empty (default), which will use today's date/time.
* @since 1.5
*/
public static function recalculate_product_expiration_date( $expiration_date, $product_id, $from_date )
{
}
/**
* Check if a given timestamp (in the UTC timezone) is equivalent to today in the site's time.
*
* @param int $timestamp A time in UTC timezone to compare to today.
*/
public static function is_today( $timestamp )
{
}
/**
* Filters WC_Subscriptions_Order::get_sign_up_fee() to make sure the sign-up fee for a subscription product
* that is synchronised is returned correctly.
*
* @param float The initial sign-up fee charged when the subscription product in the order was first purchased, if any.
* @param mixed $order A WC_Order object or the ID of the order which the subscription was purchased in.
* @param int $product_id The post ID of the subscription WC_Product object purchased in the order. Defaults to the ID of the first product purchased in the order.
* @return float The initial sign-up fee charged when the subscription product in the order was first purchased, if any.
* @since 2.0
*/
public static function get_synced_sign_up_fee( $sign_up_fee, $subscription, $product_id )
{
}
/**
* Removes the "set_subscription_prices_for_calculation" filter from the WC Product's woocommerce_get_price hook once
*
* @since 1.5.10
*
* @param int $price The current price.
* @param WC_Product $product The product object.
*
* @return int
*/
public static function set_prorated_price_for_calculation( $price, $product )
{
}
/**
* Retrieve the full translated weekday word.
*
* Week starts on translated Monday and can be fetched
* by using 1 (one). So the week starts with 1 (one)
* and ends on Sunday with is fetched by using 7 (seven).
*
* @since 1.5.8
* @access public
*
* @param int $weekday_number 1 for Monday through 7 Sunday
* @return string Full translated weekday
*/
public static function get_weekday( $weekday_number )
{
}
/**
* Override quantities used to lower stock levels by when using synced subscriptions. If it's a synced product
* that does not have proration enabled and the payment date is not today, do not lower stock levels.
*
* @param integer $qty the original quantity that would be taken out of the stock level
* @param array $order order data
* @param array $item item data for each item in the order
*
* @return int
*/
public static function maybe_do_not_reduce_stock( $qty, $order, $order_item )
{
}
/**
* Add subscription meta for subscription that contains a synced product.
*
* @param WC_Order Parent order for the subscription
* @param WC_Subscription new subscription
* @since 2.0
*/
public static function maybe_add_subscription_meta( $post_id )
{
}
/**
* When adding an item to an order/subscription via the Add/Edit Subscription administration interface, check if we should be setting
* the sync meta on the subscription.
*
* @param int The order item ID of an item that was just added to the order
* @param array The order item details
* @since 2.0
*/
public static function ajax_maybe_add_meta_for_item( $item_id, $item )
{
}
/**
* When adding a product to an order/subscription via the WC_Subscription::add_product() method, check if we should be setting
* the sync meta on the subscription.
*
* @param int The post ID of a WC_Order or child object
* @param int The order item ID of an item that was just added to the order
* @param object The WC_Product for which an item was just added
* @since 2.0
*/
public static function maybe_add_meta_for_new_product( $subscription_id, $item_id, $product )
{
}
/**
* Check if a given subscription is synced to a certain day.
*
* @param int|WC_Subscription Accepts either a subscription object of post id
* @return bool
* @since 2.0
*/
public static function subscription_contains_synced_product( $subscription_id )
{
}
/**
* If the cart item is synced, add a '_synced' string to the recurring cart key.
*
* @since 2.0
*/
public static function add_to_recurring_cart_key( $cart_key, $cart_item )
{
}
/**
* When adding a product line item to an order/subscription via the WC_Abstract_Order::add_product() method, check if we should be setting
* the sync meta on the subscription.
*
* Attached to WC 3.0+ hooks and uses WC 3.0 methods.
*
* @param int The new line item id
* @param WC_Order_Item
* @param int The post ID of a WC_Subscription
* @since 2.2.3
*/
public static function maybe_add_meta_for_new_line_item( $item_id, $item, $subscription_id )
{
}
/**
* Store a synced product's signup fee on the line item on the subscription and order.
*
* When calculating prorated sign up fees during switches it's necessary to get the sign-up fee paid.
* For synced product purchases we cannot rely on the order line item price as that might include a prorated recurring price or no recurring price all.
*
* Attached to WC 3.0+ hooks and uses WC 3.0 methods.
*
* @param WC_Order_Item_Product $item The order item object.
* @param string $cart_item_key The hash used to identify the item in the cart
* @param array $cart_item The cart item's data.
* @since 2.3.0
*/
public static function maybe_add_line_item_meta( $item, $cart_item_key, $cart_item )
{
}
/**
* Store a synced product's signup fee on the line item on the subscription and order.
*
* This function is a pre WooCommerce 3.0 version of @see WC_Subscriptions_Synchroniser::maybe_add_line_item_meta()
*
* @param int $item_id The order item ID.
* @param array $cart_item The cart item's data.
* @since 2.3.0
*/
public static function maybe_add_order_item_meta( $item_id, $cart_item )
{
}
/**
* Hides synced subscription meta on the edit order and subscription screen on non-debug sites.
*
* @since 2.6.2
* @param array $hidden_meta_keys the list of meta keys hidden on the edit order and subscription screen.
* @return array $hidden_meta_keys
*/
public static function hide_order_itemmeta( $hidden_meta_keys )
{
}
/**
* Gets the number of sign-up grace period days.
*
* @since 3.0.6
* @return int The number of days in the grace period. 0 will be returned if the stroe isn't charging the full recurring price on sign-up -- a prerequiste for setting a grace period.
*/
private static function get_number_of_grace_period_days()
{
}
}
/**
* Check if a given object is a WC_Subscription (or child class of WC_Subscription), or if a given ID
* belongs to a post with the subscription post type ('shop_subscription')

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
@ -42,12 +43,12 @@ class PaymentToken {
/**
* PaymentToken constructor.
*
* @param string $id The Id.
* @param string $type The type.
* @param \stdClass $source The source.
* @param string $id The Id.
* @param stdClass $source The source.
* @param string $type The type.
* @throws RuntimeException When the type is not valid.
*/
public function __construct( string $id, string $type = self::TYPE_PAYMENT_METHOD_TOKEN, \stdClass $source ) {
public function __construct( string $id, stdClass $source, string $type = self::TYPE_PAYMENT_METHOD_TOKEN ) {
if ( ! in_array( $type, self::get_valid_types(), true ) ) {
throw new RuntimeException(
__( 'Not a valid payment source type.', 'woocommerce-paypal-payments' )

View file

@ -119,4 +119,26 @@ class PayPalApiException extends RuntimeException {
public function status_code(): int {
return $this->status_code;
}
/**
* Return exception details if exists.
*
* @param string $error The error to return in case no details found.
* @return string
*/
public function get_details( string $error ): string {
if ( empty( $this->details() ) ) {
return $error;
}
$details = '';
foreach ( $this->details() as $detail ) {
$issue = $detail->issue ?? '';
$field = $detail->field ?? '';
$description = $detail->description ?? '';
$details .= $issue . ' ' . $field . ' ' . $description . '<br>';
}
return $details;
}
}

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
@ -25,7 +26,7 @@ class PaymentTokenFactory {
* @return PaymentToken
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( \stdClass $data ): PaymentToken {
public function from_paypal_response( stdClass $data ): PaymentToken {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'No id for payment token given', 'woocommerce-paypal-payments' )
@ -34,8 +35,8 @@ class PaymentTokenFactory {
return new PaymentToken(
$data->id,
( isset( $data->type ) ) ? $data->type : PaymentToken::TYPE_PAYMENT_METHOD_TOKEN,
$data->source
$data->source,
( isset( $data->type ) ) ? $data->type : PaymentToken::TYPE_PAYMENT_METHOD_TOKEN
);
}

View file

@ -100,7 +100,8 @@ const bootstrap = () => {
if (PayPalCommerceGateway.mini_cart_buttons_enabled === '1') {
const miniCartBootstrap = new MiniCartBootstap(
PayPalCommerceGateway,
renderer
renderer,
errorHandler,
);
miniCartBootstrap.init();
@ -112,6 +113,7 @@ const bootstrap = () => {
PayPalCommerceGateway,
renderer,
messageRenderer,
errorHandler,
);
singleProductBootstrap.init();
@ -121,6 +123,7 @@ const bootstrap = () => {
const cartBootstrap = new CartBootstrap(
PayPalCommerceGateway,
renderer,
errorHandler,
);
cartBootstrap.init();
@ -131,7 +134,8 @@ const bootstrap = () => {
PayPalCommerceGateway,
renderer,
messageRenderer,
spinner
spinner,
errorHandler,
);
checkoutBootstap.init();
@ -142,7 +146,8 @@ const bootstrap = () => {
PayPalCommerceGateway,
renderer,
messageRenderer,
spinner
spinner,
errorHandler,
);
payNowBootstrap.init();
}

View file

@ -59,14 +59,16 @@ class CheckoutActionHandler {
);
} else {
errorHandler.clear();
if (data.data.details.length > 0) {
if (data.data.errors.length > 0) {
errorHandler.messages(data.data.errors);
} else if (data.data.details.length > 0) {
errorHandler.message(data.data.details.map(d => `${d.issue} ${d.description}`).join('<br/>'), true);
} else {
errorHandler.message(data.data.message, true);
}
}
throw new Error(data.data.message);
throw {type: 'create-order-error', data: data.data};
}
const input = document.createElement('input');
input.setAttribute('type', 'hidden');
@ -82,9 +84,15 @@ class CheckoutActionHandler {
onCancel: () => {
spinner.unblock();
},
onError: () => {
this.errorHandler.genericError();
onError: (err) => {
console.error(err);
spinner.unblock();
if (err && err.type === 'create-order-error') {
return;
}
this.errorHandler.genericError();
}
}
}

View file

@ -1,10 +1,10 @@
import CartActionHandler from '../ActionHandler/CartActionHandler';
import ErrorHandler from '../ErrorHandler';
class CartBootstrap {
constructor(gateway, renderer) {
constructor(gateway, renderer, errorHandler) {
this.gateway = gateway;
this.renderer = renderer;
this.errorHandler = errorHandler;
}
init() {
@ -28,7 +28,7 @@ class CartBootstrap {
render() {
const actionHandler = new CartActionHandler(
PayPalCommerceGateway,
new ErrorHandler(this.gateway.labels.error.generic),
this.errorHandler,
);
this.renderer.render(

View file

@ -1,4 +1,3 @@
import ErrorHandler from '../ErrorHandler';
import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler';
import {setVisible, setVisibleByClass} from '../Helper/Hiding';
import {
@ -8,11 +7,12 @@ import {
} from "../Helper/CheckoutMethodState";
class CheckoutBootstap {
constructor(gateway, renderer, messages, spinner) {
constructor(gateway, renderer, messages, spinner, errorHandler) {
this.gateway = gateway;
this.renderer = renderer;
this.messages = messages;
this.spinner = spinner;
this.errorHandler = errorHandler;
this.standardOrderButtonSelector = ORDER_BUTTON_SELECTOR;
}
@ -60,7 +60,7 @@ class CheckoutBootstap {
}
const actionHandler = new CheckoutActionHandler(
PayPalCommerceGateway,
new ErrorHandler(this.gateway.labels.error.generic),
this.errorHandler,
this.spinner
);

View file

@ -1,10 +1,10 @@
import ErrorHandler from '../ErrorHandler';
import CartActionHandler from '../ActionHandler/CartActionHandler';
class MiniCartBootstap {
constructor(gateway, renderer) {
constructor(gateway, renderer, errorHandler) {
this.gateway = gateway;
this.renderer = renderer;
this.errorHandler = errorHandler;
this.actionHandler = null;
}
@ -12,7 +12,7 @@ class MiniCartBootstap {
this.actionHandler = new CartActionHandler(
PayPalCommerceGateway,
new ErrorHandler(this.gateway.labels.error.generic),
this.errorHandler,
);
this.render();

View file

@ -2,8 +2,8 @@ import CheckoutBootstap from './CheckoutBootstap'
import {isChangePaymentPage} from "../Helper/Subscriptions";
class PayNowBootstrap extends CheckoutBootstap {
constructor(gateway, renderer, messages, spinner) {
super(gateway, renderer, messages, spinner)
constructor(gateway, renderer, messages, spinner, errorHandler) {
super(gateway, renderer, messages, spinner, errorHandler)
}
updateUi() {

View file

@ -1,12 +1,12 @@
import ErrorHandler from '../ErrorHandler';
import UpdateCart from "../Helper/UpdateCart";
import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler";
class SingleProductBootstap {
constructor(gateway, renderer, messages) {
constructor(gateway, renderer, messages, errorHandler) {
this.gateway = gateway;
this.renderer = renderer;
this.messages = messages;
this.errorHandler = errorHandler;
}
@ -81,7 +81,7 @@ class SingleProductBootstap {
this.messages.hideMessages();
},
document.querySelector('form.cart'),
new ErrorHandler(this.gateway.labels.error.generic),
this.errorHandler,
);
this.renderer.render(

View file

@ -149,6 +149,7 @@ return array(
$early_order_handler,
$registration_needed,
$container->get( 'wcgateway.settings.card_billing_data_mode' ),
$container->get( 'button.early-wc-checkout-validation-enabled' ),
$logger
);
},
@ -223,6 +224,14 @@ return array(
* The filter allowing to disable the basic client-side validation of the checkout form
* when the PayPal button is clicked.
*/
return (bool) apply_filters( 'woocommerce_paypal_payments_basic_checkout_validation_enabled', true );
return (bool) apply_filters( 'woocommerce_paypal_payments_basic_checkout_validation_enabled', false );
},
'button.early-wc-checkout-validation-enabled' => static function ( ContainerInterface $container ): bool {
/**
* The filter allowing to disable the WC validation of the checkout form
* when the PayPal button is clicked.
* The validation is triggered in a non-standard way and may cause issues on some sites.
*/
return (bool) apply_filters( 'woocommerce_paypal_payments_early_wc_checkout_validation_enabled', true );
},
);

View file

@ -373,6 +373,7 @@ class SmartButton implements SmartButtonInterface {
if (
( is_product() || wc_post_content_has_shortcode( 'product_page' ) )
&& ! $not_enabled_on_product_page
&& ! is_checkout()
) {
add_action(
$this->single_product_renderer_hook(),
@ -413,6 +414,7 @@ class SmartButton implements SmartButtonInterface {
// TODO: it seems like there is no easy way to properly handle vaulted PayPal free trial,
// so disable the buttons for now everywhere except checkout for free trial.
&& ! $this->is_free_trial_product()
&& ! is_checkout()
) {
add_action(
$this->single_product_renderer_hook(),

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use stdClass;
use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
@ -25,6 +26,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
@ -128,6 +131,13 @@ class CreateOrderEndpoint implements EndpointInterface {
*/
protected $card_billing_data_mode;
/**
* Whether to execute WC validation of the checkout form.
*
* @var bool
*/
protected $early_validation_enabled;
/**
* The logger.
*
@ -148,6 +158,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* @param EarlyOrderHandler $early_order_handler The EarlyOrderHandler object.
* @param bool $registration_needed Whether a new user must be registered during checkout.
* @param string $card_billing_data_mode The value of card_billing_data_mode from the settings.
* @param bool $early_validation_enabled Whether to execute WC validation of the checkout form.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
@ -161,6 +172,7 @@ class CreateOrderEndpoint implements EndpointInterface {
EarlyOrderHandler $early_order_handler,
bool $registration_needed,
string $card_billing_data_mode,
bool $early_validation_enabled,
LoggerInterface $logger
) {
@ -174,6 +186,7 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->early_order_handler = $early_order_handler;
$this->registration_needed = $registration_needed;
$this->card_billing_data_mode = $card_billing_data_mode;
$this->early_validation_enabled = $early_validation_enabled;
$this->logger = $logger;
}
@ -233,8 +246,14 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->set_bn_code( $data );
if ( 'pay-now' === $data['context'] && get_option( 'woocommerce_terms_page_id', '' ) !== '' ) {
$this->validate_paynow_form( $data['form'] );
$form_fields = $data['form'] ?? null;
if ( $this->early_validation_enabled && is_array( $form_fields ) ) {
$this->validate_form( $form_fields );
}
if ( 'pay-now' === $data['context'] && is_array( $form_fields ) && get_option( 'woocommerce_terms_page_id', '' ) !== '' ) {
$this->validate_paynow_form( $form_fields );
}
try {
@ -264,6 +283,13 @@ class CreateOrderEndpoint implements EndpointInterface {
wp_send_json_success( $order->to_array() );
return true;
} catch ( ValidationException $error ) {
wp_send_json_error(
array(
'message' => $error->getMessage(),
'errors' => $error->errors(),
)
);
} catch ( \RuntimeException $error ) {
$this->logger->error( 'Order creation failed: ' . $error->getMessage() );
@ -481,16 +507,33 @@ class CreateOrderEndpoint implements EndpointInterface {
return $payment_method;
}
/**
* Checks whether the form fields are valid.
*
* @param array $form_fields The form fields.
* @throws ValidationException When fields are not valid.
*/
private function validate_form( array $form_fields ): void {
try {
$v = new CheckoutFormValidator();
$v->validate( $form_fields );
} catch ( ValidationException $exception ) {
throw $exception;
} catch ( Throwable $exception ) {
$this->logger->error( "Form validation execution failed. {$exception->getMessage()} {$exception->getFile()}:{$exception->getLine()}" );
}
}
/**
* Checks whether the terms input field is checked.
*
* @param array $form_fields The form fields.
* @throws \RuntimeException When field is not checked.
* @throws ValidationException When field is not checked.
*/
private function validate_paynow_form( array $form_fields ) {
private function validate_paynow_form( array $form_fields ): void {
if ( isset( $form_fields['terms-field'] ) && ! isset( $form_fields['terms'] ) ) {
throw new \RuntimeException(
__( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce-paypal-payments' )
throw new ValidationException(
array( __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce-paypal-payments' ) )
);
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* ValidationException.
*
* @package WooCommerce\PayPalCommerce\Button\Exception
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Exception;
/**
* Class ValidationException
*/
class ValidationException extends RuntimeException {
/**
* The error messages.
*
* @var string[]
*/
protected $errors;
/**
* ValidationException constructor.
*
* @param string[] $errors The validation error messages.
* @param string $message The error message.
*/
public function __construct( array $errors, string $message = '' ) {
$this->errors = $errors;
if ( ! $message ) {
$message = implode( ' ', $errors );
}
parent::__construct( $message );
}
/**
* The error messages.
*
* @return string[]
*/
public function errors(): array {
return $this->errors;
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* Executes WC checkout validation.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Validation;
use WC_Checkout;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WP_Error;
/**
* Class FormValidator
*/
class CheckoutFormValidator extends WC_Checkout {
/**
* Validates the form data.
*
* @param array $data The form data.
* @return void
* @throws ValidationException When validation fails.
*/
public function validate( array $data ) {
$errors = new WP_Error();
if ( isset( $data['terms-field'] ) ) {
// WC checks this field via $_POST https://github.com/woocommerce/woocommerce/issues/35328 .
$_POST['terms-field'] = $data['terms-field'];
}
// It throws some notices when checking fields etc., also from other plugins via hooks.
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@$this->validate_checkout( $data, $errors );
if ( $errors->has_errors() ) {
throw new ValidationException( $errors->get_error_messages() );
}
}
}

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat\PPEC;
use stdClass;
use WooCommerce\PayPalCommerce\Subscription\RenewalHandler;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
@ -125,7 +126,7 @@ class SubscriptionsHandler {
$billing_agreement_id = $order->get_meta( '_ppec_billing_agreement_id', true );
if ( $billing_agreement_id ) {
$token = new PaymentToken( $billing_agreement_id, 'BILLING_AGREEMENT', new \stdClass() );
$token = new PaymentToken( $billing_agreement_id, new stdClass(), 'BILLING_AGREEMENT' );
}
}

View file

@ -171,7 +171,11 @@ class OrderTrackingEndpoint {
throw $error;
}
update_post_meta( $order_id, '_ppcp_paypal_tracking_number', $data['tracking_number'] ?? '' );
$wc_order = wc_get_order( $order_id );
if ( is_a( $wc_order, WC_Order::class ) ) {
$wc_order->update_meta_data( '_ppcp_paypal_tracking_number', $data['tracking_number'] ?? '' );
$wc_order->save();
}
do_action( 'woocommerce_paypal_payments_after_tracking_is_added', $order_id, $response );
}
@ -300,7 +304,11 @@ class OrderTrackingEndpoint {
throw $error;
}
update_post_meta( $order_id, '_ppcp_paypal_tracking_number', $data['tracking_number'] ?? '' );
$wc_order = wc_get_order( $order_id );
if ( is_a( $wc_order, WC_Order::class ) ) {
$wc_order->update_meta_data( '_ppcp_paypal_tracking_number', $data['tracking_number'] ?? '' );
$wc_order->save();
}
do_action( 'woocommerce_paypal_payments_after_tracking_is_updated', $order_id, $response );
}

View file

@ -13,6 +13,7 @@ use WC_Order;
use WC_Product;
use WC_Subscription;
use WC_Subscriptions_Product;
use WC_Subscriptions_Synchroniser;
/**
* Class FreeTrialHandlerTrait
@ -37,10 +38,7 @@ trait FreeTrialHandlerTrait {
foreach ( $cart->get_cart() as $item ) {
$product = $item['data'] ?? null;
if ( ! $product instanceof WC_Product ) {
continue;
}
if ( WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) {
if ( $product && WC_Subscriptions_Product::is_subscription( $product ) ) {
return true;
}
}
@ -60,9 +58,22 @@ trait FreeTrialHandlerTrait {
$product = wc_get_product();
return $product
&& WC_Subscriptions_Product::is_subscription( $product )
&& WC_Subscriptions_Product::get_trial_length( $product ) > 0;
if ( ! $product || ! WC_Subscriptions_Product::is_subscription( $product ) ) {
return false;
}
if ( WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) {
return true;
}
if ( WC_Subscriptions_Synchroniser::is_product_synced( $product ) && ! WC_Subscriptions_Synchroniser::is_payment_upfront( $product ) ) {
$date = WC_Subscriptions_Synchroniser::calculate_first_payment_date( $product, 'timestamp' );
if ( ! WC_Subscriptions_Synchroniser::is_today( $date ) ) {
return true;
}
}
return false;
}
/**
@ -82,13 +93,6 @@ trait FreeTrialHandlerTrait {
$subs = wcs_get_subscriptions_for_order( $wc_order );
return ! empty(
array_filter(
$subs,
function ( WC_Subscription $sub ): bool {
return (float) $sub->get_total_initial_payment() <= 0;
}
)
);
return ! empty( $subs );
}
}

View file

@ -120,8 +120,11 @@ class SubscriptionHelper {
* @return bool Whether page is change subscription or not.
*/
public function is_subscription_change_payment(): bool {
$pay_for_order = filter_input( INPUT_GET, 'pay_for_order', FILTER_SANITIZE_STRING );
$change_payment_method = filter_input( INPUT_GET, 'change_payment_method', FILTER_SANITIZE_STRING );
return ( isset( $pay_for_order ) && isset( $change_payment_method ) );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['pay_for_order'] ) || ! isset( $_GET['change_payment_method'] ) ) {
return false;
}
return true;
}
}

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Subscription;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
@ -141,17 +142,27 @@ class RenewalHandler {
public function renew( \WC_Order $wc_order ) {
try {
$this->process_order( $wc_order );
} catch ( \Exception $error ) {
$this->logger->error(
sprintf(
'An error occurred while trying to renew the subscription for order %1$d: %2$s',
$wc_order->get_id(),
$error->getMessage()
)
} catch ( \Exception $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$wc_order->update_status(
'failed',
$error
);
$error_message = sprintf(
'An error occurred while trying to renew the subscription for order %1$d: %2$s',
$wc_order->get_id(),
$error
);
$this->logger->error( $error_message );
return;
}
$this->logger->info(
sprintf(
'Renewal for order %d is completed.',

View file

@ -100,7 +100,8 @@ class SubscriptionModule implements ModuleInterface {
add_filter(
'ppcp_create_order_request_body_data',
function( array $data ) use ( $c ) {
$wc_order_action = filter_input( INPUT_POST, 'wc_order_action', FILTER_SANITIZE_STRING ) ?? '';
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) );
if (
$wc_order_action === 'wcs_process_renewal'
&& isset( $data['payment_source']['token'] ) && $data['payment_source']['token']['type'] === 'PAYMENT_METHOD_TOKEN'
@ -175,9 +176,9 @@ class SubscriptionModule implements ModuleInterface {
try {
$tokens = $payment_token_repository->all_for_user_id( $subscription->get_customer_id() );
if ( $tokens ) {
$subscription_id = $subscription->get_id();
$latest_token_id = end( $tokens )->id() ? end( $tokens )->id() : '';
update_post_meta( $subscription_id, 'payment_token_id', $latest_token_id, true );
$subscription->update_meta_data( 'payment_token_id', $latest_token_id );
$subscription->save();
}
} catch ( RuntimeException $error ) {
$message = sprintf(
@ -216,7 +217,7 @@ class SubscriptionModule implements ModuleInterface {
&& PayPalGateway::ID === $id
&& $subscription_helper->is_subscription_change_payment()
) {
$tokens = $payment_token_repository->all_for_user_id( get_current_user_id() );
$tokens = $payment_token_repository->all_for_user_id( get_current_user_id() );
if ( ! $tokens || ! $payment_token_repository->tokens_contains_paypal( $tokens ) ) {
return esc_html__(
'No PayPal payments saved, in order to use a saved payment you first need to create it through a purchase.',
@ -224,10 +225,10 @@ class SubscriptionModule implements ModuleInterface {
);
}
$output = sprintf(
'<p class="form-row form-row-wide"><label>%1$s</label><select id="saved-paypal-payment" name="saved_paypal_payment">',
esc_html__( 'Select a saved PayPal payment', 'woocommerce-paypal-payments' )
);
$output = sprintf(
'<p class="form-row form-row-wide"><label>%1$s</label><select id="saved-paypal-payment" name="saved_paypal_payment">',
esc_html__( 'Select a saved PayPal payment', 'woocommerce-paypal-payments' )
);
foreach ( $tokens as $token ) {
if ( isset( $token->source()->paypal ) ) {
$output .= sprintf(

View file

@ -53,8 +53,9 @@ class CustomerApprovalListener {
* @return void
*/
public function listen(): void {
$token = filter_input( INPUT_GET, 'approval_token_id', FILTER_SANITIZE_STRING );
if ( ! is_string( $token ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$token = wc_clean( wp_unslash( $_GET['approval_token_id'] ?? '' ) );
if ( ! $token || is_array( $token ) ) {
return;
}
@ -94,7 +95,9 @@ class CustomerApprovalListener {
add_action(
'woocommerce_init',
function () use ( $message ): void {
wc_add_notice( $message, 'error' );
if ( function_exists( 'wc_add_notice' ) ) {
wc_add_notice( $message, 'error' );
}
}
);
}

View file

@ -143,15 +143,16 @@ class VaultedCreditCardHandler {
string $saved_credit_card,
WC_Order $wc_order
): WC_Order {
$change_payment = filter_input( INPUT_POST, 'woocommerce_change_payment', FILTER_SANITIZE_STRING );
if (
$change_payment
// phpcs:ignore WordPress.Security.NonceVerification.Missing
isset( $_POST['woocommerce_change_payment'] )
&& $this->subscription_helper->has_subscription( $wc_order->get_id() )
&& $this->subscription_helper->is_subscription_change_payment()
&& $saved_credit_card
) {
update_post_meta( $wc_order->get_id(), 'payment_token_id', $saved_credit_card );
$wc_order->update_meta_data( 'payment_token_id', $saved_credit_card );
$wc_order->save();
return $wc_order;
}

View file

@ -113,7 +113,11 @@ class VaultingModule implements ModuleInterface {
add_action(
'woocommerce_created_customer',
function( int $customer_id ) use ( $subscription_helper ) {
$guest_customer_id = WC()->session->get( 'ppcp_guest_customer_id' );
$session = WC()->session;
if ( ! $session ) {
return;
}
$guest_customer_id = $session->get( 'ppcp_guest_customer_id' );
if ( $guest_customer_id && $subscription_helper->cart_contains_subscription() ) {
update_user_meta( $customer_id, 'ppcp_guest_customer_id', $guest_customer_id );
}

View file

@ -1459,10 +1459,6 @@ return array(
$fields['disable_cards']['options'] = $card_options;
$fields['card_icons']['options'] = array_merge( $dark_versions, $card_options );
if ( defined( 'PPCP_FLAG_SEPARATE_APM_BUTTONS' ) && PPCP_FLAG_SEPARATE_APM_BUTTONS === false ) {
unset( $fields['allow_card_button_gateway'] );
}
return $fields;
},
@ -1578,7 +1574,9 @@ return array(
$container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'wcgateway.pay-upon-invoice-helper' ),
$container->get( 'wcgateway.checkout-helper' )
$container->get( 'wcgateway.checkout-helper' ),
$container->get( 'onboarding.state' ),
$container->get( 'wcgateway.processor.refunds' )
);
},
'wcgateway.pay-upon-invoice-fraudnet-session-id' => static function ( ContainerInterface $container ): FraudNetSessionId {
@ -1729,10 +1727,6 @@ return array(
return $container->get( 'api.shop.is-latin-america' );
},
'wcgateway.settings.allow_card_button_gateway' => static function ( ContainerInterface $container ): bool {
if ( defined( 'PPCP_FLAG_SEPARATE_APM_BUTTONS' ) && PPCP_FLAG_SEPARATE_APM_BUTTONS === false ) {
return false;
}
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof ContainerInterface );

View file

@ -127,14 +127,15 @@ class SettingsPageAssets {
}
$screen = get_current_screen();
$tab = filter_input( INPUT_GET, 'tab', FILTER_SANITIZE_STRING );
$section = filter_input( INPUT_GET, 'section', FILTER_SANITIZE_STRING );
if ( ! 'woocommerce_page_wc-settings' === $screen->id ) {
if ( $screen->id !== 'woocommerce_page_wc-settings' ) {
return false;
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$tab = wc_clean( wp_unslash( $_GET['tab'] ?? '' ) );
$section = wc_clean( wp_unslash( $_GET['section'] ?? '' ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return 'checkout' === $tab && 'ppcp-gateway' === $section;
}

View file

@ -275,9 +275,11 @@ class CardButtonGateway extends \WC_Payment_Gateway {
* If customer has chosen change Subscription payment.
*/
if ( $this->subscription_helper->has_subscription( $order_id ) && $this->subscription_helper->is_subscription_change_payment() ) {
$saved_paypal_payment = filter_input( INPUT_POST, 'saved_paypal_payment', FILTER_SANITIZE_STRING );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$saved_paypal_payment = wc_clean( wp_unslash( $_POST['saved_paypal_payment'] ?? '' ) );
if ( $saved_paypal_payment ) {
update_post_meta( $order_id, 'payment_token_id', $saved_paypal_payment );
$wc_order->update_meta_data( 'payment_token_id', $saved_paypal_payment );
$wc_order->save();
return $this->handle_payment_success( $wc_order );
}

View file

@ -360,7 +360,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
/**
* If customer has chosen a saved credit card payment.
*/
$saved_credit_card = filter_input( INPUT_POST, 'saved_credit_card', FILTER_SANITIZE_STRING );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$saved_credit_card = wc_clean( wp_unslash( $_POST['saved_credit_card'] ?? '' ) );
if ( $saved_credit_card ) {
try {
$wc_order = $this->vaulted_credit_card_handler->handle_payment(

View file

@ -138,7 +138,8 @@ class OXXO {
'add_meta_boxes',
function( string $post_type ) {
if ( $post_type === 'shop_order' ) {
$post_id = filter_input( INPUT_GET, 'post', FILTER_SANITIZE_STRING );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$post_id = wc_clean( wp_unslash( $_GET['post'] ?? '' ) );
$order = wc_get_order( $post_id );
if ( is_a( $order, WC_Order::class ) && $order->get_payment_method() === OXXOGateway::ID ) {
$payer_action = $order->get_meta( 'ppcp_oxxo_payer_action' );
@ -182,7 +183,8 @@ class OXXO {
return false;
}
$billing_country = filter_input( INPUT_POST, 'country', FILTER_SANITIZE_STRING ) ?? null;
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$billing_country = wc_clean( wp_unslash( $_POST['country'] ?? '' ) );
if ( $billing_country && 'MX' !== $billing_country ) {
return false;
}

View file

@ -77,7 +77,7 @@ class OXXOGateway extends WC_Payment_Gateway {
$this->id = self::ID;
$this->method_title = __( 'OXXO', 'woocommerce-paypal-payments' );
$this->method_description = __( 'OXXO is a Mexican chain of convenience stores.', 'woocommerce-paypal-payments' );
$this->method_description = __( 'OXXO is a Mexican chain of convenience stores.<br />*Get PayPal account permission to use OXXO payment functionality by contacting us at (+52) 800-925-0304', 'woocommerce-paypal-payments' );
$this->title = $this->get_option( 'title', $this->method_title );
$this->description = $this->get_option( 'description', __( 'OXXO allows you to pay bills and online purchases in-store with cash.', 'woocommerce-paypal-payments' ) );
@ -167,17 +167,8 @@ class OXXOGateway extends WC_Payment_Gateway {
}
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) && is_array( $exception->details() ) ) {
$details = '';
foreach ( $exception->details() as $detail ) {
$issue = $detail->issue ?? '';
$field = $detail->field ?? '';
$description = $detail->description ?? '';
$details .= $issue . ' ' . $field . ' ' . $description . '<br>';
}
$error = $details;
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$this->logger->error( $error );

View file

@ -423,8 +423,8 @@ class PayPalGateway extends \WC_Payment_Gateway {
);
}
$funding_source = filter_input( INPUT_POST, 'ppcp-funding-source', FILTER_SANITIZE_STRING );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$funding_source = wc_clean( wp_unslash( $_POST['ppcp-funding-source'] ?? '' ) );
if ( 'card' !== $funding_source && $this->is_free_trial_order( $wc_order ) ) {
$user_id = (int) $wc_order->get_customer_id();
$tokens = $this->payment_token_repository->all_for_user_id( $user_id );
@ -446,9 +446,11 @@ class PayPalGateway extends \WC_Payment_Gateway {
* If customer has chosen change Subscription payment.
*/
if ( $this->subscription_helper->has_subscription( $order_id ) && $this->subscription_helper->is_subscription_change_payment() ) {
$saved_paypal_payment = filter_input( INPUT_POST, 'saved_paypal_payment', FILTER_SANITIZE_STRING );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$saved_paypal_payment = wc_clean( wp_unslash( $_POST['saved_paypal_payment'] ?? '' ) );
if ( $saved_paypal_payment ) {
update_post_meta( $order_id, 'payment_token_id', $saved_paypal_payment );
$wc_order->update_meta_data( 'payment_token_id', $saved_paypal_payment );
$wc_order->save();
return $this->handle_payment_success( $wc_order );
}

View file

@ -33,7 +33,8 @@ class FraudNetSessionId {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['pay_for_order'] ) && 'true' === $_GET['pay_for_order'] ) {
$pui_pay_for_order_session_id = filter_input( INPUT_POST, 'pui_pay_for_order_session_id', FILTER_SANITIZE_STRING );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$pui_pay_for_order_session_id = wc_clean( wp_unslash( $_POST['pui_pay_for_order_session_id'] ?? '' ) );
if ( $pui_pay_for_order_session_id && '' !== $pui_pay_for_order_session_id ) {
return $pui_pay_for_order_session_id;
}

View file

@ -251,16 +251,24 @@ class PayUponInvoice {
$order = $this->pui_order_endpoint->order( $order_id );
$payment_instructions = array(
$order->payment_source->pay_upon_invoice->payment_reference,
$order->payment_source->pay_upon_invoice->deposit_bank_details,
);
$wc_order->update_meta_data(
'ppcp_ratepay_payment_instructions_payment_reference',
$payment_instructions
);
$wc_order->save_meta_data();
$this->logger->info( "Ratepay payment instructions added to order #{$wc_order->get_id()}." );
if (
property_exists( $order, 'payment_source' )
&& property_exists( $order->payment_source, 'pay_upon_invoice' )
&& property_exists( $order->payment_source->pay_upon_invoice, 'payment_reference' )
&& property_exists( $order->payment_source->pay_upon_invoice, 'deposit_bank_details' )
) {
$payment_instructions = array(
$order->payment_source->pay_upon_invoice->payment_reference,
$order->payment_source->pay_upon_invoice->deposit_bank_details,
);
$wc_order->update_meta_data(
'ppcp_ratepay_payment_instructions_payment_reference',
$payment_instructions
);
$wc_order->save_meta_data();
$this->logger->info( "Ratepay payment instructions added to order #{$wc_order->get_id()}." );
}
$capture = $this->capture_factory->from_paypal_response( $order->purchase_units[0]->payments->captures[0] );
$breakdown = $capture->seller_receivable_breakdown();
@ -409,7 +417,8 @@ class PayUponInvoice {
add_action(
'woocommerce_after_checkout_validation',
function( array $fields, WP_Error $errors ) {
$payment_method = filter_input( INPUT_POST, 'payment_method', FILTER_SANITIZE_STRING );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$payment_method = wc_clean( wp_unslash( $_POST['payment_method'] ?? '' ) );
if ( PayUponInvoiceGateway::ID !== $payment_method ) {
return;
}
@ -418,12 +427,14 @@ class PayUponInvoice {
$errors->add( 'validation', __( 'Billing country not available.', 'woocommerce-paypal-payments' ) );
}
$birth_date = filter_input( INPUT_POST, 'billing_birth_date', FILTER_SANITIZE_STRING );
if ( ( $birth_date && ! $this->checkout_helper->validate_birth_date( $birth_date ) ) || $birth_date === '' ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$birth_date = wc_clean( wp_unslash( $_POST['billing_birth_date'] ?? '' ) );
if ( ( $birth_date && is_string( $birth_date ) && ! $this->checkout_helper->validate_birth_date( $birth_date ) ) || $birth_date === '' ) {
$errors->add( 'validation', __( 'Invalid birth date.', 'woocommerce-paypal-payments' ) );
}
$national_number = filter_input( INPUT_POST, 'billing_phone', FILTER_SANITIZE_STRING );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$national_number = wc_clean( wp_unslash( $_POST['billing_phone'] ?? '' ) );
if ( ! $national_number ) {
$errors->add( 'validation', __( 'Phone field cannot be empty.', 'woocommerce-paypal-payments' ) );
}
@ -484,18 +495,9 @@ class PayUponInvoice {
add_action(
'woocommerce_update_options_checkout_ppcp-pay-upon-invoice-gateway',
function () {
$customer_service_instructions = filter_input( INPUT_POST, 'woocommerce_ppcp-pay-upon-invoice-gateway_customer_service_instructions', FILTER_SANITIZE_STRING );
if ( '' === $customer_service_instructions ) {
$gateway_settings = get_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings' );
$gateway_enabled = $gateway_settings['enabled'] ?? '';
if ( 'yes' === $gateway_enabled ) {
$gateway_settings['enabled'] = 'no';
update_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings', $gateway_settings );
$redirect_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-pay-upon-invoice-gateway' );
wp_safe_redirect( $redirect_url );
exit;
}
$gateway = WC()->payment_gateways()->payment_gateways()[ PayUponInvoiceGateway::ID ];
if ( $gateway && $gateway->get_option( 'customer_service_instructions' ) === '' ) {
$gateway->update_option( 'enabled', 'no' );
}
}
);
@ -509,13 +511,18 @@ class PayUponInvoice {
) {
$error_messages = array();
$pui_gateway = WC()->payment_gateways->payment_gateways()[ PayUponInvoiceGateway::ID ];
if ( $pui_gateway->get_option( 'brand_name' ) === '' ) {
$error_messages[] = esc_html__( 'Could not enable gateway because "Brand name" field is empty.', 'woocommerce-paypal-payments' );
}
if ( $pui_gateway->get_option( 'logo_url' ) === '' ) {
$error_messages[] = esc_html__( 'Could not enable gateway because "Logo URL" field is empty.', 'woocommerce-paypal-payments' );
}
if ( $pui_gateway->get_option( 'customer_service_instructions' ) === '' ) {
$error_messages[] = esc_html__( 'Could not enable gateway because "Customer service instructions" field is empty.', 'woocommerce-paypal-payments' );
}
if ( count( $error_messages ) > 0 ) { ?>
if ( count( $error_messages ) > 0 ) {
$pui_gateway->update_option( 'enabled', 'no' );
?>
<div class="notice notice-error">
<?php
array_map(
@ -537,7 +544,8 @@ class PayUponInvoice {
'add_meta_boxes',
function( string $post_type ) {
if ( $post_type === 'shop_order' ) {
$post_id = filter_input( INPUT_GET, 'post', FILTER_SANITIZE_STRING );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$post_id = wc_clean( wp_unslash( $_GET['post'] ?? '' ) );
$order = wc_get_order( $post_id );
if ( is_a( $order, WC_Order::class ) && $order->get_payment_method() === PayUponInvoiceGateway::ID ) {
$instructions = $order->get_meta( 'ppcp_ratepay_payment_instructions_payment_reference' );

View file

@ -17,10 +17,12 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PayUponInvoiceOrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper;
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class PayUponInvoiceGateway.
@ -87,6 +89,20 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
*/
protected $checkout_helper;
/**
* The onboarding state.
*
* @var State
*/
protected $state;
/**
* The refund processor.
*
* @var RefundProcessor
*/
protected $refund_processor;
/**
* PayUponInvoiceGateway constructor.
*
@ -98,6 +114,8 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
* @param LoggerInterface $logger The logger.
* @param PayUponInvoiceHelper $pui_helper The PUI helper.
* @param CheckoutHelper $checkout_helper The checkout helper.
* @param State $state The onboarding state.
* @param RefundProcessor $refund_processor The refund processor.
*/
public function __construct(
PayUponInvoiceOrderEndpoint $order_endpoint,
@ -107,7 +125,9 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
TransactionUrlProvider $transaction_url_provider,
LoggerInterface $logger,
PayUponInvoiceHelper $pui_helper,
CheckoutHelper $checkout_helper
CheckoutHelper $checkout_helper,
State $state,
RefundProcessor $refund_processor
) {
$this->id = self::ID;
@ -137,6 +157,12 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
$this->transaction_url_provider = $transaction_url_provider;
$this->pui_helper = $pui_helper;
$this->checkout_helper = $checkout_helper;
$this->state = $state;
if ( $state->current_state() === State::STATE_ONBOARDED ) {
$this->supports = array( 'refunds' );
}
$this->refund_processor = $refund_processor;
}
/**
@ -202,10 +228,11 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
$birth_date = filter_input( INPUT_POST, 'billing_birth_date', FILTER_SANITIZE_STRING ) ?? '';
$pay_for_order = filter_input( INPUT_GET, 'pay_for_order', FILTER_SANITIZE_STRING );
$wc_order = wc_get_order( $order_id );
// phpcs:disable WordPress.Security.NonceVerification.Missing
$birth_date = wc_clean( wp_unslash( $_POST['billing_birth_date'] ?? '' ) );
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$pay_for_order = wc_clean( wp_unslash( $_GET['pay_for_order'] ?? '' ) );
if ( 'true' === $pay_for_order ) {
if ( ! $this->checkout_helper->validate_birth_date( $birth_date ) ) {
wc_add_notice( 'Invalid birth date.', 'error' );
@ -215,7 +242,8 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
}
}
$phone_number = filter_input( INPUT_POST, 'billing_phone', FILTER_SANITIZE_STRING ) ?? '';
$phone_number = wc_clean( wp_unslash( $_POST['billing_phone'] ?? '' ) );
// phpcs:enable WordPress.Security.NonceVerification.Missing
if ( $phone_number ) {
$wc_order->set_billing_phone( $phone_number );
$wc_order->save();
@ -246,17 +274,8 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
);
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) && is_array( $exception->details() ) ) {
$details = '';
foreach ( $exception->details() as $detail ) {
$issue = $detail->issue ?? '';
$field = $detail->field ?? '';
$description = $detail->description ?? '';
$details .= $issue . ' ' . $field . ' ' . $description . '<br>';
}
$error = $details;
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$this->logger->error( $error );
@ -274,6 +293,22 @@ class PayUponInvoiceGateway extends WC_Payment_Gateway {
}
}
/**
* Process refund.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, \WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
/**
* Return transaction url for this gateway and given order.
*

View file

@ -24,8 +24,12 @@ class PaymentSourceFactory {
* @return PaymentSource
*/
public function from_wc_order( WC_Order $order, string $birth_date ) {
$address = $order->get_address();
$phone = filter_input( INPUT_POST, 'billing_phone', FILTER_SANITIZE_STRING ) ?? $address['phone'] ?: '';
$address = $order->get_address();
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$phone = wc_clean( wp_unslash( $_POST['billing_phone'] ?? '' ) );
if ( ! $phone ) {
$phone = $address['phone'] ?? '';
}
$phone_country_code = WC()->countries->get_country_calling_code( $address['country'] );
$phone_country_code = is_array( $phone_country_code ) && ! empty( $phone_country_code ) ? $phone_country_code[0] : $phone_country_code;
if ( is_string( $phone_country_code ) && '' !== $phone_country_code ) {

View file

@ -35,14 +35,6 @@ class CheckoutHelper {
if ( $cart_total < $minimum || $cart_total > $maximum ) {
return false;
}
$items = $cart->get_cart_contents();
foreach ( $items as $item ) {
$product = wc_get_product( $item['product_id'] );
if ( is_a( $product, WC_Product::class ) && ! $this->is_physical_product( $product ) ) {
return false;
}
}
}
if ( is_wc_endpoint_url( 'order-pay' ) ) {
@ -61,15 +53,6 @@ class CheckoutHelper {
if ( $order_total < $minimum || $order_total > $maximum ) {
return false;
}
foreach ( $order->get_items() as $item_id => $item ) {
if ( is_a( $item, WC_Order_Item_Product::class ) ) {
$product = wc_get_product( $item->get_product_id() );
if ( is_a( $product, WC_Product::class ) && ! $this->is_physical_product( $product ) ) {
return false;
}
}
}
}
}
}

View file

@ -10,6 +10,7 @@ declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Helper;
use WC_Order;
use WC_Order_Item_Product;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -54,22 +55,83 @@ class PayUponInvoiceHelper {
return false;
}
$billing_country = filter_input( INPUT_POST, 'country', FILTER_SANITIZE_STRING ) ?? null;
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$billing_country = wc_clean( wp_unslash( $_POST['country'] ?? '' ) );
if ( $billing_country && 'DE' !== $billing_country ) {
return false;
}
if ( ! $this->is_valid_currency() ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$shipping_country = wc_clean( wp_unslash( $_POST['s_country'] ?? '' ) );
if ( $shipping_country && 'DE' !== $shipping_country ) {
return false;
}
if ( ! $this->checkout_helper->is_checkout_amount_allowed( 5, 2500 ) ) {
if (
! $this->is_valid_product()
|| ! $this->is_valid_currency()
|| ! $this->checkout_helper->is_checkout_amount_allowed( 5, 2500 )
) {
return false;
}
return true;
}
/**
* Checks whether PUI gateway is enabled.
*
* @return bool True if PUI gateway is enabled, otherwise false.
*/
public function is_pui_gateway_enabled(): bool {
$gateway_settings = get_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings' );
return isset( $gateway_settings['enabled'] ) && $gateway_settings['enabled'] === 'yes' && 'DE' === $this->api_shop_country;
}
/**
* Checks if product is valid for PUI.
*
* @return bool
*/
private function is_valid_product(): bool {
$cart = WC()->cart ?? null;
if ( $cart && ! is_checkout_pay_page() ) {
$items = $cart->get_cart_contents();
foreach ( $items as $item ) {
$product = wc_get_product( $item['product_id'] );
if ( $product && ! $this->checkout_helper->is_physical_product( $product ) ) {
return false;
}
}
}
if ( is_wc_endpoint_url( 'order-pay' ) ) {
/**
* Needed for WordPress `query_vars`.
*
* @psalm-suppress InvalidGlobal
*/
global $wp;
if ( isset( $wp->query_vars['order-pay'] ) && absint( $wp->query_vars['order-pay'] ) > 0 ) {
$order_id = absint( $wp->query_vars['order-pay'] );
$order = wc_get_order( $order_id );
if ( is_a( $order, WC_Order::class ) ) {
foreach ( $order->get_items() as $item_id => $item ) {
if ( is_a( $item, WC_Order_Item_Product::class ) ) {
$product = wc_get_product( $item->get_product_id() );
if ( $product && ! $this->checkout_helper->is_physical_product( $product ) ) {
return false;
}
}
}
}
}
}
return true;
}
/**
* Checks if currency is allowed for PUI.
*
@ -89,14 +151,4 @@ class PayUponInvoiceHelper {
return false;
}
/**
* Checks whether PUI gateway is enabled.
*
* @return bool True if PUI gateway is enabled, otherwise false.
*/
public function is_pui_gateway_enabled(): bool {
$gateway_settings = get_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings' );
return isset( $gateway_settings['enabled'] ) && $gateway_settings['enabled'] === 'yes' && 'DE' === $this->api_shop_country;
}
}

View file

@ -252,9 +252,7 @@ class WCGatewayModule implements ModuleInterface {
( $c->get( 'wcgateway.pay-upon-invoice' ) )->init();
}
if ( defined( 'PPCP_FLAG_OXXO' ) && PPCP_FLAG_OXXO === true ) {
( $c->get( 'wcgateway.oxxo' ) )->init();
}
( $c->get( 'wcgateway.oxxo' ) )->init();
}
);
@ -288,10 +286,6 @@ class WCGatewayModule implements ModuleInterface {
add_action(
'wc_ajax_ppc-oxxo',
static function () use ( $c ) {
if ( defined( 'PPCP_FLAG_OXXO' ) && PPCP_FLAG_OXXO === false ) {
return;
}
$endpoint = $c->get( 'wcgateway.endpoint.oxxo' );
$endpoint->handle_request();
}
@ -364,7 +358,7 @@ class WCGatewayModule implements ModuleInterface {
$methods[] = $container->get( 'wcgateway.pay-upon-invoice-gateway' );
}
if ( defined( 'PPCP_FLAG_OXXO' ) && PPCP_FLAG_OXXO === true && 'MX' === $shop_country ) {
if ( 'MX' === $shop_country ) {
$methods[] = $container->get( 'wcgateway.oxxo-gateway' );
}

View file

@ -166,6 +166,7 @@ class CreateOrderEndpointTest extends TestCase
$early_order_handler,
false,
CardBillingMode::MINIMAL_INPUT,
false,
new NullLogger()
);
return array($payer_factory, $testee);

View file

@ -52,7 +52,7 @@ class PaymentTokenRepositoryTest extends TestCase
{
$id = 1;
$source = new \stdClass();
$paymentToken = new PaymentToken('foo', 'PAYMENT_METHOD_TOKEN', $source);
$paymentToken = new PaymentToken('foo', $source, 'PAYMENT_METHOD_TOKEN');
when('get_user_meta')->justReturn([]);
$this->endpoint->shouldReceive('for_user')

View file

@ -70,12 +70,15 @@ class VaultedCreditCardHandlerTest extends TestCase
public function testHandlePaymentChangingPayment()
{
when('filter_input')->justReturn(1);
$_POST['woocommerce_change_payment'] = 1;
$wcOrder = Mockery::mock(\WC_Order::class);
$wcOrder->shouldReceive('get_id')->andReturn(1);
$wcOrder->shouldReceive('update_meta_data')
->with('payment_token_id', 'abc123')
->andReturn(1);
$wcOrder->shouldReceive('save')->andReturn(1);
$this->subscriptionHelper->shouldReceive('has_subscription')->andReturn(true);
$this->subscriptionHelper->shouldReceive('is_subscription_change_payment')->andReturn(true);
expect('update_post_meta')->with(1, 'payment_token_id', 'abc123');
$customer = Mockery::mock(WC_Customer::class);
@ -85,6 +88,8 @@ class VaultedCreditCardHandlerTest extends TestCase
public function testHandlePayment()
{
$_POST['woocommerce_change_payment'] = null;
$wcOrder = Mockery::mock(\WC_Order::class);
$wcOrder->shouldReceive('get_id')->andReturn(1);
$wcOrder->shouldReceive('get_customer_id')->andReturn(1);

View file

@ -55,6 +55,8 @@ class CreditCardGatewayTest extends TestCase
$this->config->shouldReceive('has')->andReturn(true);
$this->config->shouldReceive('get')->andReturn('');
when('wc_clean')->returnArg();
$this->testee = new CreditCardGateway(
$this->settingsRenderer,
$this->orderProcessor,
@ -94,7 +96,7 @@ class CreditCardGatewayTest extends TestCase
when('wc_get_order')->justReturn($wc_order);
$savedCreditCard = 'abc123';
when('filter_input')->justReturn($savedCreditCard);
$_POST['saved_credit_card'] = $savedCreditCard;
$this->vaultedCreditCardHandler
->shouldReceive('handle_payment')

View file

@ -11,10 +11,12 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper;
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use function Brain\Monkey\Functions\when;
class PayUponInvoiceGatewayTest extends TestCase
@ -28,6 +30,8 @@ class PayUponInvoiceGatewayTest extends TestCase
private $testee;
private $pui_helper;
private $checkout_helper;
private $state;
private $refund_processor;
public function setUp(): void
{
@ -42,7 +46,13 @@ class PayUponInvoiceGatewayTest extends TestCase
$this->pui_helper = Mockery::mock(PayUponInvoiceHelper::class);
$this->checkout_helper = Mockery::mock(CheckoutHelper::class);
$this->state = Mockery::mock(State::class);
$this->state->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED);
$this->refund_processor = Mockery::mock(RefundProcessor::class);
$this->setInitStubs();
when('wc_clean')->returnArg();
$this->testee = new PayUponInvoiceGateway(
$this->order_endpoint,
@ -52,7 +62,9 @@ class PayUponInvoiceGatewayTest extends TestCase
$this->transaction_url_provider,
$this->logger,
$this->pui_helper,
$this->checkout_helper
$this->checkout_helper,
$this->state,
$this->refund_processor
);
}

View file

@ -46,6 +46,7 @@ class WcGatewayTest extends TestCase
expect('is_admin')->andReturnUsing(function () {
return $this->isAdmin;
});
when('wc_clean')->returnArg();
$this->settingsRenderer = Mockery::mock(SettingsRenderer::class);
$this->orderProcessor = Mockery::mock(OrderProcessor::class);

View file

@ -26,8 +26,6 @@ define( 'PAYPAL_SANDBOX_API_URL', 'https://api.sandbox.paypal.com' );
define( 'PAYPAL_INTEGRATION_DATE', '2022-04-13' );
define( 'PPCP_FLAG_SUBSCRIPTION', true );
define( 'PPCP_FLAG_OXXO', apply_filters( 'woocommerce_paypal_payments_enable_oxxo_feature', false ) );
define( 'PPCP_FLAG_SEPARATE_APM_BUTTONS', apply_filters( 'woocommerce_paypal_payments_enable_separate_apm_buttons_feature', false ) );
! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' );
! defined( 'CONNECT_WOO_SANDBOX_CLIENT_ID' ) && define( 'CONNECT_WOO_SANDBOX_CLIENT_ID', 'AYmOHbt1VHg-OZ_oihPdzKEVbU3qg0qXonBcAztuzniQRaKE0w1Hr762cSFwd4n8wxOl-TCWohEa0XM_' );
@ -183,6 +181,15 @@ define( 'PPCP_FLAG_SEPARATE_APM_BUTTONS', apply_filters( 'woocommerce_paypal_pay
2
);
add_action(
'before_woocommerce_init',
function() {
if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
}
}
);
/**
* Check if WooCommerce is active.
*