Merge branch 'trunk' into PCP-190-override-language-used-to-display-PayPal-buttons

# Conflicts:
#	modules/ppcp-button/services.php
#	modules/ppcp-button/src/Assets/SmartButton.php
#	modules/ppcp-wc-gateway/services.php
This commit is contained in:
Narek Zakarian 2023-10-31 15:19:44 +04:00
commit e3fbadf419
No known key found for this signature in database
GPG key ID: 07AFD7E7A9C164A7
465 changed files with 41582 additions and 6679 deletions

View file

@ -0,0 +1,10 @@
name: phpmyadmin
repository: ddev/ddev-phpmyadmin
version: v0.3.0
install_date: "2023-08-02T12:20:36+02:00"
project_files:
- docker-compose.phpmyadmin.yaml
- docker-compose.phpmyadmin-norouter.yaml
- commands/host/phpmyadmin
global_files: []
removal_actions: []

View file

@ -0,0 +1,14 @@
name: playwright
repository: julienloizelet/ddev-playwright
version: v2.0.1
install_date: "2023-07-28T14:52:54+02:00"
project_files:
- commands/playwright/playwright
- commands/playwright/playwright-install
- playwright-build/Dockerfile
- playwright-build/kasmvnc.yaml
- playwright-build/xstartup
- playwright-build/entrypoint.sh
- docker-compose.playwright.yaml
global_files: []
removal_actions: []

52
.ddev/commands/host/phpmyadmin Executable file
View file

@ -0,0 +1,52 @@
#!/bin/bash
## #ddev-generated: If you want to edit and own this file, remove this line.
## Description: Launch a browser with PhpMyAdmin
## Usage: phpmyadmin
## Example: "ddev phpmyadmin"
DDEV_PHPMYADMIN_PORT=8036
DDEV_PHPMYADMIN_HTTPS_PORT=8037
FULLURL=${DDEV_PRIMARY_URL}
HTTPS=""
if [ ${DDEV_PRIMARY_URL%://*} = "https" ]; then HTTPS=true; fi
if [[ ! -z "${GITPOD_INSTANCE_ID}" ]] || [[ "${CODESPACES}" == "true" ]]; then
FULLURL="${FULLURL/-${DDEV_HOST_WEBSERVER_PORT}/-${DDEV_PHPMYADMIN_PORT}}"
else
if [ "${HTTPS}" = "" ]; then
FULLURL="${FULLURL%:[0-9]*}:${DDEV_PHPMYADMIN_PORT}"
else
FULLURL="${FULLURL%:[0-9]*}:${DDEV_PHPMYADMIN_HTTPS_PORT}"
fi
fi
if [ -n "${1:-}" ] ; then
if [[ ${1::1} != "/" ]] ; then
FULLURL="${FULLURL}/";
fi
FULLURL="${FULLURL}${1}";
fi
if [ "${DDEV_DEBUG:-}" = "true" ]; then
printf "FULLURL $FULLURL\n" && exit 0
fi
case $OSTYPE in
linux-gnu)
if [[ ! -z "${GITPOD_INSTANCE_ID}" ]]; then
gp preview ${FULLURL}
else
xdg-open ${FULLURL}
fi
;;
"darwin"*)
open ${FULLURL}
;;
"win*"* | "msys"*)
start ${FULLURL}
;;
esac

View file

@ -0,0 +1,14 @@
#!/bin/bash
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
cd /var/www/html || exit 1
cd "${PLAYWRIGHT_TEST_DIR}" || exit 1
export PLAYWRIGHT_BROWSERS_PATH=0
PRE="sudo -u pwuser PLAYWRIGHT_BROWSERS_PATH=0 "
$PRE yarn playwright "$@"

View file

@ -0,0 +1,17 @@
#!/bin/bash
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
cd /var/www/html || exit 1
cd "${PLAYWRIGHT_TEST_DIR}" || exit 1
export PLAYWRIGHT_BROWSERS_PATH=0
PRE="sudo -u pwuser PLAYWRIGHT_BROWSERS_PATH=0 "
$PRE yarn install
$PRE yarn playwright install --with-deps
# Conditionally copy an .env file if an example file exists
[ -f .env.example ] && [ ! -f .env ] && $PRE cp -n .env.example .env; exit 0

38
.ddev/commands/web/wp-cleanup Executable file
View file

@ -0,0 +1,38 @@
#!/bin/bash
show-help() {
echo -e "\nDelete all posts -p [post type]"
echo -e "\tExample: ddev wp-cleanup -p shop_order,product"
echo -e "\nDelete all logs -l [wp-content path]"
echo -e "\tExample: ddev wp-cleanup -l uploads/wc-logs\n"
}
delete-posts() {
for post in $(wp post list --post_type=$1 --format=ids --path=.ddev/wordpress); do
wp post delete $post --force --path=.ddev/wordpress
done
}
delete-logs() {
rm .ddev/wordpress/wp-content/$1/*.log
}
declare -i param_counter=0
while getopts "p:l:h" arg; do
case $arg in
p)
delete-posts $OPTARG
param_counter+=1
;;
l)
delete-logs $OPTARG
param_counter+=1
;;
h) show-help ;;
esac
done
if [ $param_counter -eq 0 ]; then
show-help
fi

View file

@ -18,7 +18,7 @@ hooks:
pre-start:
- exec-host: "mkdir -p .ddev/wordpress/wp-content/plugins/${DDEV_PROJECT}"
web_environment:
- WP_VERSION=5.9.3
- WP_VERSION=6.2.2
- WP_LOCALE=en_US
- WP_TITLE=WooCommerce PayPal Payments
- WP_MULTISITE=true
@ -26,7 +26,7 @@ web_environment:
- ADMIN_USER=admin
- ADMIN_PASS=admin
- ADMIN_EMAIL=admin@example.com
- WC_VERSION=6.1.0
- WC_VERSION=7.7.2
# Key features of ddev's config.yaml:

View file

@ -0,0 +1,4 @@
#ddev-generated
# If omit_containers[ddev-router] then this file will be replaced
# with another with a `ports` statement to directly expose port 80 to 8036
services: {}

View file

@ -0,0 +1,27 @@
#ddev-generated
services:
phpmyadmin:
container_name: ddev-${DDEV_SITENAME}-phpmyadmin
image: phpmyadmin:5
working_dir: "/root"
restart: "no"
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: $DDEV_APPROOT
expose:
- "80"
environment:
- PMA_USER=root
- PMA_PASSWORD=root
- PMA_HOST=db
- PMA_PORT=3306
- VIRTUAL_HOST=$DDEV_HOSTNAME
- UPLOAD_LIMIT=4000M
- HTTP_EXPOSE=8036:80
- HTTPS_EXPOSE=8037:80
healthcheck:
interval: 120s
timeout: 2s
retries: 1
depends_on:
- db

View file

@ -0,0 +1,38 @@
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
services:
playwright:
build:
context: playwright-build
container_name: ddev-${DDEV_SITENAME}-playwright
hostname: ${DDEV_SITENAME}-playwright
# These labels ensure this service is discoverable by ddev.
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: $DDEV_APPROOT
environment:
# Modify the PLAYWRIGHT_TEST_DIR folder path to suit your needs
- PLAYWRIGHT_TEST_DIR=tests/Playwright
- NETWORK_IFACE=eth0
- DISPLAY=:1
- VIRTUAL_HOST=$DDEV_HOSTNAME
- HTTP_EXPOSE=8443:8444,9322:9323
- HTTPS_EXPOSE=8444:8444,9323:9323
- DDEV_UID=${DDEV_UID}
- DDEV_GID=${DDEV_GID}
expose:
- "8444"
- "9323"
depends_on:
- web
volumes:
- .:/mnt/ddev_config
- ddev-global-cache:/mnt/ddev-global-cache
- ../:/var/www/html:rw
external_links:
- ddev-router:${DDEV_HOSTNAME}
working_dir: /var/www/html

View file

@ -0,0 +1,57 @@
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
# If on arm64 machine, edit to use mcr.microsoft.com/playwright:focal-arm64
FROM mcr.microsoft.com/playwright:focal
# Debian images by default disable apt caching, so turn it on until we finish
# the build.
RUN mv /etc/apt/apt.conf.d/docker-clean /etc/apt/docker-clean-disabled
USER root
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y sudo
# Give the pwuser user full `sudo` privileges
RUN echo "pwuser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/pwuser \
&& chmod 0440 /etc/sudoers.d/pwuser
# CAROOT for `mkcert` to use, has the CA config
ENV CAROOT=/mnt/ddev-global-cache/mkcert
# Install the correct architecture binary of `mkcert`
RUN export TARGETPLATFORM=linux/$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') && mkdir -p /usr/local/bin && curl --fail -JL -s -o /usr/local/bin/mkcert "https://dl.filippo.io/mkcert/latest?for=${TARGETPLATFORM}"
RUN chmod +x /usr/local/bin/mkcert
# Install a window manager.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y icewm xauth
# Install kasmvnc for remote access.
RUN /bin/bash -c 'if [ $(arch) == "aarch64" ]; then KASM_ARCH=arm64; else KASM_ARCH=amd64; fi; wget https://github.com/kasmtech/KasmVNC/releases/download/v1.1.0/kasmvncserver_bullseye_1.1.0_${KASM_ARCH}.deb'
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get install -y ./kasmvncserver*.deb
# We're done with apt so disable caching again for the final image.
RUN mv /etc/apt/docker-clean-disabled /etc/apt/apt.conf.d/docker-clean
# prepare KasmVNC
RUN sudo -u pwuser mkdir /home/pwuser/.vnc
COPY kasmvnc.yaml xstartup /home/pwuser/.vnc/
RUN chown pwuser:pwuser /home/pwuser/.vnc/*
RUN sudo -u pwuser touch /home/pwuser/.vnc/.de-was-selected
RUN sudo -u pwuser /bin/bash -c 'echo -e "secret\nsecret\n" | kasmvncpasswd -wo -u pwuser' # We actually disable auth, but KASM complains without it
COPY entrypoint.sh /root/entrypoint.sh
ENTRYPOINT "/root/entrypoint.sh"

View file

@ -0,0 +1,18 @@
#!/bin/bash
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
# Change pwuser IDs to the host IDs supplied by DDEV
usermod -u ${DDEV_UID} pwuser
groupmod -g ${DDEV_GID} pwuser
usermod -a -G ssl-cert pwuser
# Install DDEV certificate
mkcert -install
# Run CMD from parameters as pwuser
sudo -u pwuser vncserver -fg -disableBasicAuth

View file

@ -0,0 +1,14 @@
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
logging:
log_writer_name: all
log_dest: syslog
level: 100
network:
ssl:
require_ssl: false

32
.ddev/playwright-build/xstartup Executable file
View file

@ -0,0 +1,32 @@
#!/bin/sh
#ddev-generated
# Remove the line above if you don't want this file to be overwritten when you run
# ddev get julienloizelet/ddev-playwright
#
# This file comes from https://github.com/julienloizelet/ddev-playwright
#
export DISPLAY=:1
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
OS=`uname -s`
if [ $OS = 'Linux' ]; then
case "$WINDOWMANAGER" in
*gnome*)
if [ -e /etc/SuSE-release ]; then
PATH=$PATH:/opt/gnome/bin
export PATH
fi
;;
esac
fi
if [ -x /etc/X11/xinit/xinitrc ]; then
exec /etc/X11/xinit/xinitrc
fi
if [ -f /etc/X11/xinit/xinitrc ]; then
exec sh /etc/X11/xinit/xinitrc
fi
[ -r $HOME/.Xresources ] && xrdb $HOME/.Xresources
xterm -geometry 80x24+10+10 -ls -title "$VNCDESKTOP Desktop" &
icewm-session

View file

@ -1 +1,38 @@
PPCP_E2E_WP_DIR=${ROOT_DIR}/.ddev/wordpress
BASEURL="https://woocommerce-paypal-payments.ddev.site"
AUTHORIZATION="Bearer ABC123"
CHECKOUT_URL="/checkout"
CHECKOUT_PAGE_ID=7
CART_URL="/cart"
BLOCK_CHECKOUT_URL="/checkout-block"
BLOCK_CHECKOUT_PAGE_ID=22
BLOCK_CART_URL="/cart-block"
PRODUCT_URL="/product/prod"
PRODUCT_ID=123
SUBSCRIPTION_URL="/product/sub"
APM_ID="sofort"
WP_MERCHANT_USER="admin"
WP_MERCHANT_PASSWORD="admin"
WP_CUSTOMER_USER="customer"
WP_CUSTOMER_PASSWORD="password"
CUSTOMER_EMAIL="customer@example.com"
CUSTOMER_PASSWORD="password"
CUSTOMER_FIRST_NAME="John"
CUSTOMER_LAST_NAME="Doe"
CUSTOMER_COUNTRY="DE"
CUSTOMER_ADDRESS="street 1"
CUSTOMER_POSTCODE="12345"
CUSTOMER_CITY="city"
CUSTOMER_PHONE="1234567890"
CREDIT_CARD_NUMBER="1234567890"
CREDIT_CARD_EXPIRATION="01/2042"
CREDIT_CARD_CVV="123"

View file

@ -7,25 +7,21 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.2', '7.4', '8.1']
wc-versions: ['5.9.5', '7.1.0']
exclude:
- php-versions: 7.2
wc-versions: 7.1.0
php-versions: ['7.3', '7.4', '8.1']
wc-versions: ['5.9.5', '7.7.2']
name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }}
steps:
- uses: satackey/action-docker-layer-caching@v0.0.11
continue-on-error: true
- uses: jonaseberle/github-action-setup-ddev@v1
- uses: ddev/github-action-setup-ddev@v1
with:
autostart: false
- uses: actions/checkout@v1
- name: Configure DDEV
run: ddev config --php-version ${{ matrix.php-versions }} --web-environment-add="WC_VERSION=${{ matrix.wc-versions }}"
- name: Configure DDEV PHP
run: ddev config --php-version ${{ matrix.php-versions }}
- name: Configure DDEV WC
run: ddev config --web-environment-add="WC_VERSION=${{ matrix.wc-versions }}"
- name: Start DDEV
run: ddev start

2
.gitignore vendored
View file

@ -11,3 +11,5 @@ modules/ppcp-wc-gateway/assets/css
.env
.env.e2e
auth.json
.DS_Store
tests/.DS_Store

View file

@ -50,6 +50,27 @@ namespace Vendidero\Germanized\Shipments {
public function add_note( $note, $added_by_user = false ) {
}
/**
* Return an array of items within this shipment.
*
* @return ShipmentItem[]
*/
public function get_items() {
}
}
class ShipmentItem extends WC_Data {
/**
* Get order ID this meta belongs to.
*
* @param string $context What the value is for. Valid values are 'view' and 'edit'.
* @return int
*/
public function get_order_item_id( $context = 'view' ) {
}
}
}

View file

@ -1,4 +1,7 @@
<?php
if (!defined('PAYPAL_INTEGRATION_DATE')) {
define('PAYPAL_INTEGRATION_DATE', '2023-06-02');
}
if (!defined('EP_PAGES')) {
define('EP_PAGES', 4096);
}
@ -8,6 +11,13 @@ if (!defined('MONTH_IN_SECONDS')) {
if (!defined('HOUR_IN_SECONDS')) {
define('HOUR_IN_SECONDS', 60 * MINUTE_IN_SECONDS);
}
if (!defined('MINUTE_IN_SECONDS')) {
define( 'MINUTE_IN_SECONDS', 60 );
}
if (!defined('ABSPATH')) {
define('ABSPATH', '');
}
/**
* Cancel the next occurrence of a scheduled action.
@ -25,6 +35,17 @@ if (!defined('HOUR_IN_SECONDS')) {
*
* @return string|null The scheduled action ID if a scheduled action was found, or null if no matching action found.
*/
function as_unschedule_action($hook, $args = array(), $group = '')
{
}
function as_unschedule_action($hook, $args = array(), $group = '') {}
/**
* Schedule an action to run one time
*
* @param int $timestamp When the job will run.
* @param string $hook The hook to trigger.
* @param array $args Arguments to pass when the hook triggers.
* @param string $group The group to assign this job to.
* @param bool $unique Whether the action should be unique.
*
* @return int The action ID.
*/
function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false ) {}

381
.psalm/wcblocks.php Normal file
View file

@ -0,0 +1,381 @@
<?php
namespace Automattic\WooCommerce\Blocks\Integrations {
/**
* Integration.Interface
*
* Integrations must use this interface when registering themselves with blocks,
*/
interface IntegrationInterface
{
/**
* The name of the integration.
*
* @return string
*/
public function get_name();
/**
* When called invokes any initialization/setup for the integration.
*/
public function initialize();
/**
* Returns an array of script handles to enqueue in the frontend context.
*
* @return string[]
*/
public function get_script_handles();
/**
* Returns an array of script handles to enqueue in the editor context.
*
* @return string[]
*/
public function get_editor_script_handles();
/**
* An array of key, value pairs of data made available to the block on the client side.
*
* @return array
*/
public function get_script_data();
}
}
namespace Automattic\WooCommerce\Blocks\Payments {
use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface;
interface PaymentMethodTypeInterface extends IntegrationInterface
{
/**
* Returns if this payment method should be active. If false, the scripts will not be enqueued.
*
* @return boolean
*/
public function is_active();
/**
* Returns an array of script handles to enqueue for this payment method in
* the frontend context
*
* @return string[]
*/
public function get_payment_method_script_handles();
/**
* Returns an array of script handles to enqueue for this payment method in
* the admin context
*
* @return string[]
*/
public function get_payment_method_script_handles_for_admin();
/**
* An array of key, value pairs of data made available to payment methods
* client side.
*
* @return array
*/
public function get_payment_method_data();
/**
* Get array of supported features.
*
* @return string[]
*/
public function get_supported_features();
}
}
namespace Automattic\WooCommerce\Blocks\Payments\Integrations
{
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodTypeInterface;
/**
* AbstractPaymentMethodType class.
*
* @since 2.6.0
*/
abstract class AbstractPaymentMethodType implements PaymentMethodTypeInterface
{
/**
* Payment method name defined by payment methods extending this class.
*
* @var string
*/
protected $name = '';
/**
* Settings from the WP options table
*
* @var array
*/
protected $settings = [];
/**
* Get a setting from the settings array if set.
*
* @param string $name Setting name.
* @param mixed $default Value that is returned if the setting does not exist.
* @return mixed
*/
protected function get_setting($name, $default = '')
{
}
/**
* Returns the name of the payment method.
*/
public function get_name()
{
}
/**
* Returns if this payment method should be active. If false, the scripts will not be enqueued.
*
* @return boolean
*/
public function is_active()
{
}
/**
* Returns an array of script handles to enqueue for this payment method in
* the frontend context
*
* @return string[]
*/
public function get_payment_method_script_handles()
{
}
/**
* Returns an array of script handles to enqueue for this payment method in
* the admin context
*
* @return string[]
*/
public function get_payment_method_script_handles_for_admin()
{
}
/**
* Returns an array of supported features.
*
* @return string[]
*/
public function get_supported_features()
{
}
/**
* An array of key, value pairs of data made available to payment methods
* client side.
*
* @return array
*/
public function get_payment_method_data()
{
}
/**
* Returns an array of script handles to enqueue in the frontend context.
*
* Alias of get_payment_method_script_handles. Defined by IntegrationInterface.
*
* @return string[]
*/
public function get_script_handles()
{
}
/**
* Returns an array of script handles to enqueue in the admin context.
*
* Alias of get_payment_method_script_handles_for_admin. Defined by IntegrationInterface.
*
* @return string[]
*/
public function get_editor_script_handles()
{
}
/**
* An array of key, value pairs of data made available to the block on the client side.
*
* Alias of get_payment_method_data. Defined by IntegrationInterface.
*
* @return array
*/
public function get_script_data()
{
}
}
}
namespace Automattic\WooCommerce\Blocks\Integrations {
/**
* Class used for tracking registered integrations with various Block types.
*/
class IntegrationRegistry
{
/**
* Integration identifier is used to construct hook names and is given when the integration registry is initialized.
*
* @var string
*/
protected $registry_identifier = '';
/**
* Registered integrations, as `$name => $instance` pairs.
*
* @var IntegrationInterface[]
*/
protected $registered_integrations = [];
/**
* Initializes all registered integrations.
*
* Integration identifier is used to construct hook names and is given when the integration registry is initialized.
*
* @param string $registry_identifier Identifier for this registry.
*/
public function initialize($registry_identifier = '')
{
}
/**
* Registers an integration.
*
* @param IntegrationInterface $integration An instance of IntegrationInterface.
*
* @return boolean True means registered successfully.
*/
public function register(IntegrationInterface $integration)
{
}
/**
* Checks if an integration is already registered.
*
* @param string $name Integration name.
* @return bool True if the integration is registered, false otherwise.
*/
public function is_registered($name)
{
}
/**
* Un-register an integration.
*
* @param string|IntegrationInterface $name Integration name, or alternatively a IntegrationInterface instance.
* @return boolean|IntegrationInterface Returns the unregistered integration instance if unregistered successfully.
*/
public function unregister($name)
{
}
/**
* Retrieves a registered Integration by name.
*
* @param string $name Integration name.
* @return IntegrationInterface|null The registered integration, or null if it is not registered.
*/
public function get_registered($name)
{
}
/**
* Retrieves all registered integrations.
*
* @return IntegrationInterface[]
*/
public function get_all_registered()
{
}
/**
* Gets an array of all registered integration's script handles for the editor.
*
* @return string[]
*/
public function get_all_registered_editor_script_handles()
{
}
/**
* Gets an array of all registered integration's script handles.
*
* @return string[]
*/
public function get_all_registered_script_handles()
{
}
/**
* Gets an array of all registered integration's script data.
*
* @return array
*/
public function get_all_registered_script_data()
{
}
}
}
namespace Automattic\WooCommerce\Blocks\Payments {
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
/**
* Class used for interacting with payment method types.
*
* @since 2.6.0
*/
final class PaymentMethodRegistry extends IntegrationRegistry
{
/**
* Integration identifier is used to construct hook names and is given when the integration registry is initialized.
*
* @var string
*/
protected $registry_identifier = 'payment_method_type';
/**
* Retrieves all registered payment methods that are also active.
*
* @return PaymentMethodTypeInterface[]
*/
public function get_all_active_registered()
{
}
/**
* Gets an array of all registered payment method script handles, but only for active payment methods.
*
* @return string[]
*/
public function get_all_active_payment_method_script_dependencies()
{
}
/**
* Gets an array of all registered payment method script data, but only for active payment methods.
*
* @return array
*/
public function get_all_registered_script_data()
{
}
}
}
/**
* Registers and validates payment requirements callbacks.
*
* @see Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema::register_payment_requirements()
*
* @param array $args Args to pass to register_payment_requirements.
* @returns boolean|\WP_Error True on success, WP_Error on fail.
*/
function woocommerce_store_api_register_payment_requirements( $args ) {
}

View file

@ -1631,7 +1631,7 @@ function wcs_get_order_items_product_id($item_id)
*
* When acting on cart items or order items, Subscriptions often needs to use an item's canonical product ID. For
* items representing a variation, that means the 'variation_id' value, if the item is not a variation, that means
* the 'product_id value. This function helps save keystrokes on the idiom to check if an item is to a variation or not.
* the product_id value. This function helps save keystrokes on the idiom to check if an item is to a variation or not.
*
* @param array or object $item Either a cart item, order/subscription line item, or a product.
*/
@ -2084,3 +2084,39 @@ function wcs_find_matching_line_item($order, $subscription_item, $match_type = '
function wcs_order_contains_product($order, $product)
{
}
/**
* Get page ID for a specific WC resource.
*
* @param string $for Name of the resource.
*
* @return string Page ID. Empty string if resource not found.
*/
function wc_get_page_screen_id( $for ) {}
/**
* Subscription Product Variation Class
*
* The subscription product variation class extends the WC_Product_Variation product class
* to create subscription product variations.
*
* @class WC_Product_Subscription
* @package WooCommerce Subscriptions
* @category Class
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v1.3
*
*/
class WC_Product_Subscription_Variation extends WC_Product_Variation {}
/**
* Variable Subscription Product Class
*
* This class extends the WC Variable product class to create variable products with recurring payments.
*
* @class WC_Product_Variable_Subscription
* @package WooCommerce Subscriptions
* @category Class
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v1.3
*
*/
class WC_Product_Variable_Subscription extends WC_Product_Variable {}

651
.psalm/wpcli.php Normal file
View file

@ -0,0 +1,651 @@
<?php
/**
* Various utilities for WP-CLI commands.
*/
class WP_CLI {
/**
* Set the logger instance.
*
* @param object $logger Logger instance to set.
*/
public static function set_logger( $logger ) {
}
/**
* Get the logger instance.
*
* @return object $logger Logger instance.
*/
public static function get_logger() {
}
/**
* Get the Configurator instance
*
* @return Configurator
*/
public static function get_configurator() {
}
public static function get_root_command() {
}
public static function get_runner() {
}
/**
* @return FileCache
*/
public static function get_cache() {
}
/**
* Set the context in which WP-CLI should be run
*/
public static function set_url( $url ) {
}
/**
* @return WpHttpCacheManager
*/
public static function get_http_cache_manager() {
}
/**
* Colorize a string for output.
*
* Yes, you can change the color of command line text too. For instance,
* here's how `WP_CLI::success()` colorizes "Success: "
*
* ```
* WP_CLI::colorize( "%GSuccess:%n " )
* ```
*
* Uses `\cli\Colors::colorize()` to transform color tokens to display
* settings. Choose from the following tokens (and note 'reset'):
*
* * %y => ['color' => 'yellow'],
* * %g => ['color' => 'green'],
* * %b => ['color' => 'blue'],
* * %r => ['color' => 'red'],
* * %p => ['color' => 'magenta'],
* * %m => ['color' => 'magenta'],
* * %c => ['color' => 'cyan'],
* * %w => ['color' => 'grey'],
* * %k => ['color' => 'black'],
* * %n => ['color' => 'reset'],
* * %Y => ['color' => 'yellow', 'style' => 'bright'],
* * %G => ['color' => 'green', 'style' => 'bright'],
* * %B => ['color' => 'blue', 'style' => 'bright'],
* * %R => ['color' => 'red', 'style' => 'bright'],
* * %P => ['color' => 'magenta', 'style' => 'bright'],
* * %M => ['color' => 'magenta', 'style' => 'bright'],
* * %C => ['color' => 'cyan', 'style' => 'bright'],
* * %W => ['color' => 'grey', 'style' => 'bright'],
* * %K => ['color' => 'black', 'style' => 'bright'],
* * %N => ['color' => 'reset', 'style' => 'bright'],
* * %3 => ['background' => 'yellow'],
* * %2 => ['background' => 'green'],
* * %4 => ['background' => 'blue'],
* * %1 => ['background' => 'red'],
* * %5 => ['background' => 'magenta'],
* * %6 => ['background' => 'cyan'],
* * %7 => ['background' => 'grey'],
* * %0 => ['background' => 'black'],
* * %F => ['style' => 'blink'],
* * %U => ['style' => 'underline'],
* * %8 => ['style' => 'inverse'],
* * %9 => ['style' => 'bright'],
* * %_ => ['style' => 'bright']
*
* @access public
* @category Output
*
* @param string $string String to colorize for output, with color tokens.
* @return string Colorized string.
*/
public static function colorize( $string ) {
}
/**
* Schedule a callback to be executed at a certain point.
*
* Hooks conceptually are very similar to WordPress actions. WP-CLI hooks
* are typically called before WordPress is loaded.
*
* WP-CLI hooks include:
*
* * `before_add_command:<command>` - Before the command is added.
* * `after_add_command:<command>` - After the command was added.
* * `before_invoke:<command>` (1) - Just before a command is invoked.
* * `after_invoke:<command>` (1) - Just after a command is invoked.
* * `find_command_to_run_pre` - Just before WP-CLI finds the command to run.
* * `before_registering_contexts` (1) - Before the contexts are registered.
* * `before_wp_load` - Just before the WP load process begins.
* * `before_wp_config_load` - After wp-config.php has been located.
* * `after_wp_config_load` - After wp-config.php has been loaded into scope.
* * `after_wp_load` - Just after the WP load process has completed.
* * `before_run_command` (3) - Just before the command is executed.
*
* The parentheses behind the hook name denote the number of arguments
* being passed into the hook. For such hooks, the callback should return
* the first argument again, making them work like a WP filter.
*
* WP-CLI commands can create their own hooks with `WP_CLI::do_hook()`.
*
* If additional arguments are passed through the `WP_CLI::do_hook()` call,
* these will be passed on to the callback provided by `WP_CLI::add_hook()`.
*
* ```
* # `wp network meta` confirms command is executing in multisite context.
* WP_CLI::add_command( 'network meta', 'Network_Meta_Command', array(
* 'before_invoke' => function ( $name ) {
* if ( !is_multisite() ) {
* WP_CLI::error( 'This is not a multisite installation.' );
* }
* }
* ) );
* ```
*
* @access public
* @category Registration
*
* @param string $when Identifier for the hook.
* @param mixed $callback Callback to execute when hook is called.
* @return null
*/
public static function add_hook( $when, $callback ) {
}
/**
* Execute callbacks registered to a given hook.
*
* See `WP_CLI::add_hook()` for details on WP-CLI's internal hook system.
* Commands can provide and call their own hooks.
*
* @access public
* @category Registration
*
* @param string $when Identifier for the hook.
* @param mixed ...$args Optional. Arguments that will be passed onto the
* callback provided by `WP_CLI::add_hook()`.
* @return null|mixed Returns the first optional argument if optional
* arguments were passed, otherwise returns null.
*/
public static function do_hook( $when, ...$args ) {
}
/**
* Add a callback to a WordPress action or filter.
*
* `add_action()` without needing access to `add_action()`. If WordPress is
* already loaded though, you should use `add_action()` (and `add_filter()`)
* instead.
*
* @access public
* @category Registration
*
* @param string $tag Named WordPress action or filter.
* @param mixed $function_to_add Callable to execute when the action or filter is evaluated.
* @param integer $priority Priority to add the callback as.
* @param integer $accepted_args Number of arguments to pass to callback.
* @return true
*/
public static function add_wp_hook( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
}
/**
* Register a command to WP-CLI.
*
* WP-CLI supports using any callable class, function, or closure as a
* command. `WP_CLI::add_command()` is used for both internal and
* third-party command registration.
*
* Command arguments are parsed from PHPDoc by default, but also can be
* supplied as an optional third argument during registration.
*
* ```
* # Register a custom 'foo' command to output a supplied positional param.
* #
* # $ wp foo bar --append=qux
* # Success: bar qux
*
* /**
* * My awesome closure command
* *
* * <message>
* * : An awesome message to display
* *
* * --append=<message>
* * : An awesome message to append to the original message.
* *
* * @when before_wp_load
* *\/
* $foo = function( $args, $assoc_args ) {
* WP_CLI::success( $args[0] . ' ' . $assoc_args['append'] );
* };
* WP_CLI::add_command( 'foo', $foo );
* ```
*
* @access public
* @category Registration
*
* @param string $name Name for the command (e.g. "post list" or "site empty").
* @param callable $callable Command implementation as a class, function or closure.
* @param array $args {
* Optional. An associative array with additional registration parameters.
*
* @type callable $before_invoke Callback to execute before invoking the command.
* @type callable $after_invoke Callback to execute after invoking the command.
* @type string $shortdesc Short description (80 char or less) for the command.
* @type string $longdesc Description of arbitrary length for examples, etc.
* @type string $synopsis The synopsis for the command (string or array).
* @type string $when Execute callback on a named WP-CLI hook (e.g. before_wp_load).
* @type bool $is_deferred Whether the command addition had already been deferred.
* }
* @return bool True on success, false if deferred, hard error if registration failed.
*/
public static function add_command( $name, $callable, $args = [] ) {
}
/**
* Get the list of outstanding deferred command additions.
*
* @return array Array of outstanding command additions.
*/
public static function get_deferred_additions() {
}
/**
* Remove a command addition from the list of outstanding deferred additions.
*/
public static function remove_deferred_addition( $name ) {
}
/**
* Display informational message without prefix, and ignore `--quiet`.
*
* Message is written to STDOUT. `WP_CLI::log()` is typically recommended;
* `WP_CLI::line()` is included for historical compat.
*
* @access public
* @category Output
*
* @param string $message Message to display to the end user.
* @return null
*/
public static function line( $message = '' ) {
}
/**
* Display informational message without prefix.
*
* Message is written to STDOUT, or discarded when `--quiet` flag is supplied.
*
* ```
* # `wp cli update` lets user know of each step in the update process.
* WP_CLI::log( sprintf( 'Downloading from %s...', $download_url ) );
* ```
*
* @access public
* @category Output
*
* @param string $message Message to write to STDOUT.
*/
public static function log( $message ) {
}
/**
* Display success message prefixed with "Success: ".
*
* Success message is written to STDOUT.
*
* Typically recommended to inform user of successful script conclusion.
*
* ```
* # wp rewrite flush expects 'rewrite_rules' option to be set after flush.
* flush_rewrite_rules( \WP_CLI\Utils\get_flag_value( $assoc_args, 'hard' ) );
* if ( ! get_option( 'rewrite_rules' ) ) {
* WP_CLI::warning( "Rewrite rules are empty." );
* } else {
* WP_CLI::success( 'Rewrite rules flushed.' );
* }
* ```
*
* @access public
* @category Output
*
* @param string $message Message to write to STDOUT.
* @return null
*/
public static function success( $message ) {
}
/**
* Display debug message prefixed with "Debug: " when `--debug` is used.
*
* Debug message is written to STDERR, and includes script execution time.
*
* Helpful for optionally showing greater detail when needed. Used throughout
* WP-CLI bootstrap process for easier debugging and profiling.
*
* ```
* # Called in `WP_CLI\Runner::set_wp_root()`.
* private static function set_wp_root( $path ) {
* define( 'ABSPATH', Utils\trailingslashit( $path ) );
* WP_CLI::debug( 'ABSPATH defined: ' . ABSPATH );
* $_SERVER['DOCUMENT_ROOT'] = realpath( $path );
* }
*
* # Debug details only appear when `--debug` is used.
* # $ wp --debug
* # [...]
* # Debug: ABSPATH defined: /srv/www/wordpress-develop.dev/src/ (0.225s)
* ```
*
* @access public
* @category Output
*
* @param string|WP_Error|Exception|Throwable $message Message to write to STDERR.
* @param string|bool $group Organize debug message to a specific group.
* Use `false` to not group the message.
* @return null
*/
public static function debug( $message, $group = false ) {
}
/**
* Display warning message prefixed with "Warning: ".
*
* Warning message is written to STDERR.
*
* Use instead of `WP_CLI::debug()` when script execution should be permitted
* to continue.
*
* ```
* # `wp plugin activate` skips activation when plugin is network active.
* $status = $this->get_status( $plugin->file );
* // Network-active is the highest level of activation status
* if ( 'active-network' === $status ) {
* WP_CLI::warning( "Plugin '{$plugin->name}' is already network active." );
* continue;
* }
* ```
*
* @access public
* @category Output
*
* @param string|WP_Error|Exception|Throwable $message Message to write to STDERR.
* @return null
*/
public static function warning( $message ) {
}
/**
* Display error message prefixed with "Error: " and exit script.
*
* Error message is written to STDERR. Defaults to halting script execution
* with return code 1.
*
* Use `WP_CLI::warning()` instead when script execution should be permitted
* to continue.
*
* ```
* # `wp cache flush` considers flush failure to be a fatal error.
* if ( false === wp_cache_flush() ) {
* WP_CLI::error( 'The object cache could not be flushed.' );
* }
* ```
*
* @access public
* @category Output
*
* @param string|WP_Error|Exception|Throwable $message Message to write to STDERR.
* @param boolean|integer $exit True defaults to exit(1).
* @return null
*/
public static function error( $message, $exit = true ) {
}
/**
* Halt script execution with a specific return code.
*
* Permits script execution to be overloaded by `WP_CLI::runcommand()`
*
* @access public
* @category Output
*
* @param integer $return_code
* @return never
*/
public static function halt( $return_code ) {
}
/**
* Display a multi-line error message in a red box. Doesn't exit script.
*
* Error message is written to STDERR.
*
* @access public
* @category Output
*
* @param array $message_lines Multi-line error message to be displayed.
*/
public static function error_multi_line( $message_lines ) {
}
/**
* Ask for confirmation before running a destructive operation.
*
* If 'y' is provided to the question, the script execution continues. If
* 'n' or any other response is provided to the question, script exits.
*
* ```
* # `wp db drop` asks for confirmation before dropping the database.
*
* WP_CLI::confirm( "Are you sure you want to drop the database?", $assoc_args );
* ```
*
* @access public
* @category Input
*
* @param string $question Question to display before the prompt.
* @param array $assoc_args Skips prompt if 'yes' is provided.
*/
public static function confirm( $question, $assoc_args = [] ) {
}
/**
* Read value from a positional argument or from STDIN.
*
* @param array $args The list of positional arguments.
* @param int $index At which position to check for the value.
*
* @return string
*/
public static function get_value_from_arg_or_stdin( $args, $index ) {
}
/**
* Read a value, from various formats.
*
* @access public
* @category Input
*
* @param mixed $raw_value
* @param array $assoc_args
*/
public static function read_value( $raw_value, $assoc_args = [] ) {
}
/**
* Display a value, in various formats
*
* @param mixed $value Value to display.
* @param array $assoc_args Arguments passed to the command, determining format.
*/
public static function print_value( $value, $assoc_args = [] ) {
}
/**
* Convert a WP_Error or Exception into a string
*
* @param string|WP_Error|Exception|Throwable $errors
* @throws InvalidArgumentException
*
* @return string
*/
public static function error_to_string( $errors ) {
}
/**
* Launch an arbitrary external process that takes over I/O.
*
* ```
* # `wp core download` falls back to the `tar` binary when PharData isn't available
* if ( ! class_exists( 'PharData' ) ) {
* $cmd = "tar xz --strip-components=1 --directory=%s -f $tarball";
* WP_CLI::launch( Utils\esc_cmd( $cmd, $dest ) );
* return;
* }
* ```
*
* @access public
* @category Execution
*
* @param string $command External process to launch.
* @param boolean $exit_on_error Whether to exit if the command returns an elevated return code.
* @param boolean $return_detailed Whether to return an exit status (default) or detailed execution results.
* @return int|ProcessRun The command exit status, or a ProcessRun object for full details.
*/
public static function launch( $command, $exit_on_error = true, $return_detailed = false ) {
}
/**
* Run a WP-CLI command in a new process reusing the current runtime arguments.
*
* Use `WP_CLI::runcommand()` instead, which is easier to use and works better.
*
* Note: While this command does persist a limited set of runtime arguments,
* it *does not* persist environment variables. Practically speaking, WP-CLI
* packages won't be loaded when using WP_CLI::launch_self() because the
* launched process doesn't have access to the current process $HOME.
*
* @access public
* @category Execution
*
* @param string $command WP-CLI command to call.
* @param array $args Positional arguments to include when calling the command.
* @param array $assoc_args Associative arguments to include when calling the command.
* @param bool $exit_on_error Whether to exit if the command returns an elevated return code.
* @param bool $return_detailed Whether to return an exit status (default) or detailed execution results.
* @param array $runtime_args Override one or more global args (path,url,user,allow-root)
* @return int|ProcessRun The command exit status, or a ProcessRun instance
*/
public static function launch_self( $command, $args = [], $assoc_args = [], $exit_on_error = true, $return_detailed = false, $runtime_args = [] ) {
}
/**
* Get the path to the PHP binary used when executing WP-CLI.
*
* Environment values permit specific binaries to be indicated.
*
* Note: moved to Utils, left for BC.
*
* @access public
* @category System
*
* @return string
*/
public static function get_php_binary() {
}
/**
* Confirm that a global configuration parameter does exist.
*
* @access public
* @category Input
*
* @param string $key Config parameter key to check.
*
* @return bool
*/
public static function has_config( $key ) {
}
/**
* Get values of global configuration parameters.
*
* Provides access to `--path=<path>`, `--url=<url>`, and other values of
* the [global configuration parameters](https://wp-cli.org/config/).
*
* ```
* WP_CLI::log( 'The --url=<url> value is: ' . WP_CLI::get_config( 'url' ) );
* ```
*
* @access public
* @category Input
*
* @param string $key Get value for a specific global configuration parameter.
* @return mixed
*/
public static function get_config( $key = null ) {
}
/**
* Run a WP-CLI command.
*
* Launches a new child process to run a specified WP-CLI command.
* Optionally:
*
* * Run the command in an existing process.
* * Prevent halting script execution on error.
* * Capture and return STDOUT, or full details about command execution.
* * Parse JSON output if the command rendered it.
*
* ```
* $options = array(
* 'return' => true, // Return 'STDOUT'; use 'all' for full object.
* 'parse' => 'json', // Parse captured STDOUT to JSON array.
* 'launch' => false, // Reuse the current process.
* 'exit_error' => true, // Halt script execution on error.
* );
* $plugins = WP_CLI::runcommand( 'plugin list --format=json', $options );
* ```
*
* @access public
* @category Execution
*
* @param string $command WP-CLI command to run, including arguments.
* @param array $options Configuration options for command execution.
* @return mixed
*/
public static function runcommand( $command, $options = [] ) {
}
/**
* Run a given command within the current process using the same global
* parameters.
*
* Use `WP_CLI::runcommand()` instead, which is easier to use and works better.
*
* To run a command using a new process with the same global parameters,
* use WP_CLI::launch_self(). To run a command using a new process with
* different global parameters, use WP_CLI::launch().
*
* ```
* ob_start();
* WP_CLI::run_command( array( 'cli', 'cmd-dump' ) );
* $ret = ob_get_clean();
* ```
*
* @access public
* @category Execution
*
* @param array $args Positional arguments including command name.
* @param array $assoc_args
*/
public static function run_command( $args, $assoc_args = [] ) {
}
}

View file

@ -73,6 +73,8 @@ Enable xdebug via `$ ddev xdebug`, and press `Start Listening for PHP Debug Conn
After creating the server in the PHPStorm dialog, you need to set the local project path for the server plugin path.
It should look [like this](https://i.imgur.com/ofsF1Mc.png).
See [tests/playwright](tests/playwright) for e2e (browser-based) tests.
## Test account setup
You will need a PayPal sandbox merchant and customer accounts to configure the plugin and make test purchases with it.

121
api/order-functions.php Normal file
View file

@ -0,0 +1,121 @@
<?php
/**
* The API for operations with orders.
*
* @package WooCommerce\PayPalCommerce\Api
*
* @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Api;
use Exception;
use InvalidArgumentException;
use RuntimeException;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\PPCP;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Returns the PayPal order.
*
* @param string|WC_Order $paypal_id_or_wc_order The ID of PayPal order or a WC order (with the ID in meta).
* @throws InvalidArgumentException When the argument cannot be used for retrieving the order.
* @throws Exception When the operation fails.
*/
function ppcp_get_paypal_order( $paypal_id_or_wc_order ): Order {
if ( $paypal_id_or_wc_order instanceof WC_Order ) {
$paypal_id_or_wc_order = $paypal_id_or_wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $paypal_id_or_wc_order ) {
throw new InvalidArgumentException( 'PayPal order ID not found in meta.' );
}
}
if ( ! is_string( $paypal_id_or_wc_order ) ) {
throw new InvalidArgumentException( 'Invalid PayPal order ID, string expected.' );
}
$order_endpoint = PPCP::container()->get( 'api.endpoint.order' );
assert( $order_endpoint instanceof OrderEndpoint );
return $order_endpoint->order( $paypal_id_or_wc_order );
}
/**
* Captures the PayPal order.
*
* @param WC_Order $wc_order The WC order.
* @throws InvalidArgumentException When the order cannot be captured.
* @throws Exception When the operation fails.
*/
function ppcp_capture_order( WC_Order $wc_order ): void {
$intent = strtoupper( (string) $wc_order->get_meta( PayPalGateway::INTENT_META_KEY ) );
if ( $intent !== 'AUTHORIZE' ) {
throw new InvalidArgumentException( 'Only orders with "authorize" intent can be captured.' );
}
$captured = wc_string_to_bool( $wc_order->get_meta( AuthorizedPaymentsProcessor::CAPTURED_META_KEY ) );
if ( $captured ) {
throw new InvalidArgumentException( 'The order is already captured.' );
}
$authorized_payment_processor = PPCP::container()->get( 'wcgateway.processor.authorized-payments' );
assert( $authorized_payment_processor instanceof AuthorizedPaymentsProcessor );
if ( ! $authorized_payment_processor->capture_authorized_payment( $wc_order ) ) {
throw new RuntimeException( 'Capture failed.' );
}
}
/**
* Refunds the PayPal order.
* Note that you can use wc_refund_payment() to trigger the refund in WC and PayPal.
*
* @param WC_Order $wc_order The WC order.
* @param float $amount The refund amount.
* @param string $reason The reason for the refund.
* @return string The PayPal refund ID.
* @throws InvalidArgumentException When the order cannot be refunded.
* @throws Exception When the operation fails.
*/
function ppcp_refund_order( WC_Order $wc_order, float $amount, string $reason = '' ): string {
$order = ppcp_get_paypal_order( $wc_order );
$refund_processor = PPCP::container()->get( 'wcgateway.processor.refunds' );
assert( $refund_processor instanceof RefundProcessor );
return $refund_processor->refund( $order, $wc_order, $amount, $reason );
}
/**
* Voids the authorization.
*
* @param WC_Order $wc_order The WC order.
* @throws InvalidArgumentException When the order cannot be voided.
* @throws Exception When the operation fails.
*/
function ppcp_void_order( WC_Order $wc_order ): void {
$order = ppcp_get_paypal_order( $wc_order );
$refund_processor = PPCP::container()->get( 'wcgateway.processor.refunds' );
assert( $refund_processor instanceof RefundProcessor );
$refund_processor->void( $order );
}
/**
* Updates the PayPal refund fees totals on an order.
*
* @param WC_Order $wc_order The WC order.
*/
function ppcp_update_order_refund_fees( WC_Order $wc_order ): void {
$updater = PPCP::container()->get( 'wcgateway.helper.refund-fees-updater' );
assert( $updater instanceof RefundFeesUpdater );
$updater->update( $wc_order );
}

View file

@ -16,10 +16,18 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return function (
string $root_dir,
ContainerInterface ...$additional_containers
array $additional_containers = array(),
array $additional_modules = array()
): ContainerInterface {
/**
* Skip path check.
*
* @psalm-suppress UnresolvableInclude
*/
$modules = ( require "$root_dir/modules.php" )( $root_dir );
$modules = array_merge( $modules, $additional_modules );
/**
* Use this filter to add custom module or remove some of existing ones.
* Modules able to access container, add services and modify existing ones.
@ -39,6 +47,11 @@ return function (
// may want to consider fixing it later (pass proxy as parent to DelegatingContainer)
// for now not fixed since we were using this behavior for long time and fixing it now may break things.
$container = new DelegatingContainer( $provider );
/**
* Skip iterable vs array check.
*
* @psalm-suppress PossiblyInvalidArgument
*/
$app_container = new CachingContainer(
new CompositeContainer(
array_merge(

View file

@ -1,5 +1,192 @@
*** Changelog ***
= 2.4.0 - 2023-10-31 =
* Fix - Mini-Cart Bug cause of wrong DOM-Structure in v2.3.1 #1735
* Fix - ACDC disappearing after plugin updates #1751
* Fix - Subscription module hooks #1748
* Fix - Ensure PayPal Subscriptions API products description is 1-127 characters #1738
* Fix - Add validation on the Plan Name field to not accept a blank value #1754
* Enhancement - Improve Pay Later messages and add Shop, Home locations #1770
* Enhancement - Use api-m PayPal API URLs #1740
* Enhancement - Google Pay Settings improvements #1719
* Enhancement - Apple Pay transaction improvements #1767
* Enhancement - Change default ACDC title #1750
* Enhancement - Cart simulation improvements #1753
* Enhancement - Billing schedule fields not greyed out when PayPal Subscriptions product is connected #1755
* Enhancement - Check validation errors when submitting in block #1528
* Enhancement - Improve handling of server error when submitting block #1785
* Enhancement - Extend Apple Pay country eligibility #1781
* Enhancement - Apple Pay validation notice improvements #1783
* Enhancement - Apple Pay payment process issues #1789
* Enhancement - Disable the tracking if payment is not captured #1780
* Enhancement - Place order button remains - Could not retrieve order #1786
* Enhancement - Google Pay for variable product greyed out but clickable #1788
* Enhancement - Merchant credential validation & remove PAYEE object #1795
= 2.3.1 - 2023-09-26 =
* Fix - Fatal error when saving product while WooCommerce Subscriptions plugin is not active #1731
* Fix - Validate tracking data only for add/update Package Tracking #1729
* Fix - Disable Package Tracking for order if transaction ID doesn't exist #1727
= 2.3.0 - 2023-09-26 =
* Fix - Plus sign in PayPal account email address gets converted to space #771
* Fix - Payment method dropdown option label on edit order screen for ppcp-gateway option displaying wrong name #1639
* Fix - WooCommerce Bookings products don't remain in Cart as a guest when PayPal button active on single product #1645
* Fix - Since version > 2.2.0 the PayPal Checkout button on single product pages does not redirect anymore #1664
* Fix - PayPal fee and PayPal Payout do not change on order if we do partial refund #1578
* Fix - Order does not contain intent error when using ACDC payment token while buyer is not present #1506
* Fix - Error when product description linked with a PayPal subscription exceeds 127 characters #1700
* Fix - $_POST uses the wrong key to hold the shipping method #1652
* Fix - WC Payment Token created multiple times when webhook is received #1663
* Fix - Subtotal mismatch line name shows on Account settings page when merchant is disconnected #1702
* Fix - Warning prevents payments on Pay for Order page when debugging is enabled #1703
* Fix - paypal-overlay-uid_ blocks page after closing PayPal popup on Pay for Order page | Terms checkbox validation fails on Pay for Order page #1704
* Enhancement - Add support for HPOS for tracking module #1676
* Enhancement - Billing agreements endpoint called too frequently for Reference Transactions check #1646
* Enhancement - Do not declare subscription support for PayPal when only ACDC vaulting #1669
* Enhancement - Apply Capture On Status Change only when order contains PayPal payment method #1595
* Enhancement - Do not use transient expiration longer than one month to support memcached #1448
* Enhancement - By disconnecting or disabling the plugin the connection should clear the Onboarding links from cache #1668
* Enhancement - Upgrade tracking integration #1562
* Enhancement - Include url & image_url in create order call #1649
* Enhancement - Add compat layer for Yith tracking #1656
* Enhancement - Improve invalid currency backend notice (1926) #1588
* Enhancement - Hide ACDC footer frame via CSS to avoid empty space #1613
* Enhancement - Compatibility with WooCommerce Product Add-Ons plugin #1586
* Enhancement - Remove "no shipment" message after adding tracking #1674
* Enhancement - Improve error & success validation messages #1675
* Enhancement - Compatibility with third-party "Product Add-Ons" plugins #1601
* Enhancement - PayPal logo flashes when switching between tabs #1345
* Enhancement - Include url & image_url in create order call #1649
* Enhancement - Include item_url & image_url to tracking call #1712
* Enhancement - Update strings for tracking metabox #1714
* Enhancement - Validate email address API credentials field #1691
* Enhancement - Set payment method title for order edit page only if our gateway #1661
* Enhancement - Fix missing Pay Later messages in cart + refactoring #1683
* Enhancement - Product page PP button keep loading popup - "wc_add_to_cart_params is not defined" error in WooCommerce #1655
* Enhancement - Remove PayPal Subscriptions API feature flag #1690
* Enhancement - Don't send image_url when it is empty #1678
* Enhancement - Subscription support depending on Vaulting setting instead of subscription mode setting #1697
* Enhancement - Wrong PayPal subscription id on vaulted subscriptions #1699
* Enhancement - Remove payment vaulted checker functionality (2030) #1711
* Feature preview - Apple Pay integration #1514
* Feature preview - Google Pay integration #1654
= 2.2.2 - 2023-08-29 =
* Fix - High rate of auth voids on vaulted subscriptions for guest users #1529
* Enhancement - HPOS compatibility issues #1594
* Feature preview - PayPal Subscriptions API fixes and improvements #1600 #1607
= 2.2.1 - 2023-08-24 =
* Fix - One-page checkout causes mini cart not showing the PP button on certain pages #1536
* Fix - When onboarding loading the return_url too fast may cause the onboarding to fail #1565
* Fix - PayPal button doesn't work for variable products on product page after recent 2.2.0 release #1533
* Fix - Send payee_preferred correctly for instant payments #1489
* Fix - Auto-disabled ACDC vaulting after updating to 2.1.0 #1490
* Fix - PayPal Payments serializing formData of array inputs #1501
* Fix - Buttons not working on single product page for WooCommerce Bookings product #1478
* Enhancement - PayPal Later message price amount doesn't update dynamically #1585
* Enhancement - Improve WC order creation in webhook #1530
* Enhancement - Refactor hosted fields for early card detection #1554
* Enhancement - Pay Later button and message get hidden when product/cart/checkout value is outside of range #1511
* Enhancement - Add link to manual credential docs #1430
* Enhancement - Validate Merchant ID field format when saving settings #1509
* Enhancement - Include soft descriptor for card's activity #1427
* Enhancement - Update Pay Later amount on the cart page and checkout when total changes #1441
* Enhancement - Log Subscription Mode configuration in system report #1507
* Enhancement - HPOS compatibility issues #1555
* Feature preview - PayPal Subscriptions API fixes and improvements #1443
= 2.2.0 - 2023-07-17 =
* Fix - Improve handling of APM payments when buyer did not return to Checkout #1233
* Fix - Use order currency instead of shop currency on order-pay page #1363
* Fix - Do not show broken card button gateway when no checkout location #1358
* Fix - Smart buttons not greyed out/removed on single product when deselecting product variation #1469
* Fix - Type error with advanced columns pro #1367
* Fix - Undefined array key 0 when checking $retry_errors in process_payment method #1375
* Fix - Advanced Card Processing gateway becomes invisible post-plugin update unless admin pages are accessed once #1432
* Fix - Incompatibility with WooCommerce One Page Checkout (or similar use cases) in Version 2.1.0 #1473
* Fix - Prevent Repetitive Token Migration and Database Overload After 2.1.0 Update #1461
* Fix - Onboarding from connection page with CSRF parameter manipulates email and merchant id fields #1502
* Fix - Do not complete non-checkout button orders via webhooks #1513
* Enhancement - Remove feature flag requirement for express cart/checkout block integration #1483
* Enhancement - Add notice when shop currency is unsupported #1433
* Enhancement - Improve ACDC error message when empty fields #1360
* Enhancement - Do not exclude free items #1362
* Enhancement - Trigger WC checkout_error event #1384
* Enhancement - Update wording in buttons previews #1408
* Enhancement - Filter to conditionally block the PayPal buttons #1485
* Enhancement - Display funding source on the admin order page #1450
* Enhancement - Update system report plugin status for Vaulting #1471
* Enhancement - Revert Elementor Pro Checkout hook compatibility #1482
= 2.1.0 - 2023-06-13 =
* Fix - Performance issue #1182
* Fix - Webhooks not registered when onboarding with manual credentials #1223
* Fix - Boolean false type sent as empty value when setting cache #1313
* Fix - Ajax vulnerabilities #1411
* Enhancement - Save and display vaulted payment methods in WC Payment Token API #1059
* Enhancement - Cache webhook verification results #1379
* Enhancement - Refresh checkout totals after validation if needed #1294
* Enhancement - Improve Divi and Elementor Pro compatibility #1254
* Enhancement - Add MX and JP to ACDC #1415
* Enhancement - Add fraudnet script to SGO filter #1366
* Feature preview - Add express cart/checkout block #1346
* Feature preview - Integrate PayPal Subscriptions API #1217
= 2.0.5 - 2023-05-31 =
* Fix - Potential invalidation of merchant credentials #1339
= 2.0.4 - 2023-04-03 =
* Fix - Allow Pay Later in mini-cart #1221
* Fix - Duplicated auth error when credentials become wrong #1229
* Fix - Webhook issues when switching sandbox, and delete all webhooks when unsubscribing #1239
* Fix - High volume of traffic from merchant-integrations endpoint #1273
* Fix - Add content type json to all fetch ajax endpoints #1275
* Enhancement - Remove shortcodes from description #1226
* Enhancement - Handle price suffix with price for product button check #1234
* Enhancement - Show funding source as payment method #1220
* Enhancement - Change "Enabled" to "Available" in status text #1237
* Enhancement - Programmatically capturing/voiding authorized payments #590
= 2.0.3 - 2023-03-14 =
* Fix - `DEVICE_DATA_NOT_AVAILABLE` error message when FraudNet is enabled #1177
* Fix - Redirect to connection tab after manual credentials input #1201
* Fix - Asking for address fields in checkout when not using them #1089
* Fix - Validate before free trial #1170
* Fix - Validate new user creation #1131
* Fix - After Updating to 2.0.2, Site Health reports REST API error #1195
* Fix - Do not send buyer-country for previews in live mode to avoid error #1186
* Fix - PPEC compatibility layer does not take over subscriptions #1193
* Fix - Checkout conflict with "All products for subscriptions" plugin #629
* Fix - Pay Later on order pay page #1214
* Fix - High volume of traffic from merchant-integrations endpoint #1241
* Enhancement - Save checkout form before free trial redirect #1135
* Enhancement - Add filter for controlling the ditching of items/breakdown #1146
* Enhancement - Add patch order data filter #1147
* Enhancement - Add filter for disabling fees on wc order admin pages #1153
* Enhancement - Use wp_loaded for fraudnet loading to avoid warnings #1172
* Enhancement - reCaptcha for WooCommerce support #1093
* Enhancement - Make it possible to hide missing funding resource Trustly #1155
* Enhancement - Add white color option #1167
* Enhancement - Checkout validation for other fields #861
* Enhancement - Mention PUI only for German shops and add line breaks #1169
* Enhancement - Add filter to fallback tracking_data['carrier'] #1188
* Enhancement - Error notices in checkout do not update / or are shown twice #1168
* Enhancement - capture authorized payment by changing order status (or programmatically) #587
= 2.0.2 - 2023-01-31 =
* Fix - Do not call PayPal get order by ID if it does not exist #1029
* Fix - Type check error conflict with German Market #1056
* Fix - Backend Storage for the PayPalRequestIdRepository does not scale #983
* Fix - Ensure WC()->payment_gateways is not null #1128
* Enhancement - Remove plugin data after uninstalling #1075
* Enhancement - Add FraudNet to all payments #1040
* Enhancement - Update "Standard Payments" tab settings #1065
* Enhancement - Update PHP 7.2 requirement in all relevant files #1084
* Enhancement - When PUI is enabled FraudNet should be also enabled #1129
* Enhancement - Add PayPal-Request-Id if payment source exist #1132
= 2.0.1 - 2022-12-13 =
* Fix - Error while syncing tracking data to PayPal -> Sync GZD Tracking #1020
* Fix - Fix product price retrieval for variable product buttons #1000

View file

@ -29,8 +29,12 @@
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\": "src",
"WooCommerce\\PayPalCommerce\\Common\\": "lib/common/",
"WooCommerce\\PayPalCommerce\\Vendor\\": "lib/packages/"
}
},
"files": [
"api/order-functions.php"
]
},
"autoload-dev": {
"psr-4": {

198
composer.lock generated
View file

@ -521,16 +521,16 @@
},
{
"name": "wikimedia/composer-merge-plugin",
"version": "v2.0.1",
"version": "v2.1.0",
"source": {
"type": "git",
"url": "https://github.com/wikimedia/composer-merge-plugin.git",
"reference": "8ca2ed8ab97c8ebce6b39d9943e9909bb4f18912"
"reference": "a03d426c8e9fb2c9c569d9deeb31a083292788bc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/8ca2ed8ab97c8ebce6b39d9943e9909bb4f18912",
"reference": "8ca2ed8ab97c8ebce6b39d9943e9909bb4f18912",
"url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/a03d426c8e9fb2c9c569d9deeb31a083292788bc",
"reference": "a03d426c8e9fb2c9c569d9deeb31a083292788bc",
"shasum": ""
},
"require": {
@ -539,9 +539,12 @@
},
"require-dev": {
"composer/composer": "^1.1||^2.0",
"php-parallel-lint/php-parallel-lint": "~1.1.0",
"ext-json": "*",
"mediawiki/mediawiki-phan-config": "0.11.1",
"php-parallel-lint/php-parallel-lint": "~1.3.1",
"phpspec/prophecy": "~1.15.0",
"phpunit/phpunit": "^8.5||^9.0",
"squizlabs/php_codesniffer": "~3.5.4"
"squizlabs/php_codesniffer": "~3.7.1"
},
"type": "composer-plugin",
"extra": {
@ -568,9 +571,9 @@
"description": "Composer plugin to merge multiple composer.json files",
"support": {
"issues": "https://github.com/wikimedia/composer-merge-plugin/issues",
"source": "https://github.com/wikimedia/composer-merge-plugin/tree/v2.0.1"
"source": "https://github.com/wikimedia/composer-merge-plugin/tree/v2.1.0"
},
"time": "2021-02-24T05:28:06+00:00"
"time": "2023-04-15T19:07:00+00:00"
},
{
"name": "wp-oop/wordpress-interface",
@ -794,16 +797,16 @@
},
{
"name": "antecedent/patchwork",
"version": "2.1.21",
"version": "2.1.25",
"source": {
"type": "git",
"url": "https://github.com/antecedent/patchwork.git",
"reference": "25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d"
"reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antecedent/patchwork/zipball/25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d",
"reference": "25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d",
"url": "https://api.github.com/repos/antecedent/patchwork/zipball/17314e042d45e0dacb0a494c2d1ef50e7621136a",
"reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a",
"shasum": ""
},
"require": {
@ -836,9 +839,9 @@
],
"support": {
"issues": "https://github.com/antecedent/patchwork/issues",
"source": "https://github.com/antecedent/patchwork/tree/2.1.21"
"source": "https://github.com/antecedent/patchwork/tree/2.1.25"
},
"time": "2022-02-07T07:28:34+00:00"
"time": "2023-02-19T12:51:24+00:00"
},
{
"name": "brain/monkey",
@ -1430,30 +1433,30 @@
},
{
"name": "doctrine/instantiator",
"version": "1.4.1",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
"reference": "10dcfce151b967d20fde1b34ae6640712c3891bc"
"reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc",
"reference": "10dcfce151b967d20fde1b34ae6640712c3891bc",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
"reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^9",
"doctrine/coding-standard": "^9 || ^11",
"ext-pdo": "*",
"ext-phar": "*",
"phpbench/phpbench": "^0.16 || ^1",
"phpstan/phpstan": "^1.4",
"phpstan/phpstan-phpunit": "^1",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"vimeo/psalm": "^4.22"
"vimeo/psalm": "^4.30 || ^5.4"
},
"type": "library",
"autoload": {
@ -1480,7 +1483,7 @@
],
"support": {
"issues": "https://github.com/doctrine/instantiator/issues",
"source": "https://github.com/doctrine/instantiator/tree/1.4.1"
"source": "https://github.com/doctrine/instantiator/tree/1.5.0"
},
"funding": [
{
@ -1496,7 +1499,7 @@
"type": "tidelift"
}
],
"time": "2022-03-03T08:28:38+00:00"
"time": "2022-12-30T00:15:36+00:00"
},
{
"name": "felixfbecker/advanced-json-rpc",
@ -1601,24 +1604,24 @@
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.0",
"version": "v1.1.1",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "a878d45c1914464426dc94da61c9e1d36ae262a8"
"reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/a878d45c1914464426dc94da61c9e1d36ae262a8",
"reference": "a878d45c1914464426dc94da61c9e1d36ae262a8",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831",
"reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9"
"phpoption/phpoption": "^1.9.1"
},
"require-dev": {
"phpunit/phpunit": "^8.5.28 || ^9.5.21"
"phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12"
},
"type": "library",
"autoload": {
@ -1647,7 +1650,7 @@
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.0"
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1"
},
"funding": [
{
@ -1659,7 +1662,7 @@
"type": "tidelift"
}
],
"time": "2022-07-30T15:56:11+00:00"
"time": "2023-02-25T20:23:15+00:00"
},
{
"name": "hamcrest/hamcrest-php",
@ -1783,16 +1786,16 @@
},
{
"name": "myclabs/deep-copy",
"version": "1.11.0",
"version": "1.11.1",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614"
"reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
"reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
"shasum": ""
},
"require": {
@ -1830,7 +1833,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.11.0"
"source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
},
"funding": [
{
@ -1838,20 +1841,20 @@
"type": "tidelift"
}
],
"time": "2022-03-03T13:19:32+00:00"
"time": "2023-03-08T13:26:56+00:00"
},
{
"name": "netresearch/jsonmapper",
"version": "v4.1.0",
"version": "v4.2.0",
"source": {
"type": "git",
"url": "https://github.com/cweiske/jsonmapper.git",
"reference": "cfa81ea1d35294d64adb9c68aa4cb9e92400e53f"
"reference": "f60565f8c0566a31acf06884cdaa591867ecc956"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/cfa81ea1d35294d64adb9c68aa4cb9e92400e53f",
"reference": "cfa81ea1d35294d64adb9c68aa4cb9e92400e53f",
"url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/f60565f8c0566a31acf06884cdaa591867ecc956",
"reference": "f60565f8c0566a31acf06884cdaa591867ecc956",
"shasum": ""
},
"require": {
@ -1887,22 +1890,22 @@
"support": {
"email": "cweiske@cweiske.de",
"issues": "https://github.com/cweiske/jsonmapper/issues",
"source": "https://github.com/cweiske/jsonmapper/tree/v4.1.0"
"source": "https://github.com/cweiske/jsonmapper/tree/v4.2.0"
},
"time": "2022-12-08T20:46:14+00:00"
"time": "2023-04-09T17:37:40+00:00"
},
{
"name": "nikic/php-parser",
"version": "v4.15.2",
"version": "v4.16.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc"
"reference": "19526a33fb561ef417e822e85f08a00db4059c17"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
"reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17",
"reference": "19526a33fb561ef417e822e85f08a00db4059c17",
"shasum": ""
},
"require": {
@ -1943,9 +1946,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2"
"source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0"
},
"time": "2022-11-12T15:38:23+00:00"
"time": "2023-06-25T14:52:30+00:00"
},
{
"name": "openlss/lib-array2xml",
@ -2157,27 +2160,25 @@
},
{
"name": "php-stubs/wordpress-stubs",
"version": "v5.9.5",
"version": "v5.9.6",
"source": {
"type": "git",
"url": "https://github.com/php-stubs/wordpress-stubs.git",
"reference": "13ecf204a7e6d215a7c0d23e2aa27940fe617717"
"reference": "6a18d938d0aef39d091505a4a35b025fb6c10098"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/13ecf204a7e6d215a7c0d23e2aa27940fe617717",
"reference": "13ecf204a7e6d215a7c0d23e2aa27940fe617717",
"url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/6a18d938d0aef39d091505a4a35b025fb6c10098",
"reference": "6a18d938d0aef39d091505a4a35b025fb6c10098",
"shasum": ""
},
"replace": {
"giacocorsiglia/wordpress-stubs": "*"
},
"require-dev": {
"nikic/php-parser": "< 4.12.0",
"php": "~7.3 || ~8.0",
"php-stubs/generator": "^0.8.1",
"php-stubs/generator": "^0.8.3",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpstan": "^1.2"
"phpstan/phpstan": "^1.10.12",
"phpunit/phpunit": "^9.5"
},
"suggest": {
"paragonie/sodium_compat": "Pure PHP implementation of libsodium",
@ -2198,9 +2199,9 @@
],
"support": {
"issues": "https://github.com/php-stubs/wordpress-stubs/issues",
"source": "https://github.com/php-stubs/wordpress-stubs/tree/v5.9.5"
"source": "https://github.com/php-stubs/wordpress-stubs/tree/v5.9.6"
},
"time": "2022-11-09T05:32:14+00:00"
"time": "2023-05-18T04:34:27+00:00"
},
{
"name": "phpcompatibility/php-compatibility",
@ -2538,24 +2539,24 @@
},
{
"name": "phpoption/phpoption",
"version": "1.9.0",
"version": "1.9.1",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "dc5ff11e274a90cc1c743f66c9ad700ce50db9ab"
"reference": "dd3a383e599f49777d8b628dadbb90cae435b87e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dc5ff11e274a90cc1c743f66c9ad700ce50db9ab",
"reference": "dc5ff11e274a90cc1c743f66c9ad700ce50db9ab",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e",
"reference": "dd3a383e599f49777d8b628dadbb90cae435b87e",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8",
"phpunit/phpunit": "^8.5.28 || ^9.5.21"
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12"
},
"type": "library",
"extra": {
@ -2597,7 +2598,7 @@
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.0"
"source": "https://github.com/schmittjoh/php-option/tree/1.9.1"
},
"funding": [
{
@ -2609,7 +2610,7 @@
"type": "tidelift"
}
],
"time": "2022-07-30T15:51:26+00:00"
"time": "2023-02-25T19:38:58+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -2910,16 +2911,16 @@
},
{
"name": "phpunit/phpunit",
"version": "8.5.31",
"version": "8.5.33",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "33c126b09a42de5c99e5e8032b54e8221264a74e"
"reference": "7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33c126b09a42de5c99e5e8032b54e8221264a74e",
"reference": "33c126b09a42de5c99e5e8032b54e8221264a74e",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e",
"reference": "7d1ff0e8c6b35db78ff13e3e05517d7cbf7aa32e",
"shasum": ""
},
"require": {
@ -2987,7 +2988,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.31"
"source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.33"
},
"funding": [
{
@ -3003,7 +3004,7 @@
"type": "tidelift"
}
],
"time": "2022-10-28T05:57:37+00:00"
"time": "2023-02-27T13:04:50+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
@ -3136,16 +3137,16 @@
},
{
"name": "sebastian/diff",
"version": "3.0.3",
"version": "3.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211"
"reference": "6296a0c086dd0117c1b78b059374d7fcbe7545ae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/6296a0c086dd0117c1b78b059374d7fcbe7545ae",
"reference": "6296a0c086dd0117c1b78b059374d7fcbe7545ae",
"shasum": ""
},
"require": {
@ -3190,7 +3191,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"source": "https://github.com/sebastianbergmann/diff/tree/3.0.3"
"source": "https://github.com/sebastianbergmann/diff/tree/3.0.4"
},
"funding": [
{
@ -3198,7 +3199,7 @@
"type": "github"
}
],
"time": "2020-11-30T07:59:04+00:00"
"time": "2023-05-07T05:30:20+00:00"
},
{
"name": "sebastian/environment",
@ -3736,16 +3737,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.7.1",
"version": "3.7.2",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879",
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879",
"shasum": ""
},
"require": {
@ -3781,27 +3782,28 @@
"homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards"
"standards",
"static analysis"
],
"support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"time": "2022-06-18T07:21:10+00:00"
"time": "2023-02-22T23:07:41+00:00"
},
{
"name": "symfony/console",
"version": "v5.4.16",
"version": "v5.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "8e9b9c8dfb33af6057c94e1b44846bee700dc5ef"
"reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/8e9b9c8dfb33af6057c94e1b44846bee700dc5ef",
"reference": "8e9b9c8dfb33af6057c94e1b44846bee700dc5ef",
"url": "https://api.github.com/repos/symfony/console/zipball/b504a3d266ad2bb632f196c0936ef2af5ff6e273",
"reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273",
"shasum": ""
},
"require": {
@ -3866,12 +3868,12 @@
"homepage": "https://symfony.com",
"keywords": [
"cli",
"command line",
"command-line",
"console",
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.4.16"
"source": "https://github.com/symfony/console/tree/v5.4.26"
},
"funding": [
{
@ -3887,7 +3889,7 @@
"type": "tidelift"
}
],
"time": "2022-11-25T14:09:27+00:00"
"time": "2023-07-19T20:11:33+00:00"
},
{
"name": "symfony/deprecation-contracts",
@ -4450,16 +4452,16 @@
},
{
"name": "symfony/string",
"version": "v5.4.15",
"version": "v5.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed"
"reference": "1181fe9270e373537475e826873b5867b863883c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/571334ce9f687e3e6af72db4d3b2a9431e4fd9ed",
"reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed",
"url": "https://api.github.com/repos/symfony/string/zipball/1181fe9270e373537475e826873b5867b863883c",
"reference": "1181fe9270e373537475e826873b5867b863883c",
"shasum": ""
},
"require": {
@ -4516,7 +4518,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v5.4.15"
"source": "https://github.com/symfony/string/tree/v5.4.26"
},
"funding": [
{
@ -4532,7 +4534,7 @@
"type": "tidelift"
}
],
"time": "2022-10-05T15:16:54+00:00"
"time": "2023-06-28T12:46:07+00:00"
},
{
"name": "theseer/tokenizer",

View file

@ -1,5 +1,10 @@
## packages
The packages that are likely to cause conflicts with other plugins (by loading multiple incompatible versions).
Their namespaces are isolated by [Mozart](https://github.com/coenjacobs/mozart).
Currently, the packages are simply added in the repo to avoid making the build process more complex (Mozart has different PHP requirements).
We need to isolate only PSR-11 containers and Dhii modularity packages, which are not supposed to change often.
## common
This folder contains reusable classes or components that do not fit into any specific module.
They are designed to be versatile and can be used by any module within the plugin.

View file

@ -0,0 +1,68 @@
<?php
/**
* The Singleton Trait can be used to wrap an execution block, so it behaves like a Singleton.
* It executes the callable once, on subsequent calls returns the same result.
*/
namespace WooCommerce\PayPalCommerce\Common\Pattern;
/**
* Class SingletonDecorator.
*/
class SingletonDecorator {
/**
* The callable with the executing code
*
* @var callable
*/
private $callable;
/**
* The execution result
*
* @var mixed
*/
private $result;
/**
* Indicates if the callable is resolved
*
* @var bool
*/
private $executed = false;
/**
* SingletonDecorator constructor.
*
* @param callable $callable
*/
public function __construct( callable $callable ) {
$this->callable = $callable;
}
/**
* The make constructor.
*
* @param callable $callable
* @return self
*/
public static function make( callable $callable ): self {
return new static( $callable );
}
/**
* Invokes a callable once and returns the same result on subsequent invokes.
*
* @param mixed ...$args Arguments to be passed to the callable.
* @return mixed
*/
public function __invoke( ...$args ) {
if ( ! $this->executed ) {
$this->result = call_user_func_array( $this->callable, $args );
$this->executed = true;
}
return $this->result;
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* The Singleton Trait can be used to add singleton behaviour to a class.
*
* @package WooCommerce\PayPalCommerce\Common\Pattern
*/
namespace WooCommerce\PayPalCommerce\Common\Pattern;
/**
* Class SingletonTrait.
*/
trait SingletonTrait {
/**
* The single instance of the class.
*
* @var self
*/
protected static $instance = null;
/**
* Static method to get the instance of the Singleton class
*
* @return self|null
*/
public static function get_instance(): ?self {
return self::$instance;
}
/**
* Static method to get the instance of the Singleton class
*
* @param self $instance
* @return self
*/
protected static function set_instance( self $instance ): self {
self::$instance = $instance;
return self::$instance;
}
}

View file

@ -26,7 +26,31 @@ return function ( string $root_dir ): iterable {
( require "$modules_dir/ppcp-vaulting/module.php" )(),
( require "$modules_dir/ppcp-order-tracking/module.php" )(),
( require "$modules_dir/ppcp-uninstall/module.php" )(),
( require "$modules_dir/ppcp-blocks/module.php" )(),
);
if ( apply_filters(
// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
'woocommerce.feature-flags.woocommerce_paypal_payments.applepay_enabled',
getenv( 'PCP_APPLEPAY_ENABLED' ) !== '0'
) ) {
$modules[] = ( require "$modules_dir/ppcp-applepay/module.php" )();
}
if ( apply_filters(
//phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
'woocommerce.feature-flags.woocommerce_paypal_payments.googlepay_enabled',
getenv( 'PCP_GOOGLEPAY_ENABLED' ) !== '0'
) ) {
$modules[] = ( require "$modules_dir/ppcp-googlepay/module.php" )();
}
if ( apply_filters(
//phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
'woocommerce.deprecated_flags.woocommerce_paypal_payments.saved_payment_checker_enabled',
getenv( 'PCP_SAVED_PAYMENT_CHECKER_ENABLED' ) === '1'
) ) {
$modules[] = ( require "$modules_dir/ppcp-saved-payment-checker/module.php" )();
}
return $modules;
};

View file

@ -35,17 +35,26 @@ class Message {
*/
private $dismissable;
/**
* The wrapper selector that will contain the notice.
*
* @var string
*/
private $wrapper;
/**
* Message constructor.
*
* @param string $message The message text.
* @param string $type The message type.
* @param bool $dismissable Whether the message is dismissable.
* @param string $wrapper The wrapper selector that will contain the notice.
*/
public function __construct( string $message, string $type, bool $dismissable = true ) {
public function __construct( string $message, string $type, bool $dismissable = true, string $wrapper = '' ) {
$this->type = $type;
$this->message = $message;
$this->dismissable = $dismissable;
$this->wrapper = $wrapper;
}
/**
@ -74,4 +83,13 @@ class Message {
public function is_dismissable(): bool {
return $this->dismissable;
}
/**
* Returns the wrapper selector that will contain the notice.
*
* @return string
*/
public function wrapper(): string {
return $this->wrapper;
}
}

View file

@ -41,9 +41,10 @@ class Renderer implements RendererInterface {
$messages = $this->repository->current_message();
foreach ( $messages as $message ) {
printf(
'<div class="notice notice-%s %s"><p>%s</p></div>',
'<div class="notice notice-%s %s" %s><p>%s</p></div>',
$message->type(),
( $message->is_dismissable() ) ? 'is-dismissible' : '',
( $message->wrapper() ? sprintf( 'data-ppcp-wrapper="%s"', esc_attr( $message->wrapper() ) ) : '' ),
wp_kses_post( $message->message() )
);
}

View file

@ -9,6 +9,21 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerPayableBreakdown;
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
@ -49,12 +64,13 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
@ -105,7 +121,8 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'api.factory.sellerstatus' ),
$container->get( 'api.partner_merchant_id' ),
$container->get( 'api.merchant_id' )
$container->get( 'api.merchant_id' ),
$container->get( 'api.helper.failure-registry' )
);
},
'api.factory.sellerstatus' => static function ( ContainerInterface $container ) : SellerStatusFactory {
@ -118,8 +135,7 @@ return array(
$container->get( 'api.factory.payment-token' ),
$container->get( 'api.factory.payment-token-action-links' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'api.repository.customer' ),
$container->get( 'api.repository.paypal-request-id' )
$container->get( 'api.repository.customer' )
);
},
'api.endpoint.webhook' => static function ( ContainerInterface $container ) : WebhookEndpoint {
@ -179,15 +195,15 @@ return array(
$patch_collection_factory = $container->get( 'api.factory.patch-collection-factory' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
/**
* The settings.
*
* @var Settings $settings
*/
$session_handler = $container->get( 'session.handler' );
assert( $session_handler instanceof SessionHandler );
$bn_code = $session_handler->bn_code();
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$intent = $settings->has( 'intent' ) && strtoupper( (string) $settings->get( 'intent' ) ) === 'AUTHORIZE' ? 'AUTHORIZE' : 'CAPTURE';
$application_context_repository = $container->get( 'api.repository.application-context' );
$paypal_request_id = $container->get( 'api.repository.paypal-request-id' );
$subscription_helper = $container->get( 'subscription.helper' );
return new OrderEndpoint(
$container->get( 'api.host' ),
@ -197,10 +213,10 @@ return array(
$intent,
$logger,
$application_context_repository,
$paypal_request_id,
$subscription_helper,
$container->get( 'wcgateway.is-fraudnet-enabled' ),
$container->get( 'wcgateway.fraudnet' )
$container->get( 'wcgateway.fraudnet' ),
$bn_code
);
},
'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): BillingAgreementsEndpoint {
@ -210,8 +226,29 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.repository.paypal-request-id' => static function( ContainerInterface $container ) : PayPalRequestIdRepository {
return new PayPalRequestIdRepository();
'api.endpoint.catalog-products' => static function ( ContainerInterface $container ): CatalogProducts {
return new CatalogProducts(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'api.factory.product' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.billing-plans' => static function( ContainerInterface $container ): BillingPlans {
return new BillingPlans(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'api.factory.billing-cycle' ),
$container->get( 'api.factory.plan' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.billing-subscriptions' => static function( ContainerInterface $container ): BillingSubscriptions {
return new BillingSubscriptions(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.repository.application-context' => static function( ContainerInterface $container ) : ApplicationContextRepository {
@ -261,24 +298,32 @@ return array(
$container->get( 'api.factory.fraud-processor-response' )
);
},
'api.factory.refund' => static function ( ContainerInterface $container ): RefundFactory {
$amount_factory = $container->get( 'api.factory.amount' );
return new RefundFactory(
$amount_factory,
$container->get( 'api.factory.seller-payable-breakdown' ),
$container->get( 'api.factory.refund_payer' )
);
},
'api.factory.purchase-unit' => static function ( ContainerInterface $container ): PurchaseUnitFactory {
$amount_factory = $container->get( 'api.factory.amount' );
$payee_repository = $container->get( 'api.repository.payee' );
$payee_factory = $container->get( 'api.factory.payee' );
$item_factory = $container->get( 'api.factory.item' );
$shipping_factory = $container->get( 'api.factory.shipping' );
$payments_factory = $container->get( 'api.factory.payments' );
$prefix = $container->get( 'api.prefix' );
$soft_descriptor = $container->get( 'wcgateway.soft-descriptor' );
$sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' );
return new PurchaseUnitFactory(
$amount_factory,
$payee_repository,
$payee_factory,
$item_factory,
$shipping_factory,
$payments_factory,
$prefix
$prefix,
$soft_descriptor,
$sanitizer
);
},
'api.factory.patch-collection-factory' => static function ( ContainerInterface $container ): PatchCollectionFactory {
@ -293,12 +338,19 @@ return array(
);
},
'api.factory.shipping' => static function ( ContainerInterface $container ): ShippingFactory {
$address_factory = $container->get( 'api.factory.address' );
return new ShippingFactory( $address_factory );
return new ShippingFactory(
$container->get( 'api.factory.address' ),
$container->get( 'api.factory.shipping-option' )
);
},
'api.factory.shipping-preference' => static function ( ContainerInterface $container ): ShippingPreferenceFactory {
return new ShippingPreferenceFactory();
},
'api.factory.shipping-option' => static function ( ContainerInterface $container ): ShippingOptionFactory {
return new ShippingOptionFactory(
$container->get( 'api.factory.money' )
);
},
'api.factory.amount' => static function ( ContainerInterface $container ): AmountFactory {
$item_factory = $container->get( 'api.factory.item' );
return new AmountFactory(
@ -314,6 +366,9 @@ return array(
$address_factory = $container->get( 'api.factory.address' );
return new PayerFactory( $address_factory );
},
'api.factory.refund_payer' => static function ( ContainerInterface $container ): RefundPayerFactory {
return new RefundPayerFactory();
},
'api.factory.address' => static function ( ContainerInterface $container ): AddressFactory {
return new AddressFactory();
},
@ -337,7 +392,8 @@ return array(
'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory {
$authorizations_factory = $container->get( 'api.factory.authorization' );
$capture_factory = $container->get( 'api.factory.capture' );
return new PaymentsFactory( $authorizations_factory, $capture_factory );
$refund_factory = $container->get( 'api.factory.refund' );
return new PaymentsFactory( $authorizations_factory, $capture_factory, $refund_factory );
},
'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory {
return new AuthorizationFactory();
@ -358,9 +414,30 @@ return array(
$container->get( 'api.factory.platform-fee' )
);
},
'api.factory.seller-payable-breakdown' => static function ( ContainerInterface $container ): SellerPayableBreakdownFactory {
return new SellerPayableBreakdownFactory(
$container->get( 'api.factory.money' ),
$container->get( 'api.factory.platform-fee' )
);
},
'api.factory.fraud-processor-response' => static function ( ContainerInterface $container ): FraudProcessorResponseFactory {
return new FraudProcessorResponseFactory();
},
'api.factory.product' => static function( ContainerInterface $container ): ProductFactory {
return new ProductFactory();
},
'api.factory.billing-cycle' => static function( ContainerInterface $container ): BillingCycleFactory {
return new BillingCycleFactory( $container->get( 'api.shop.currency' ) );
},
'api.factory.payment-preferences' => static function( ContainerInterface $container ):PaymentPreferencesFactory {
return new PaymentPreferencesFactory( $container->get( 'api.shop.currency' ) );
},
'api.factory.plan' => static function( ContainerInterface $container ): PlanFactory {
return new PlanFactory(
$container->get( 'api.factory.billing-cycle' ),
$container->get( 'api.factory.payment-preferences' )
);
},
'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies {
return new DccApplies(
$container->get( 'api.dcc-supported-country-currency-matrix' ),
@ -635,6 +712,27 @@ return array(
'SGD',
'USD',
),
'MX' => array(
'MXN',
),
'JP' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
)
);
},
@ -691,6 +789,17 @@ return array(
'amex' => array( 'CAD' ),
'jcb' => array( 'CAD' ),
),
'MX' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array(),
),
'JP' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'JPY' ),
'jcb' => array( 'JPY' ),
),
)
);
},
@ -730,4 +839,23 @@ return array(
'api.order-helper' => static function( ContainerInterface $container ): OrderHelper {
return new OrderHelper();
},
'api.helper.order-transient' => static function( ContainerInterface $container ): OrderTransient {
$cache = new Cache( 'ppcp-paypal-bearer' );
$purchase_unit_sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' );
return new OrderTransient( $cache, $purchase_unit_sanitizer );
},
'api.helper.failure-registry' => static function( ContainerInterface $container ): FailureRegistry {
$cache = new Cache( 'ppcp-paypal-api-status-cache' );
return new FailureRegistry( $cache );
},
'api.helper.purchase-unit-sanitizer' => SingletonDecorator::make(
static function( ContainerInterface $container ): PurchaseUnitSanitizer {
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$behavior = $settings->has( 'subtotal_mismatch_behavior' ) ? $settings->get( 'subtotal_mismatch_behavior' ) : null;
$line_name = $settings->has( 'subtotal_mismatch_line_name' ) ? $settings->get( 'subtotal_mismatch_line_name' ) : null;
return new PurchaseUnitSanitizer( $behavior, $line_name );
}
),
);

View file

@ -9,10 +9,14 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
/**
* Class ApiModule
@ -40,6 +44,56 @@ class ApiModule implements ModuleInterface {
WC()->session->set( 'ppcp_fees', $fees );
}
);
add_filter(
'ppcp_create_order_request_body_data',
function( array $data ) use ( $c ) {
foreach ( $data['purchase_units'] as $purchase_unit_index => $purchase_unit ) {
foreach ( $purchase_unit['items'] as $item_index => $item ) {
$data['purchase_units'][ $purchase_unit_index ]['items'][ $item_index ]['name'] =
apply_filters( 'woocommerce_paypal_payments_cart_line_item_name', $item['name'], $item['cart_item_key'] ?? null );
}
}
return $data;
}
);
add_action(
'woocommerce_paypal_payments_paypal_order_created',
function ( Order $order ) use ( $c ) {
$transient = $c->has( 'api.helper.order-transient' ) ? $c->get( 'api.helper.order-transient' ) : null;
if ( $transient instanceof OrderTransient ) {
$transient->on_order_created( $order );
}
},
10,
1
);
add_action(
'woocommerce_paypal_payments_woocommerce_order_created',
function ( WC_Order $wc_order, Order $order ) use ( $c ) {
$transient = $c->has( 'api.helper.order-transient' ) ? $c->get( 'api.helper.order-transient' ) : null;
if ( $transient instanceof OrderTransient ) {
$transient->on_woocommerce_order_created( $wc_order, $order );
}
},
10,
2
);
add_action(
'woocommerce_paypal_payments_clear_apm_product_status',
function () use ( $c ) {
$failure_registry = $c->has( 'api.helper.failure-registry' ) ? $c->get( 'api.helper.failure-registry' ) : null;
if ( $failure_registry instanceof FailureRegistry ) {
$failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY );
}
},
10,
2
);
}
/**

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Exception;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
@ -120,6 +121,10 @@ class BillingAgreementsEndpoint {
*/
public function reference_transaction_enabled(): bool {
try {
if ( wc_string_to_bool( get_transient( 'ppcp_reference_transaction_enabled' ) ) === true ) {
return true;
}
$this->is_request_logging_enabled = false;
try {
@ -130,10 +135,12 @@ class BillingAgreementsEndpoint {
);
} finally {
$this->is_request_logging_enabled = true;
set_transient( 'ppcp_reference_transaction_enabled', true, MONTH_IN_SECONDS );
}
return true;
} catch ( PayPalApiException $exception ) {
} catch ( Exception $exception ) {
delete_transient( 'ppcp_reference_transaction_enabled' );
return false;
}
}

View file

@ -0,0 +1,263 @@
<?php
/**
* The Billing Plans endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\BillingCycle;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Plan;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory;
/**
* Class BillingPlans
*/
class BillingPlans {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* Billing cycle factory
*
* @var BillingCycleFactory
*/
private $billing_cycle_factory;
/**
* Plan factory
*
* @var PlanFactory
*/
private $plan_factory;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* BillingPlans constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param BillingCycleFactory $billing_cycle_factory Billing cycle factory.
* @param PlanFactory $plan_factory Plan factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
BillingCycleFactory $billing_cycle_factory,
PlanFactory $plan_factory,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->billing_cycle_factory = $billing_cycle_factory;
$this->plan_factory = $plan_factory;
$this->logger = $logger;
}
/**
* Creates a subscription plan.
*
* @param string $name Product name.
* @param string $product_id Product ID.
* @param array $billing_cycles Billing cycles.
* @param array $payment_preferences Payment preferences.
*
* @return Plan
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function create(
string $name,
string $product_id,
array $billing_cycles,
array $payment_preferences
): Plan {
$data = array(
'name' => $name,
'product_id' => $product_id,
'billing_cycles' => $billing_cycles,
'payment_preferences' => $payment_preferences,
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/plans';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to create plan.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $this->plan_factory->from_paypal_response( $json );
}
/**
* Returns a plan,
*
* @param string $id Plan ID.
*
* @return Plan
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function plan( string $id ): Plan {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/plans/' . $id;
$args = array(
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to get plan.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $this->plan_factory->from_paypal_response( $json );
}
/**
* Updates pricing.
*
* @param string $id Plan ID.
* @param BillingCycle $billing_cycle Billing cycle.
*
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function update_pricing( string $id, BillingCycle $billing_cycle ): void {
$data = array(
'pricing_schemes' => array(
(object) array(
'billing_cycle_sequence' => 1,
'pricing_scheme' => $billing_cycle->pricing_scheme(),
),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/plans/' . $id . '/update-pricing-schemes';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Could not update pricing.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
}
/**
* Deactivates a Subscription Plan.
*
* @param string $billing_plan_id The Plan ID.
*
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function deactivate_plan( string $billing_plan_id ) {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/plans/' . $billing_plan_id . '/deactivate';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Could not deactivate plan.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
}
}

View file

@ -0,0 +1,219 @@
<?php
/**
* The Billing Subscriptions endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class BillingSubscriptions
*/
class BillingSubscriptions {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* BillingSubscriptions constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
*/
public function __construct( string $host, Bearer $bearer, LoggerInterface $logger ) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
}
/**
* Suspends a subscription.
*
* @param string $id Subscription ID.
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function suspend( string $id ):void {
$data = array(
'reason' => 'Suspended by customer',
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id . '/suspend';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to suspend subscription.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
}
/**
* Activates a subscription.
*
* @param string $id Subscription ID.
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function activate( string $id ): void {
$data = array(
'reason' => 'Reactivated by customer',
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id . '/activate';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to reactivate subscription.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
}
/**
* Cancels a Subscription.
*
* @param string $id Subscription ID.
*
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function cancel( string $id ): void {
$data = array(
'reason' => 'Cancelled by customer',
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id . '/cancel';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to cancel subscription.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
}
/**
* Returns a Subscription object from the given ID.
*
* @param string $id Subscription ID.
*
* @return stdClass
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function subscription( string $id ): stdClass {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id;
$args = array(
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to get subscription.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $json;
}
}

View file

@ -0,0 +1,196 @@
<?php
/**
* The Catalog Products endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Product;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory;
/**
* Class CatalogProduct
*/
class CatalogProducts {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* Product factory.
*
* @var ProductFactory
*/
private $product_factory;
/**
* CatalogProducts constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param ProductFactory $product_factory Product factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
ProductFactory $product_factory,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->product_factory = $product_factory;
$this->logger = $logger;
}
/**
* Creates a product.
*
* @param string $name Product name.
* @param string $description Product description.
*
* @return Product
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function create( string $name, string $description ): Product {
$data = array(
'name' => $name,
'description' => $description ?: $name,
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/catalogs/products';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to create product.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $this->product_factory->from_paypal_response( $json );
}
/**
* Updates a Product.
*
* @param string $id Product ID.
* @param array $data Data to update.
*
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function update( string $id, array $data ): void {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/catalogs/products/' . $id;
$args = array(
'method' => 'PATCH',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to update product.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
}
/**
* Return a Product from the given ID.
*
* @param string $id Product ID.
*
* @return Product
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function product( string $id ): Product {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/catalogs/products/' . $id;
$args = array(
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to get product.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $this->product_factory->from_paypal_response( $json );
}
}

View file

@ -103,7 +103,8 @@ class IdentityToken {
);
if (
( $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ) )
&& defined( 'PPCP_FLAG_SUBSCRIPTION' ) && PPCP_FLAG_SUBSCRIPTION
|| ( $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ) )
|| ( $this->settings->has( 'subscriptions_mode' ) && $this->settings->get( 'subscriptions_mode' ) === 'vaulting_api' )
) {
$customer_id = $this->customer_repository->customer_id_for_user( ( $user_id ) );
update_user_meta( $user_id, 'ppcp_customer_id', $customer_id );

View file

@ -16,8 +16,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PatchCollection;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentMethod;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
@ -26,7 +26,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ErrorResponse;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;
@ -116,13 +115,6 @@ class OrderEndpoint {
*/
private $bn_code;
/**
* The paypal request id repository.
*
* @var PayPalRequestIdRepository
*/
private $paypal_request_id_repository;
/**
* OrderEndpoint constructor.
*
@ -133,7 +125,6 @@ class OrderEndpoint {
* @param string $intent The intent.
* @param LoggerInterface $logger The logger.
* @param ApplicationContextRepository $application_context_repository The application context repository.
* @param PayPalRequestIdRepository $paypal_request_id_repository The paypal request id repository.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param bool $is_fraudnet_enabled true if FraudNet support is enabled in settings, otherwise false.
* @param FraudNet $fraudnet The FraudNet entity.
@ -147,7 +138,6 @@ class OrderEndpoint {
string $intent,
LoggerInterface $logger,
ApplicationContextRepository $application_context_repository,
PayPalRequestIdRepository $paypal_request_id_repository,
SubscriptionHelper $subscription_helper,
bool $is_fraudnet_enabled,
FraudNet $fraudnet,
@ -162,7 +152,6 @@ class OrderEndpoint {
$this->logger = $logger;
$this->application_context_repository = $application_context_repository;
$this->bn_code = $bn_code;
$this->paypal_request_id_repository = $paypal_request_id_repository;
$this->is_fraudnet_enabled = $is_fraudnet_enabled;
$this->subscription_helper = $subscription_helper;
$this->fraudnet = $fraudnet;
@ -189,8 +178,8 @@ class OrderEndpoint {
* @param string $shipping_preference One of ApplicationContext::SHIPPING_PREFERENCE_ values.
* @param Payer|null $payer The payer off the order.
* @param PaymentToken|null $payment_token The payment token.
* @param PaymentMethod|null $payment_method The payment method.
* @param string $paypal_request_id The paypal request id.
* @param string $user_action The user action.
*
* @return Order
* @throws RuntimeException If the request fails.
@ -200,20 +189,27 @@ class OrderEndpoint {
string $shipping_preference,
Payer $payer = null,
PaymentToken $payment_token = null,
PaymentMethod $payment_method = null,
string $paypal_request_id = ''
string $paypal_request_id = '',
string $user_action = ApplicationContext::USER_ACTION_CONTINUE
): Order {
$bearer = $this->bearer->bearer();
$data = array(
'intent' => ( $this->subscription_helper->cart_contains_subscription() || $this->subscription_helper->current_product_is_subscription() ) ? 'AUTHORIZE' : $this->intent,
'intent' => apply_filters( 'woocommerce_paypal_payments_order_intent', $this->intent ),
'purchase_units' => array_map(
static function ( PurchaseUnit $item ): array {
return $item->to_array();
static function ( PurchaseUnit $item ) use ( $shipping_preference ): array {
$data = $item->to_array();
if ( $shipping_preference !== ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE ) {
// Shipping options are not allowed to be sent when not getting the address from PayPal.
unset( $data['shipping']['options'] );
}
return $data;
},
$items
),
'application_context' => $this->application_context_repository
->current_context( $shipping_preference )->to_array(),
->current_context( $shipping_preference, $user_action )->to_array(),
);
if ( $payer && ! empty( $payer->email_address() ) ) {
$data['payer'] = $payer->to_array();
@ -221,9 +217,6 @@ class OrderEndpoint {
if ( $payment_token ) {
$data['payment_source']['token'] = $payment_token->to_array();
}
if ( $payment_method ) {
$data['payment_method'] = $payment_method->to_array();
}
/**
* The filter can be used to modify the order creation request body data.
@ -240,8 +233,6 @@ class OrderEndpoint {
'body' => wp_json_encode( $data ),
);
$paypal_request_id = $paypal_request_id ? $paypal_request_id : uniqid( 'ppcp-', true );
$args['headers']['PayPal-Request-Id'] = $paypal_request_id;
if ( $this->bn_code ) {
$args['headers']['PayPal-Partner-Attribution-Id'] = $this->bn_code;
}
@ -250,6 +241,10 @@ class OrderEndpoint {
$args['headers']['PayPal-Client-Metadata-Id'] = $this->fraudnet->session_id();
}
if ( isset( $data['payment_source'] ) ) {
$args['headers']['PayPal-Request-Id'] = uniqid( 'ppcp-', true );
}
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
@ -286,7 +281,9 @@ class OrderEndpoint {
throw $error;
}
$order = $this->order_factory->from_paypal_response( $json );
$this->paypal_request_id_repository->set_for_order( $order, $paypal_request_id );
do_action( 'woocommerce_paypal_payments_paypal_order_created', $order );
return $order;
}
@ -310,7 +307,6 @@ class OrderEndpoint {
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
'PayPal-Request-Id' => $this->paypal_request_id_repository->get_for_order( $order ),
),
);
if ( $this->bn_code ) {
@ -385,7 +381,6 @@ class OrderEndpoint {
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
'PayPal-Request-Id' => $this->paypal_request_id_repository->get_for_order( $order ),
),
);
if ( $this->bn_code ) {
@ -458,7 +453,6 @@ class OrderEndpoint {
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => $this->paypal_request_id_repository->get_for_order_id( $id ),
),
);
if ( $this->bn_code ) {
@ -469,14 +463,8 @@ class OrderEndpoint {
$error = new RuntimeException(
__( 'Could not retrieve order.', 'woocommerce-paypal-payments' )
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
$this->logger->warning( $error->getMessage() );
throw $error;
}
$json = json_decode( $response['body'] );
@ -511,8 +499,8 @@ class OrderEndpoint {
);
throw $error;
}
$order = $this->order_factory->from_paypal_response( $json );
return $order;
return $this->order_factory->from_paypal_response( $json );
}
/**
@ -530,25 +518,36 @@ class OrderEndpoint {
return $order_to_update;
}
$patches_array = $patches->to_array();
if ( ! isset( $patches_array[0]['value']['shipping'] ) ) {
$shipping = isset( $order_to_update->purchase_units()[0] ) && null !== $order_to_update->purchase_units()[0]->shipping() ? $order_to_update->purchase_units()[0]->shipping() : null;
if ( $shipping ) {
$patches_array[0]['value']['shipping'] = $shipping->to_array();
}
$this->patch( $order_to_update->id(), $patches );
$new_order = $this->order( $order_to_update->id() );
return $new_order;
}
/**
* Patches an order.
*
* @param string $order_id The PayPal order ID.
* @param PatchCollection $patches The patches.
*
* @throws RuntimeException If the request fails.
*/
public function patch( string $order_id, PatchCollection $patches ): void {
$patches_array = $patches->to_array();
/**
* The filter can be used to modify the order patching request body data (the final prices, items).
*/
$patches_array = apply_filters( 'ppcp_patch_order_request_body_data', $patches_array );
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $order_to_update->id();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $order_id;
$args = array(
'method' => 'PATCH',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
'PayPal-Request-Id' => $this->paypal_request_id_repository->get_for_order(
$order_to_update
),
),
'body' => wp_json_encode( $patches_array ),
);
@ -558,11 +557,8 @@ class OrderEndpoint {
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
__( 'Could not retrieve order.', 'woocommerce-paypal-payments' )
);
$this->logger->log(
'warning',
$error = new RuntimeException( 'Could not patch order.' );
$this->logger->warning(
$error->getMessage(),
array(
'args' => $args,
@ -578,8 +574,7 @@ class OrderEndpoint {
$json,
$status_code
);
$this->logger->log(
'warning',
$this->logger->warning(
$error->getMessage(),
array(
'args' => $args,
@ -588,9 +583,6 @@ class OrderEndpoint {
);
throw $error;
}
$new_order = $this->order( $order_to_update->id() );
return $new_order;
}
/**

View file

@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerStatus;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
/**
* Class PartnersEndpoint
@ -65,6 +66,13 @@ class PartnersEndpoint {
*/
private $merchant_id;
/**
* The failure registry.
*
* @var FailureRegistry
*/
private $failure_registry;
/**
* PartnersEndpoint constructor.
*
@ -74,6 +82,7 @@ class PartnersEndpoint {
* @param SellerStatusFactory $seller_status_factory The seller status factory.
* @param string $partner_id The partner ID.
* @param string $merchant_id The merchant ID.
* @param FailureRegistry $failure_registry The API failure registry.
*/
public function __construct(
string $host,
@ -81,7 +90,8 @@ class PartnersEndpoint {
LoggerInterface $logger,
SellerStatusFactory $seller_status_factory,
string $partner_id,
string $merchant_id
string $merchant_id,
FailureRegistry $failure_registry
) {
$this->host = $host;
$this->bearer = $bearer;
@ -89,6 +99,7 @@ class PartnersEndpoint {
$this->seller_status_factory = $seller_status_factory;
$this->partner_id = $partner_id;
$this->merchant_id = $merchant_id;
$this->failure_registry = $failure_registry;
}
/**
@ -140,9 +151,15 @@ class PartnersEndpoint {
'response' => $response,
)
);
// Register the failure on api failure registry.
$this->failure_registry->add_failure( FailureRegistry::SELLER_STATUS_KEY );
throw $error;
}
$this->failure_registry->clear_failures( FailureRegistry::SELLER_STATUS_KEY );
$status = $this->seller_status_factory->from_paypal_reponse( $json );
return $status;
}

View file

@ -112,7 +112,7 @@ class PayUponInvoiceOrderEndpoint {
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
'purchase_units' => array_map(
static function ( PurchaseUnit $item ): array {
return $item->to_array( false );
return $item->to_array( true, false );
},
$items
),
@ -133,7 +133,7 @@ class PayUponInvoiceOrderEndpoint {
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
'PayPal-Client-Metadata-Id' => $this->fraudnet->session_id(),
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
'PayPal-Request-Id' => uniqid( 'ppcp-', true ), // Request-Id header is required.
),
'body' => wp_json_encode( $data ),
);
@ -166,8 +166,11 @@ class PayUponInvoiceOrderEndpoint {
throw new PayPalApiException( $json, $status_code );
}
$order = $this->order_factory->from_paypal_response( $json );
return $this->order_factory->from_paypal_response( $json );
do_action( 'woocommerce_paypal_payments_paypal_order_created', $order );
return $order;
}
/**
@ -236,6 +239,12 @@ class PayUponInvoiceOrderEndpoint {
$tax_total = $data['purchase_units'][0]['amount']['breakdown']['tax_total']['value'];
$item_total = $data['purchase_units'][0]['amount']['breakdown']['item_total']['value'];
$shipping = $data['purchase_units'][0]['amount']['breakdown']['shipping']['value'];
$handling = isset( $data['purchase_units'][0]['amount']['breakdown']['handling'] ) ? $data['purchase_units'][0]['amount']['breakdown']['handling']['value'] : 0;
$insurance = isset( $data['purchase_units'][0]['amount']['breakdown']['insurance'] ) ? $data['purchase_units'][0]['amount']['breakdown']['insurance']['value'] : 0;
$shipping_discount = isset( $data['purchase_units'][0]['amount']['breakdown']['shipping_discount'] ) ? $data['purchase_units'][0]['amount']['breakdown']['shipping_discount']['value'] : 0;
$discount = isset( $data['purchase_units'][0]['amount']['breakdown']['discount'] ) ? $data['purchase_units'][0]['amount']['breakdown']['discount']['value'] : 0;
$order_tax_total = $wc_order->get_total_tax();
$tax_rate = round( ( $order_tax_total / $item_total ) * 100, 1 );
@ -263,7 +272,7 @@ class PayUponInvoiceOrderEndpoint {
);
$total_amount = $data['purchase_units'][0]['amount']['value'];
$breakdown_total = $item_total + $tax_total + $shipping;
$breakdown_total = $item_total + $tax_total + $shipping + $handling + $insurance - $shipping_discount - $discount;
$diff = round( $total_amount - $breakdown_total, 2 );
if ( $diff === -0.01 || $diff === 0.01 ) {
$data['purchase_units'][0]['amount']['value'] = number_format( $breakdown_total, 2, '.', '' );

View file

@ -19,7 +19,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenActionLinksFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository;
use WP_Error;
/**
* Class PaymentTokenEndpoint
@ -70,13 +70,6 @@ class PaymentTokenEndpoint {
*/
protected $customer_repository;
/**
* The request id repository.
*
* @var PayPalRequestIdRepository
*/
private $request_id_repository;
/**
* PaymentTokenEndpoint constructor.
*
@ -86,7 +79,6 @@ class PaymentTokenEndpoint {
* @param PaymentTokenActionLinksFactory $payment_token_action_links_factory The PaymentTokenActionLinks factory.
* @param LoggerInterface $logger The logger.
* @param CustomerRepository $customer_repository The customer repository.
* @param PayPalRequestIdRepository $request_id_repository The request id repository.
*/
public function __construct(
string $host,
@ -94,8 +86,7 @@ class PaymentTokenEndpoint {
PaymentTokenFactory $factory,
PaymentTokenActionLinksFactory $payment_token_action_links_factory,
LoggerInterface $logger,
CustomerRepository $customer_repository,
PayPalRequestIdRepository $request_id_repository
CustomerRepository $customer_repository
) {
$this->host = $host;
@ -104,11 +95,10 @@ class PaymentTokenEndpoint {
$this->payment_token_action_links_factory = $payment_token_action_links_factory;
$this->logger = $logger;
$this->customer_repository = $customer_repository;
$this->request_id_repository = $request_id_repository;
}
/**
* Returns the payment tokens for a user.
* Returns the payment tokens for the given user id.
*
* @param int $id The user id.
*
@ -129,7 +119,67 @@ class PaymentTokenEndpoint {
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
__( 'Could not fetch payment token.', 'woocommerce-paypal-payments' )
__( 'Could not fetch payment token for customer id.', 'woocommerce-paypal-payments' )
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
$error = new PayPalApiException(
$json,
$status_code
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
$tokens = array();
foreach ( $json->payment_tokens as $token_value ) {
$tokens[] = $this->factory->from_paypal_response( $token_value );
}
return $tokens;
}
/**
* Returns the payment tokens for the given guest customer id.
*
* @param string $customer_id The guest customer id.
*
* @return PaymentToken[]
* @throws RuntimeException If the request fails.
*/
public function for_guest( string $customer_id ): array {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/vault/payment-tokens/?customer_id=' . $customer_id;
$args = array(
'method' => 'GET',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
$error = new RuntimeException(
__( 'Could not fetch payment token for guest customer id.', 'woocommerce-paypal-payments' )
);
$this->logger->log(
'warning',
@ -208,6 +258,40 @@ class PaymentTokenEndpoint {
return wp_remote_retrieve_response_code( $response ) === 204;
}
/**
* Deletes payment token by the given id.
*
* @param string $token_id Token id.
* @return bool
*
* @throws RuntimeException If something goes wrong while deleting the token.
*/
public function delete_token_by_id( string $token_id ): bool {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/vault/payment-tokens/' . $token_id;
$args = array(
'method' => 'DELETE',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
__( 'Could not delete payment token.', 'woocommerce-paypal-payments' )
);
$this->logger->warning( $error->getMessage() );
throw $error;
}
return wp_remote_retrieve_response_code( $response ) === 204;
}
/**
* Starts the process of PayPal account vaulting (without payment), returns the links for further actions.
*
@ -243,14 +327,11 @@ class PaymentTokenEndpoint {
),
);
$request_id = uniqid( 'ppcp-vault', true );
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Request-Id' => $request_id,
),
'body' => wp_json_encode( $data ),
);
@ -277,8 +358,6 @@ class PaymentTokenEndpoint {
$links = $this->payment_token_action_links_factory->from_paypal_response( $json );
$this->request_id_repository->set( "ppcp-vault-{$user_id}", $request_id );
return $links;
}
@ -302,7 +381,6 @@ class PaymentTokenEndpoint {
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Request-Id' => $this->request_id_repository->get( "ppcp-vault-{$user_id}" ),
'Content-Type' => 'application/json',
),
);

View file

@ -13,7 +13,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundCapture;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
@ -196,13 +196,13 @@ class PaymentsEndpoint {
/**
* Refunds a payment.
*
* @param Refund $refund The refund to be processed.
* @param RefundCapture $refund The refund to be processed.
*
* @return string Refund ID.
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function refund( Refund $refund ) : string {
public function refund( RefundCapture $refund ) : string {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/payments/captures/' . $refund->for_capture()->id() . '/refund';
$args = array(

View file

@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
use Psr\Log\LoggerInterface;
use WP_Error;
/**
* Class WebhookEndpoint
@ -175,12 +176,12 @@ class WebhookEndpoint {
*
* @param Webhook $hook The webhook to delete.
*
* @return bool
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function delete( Webhook $hook ): bool {
public function delete( Webhook $hook ): void {
if ( ! $hook->id() ) {
return false;
return;
}
$bearer = $this->bearer->bearer();
@ -193,12 +194,28 @@ class WebhookEndpoint {
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
if ( $response instanceof WP_Error ) {
throw new RuntimeException(
__( 'Not able to delete the webhook.', 'woocommerce-paypal-payments' )
);
}
return wp_remote_retrieve_response_code( $response ) === 204;
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
$json = null;
/**
* Use in array as consistency check.
*
* @psalm-suppress RedundantConditionGivenDocblockType
*/
if ( is_array( $response ) ) {
$json = json_decode( $response['body'] );
}
throw new PayPalApiException(
$json,
$status_code
);
}
}
/**

View file

@ -41,6 +41,9 @@ class ApplicationContext {
self::USER_ACTION_PAY_NOW,
);
const PAYMENT_METHOD_UNRESTRICTED = 'UNRESTRICTED';
const PAYMENT_METHOD_IMMEDIATE_PAYMENT_REQUIRED = 'IMMEDIATE_PAYMENT_REQUIRED';
/**
* The brand name.
*
@ -91,11 +94,11 @@ class ApplicationContext {
private $cancel_url;
/**
* The payment method.
* The payment method preference.
*
* @var null
* @var string
*/
private $payment_method;
private $payment_method_preference;
/**
* ApplicationContext constructor.
@ -107,6 +110,7 @@ class ApplicationContext {
* @param string $landing_page The landing page.
* @param string $shipping_preference The shipping preference.
* @param string $user_action The user action.
* @param string $payment_method_preference The payment method preference.
*
* @throws RuntimeException When values are not valid.
*/
@ -117,7 +121,8 @@ class ApplicationContext {
string $locale = '',
string $landing_page = self::LANDING_PAGE_NO_PREFERENCE,
string $shipping_preference = self::SHIPPING_PREFERENCE_NO_SHIPPING,
string $user_action = self::USER_ACTION_CONTINUE
string $user_action = self::USER_ACTION_CONTINUE,
string $payment_method_preference = self::PAYMENT_METHOD_IMMEDIATE_PAYMENT_REQUIRED
) {
if ( ! in_array( $landing_page, self::VALID_LANDING_PAGE_VALUES, true ) ) {
@ -136,9 +141,7 @@ class ApplicationContext {
$this->landing_page = $landing_page;
$this->shipping_preference = $shipping_preference;
$this->user_action = $user_action;
// Currently we have not implemented the payment method.
$this->payment_method = null;
$this->payment_method_preference = $payment_method_preference;
}
/**
@ -205,12 +208,10 @@ class ApplicationContext {
}
/**
* Returns the payment method.
*
* @return PaymentMethod|null
* Returns the payment method preference.
*/
public function payment_method() {
return $this->payment_method;
public function payment_method_preference(): string {
return $this->payment_method_preference;
}
/**
@ -223,9 +224,6 @@ class ApplicationContext {
if ( $this->user_action() ) {
$data['user_action'] = $this->user_action();
}
if ( $this->payment_method() ) {
$data['payment_method'] = $this->payment_method();
}
if ( $this->shipping_preference() ) {
$data['shipping_preference'] = $this->shipping_preference();
}
@ -244,6 +242,11 @@ class ApplicationContext {
if ( $this->cancel_url() ) {
$data['cancel_url'] = $this->cancel_url();
}
if ( $this->payment_method_preference ) {
$data['payment_method'] = array(
'payee_preferred' => $this->payment_method_preference,
);
}
return $data;
}
}

View file

@ -0,0 +1,134 @@
<?php
/**
* The Billing Cycle object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class BillingCycle
*/
class BillingCycle {
/**
* Frequency.
*
* @var array
*/
private $frequency;
/**
* Sequence.
*
* @var int
*/
private $sequence;
/**
* Tenure Type.
*
* @var string
*/
private $tenure_type;
/**
* Pricing scheme.
*
* @var array
*/
private $pricing_scheme;
/**
* Total cycles.
*
* @var int
*/
private $total_cycles;
/**
* BillingCycle constructor.
*
* @param array $frequency Frequency.
* @param int $sequence Sequence.
* @param string $tenure_type Tenure type.
* @param array $pricing_scheme Pricing scheme.
* @param int $total_cycles Total cycles.
*/
public function __construct(
array $frequency,
int $sequence,
string $tenure_type,
array $pricing_scheme = array(),
int $total_cycles = 1
) {
$this->frequency = $frequency;
$this->sequence = $sequence;
$this->tenure_type = $tenure_type;
$this->pricing_scheme = $pricing_scheme;
$this->total_cycles = $total_cycles;
}
/**
* Returns frequency.
*
* @return array
*/
public function frequency(): array {
return $this->frequency;
}
/**
* Returns sequence.
*
* @return int
*/
public function sequence(): int {
return $this->sequence;
}
/**
* Returns tenure type.
*
* @return string
*/
public function tenure_type(): string {
return $this->tenure_type;
}
/**
* Returns pricing scheme.
*
* @return array
*/
public function pricing_scheme(): array {
return $this->pricing_scheme;
}
/**
* Return total cycles.
*
* @return int
*/
public function total_cycles(): int {
return $this->total_cycles;
}
/**
* Returns Billing Cycle as array.
*
* @return array
*/
public function to_array(): array {
return array(
'frequency' => $this->frequency(),
'sequence' => $this->sequence(),
'tenure_type' => $this->tenure_type(),
'pricing_scheme' => $this->pricing_scheme(),
'total_cycles' => $this->total_cycles(),
);
}
}

View file

@ -66,6 +66,20 @@ class Item {
*/
private $category;
/**
* The product url.
*
* @var string
*/
protected $url;
/**
* The product image url.
*
* @var string
*/
protected $image_url;
/**
* The tax rate.
*
@ -73,6 +87,13 @@ class Item {
*/
protected $tax_rate;
/**
* The cart item key.
*
* @var string|null
*/
protected $cart_item_key;
/**
* Item constructor.
*
@ -83,7 +104,10 @@ class Item {
* @param Money|null $tax The tax.
* @param string $sku The SKU.
* @param string $category The category.
* @param string $url The product url.
* @param string $image_url The product image url.
* @param float $tax_rate The tax rate.
* @param ?string $cart_item_key The cart key for this item.
*/
public function __construct(
string $name,
@ -93,7 +117,10 @@ class Item {
Money $tax = null,
string $sku = '',
string $category = 'PHYSICAL_GOODS',
float $tax_rate = 0
string $url = '',
string $image_url = '',
float $tax_rate = 0,
string $cart_item_key = null
) {
$this->name = $name;
@ -102,9 +129,11 @@ class Item {
$this->description = $description;
$this->tax = $tax;
$this->sku = $sku;
$this->category = ( self::DIGITAL_GOODS === $category ) ? self::DIGITAL_GOODS : self::PHYSICAL_GOODS;
$this->category = $category;
$this->url = $url;
$this->image_url = $image_url;
$this->tax_rate = $tax_rate;
$this->cart_item_key = $cart_item_key;
}
/**
@ -170,6 +199,24 @@ class Item {
return $this->category;
}
/**
* Returns the url.
*
* @return string
*/
public function url():string {
return $this->url;
}
/**
* Returns the image url.
*
* @return string
*/
public function image_url():string {
return $this->validate_image_url() ? $this->image_url : '';
}
/**
* Returns the tax rate.
*
@ -179,12 +226,21 @@ class Item {
return round( (float) $this->tax_rate, 2 );
}
/**
* Returns the cart key for this item.
*
* @return string|null
*/
public function cart_item_key():?string {
return $this->cart_item_key;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array() {
public function to_array(): array {
$item = array(
'name' => $this->name(),
'unit_amount' => $this->unit_amount()->to_array(),
@ -192,8 +248,13 @@ class Item {
'description' => $this->description(),
'sku' => $this->sku(),
'category' => $this->category(),
'url' => $this->url(),
);
if ( $this->image_url() ) {
$item['image_url'] = $this->image_url();
}
if ( $this->tax() ) {
$item['tax'] = $this->tax()->to_array();
}
@ -202,6 +263,20 @@ class Item {
$item['tax_rate'] = (string) $this->tax_rate();
}
if ( $this->cart_item_key() ) {
$item['cart_item_key'] = (string) $this->cart_item_key();
}
return $item;
}
/**
* Validates the image url for PayPal request.
*
* @return bool true if valid, otherwise false.
*/
protected function validate_image_url(): bool {
$pattern = '/^(https:)([\/|\.|\w|\s|-])*\.(?:jpg|gif|png|jpeg|JPG|GIF|PNG|JPEG)$/';
return (bool) preg_match( $pattern, $this->image_url );
}
}

View file

@ -83,8 +83,8 @@ class Patch {
public function to_array(): array {
return array(
'op' => $this->op(),
'value' => $this->value(),
'path' => $this->path(),
'value' => $this->value(),
);
}

View file

@ -1,81 +0,0 @@
<?php
/**
* The PaymentMethod object
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class PaymentMethod
*/
class PaymentMethod {
const PAYER_SELECTED_DEFAULT = 'PAYPAL';
const PAYEE_PREFERRED_UNRESTRICTED = 'UNRESTRICTED';
const PAYEE_PREFERRED_IMMEDIATE_PAYMENT_REQUIRED = 'IMMEDIATE_PAYMENT_REQUIRED';
/**
* The preferred value.
*
* @var string
*/
private $preferred;
/**
* The selected value.
*
* @var string
*/
private $selected;
/**
* PaymentMethod constructor.
*
* @param string $preferred The preferred value.
* @param string $selected The selected value.
*/
public function __construct(
string $preferred = self::PAYEE_PREFERRED_UNRESTRICTED,
string $selected = self::PAYER_SELECTED_DEFAULT
) {
$this->preferred = $preferred;
$this->selected = $selected;
}
/**
* Returns the payer preferred value.
*
* @return string
*/
public function payee_preferred(): string {
return $this->preferred;
}
/**
* Returns the payer selected value.
*
* @return string
*/
public function payer_selected(): string {
return $this->selected;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array(
'payee_preferred' => $this->payee_preferred(),
'payer_selected' => $this->payer_selected(),
);
}
}

View file

@ -0,0 +1,115 @@
<?php
/**
* The Payment Preferences object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class PaymentPreferences
*/
class PaymentPreferences {
/**
* Setup fee.
*
* @var array
*/
private $setup_fee;
/**
* Auto bill outstanding.
*
* @var bool
*/
private $auto_bill_outstanding;
/**
* Setup fee failure action.
*
* @var string
*/
private $setup_fee_failure_action;
/**
* Payment failure threshold.
*
* @var int
*/
private $payment_failure_threshold;
/**
* PaymentPreferences constructor.
*
* @param array $setup_fee Setup fee.
* @param bool $auto_bill_outstanding Auto bill outstanding.
* @param string $setup_fee_failure_action Setup fee failure action.
* @param int $payment_failure_threshold payment failure threshold.
*/
public function __construct(
array $setup_fee,
bool $auto_bill_outstanding = true,
string $setup_fee_failure_action = 'CONTINUE',
int $payment_failure_threshold = 3
) {
$this->setup_fee = $setup_fee;
$this->auto_bill_outstanding = $auto_bill_outstanding;
$this->setup_fee_failure_action = $setup_fee_failure_action;
$this->payment_failure_threshold = $payment_failure_threshold;
}
/**
* Setup fee.
*
* @return array
*/
public function setup_fee(): array {
return $this->setup_fee;
}
/**
* Auto bill outstanding.
*
* @return bool
*/
public function auto_bill_outstanding(): bool {
return $this->auto_bill_outstanding;
}
/**
* Setup fee failure action.
*
* @return string
*/
public function setup_fee_failure_action(): string {
return $this->setup_fee_failure_action;
}
/**
* Payment failure threshold.
*
* @return int
*/
public function payment_failure_threshold(): int {
return $this->payment_failure_threshold;
}
/**
* Returns Payment Preferences as array.
*
* @return array
*/
public function to_array():array {
return array(
'setup_fee' => $this->setup_fee(),
'auto_bill_outstanding' => $this->auto_bill_outstanding(),
'setup_fee_failure_action' => $this->setup_fee_failure_action(),
'payment_failure_threshold' => $this->payment_failure_threshold(),
);
}
}

View file

@ -28,13 +28,21 @@ class Payments {
*/
private $captures;
/**
* The Refunds.
*
* @var Refund[]
*/
private $refunds;
/**
* Payments constructor.
*
* @param array $authorizations The Authorizations.
* @param array $captures The Captures.
* @param array $refunds The Refunds.
*/
public function __construct( array $authorizations, array $captures ) {
public function __construct( array $authorizations, array $captures, array $refunds = array() ) {
foreach ( $authorizations as $key => $authorization ) {
if ( is_a( $authorization, Authorization::class ) ) {
continue;
@ -47,8 +55,15 @@ class Payments {
}
unset( $captures[ $key ] );
}
foreach ( $refunds as $key => $refund ) {
if ( is_a( $refund, Refund::class ) ) {
continue;
}
unset( $refunds[ $key ] );
}
$this->authorizations = $authorizations;
$this->captures = $captures;
$this->refunds = $refunds;
}
/**
@ -70,6 +85,12 @@ class Payments {
},
$this->captures()
),
'refunds' => array_map(
static function ( Refund $refund ): array {
return $refund->to_array();
},
$this->refunds()
),
);
}
@ -90,4 +111,13 @@ class Payments {
public function captures(): array {
return $this->captures;
}
/**
* Returns the Refunds.
*
* @return Refund[]
**/
public function refunds(): array {
return $this->refunds;
}
}

View file

@ -0,0 +1,154 @@
<?php
/**
* The Plan object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class Plan
*/
class Plan {
/**
* Plan ID.
*
* @var string
*/
private $id;
/**
* Plan name.
*
* @var string
*/
private $name;
/**
* Product ID.
*
* @var string
*/
private $product_id;
/**
* Billing cycles.
*
* @var array
*/
private $billing_cycles;
/**
* Payment preferences.
*
* @var PaymentPreferences
*/
private $payment_preferences;
/**
* Plan status.
*
* @var string
*/
private $status;
/**
* Plan constructor.
*
* @param string $id Plan ID.
* @param string $name Plan name.
* @param string $product_id Product ID.
* @param array $billing_cycles Billing cycles.
* @param PaymentPreferences $payment_preferences Payment preferences.
* @param string $status Plan status.
*/
public function __construct(
string $id,
string $name,
string $product_id,
array $billing_cycles,
PaymentPreferences $payment_preferences,
string $status = ''
) {
$this->id = $id;
$this->name = $name;
$this->product_id = $product_id;
$this->billing_cycles = $billing_cycles;
$this->payment_preferences = $payment_preferences;
$this->status = $status;
}
/**
* Returns Plan ID.
*
* @return string
*/
public function id(): string {
return $this->id;
}
/**
* Returns Plan name.
*
* @return string
*/
public function name(): string {
return $this->name;
}
/**
* Returns Product ID.
*
* @return string
*/
public function product_id(): string {
return $this->product_id;
}
/**
* Returns Billing cycles.
*
* @return array
*/
public function billing_cycles(): array {
return $this->billing_cycles;
}
/**
* Returns Payment preferences.
*
* @return PaymentPreferences
*/
public function payment_preferences(): PaymentPreferences {
return $this->payment_preferences;
}
/**
* Returns Plan status.
*
* @return string
*/
public function status(): string {
return $this->status;
}
/**
* Returns Plan as array.
*
* @return array
*/
public function to_array():array {
return array(
'id' => $this->id(),
'name' => $this->name(),
'product_id' => $this->product_id(),
'billing_cycles' => $this->billing_cycles(),
'payment_preferences' => $this->payment_preferences(),
'status' => $this->status(),
);
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* The Product object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class Product
*/
class Product {
/**
* Product ID.
*
* @var string
*/
private $id;
/**
* Product name.
*
* @var string
*/
private $name;
/**
* Product description.
*
* @var string
*/
private $description;
/**
* Product constructor.
*
* @param string $id Product ID.
* @param string $name Product name.
* @param string $description Product description.
*/
public function __construct( string $id, string $name, string $description = '' ) {
$this->id = $id;
$this->name = $name;
$this->description = $description;
}
/**
* Returns the product ID.
*
* @return string
*/
public function id(): string {
return $this->id;
}
/**
* Returns the product name.
*
* @return string
*/
public function name(): string {
return $this->name;
}
/**
* Returns the product description.
*
* @return string
*/
public function description(): string {
return $this->description;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array() {
return array(
'id' => $this->id(),
'name' => $this->name(),
'description' => $this->description(),
);
}
}

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
/**
* Class PurchaseUnit
*/
@ -49,13 +51,6 @@ class PurchaseUnit {
*/
private $description;
/**
* The Payee.
*
* @var Payee|null
*/
private $payee;
/**
* The custom id.
*
@ -91,6 +86,13 @@ class PurchaseUnit {
*/
private $contains_physical_goods = false;
/**
* The sanitizer for this purchase unit output.
*
* @var PurchaseUnitSanitizer|null
*/
private $sanitizer;
/**
* PurchaseUnit constructor.
*
@ -99,7 +101,6 @@ class PurchaseUnit {
* @param Shipping|null $shipping The Shipping.
* @param string $reference_id The reference ID.
* @param string $description The description.
* @param Payee|null $payee The Payee.
* @param string $custom_id The custom ID.
* @param string $invoice_id The invoice ID.
* @param string $soft_descriptor The soft descriptor.
@ -111,7 +112,6 @@ class PurchaseUnit {
Shipping $shipping = null,
string $reference_id = 'default',
string $description = '',
Payee $payee = null,
string $custom_id = '',
string $invoice_id = '',
string $soft_descriptor = '',
@ -141,7 +141,6 @@ class PurchaseUnit {
}
)
);
$this->payee = $payee;
$this->custom_id = $custom_id;
$this->invoice_id = $invoice_id;
$this->soft_descriptor = $soft_descriptor;
@ -211,6 +210,25 @@ class PurchaseUnit {
return $this->custom_id;
}
/**
* Sets the custom ID.
*
* @param string $custom_id The value to set.
*/
public function set_custom_id( string $custom_id ): void {
$this->custom_id = $custom_id;
}
/**
* Sets the sanitizer for this purchase unit output.
*
* @param PurchaseUnitSanitizer|null $sanitizer The sanitizer.
* @return void
*/
public function set_sanitizer( ?PurchaseUnitSanitizer $sanitizer ) {
$this->sanitizer = $sanitizer;
}
/**
* Returns the invoice id.
*
@ -229,15 +247,6 @@ class PurchaseUnit {
return $this->soft_descriptor;
}
/**
* Returns the Payee.
*
* @return Payee|null
*/
public function payee() {
return $this->payee;
}
/**
* Returns the Payments.
*
@ -268,11 +277,12 @@ class PurchaseUnit {
/**
* Returns the object as array.
*
* @param bool $ditch_items_when_mismatch Whether ditch items when mismatch or not.
* @param bool $sanitize_output Whether output should be sanitized for PayPal consumption.
* @param bool $allow_ditch_items Whether to allow items to be ditched.
*
* @return array
*/
public function to_array( bool $ditch_items_when_mismatch = true ): array {
public function to_array( bool $sanitize_output = true, bool $allow_ditch_items = true ): array {
$purchase_unit = array(
'reference_id' => $this->reference_id(),
'amount' => $this->amount()->to_array(),
@ -284,14 +294,6 @@ class PurchaseUnit {
$this->items()
),
);
if ( $ditch_items_when_mismatch && $this->ditch_items_when_mismatch( $this->amount(), ...$this->items() ) ) {
unset( $purchase_unit['items'] );
unset( $purchase_unit['amount']['breakdown'] );
}
if ( $this->payee() ) {
$purchase_unit['payee'] = $this->payee()->to_array();
}
if ( $this->payments() ) {
$purchase_unit['payments'] = $this->payments()->to_array();
@ -309,101 +311,45 @@ class PurchaseUnit {
if ( $this->soft_descriptor() ) {
$purchase_unit['soft_descriptor'] = $this->soft_descriptor();
}
return $purchase_unit;
$has_ditched_items_breakdown = false;
if ( $sanitize_output && isset( $this->sanitizer ) ) {
$purchase_unit = ( $this->sanitizer->sanitize( $purchase_unit, $allow_ditch_items ) );
$has_ditched_items_breakdown = $this->sanitizer->has_ditched_items_breakdown();
}
return $this->apply_ditch_items_mismatch_filter(
$has_ditched_items_breakdown,
$purchase_unit
);
}
/**
* All money values send to PayPal can only have 2 decimal points. WooCommerce internally does
* not have this restriction. Therefore the totals of the cart in WooCommerce and the totals
* of the rounded money values of the items, we send to PayPal, can differ. In those cases,
* we can not send the line items.
* Applies the ppcp_ditch_items_breakdown filter.
* If true purchase_unit items and breakdown are ditched from PayPal.
*
* @param Amount $amount The amount.
* @param Item ...$items The items.
* @return bool
* @param bool $ditched_items_breakdown If the breakdown and items were already ditched.
* @param array $purchase_unit The purchase_unit array.
* @return array
*/
private function ditch_items_when_mismatch( Amount $amount, Item ...$items ): bool {
$breakdown = $amount->breakdown();
if ( ! $breakdown ) {
return false;
}
public function apply_ditch_items_mismatch_filter( bool $ditched_items_breakdown, array $purchase_unit ): array {
/**
* The filter can be used to control when the items and totals breakdown are removed from PayPal order info.
*/
$ditch = apply_filters( 'ppcp_ditch_items_breakdown', $ditched_items_breakdown, $this );
$item_total = $breakdown->item_total();
if ( $item_total ) {
$remaining_item_total = array_reduce(
$items,
function ( float $total, Item $item ): float {
return $total - (float) $item->unit_amount()->value_str() * (float) $item->quantity();
},
(float) $item_total->value_str()
if ( $ditch ) {
unset( $purchase_unit['items'] );
unset( $purchase_unit['amount']['breakdown'] );
if ( isset( $this->sanitizer ) && ( $ditch !== $ditched_items_breakdown ) ) {
$this->sanitizer->set_last_message(
__( 'Ditch items breakdown filter. Items and breakdown ditched.', 'woocommerce-paypal-payments' )
);
$remaining_item_total = round( $remaining_item_total, 2 );
if ( 0.0 !== $remaining_item_total ) {
return true;
}
}
$tax_total = $breakdown->tax_total();
$items_with_tax = array_filter(
$this->items,
function ( Item $item ): bool {
return null !== $item->tax();
}
);
if ( $tax_total && ! empty( $items_with_tax ) ) {
$remaining_tax_total = array_reduce(
$items,
function ( float $total, Item $item ): float {
$tax = $item->tax();
if ( $tax ) {
$total -= (float) $tax->value_str() * (float) $item->quantity();
}
return $total;
},
(float) $tax_total->value_str()
);
$remaining_tax_total = round( $remaining_tax_total, 2 );
if ( 0.0 !== $remaining_tax_total ) {
return true;
}
}
$shipping = $breakdown->shipping();
$discount = $breakdown->discount();
$shipping_discount = $breakdown->shipping_discount();
$handling = $breakdown->handling();
$insurance = $breakdown->insurance();
$amount_total = 0.0;
if ( $shipping ) {
$amount_total += (float) $shipping->value_str();
}
if ( $item_total ) {
$amount_total += (float) $item_total->value_str();
}
if ( $discount ) {
$amount_total -= (float) $discount->value_str();
}
if ( $tax_total ) {
$amount_total += (float) $tax_total->value_str();
}
if ( $shipping_discount ) {
$amount_total -= (float) $shipping_discount->value_str();
}
if ( $handling ) {
$amount_total += (float) $handling->value_str();
}
if ( $insurance ) {
$amount_total += (float) $insurance->value_str();
}
$amount_str = $amount->value_str();
$amount_total_str = ( new Money( $amount_total, $amount->currency_code() ) )->value_str();
$needs_to_ditch = $amount_str !== $amount_total_str;
return $needs_to_ditch;
return $purchase_unit;
}
}

View file

@ -1,6 +1,8 @@
<?php
/**
* The refund object.
* The refund entity.
*
* @link https://developer.paypal.com/docs/api/orders/v2/#definition-refund
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
@ -15,11 +17,32 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
class Refund {
/**
* The Capture.
* The ID.
*
* @var Capture
* @var string
*/
private $capture;
private $id;
/**
* The status.
*
* @var RefundStatus
*/
private $status;
/**
* The amount.
*
* @var Amount
*/
private $amount;
/**
* The detailed breakdown of the refund activity (fees, ...).
*
* @var SellerPayableBreakdown|null
*/
private $seller_payable_breakdown;
/**
* The invoice id.
@ -29,50 +52,97 @@ class Refund {
private $invoice_id;
/**
* The note to the payer.
* The custom id.
*
* @var string
*/
private $custom_id;
/**
* The acquirer reference number.
*
* @var string
*/
private $acquirer_reference_number;
/**
* The acquirer reference number.
*
* @var string
*/
private $note_to_payer;
/**
* The Amount.
* The payer of the refund.
*
* @var Amount|null
* @var ?RefundPayer
*/
private $amount;
private $payer;
/**
* Refund constructor.
*
* @param Capture $capture The capture where the refund is supposed to be applied at.
* @param string $id The ID.
* @param RefundStatus $status The status.
* @param Amount $amount The amount.
* @param string $invoice_id The invoice id.
* @param string $note_to_payer The note to the payer.
* @param Amount|null $amount The Amount.
* @param string $custom_id The custom id.
* @param SellerPayableBreakdown|null $seller_payable_breakdown The detailed breakdown of the refund activity (fees, ...).
* @param string $acquirer_reference_number The acquirer reference number.
* @param string $note_to_payer The note to payer.
* @param RefundPayer|null $payer The payer.
*/
public function __construct(
Capture $capture,
string $id,
RefundStatus $status,
Amount $amount,
string $invoice_id,
string $note_to_payer = '',
Amount $amount = null
string $custom_id,
?SellerPayableBreakdown $seller_payable_breakdown,
string $acquirer_reference_number,
string $note_to_payer,
?RefundPayer $payer
) {
$this->capture = $capture;
$this->invoice_id = $invoice_id;
$this->note_to_payer = $note_to_payer;
$this->id = $id;
$this->status = $status;
$this->amount = $amount;
$this->invoice_id = $invoice_id;
$this->custom_id = $custom_id;
$this->seller_payable_breakdown = $seller_payable_breakdown;
$this->acquirer_reference_number = $acquirer_reference_number;
$this->note_to_payer = $note_to_payer;
$this->payer = $payer;
}
/**
* Returns the capture for the refund.
* Returns the ID.
*
* @return Capture
* @return string
*/
public function for_capture() : Capture {
return $this->capture;
public function id() : string {
return $this->id;
}
/**
* Return the invoice id.
* Returns the status.
*
* @return RefundStatus
*/
public function status() : RefundStatus {
return $this->status;
}
/**
* Returns the amount.
*
* @return Amount
*/
public function amount() : Amount {
return $this->amount;
}
/**
* Returns the invoice id.
*
* @return string
*/
@ -81,7 +151,34 @@ class Refund {
}
/**
* Returns the note to the payer.
* Returns the custom id.
*
* @return string
*/
public function custom_id() : string {
return $this->custom_id;
}
/**
* Returns the detailed breakdown of the refund activity (fees, ...).
*
* @return SellerPayableBreakdown|null
*/
public function seller_payable_breakdown() : ?SellerPayableBreakdown {
return $this->seller_payable_breakdown;
}
/**
* The acquirer reference number.
*
* @return string
*/
public function acquirer_reference_number() : string {
return $this->acquirer_reference_number;
}
/**
* The note to payer.
*
* @return string
*/
@ -90,28 +187,38 @@ class Refund {
}
/**
* Returns the Amount.
* Returns the refund payer.
*
* @return Amount|null
* @return RefundPayer|null
*/
public function amount() {
return $this->amount;
public function payer() : ?RefundPayer {
return $this->payer;
}
/**
* Returns the object as array.
* Returns the entity as array.
*
* @return array
*/
public function to_array() : array {
$data = array(
'id' => $this->id(),
'status' => $this->status()->name(),
'amount' => $this->amount()->to_array(),
'invoice_id' => $this->invoice_id(),
'custom_id' => $this->custom_id(),
'acquirer_reference_number' => $this->acquirer_reference_number(),
'note_to_payer' => (array) $this->note_to_payer(),
);
if ( $this->note_to_payer() ) {
$data['note_to_payer'] = $this->note_to_payer();
$details = $this->status()->details();
if ( $details ) {
$data['status_details'] = array( 'reason' => $details->reason() );
}
if ( $this->amount() ) {
$data['amount'] = $this->amount()->to_array();
if ( $this->seller_payable_breakdown ) {
$data['seller_payable_breakdown'] = $this->seller_payable_breakdown->to_array();
}
if ( $this->payer ) {
$data['payer'] = $this->payer->to_array();
}
return $data;
}

View file

@ -0,0 +1,118 @@
<?php
/**
* The refund capture object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class RefundCapture
*/
class RefundCapture {
/**
* The Capture.
*
* @var Capture
*/
private $capture;
/**
* The invoice id.
*
* @var string
*/
private $invoice_id;
/**
* The note to the payer.
*
* @var string
*/
private $note_to_payer;
/**
* The Amount.
*
* @var Amount|null
*/
private $amount;
/**
* Refund constructor.
*
* @param Capture $capture The capture where the refund is supposed to be applied at.
* @param string $invoice_id The invoice id.
* @param string $note_to_payer The note to the payer.
* @param Amount|null $amount The Amount.
*/
public function __construct(
Capture $capture,
string $invoice_id,
string $note_to_payer = '',
Amount $amount = null
) {
$this->capture = $capture;
$this->invoice_id = $invoice_id;
$this->note_to_payer = $note_to_payer;
$this->amount = $amount;
}
/**
* Returns the capture for the refund.
*
* @return Capture
*/
public function for_capture() : Capture {
return $this->capture;
}
/**
* Return the invoice id.
*
* @return string
*/
public function invoice_id() : string {
return $this->invoice_id;
}
/**
* Returns the note to the payer.
*
* @return string
*/
public function note_to_payer() : string {
return $this->note_to_payer;
}
/**
* Returns the Amount.
*
* @return Amount|null
*/
public function amount() {
return $this->amount;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array() : array {
$data = array(
'invoice_id' => $this->invoice_id(),
);
if ( $this->note_to_payer() ) {
$data['note_to_payer'] = $this->note_to_payer();
}
if ( $this->amount ) {
$data['amount'] = $this->amount->to_array();
}
return $data;
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* The refund payer object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class RefundPayer
* The customer who sends the money.
*/
class RefundPayer {
/**
* The email address.
*
* @var string
*/
private $email_address;
/**
* The merchant id.
*
* @var string
*/
private $merchant_id;
/**
* RefundPayer constructor.
*
* @param string $email_address The email.
* @param string $merchant_id The merchant id.
*/
public function __construct(
string $email_address,
string $merchant_id
) {
$this->email_address = $email_address;
$this->merchant_id = $merchant_id;
}
/**
* Returns the email address.
*
* @return string
*/
public function email_address(): string {
return $this->email_address;
}
/**
* Returns the merchant id.
*
* @return string
*/
public function merchant_id(): string {
return $this->merchant_id;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array() {
$payer = array(
'email_address' => $this->email_address(),
);
if ( $this->merchant_id ) {
$payer['merchant_id'] = $this->merchant_id();
}
return $payer;
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* The RefundStatus object.
*
* @see https://developer.paypal.com/docs/api/orders/v2/#definition-refund_status
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class RefundStatus
*/
class RefundStatus {
const COMPLETED = 'COMPLETED';
const CANCELLED = 'CANCELLED';
const FAILED = 'FAILED';
const PENDING = 'PENDING';
/**
* The status.
*
* @var string
*/
private $status;
/**
* The details.
*
* @var RefundStatusDetails|null
*/
private $details;
/**
* RefundStatus constructor.
*
* @param string $status The status.
* @param RefundStatusDetails|null $details The details.
*/
public function __construct( string $status, ?RefundStatusDetails $details = null ) {
$this->status = $status;
$this->details = $details;
}
/**
* Compares the current status with a given one.
*
* @param string $status The status to compare with.
*
* @return bool
*/
public function is( string $status ): bool {
return $this->status === $status;
}
/**
* Returns the status.
*
* @return string
*/
public function name(): string {
return $this->status;
}
/**
* Returns the details.
*
* @return RefundStatusDetails|null
*/
public function details(): ?RefundStatusDetails {
return $this->details;
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* The RefundStatusDetails object.
*
* @see https://developer.paypal.com/docs/api/payments/v2/#definition-refund_status_details
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class RefundStatusDetails
*/
class RefundStatusDetails {
const ECHECK = 'ECHECK';
/**
* The reason.
*
* @var string
*/
private $reason;
/**
* RefundStatusDetails constructor.
*
* @param string $reason The reason explaining refund status.
*/
public function __construct( string $reason ) {
$this->reason = $reason;
}
/**
* Compares the current reason with a given one.
*
* @param string $reason The reason to compare with.
*
* @return bool
*/
public function is( string $reason ): bool {
return $this->reason === $reason;
}
/**
* Returns the reason explaining refund status.
* One of RefundStatusDetails constants.
*
* @return string
*/
public function reason(): string {
return $this->reason;
}
/**
* Returns the human-readable reason text explaining refund status.
*
* @return string
*/
public function text(): string {
switch ( $this->reason ) {
case self::ECHECK:
return __( 'The payer paid by an eCheck that has not yet cleared.', 'woocommerce-paypal-payments' );
default:
return $this->reason;
}
}
}

View file

@ -0,0 +1,202 @@
<?php
/**
* The info about fees and amount that will be paid by the seller.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class SellerPayableBreakdown
*/
class SellerPayableBreakdown {
/**
* The amount for this refunded payment in the currency of the transaction.
*
* @var Money|null
*/
private $gross_amount;
/**
* The applicable fee for this refunded payment in the currency of the transaction.
*
* @var Money|null
*/
private $paypal_fee;
/**
* The applicable fee for this captured payment in the receivable currency.
*
* Present only in cases the fee is charged in the receivable currency.
*
* @var Money|null
*/
private $paypal_fee_in_receivable_currency;
/**
* The net amount that the payee receives for this refunded payment in their PayPal account.
*
* Computed as gross_amount minus the paypal_fee minus the platform_fees.
*
* @var Money|null
*/
private $net_amount;
/**
* The net amount for this refunded payment in the receivable currency.
*
* @var Money|null
*/
private $net_amount_in_receivable_currency;
/**
* The total amount for this refund.
*
* @var Money|null
*/
private $total_refunded_amount;
/**
* An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment.
*
* @var PlatformFee[]
*/
private $platform_fees;
/**
* SellerPayableBreakdown constructor.
*
* @param Money|null $gross_amount The amount for this refunded payment in the currency of the transaction.
* @param Money|null $paypal_fee The applicable fee for this refunded payment in the currency of the transaction.
* @param Money|null $paypal_fee_in_receivable_currency The applicable fee for this refunded payment in the receivable currency.
* @param Money|null $net_amount The net amount that the payee receives for this refunded payment in their PayPal account.
* @param Money|null $net_amount_in_receivable_currency The net amount for this refunded payment in the receivable currency.
* @param Money|null $total_refunded_amount The total amount for this refund.
* @param PlatformFee[] $platform_fees An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment.
*/
public function __construct(
?Money $gross_amount,
?Money $paypal_fee,
?Money $paypal_fee_in_receivable_currency,
?Money $net_amount,
?Money $net_amount_in_receivable_currency,
?Money $total_refunded_amount,
array $platform_fees
) {
$this->gross_amount = $gross_amount;
$this->paypal_fee = $paypal_fee;
$this->paypal_fee_in_receivable_currency = $paypal_fee_in_receivable_currency;
$this->net_amount = $net_amount;
$this->net_amount_in_receivable_currency = $net_amount_in_receivable_currency;
$this->total_refunded_amount = $total_refunded_amount;
$this->platform_fees = $platform_fees;
}
/**
* The amount for this refunded payment in the currency of the transaction.
*
* @return Money|null
*/
public function gross_amount(): ?Money {
return $this->gross_amount;
}
/**
* The applicable fee for this refunded payment in the currency of the transaction.
*
* @return Money|null
*/
public function paypal_fee(): ?Money {
return $this->paypal_fee;
}
/**
* The applicable fee for this refunded payment in the receivable currency.
*
* Present only in cases the fee is charged in the receivable currency.
*
* @return Money|null
*/
public function paypal_fee_in_receivable_currency(): ?Money {
return $this->paypal_fee_in_receivable_currency;
}
/**
* The net amount that the payee receives for this refunded payment in their PayPal account.
*
* Computed as gross_amount minus the paypal_fee minus the platform_fees.
*
* @return Money|null
*/
public function net_amount(): ?Money {
return $this->net_amount;
}
/**
* The net amount for this refunded payment in the receivable currency.
*
* @return Money|null
*/
public function net_amount_in_receivable_currency(): ?Money {
return $this->net_amount_in_receivable_currency;
}
/**
* The total amount for this refund.
*
* @return Money|null
*/
public function total_refunded_amount(): ?Money {
return $this->total_refunded_amount;
}
/**
* An array of platform or partner fees, commissions, or brokerage fees that associated with the refunded payment.
*
* @return PlatformFee[]
*/
public function platform_fees(): array {
return $this->platform_fees;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
$data = array();
if ( $this->gross_amount ) {
$data['gross_amount'] = $this->gross_amount->to_array();
}
if ( $this->paypal_fee ) {
$data['paypal_fee'] = $this->paypal_fee->to_array();
}
if ( $this->paypal_fee_in_receivable_currency ) {
$data['paypal_fee_in_receivable_currency'] = $this->paypal_fee_in_receivable_currency->to_array();
}
if ( $this->net_amount ) {
$data['net_amount'] = $this->net_amount->to_array();
}
if ( $this->net_amount_in_receivable_currency ) {
$data['net_amount_in_receivable_currency'] = $this->net_amount_in_receivable_currency->to_array();
}
if ( $this->total_refunded_amount ) {
$data['total_refunded_amount'] = $this->total_refunded_amount->to_array();
}
if ( $this->platform_fees ) {
$data['platform_fees'] = array_map(
function ( PlatformFee $fee ) {
return $fee->to_array();
},
$this->platform_fees
);
}
return $data;
}
}

View file

@ -28,15 +28,24 @@ class Shipping {
*/
private $address;
/**
* Shipping methods.
*
* @var ShippingOption[]
*/
private $options;
/**
* Shipping constructor.
*
* @param string $name The name.
* @param Address $address The address.
* @param ShippingOption[] $options Shipping methods.
*/
public function __construct( string $name, Address $address ) {
public function __construct( string $name, Address $address, array $options = array() ) {
$this->name = $name;
$this->address = $address;
$this->options = $options;
}
/**
@ -57,17 +66,35 @@ class Shipping {
return $this->address;
}
/**
* Returns the shipping methods.
*
* @return ShippingOption[]
*/
public function options(): array {
return $this->options;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array(
$result = array(
'name' => array(
'full_name' => $this->name(),
),
'address' => $this->address()->to_array(),
);
if ( $this->options ) {
$result['options'] = array_map(
function ( ShippingOption $opt ): array {
return $opt->to_array();
},
$this->options
);
}
return $result;
}
}

View file

@ -0,0 +1,139 @@
<?php
/**
* The ShippingOption object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class ShippingOption
*/
class ShippingOption {
const TYPE_SHIPPING = 'SHIPPING';
const TYPE_PICKUP = 'PICKUP';
/**
* The name.
*
* @var string
*/
private $id;
/**
* The label.
*
* @var string
*/
private $label;
/**
* Whether the method is selected by default.
*
* @var bool
*/
private $selected;
/**
* The price.
*
* @var Money
*/
private $amount;
/**
* SHIPPING or PICKUP.
*
* @var string
*/
private $type;
/**
* ShippingOption constructor.
*
* @param string $id The name.
* @param string $label The label.
* @param bool $selected Whether the method is selected by default.
* @param Money $amount The price.
* @param string $type SHIPPING or PICKUP.
*/
public function __construct( string $id, string $label, bool $selected, Money $amount, string $type ) {
$this->id = $id;
$this->label = $label;
$this->selected = $selected;
$this->amount = $amount;
$this->type = $type;
}
/**
* The name.
*
* @return string
*/
public function id(): string {
return $this->id;
}
/**
* The label.
*
* @return string
*/
public function label(): string {
return $this->label;
}
/**
* Whether the method is selected by default.
*
* @return bool
*/
public function selected(): bool {
return $this->selected;
}
/**
* Sets whether the method is selected by default.
*
* @param bool $selected The value to be set.
*/
public function set_selected( bool $selected ): void {
$this->selected = $selected;
}
/**
* The price.
*
* @return Money
*/
public function amount(): Money {
return $this->amount;
}
/**
* SHIPPING or PICKUP.
*
* @return string
*/
public function type(): string {
return $this->type;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array(
'id' => $this->id,
'label' => $this->label,
'selected' => $this->selected,
'amount' => $this->amount->to_array(),
'type' => $this->type,
);
}
}

View file

@ -0,0 +1,84 @@
<?php
/**
* The Billing Cycle factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WC_Product;
use WooCommerce\PayPalCommerce\ApiClient\Entity\BillingCycle;
/**
* Class BillingCycleFactory
*/
class BillingCycleFactory {
/**
* The currency.
*
* @var string
*/
private $currency;
/**
* BillingCycleFactory constructor.
*
* @param string $currency The currency.
*/
public function __construct( string $currency ) {
$this->currency = $currency;
}
/**
* Returns a BillingCycle object from the given WC product.
*
* @param WC_Product $product WC product.
* @return BillingCycle
*/
public function from_wc_product( WC_Product $product ): BillingCycle {
return new BillingCycle(
array(
'interval_unit' => $product->get_meta( '_subscription_period' ),
'interval_count' => $product->get_meta( '_subscription_period_interval' ),
),
1,
'REGULAR',
array(
'fixed_price' => array(
'value' => $product->get_meta( '_subscription_price' ),
'currency_code' => $this->currency,
),
),
(int) $product->get_meta( '_subscription_length' )
);
}
/**
* Returns a BillingCycle object based off a PayPal response.
*
* @param stdClass $data the data.
* @return BillingCycle
*/
public function from_paypal_response( stdClass $data ): BillingCycle {
return new BillingCycle(
array(
'interval_unit' => $data->frequency->interval_unit,
'interval_count' => $data->frequency->interval_count,
),
$data->sequence,
$data->tenure_type,
array(
'fixed_price' => array(
'value' => $data->pricing_scheme->fixed_price->value,
'currency_code' => $data->pricing_scheme->fixed_price->currency_code,
),
),
$data->total_cycles
);
}
}

View file

@ -13,11 +13,15 @@ use WC_Product;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ItemTrait;
/**
* Class ItemFactory
*/
class ItemFactory {
use ItemTrait;
/**
* 3-letter currency code of the shop.
*
@ -45,6 +49,7 @@ class ItemFactory {
$items = array_map(
function ( array $item ): Item {
$product = $item['data'];
$cart_item_key = $item['key'] ?? null;
/**
* The WooCommerce product.
@ -52,16 +57,21 @@ class ItemFactory {
* @var \WC_Product $product
*/
$quantity = (int) $item['quantity'];
$image = wp_get_attachment_image_src( (int) $product->get_image_id(), 'full' );
$price = (float) $item['line_subtotal'] / (float) $item['quantity'];
return new Item(
mb_substr( $product->get_name(), 0, 127 ),
new Money( $price, $this->currency ),
$quantity,
substr( wp_strip_all_tags( $product->get_description() ), 0, 127 ) ?: '',
$this->prepare_description( $product->get_description() ),
null,
$product->get_sku(),
( $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS
( $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS,
$product->get_permalink(),
$image[0] ?? '',
0,
$cart_item_key
);
},
$cart->get_cart_contents()
@ -125,15 +135,18 @@ class ItemFactory {
$quantity = (int) $item->get_quantity();
$price_without_tax = (float) $order->get_item_subtotal( $item, false );
$price_without_tax_rounded = round( $price_without_tax, 2 );
$image = $product instanceof WC_Product ? wp_get_attachment_image_src( (int) $product->get_image_id(), 'full' ) : '';
return new Item(
mb_substr( $item->get_name(), 0, 127 ),
new Money( $price_without_tax_rounded, $currency ),
$quantity,
substr( wp_strip_all_tags( $product instanceof WC_Product ? $product->get_description() : '' ), 0, 127 ) ?: '',
$product instanceof WC_Product ? $this->prepare_description( $product->get_description() ) : '',
null,
$product instanceof WC_Product ? $product->get_sku() : '',
( $product instanceof WC_Product && $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS
( $product instanceof WC_Product && $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS,
$product instanceof WC_Product ? $product->get_permalink() : '',
$image[0] ?? ''
);
}
@ -187,6 +200,8 @@ class ItemFactory {
: null;
$sku = ( isset( $data->sku ) ) ? $data->sku : '';
$category = ( isset( $data->category ) ) ? $data->category : 'PHYSICAL_GOODS';
$url = ( isset( $data->url ) ) ? $data->url : '';
$image_url = ( isset( $data->image_url ) ) ? $data->image_url : '';
return new Item(
$data->name,
@ -195,7 +210,9 @@ class ItemFactory {
$description,
$tax,
$sku,
$category
$category,
$url,
$image_url
);
}
}

View file

@ -71,6 +71,14 @@ class PatchCollectionFactory {
);
$operation = $purchase_unit_from ? 'replace' : 'add';
$value = $purchase_unit_to->to_array();
if ( ! isset( $value['shipping'] ) ) {
$shipping = $purchase_unit_from && null !== $purchase_unit_from->shipping() ? $purchase_unit_from->shipping() : null;
if ( $shipping ) {
$value['shipping'] = $shipping->to_array();
}
}
$patches[] = new Patch(
$operation,
$path . "/@reference_id=='" . $purchase_unit_to->reference_id() . "'",

View file

@ -0,0 +1,69 @@
<?php
/**
* The Payment Preferences factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WC_Product;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentPreferences;
/**
* Class PaymentPreferencesFactory
*/
class PaymentPreferencesFactory {
/**
* The currency.
*
* @var string
*/
private $currency;
/**
* PaymentPreferencesFactory constructor.
*
* @param string $currency The currency.
*/
public function __construct( string $currency ) {
$this->currency = $currency;
}
/**
* Returns a PaymentPreferences object from the given WC product.
*
* @param WC_Product $product WC product.
* @return PaymentPreferences
*/
public function from_wc_product( WC_Product $product ):PaymentPreferences {
return new PaymentPreferences(
array(
'value' => $product->get_meta( '_subscription_sign_up_fee' ) ?: '0',
'currency_code' => $this->currency,
)
);
}
/**
* Returns a PaymentPreferences object based off a PayPal response.
*
* @param stdClass $data The data.
* @return PaymentPreferences
*/
public function from_paypal_response( stdClass $data ) {
return new PaymentPreferences(
array(
'value' => $data->setup_fee->value,
'currency_code' => $data->setup_fee->currency_code,
),
$data->auto_bill_outstanding,
$data->setup_fee_failure_action,
$data->payment_failure_threshold
);
}
}

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
/**
@ -32,19 +33,29 @@ class PaymentsFactory {
*/
private $capture_factory;
/**
* The Refund factory.
*
* @var RefundFactory
*/
private $refund_factory;
/**
* PaymentsFactory constructor.
*
* @param AuthorizationFactory $authorization_factory The Authorization factory.
* @param CaptureFactory $capture_factory The Capture factory.
* @param RefundFactory $refund_factory The Refund factory.
*/
public function __construct(
AuthorizationFactory $authorization_factory,
CaptureFactory $capture_factory
CaptureFactory $capture_factory,
RefundFactory $refund_factory
) {
$this->authorization_factory = $authorization_factory;
$this->capture_factory = $capture_factory;
$this->refund_factory = $refund_factory;
}
/**
@ -62,12 +73,18 @@ class PaymentsFactory {
isset( $data->authorizations ) ? $data->authorizations : array()
);
$captures = array_map(
function ( \stdClass $authorization ): Capture {
return $this->capture_factory->from_paypal_response( $authorization );
function ( \stdClass $capture ): Capture {
return $this->capture_factory->from_paypal_response( $capture );
},
isset( $data->captures ) ? $data->captures : array()
);
$payments = new Payments( $authorizations, $captures );
$refunds = array_map(
function ( \stdClass $refund ): Refund {
return $this->refund_factory->from_paypal_response( $refund );
},
isset( $data->refunds ) ? $data->refunds : array()
);
$payments = new Payments( $authorizations, $captures, $refunds );
return $payments;
}
}

View file

@ -0,0 +1,96 @@
<?php
/**
* Plan Factory.
*
* @package WooCommerce\PayPalCommerce\Webhooks\Handler
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Plan;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class PlanFactory
*/
class PlanFactory {
/**
* Billing cycle factory.
*
* @var BillingCycleFactory
*/
private $billing_cycle_factory;
/**
* Payment preferences factory.
*
* @var PaymentPreferencesFactory
*/
private $payment_preferences_factory;
/**
* PlanFactory constructor.
*
* @param BillingCycleFactory $billing_cycle_factory Billing cycle factory.
* @param PaymentPreferencesFactory $payment_preferences_factory Payment preferences factory.
*/
public function __construct(
BillingCycleFactory $billing_cycle_factory,
PaymentPreferencesFactory $payment_preferences_factory
) {
$this->billing_cycle_factory = $billing_cycle_factory;
$this->payment_preferences_factory = $payment_preferences_factory;
}
/**
* Returns a Plan from PayPal response.
*
* @param stdClass $data The data.
*
* @return Plan
*
* @throws RuntimeException If it could not create Plan.
*/
public function from_paypal_response( stdClass $data ): Plan {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'No id for given plan', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $data->name ) ) {
throw new RuntimeException(
__( 'No name for plan given', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $data->product_id ) ) {
throw new RuntimeException(
__( 'No product id for given plan', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $data->billing_cycles ) ) {
throw new RuntimeException(
__( 'No billing cycles for given plan', 'woocommerce-paypal-payments' )
);
}
$billing_cycles = array();
foreach ( $data->billing_cycles as $billing_cycle ) {
$billing_cycles[] = $this->billing_cycle_factory->from_paypal_response( $billing_cycle );
}
$payment_preferences = $this->payment_preferences_factory->from_paypal_response( $data->payment_preferences );
return new Plan(
$data->id,
$data->name,
$data->product_id,
$billing_cycles,
$payment_preferences,
$data->status ?? ''
);
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* The Product factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Product;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class ProductFactory
*/
class ProductFactory {
/**
* Creates a Product based off a PayPal response.
*
* @param stdClass $data The JSON object.
*
* @return Product
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): Product {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'No id for product given', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $data->name ) ) {
throw new RuntimeException(
__( 'No name for product given', 'woocommerce-paypal-payments' )
);
}
return new Product(
$data->id,
$data->name,
$data->description ?? ''
);
}
}

View file

@ -9,10 +9,12 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WC_Session_Handler;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
use WooCommerce\PayPalCommerce\Webhooks\CustomIds;
/**
* Class PurchaseUnitFactory
@ -26,20 +28,6 @@ class PurchaseUnitFactory {
*/
private $amount_factory;
/**
* The payee repository.
*
* @var PayeeRepository
*/
private $payee_repository;
/**
* The payee factory.
*
* @var PayeeFactory
*/
private $payee_factory;
/**
* The item factory.
*
@ -68,34 +56,48 @@ class PurchaseUnitFactory {
*/
private $prefix;
/**
* The Soft Descriptor.
*
* @var string
*/
private $soft_descriptor;
/**
* The sanitizer for purchase unit output data.
*
* @var PurchaseUnitSanitizer|null
*/
private $sanitizer;
/**
* PurchaseUnitFactory constructor.
*
* @param AmountFactory $amount_factory The amount factory.
* @param PayeeRepository $payee_repository The Payee repository.
* @param PayeeFactory $payee_factory The Payee factory.
* @param ItemFactory $item_factory The item factory.
* @param ShippingFactory $shipping_factory The shipping factory.
* @param PaymentsFactory $payments_factory The payments factory.
* @param string $prefix The prefix.
* @param string $soft_descriptor The soft descriptor.
* @param ?PurchaseUnitSanitizer $sanitizer The purchase unit to_array sanitizer.
*/
public function __construct(
AmountFactory $amount_factory,
PayeeRepository $payee_repository,
PayeeFactory $payee_factory,
ItemFactory $item_factory,
ShippingFactory $shipping_factory,
PaymentsFactory $payments_factory,
string $prefix = 'WC-'
string $prefix = 'WC-',
string $soft_descriptor = '',
PurchaseUnitSanitizer $sanitizer = null
) {
$this->amount_factory = $amount_factory;
$this->payee_repository = $payee_repository;
$this->payee_factory = $payee_factory;
$this->item_factory = $item_factory;
$this->shipping_factory = $shipping_factory;
$this->payments_factory = $payments_factory;
$this->prefix = $prefix;
$this->soft_descriptor = $soft_descriptor;
$this->sanitizer = $sanitizer;
}
/**
@ -110,7 +112,7 @@ class PurchaseUnitFactory {
$items = array_filter(
$this->item_factory->from_wc_order( $order ),
function ( Item $item ): bool {
return $item->unit_amount()->value() > 0;
return $item->unit_amount()->value() >= 0;
}
);
$shipping = $this->shipping_factory->from_wc_order( $order );
@ -123,10 +125,9 @@ class PurchaseUnitFactory {
}
$reference_id = 'default';
$description = '';
$payee = $this->payee_repository->payee();
$custom_id = (string) $order->get_id();
$invoice_id = $this->prefix . $order->get_order_number();
$soft_descriptor = '';
$soft_descriptor = $this->soft_descriptor;
$purchase_unit = new PurchaseUnit(
$amount,
@ -134,11 +135,13 @@ class PurchaseUnitFactory {
$shipping,
$reference_id,
$description,
$payee,
$custom_id,
$invoice_id,
$soft_descriptor
);
$this->init_purchase_unit( $purchase_unit );
/**
* Returns PurchaseUnit for the WC order.
*/
@ -153,10 +156,11 @@ class PurchaseUnitFactory {
* Creates a PurchaseUnit based off a WooCommerce cart.
*
* @param \WC_Cart|null $cart The cart.
* @param bool $with_shipping_options Include WC shipping methods.
*
* @return PurchaseUnit
*/
public function from_wc_cart( ?\WC_Cart $cart = null ): PurchaseUnit {
public function from_wc_cart( ?\WC_Cart $cart = null, bool $with_shipping_options = false ): PurchaseUnit {
if ( ! $cart ) {
$cart = WC()->cart ?? new \WC_Cart();
}
@ -165,14 +169,14 @@ class PurchaseUnitFactory {
$items = array_filter(
$this->item_factory->from_wc_cart( $cart ),
function ( Item $item ): bool {
return $item->unit_amount()->value() > 0;
return $item->unit_amount()->value() >= 0;
}
);
$shipping = null;
$customer = \WC()->customer;
if ( $this->shipping_needed( ... array_values( $items ) ) && is_a( $customer, \WC_Customer::class ) ) {
$shipping = $this->shipping_factory->from_wc_customer( \WC()->customer );
$shipping = $this->shipping_factory->from_wc_customer( \WC()->customer, $with_shipping_options );
if (
2 !== strlen( $shipping->address()->country_code() ) ||
( ! $shipping->address()->postal_code() && ! $this->country_without_postal_code( $shipping->address()->country_code() ) )
@ -184,23 +188,29 @@ class PurchaseUnitFactory {
$reference_id = 'default';
$description = '';
$payee = $this->payee_repository->payee();
$custom_id = '';
$session = WC()->session;
if ( $session instanceof WC_Session_Handler ) {
$session_id = $session->get_customer_unique_id();
if ( $session_id ) {
$custom_id = CustomIds::CUSTOMER_ID_PREFIX . $session_id;
}
}
$invoice_id = '';
$soft_descriptor = '';
$soft_descriptor = $this->soft_descriptor;
$purchase_unit = new PurchaseUnit(
$amount,
$items,
$shipping,
$reference_id,
$description,
$payee,
$custom_id,
$invoice_id,
$soft_descriptor
);
$this->init_purchase_unit( $purchase_unit );
return $purchase_unit;
}
@ -223,7 +233,7 @@ class PurchaseUnitFactory {
$description = ( isset( $data->description ) ) ? $data->description : '';
$custom_id = ( isset( $data->custom_id ) ) ? $data->custom_id : '';
$invoice_id = ( isset( $data->invoice_id ) ) ? $data->invoice_id : '';
$soft_descriptor = ( isset( $data->soft_descriptor ) ) ? $data->soft_descriptor : '';
$soft_descriptor = ( isset( $data->soft_descriptor ) ) ? $data->soft_descriptor : $this->soft_descriptor;
$items = array();
if ( isset( $data->items ) && is_array( $data->items ) ) {
$items = array_map(
@ -233,7 +243,6 @@ class PurchaseUnitFactory {
$data->items
);
}
$payee = isset( $data->payee ) ? $this->payee_factory->from_paypal_response( $data->payee ) : null;
$shipping = null;
try {
if ( isset( $data->shipping ) ) {
@ -257,12 +266,14 @@ class PurchaseUnitFactory {
$shipping,
$data->reference_id,
$description,
$payee,
$custom_id,
$invoice_id,
$soft_descriptor,
$payments
);
$this->init_purchase_unit( $purchase_unit );
return $purchase_unit;
}
@ -293,4 +304,16 @@ class PurchaseUnitFactory {
$countries = array( 'AE', 'AF', 'AG', 'AI', 'AL', 'AN', 'AO', 'AW', 'BB', 'BF', 'BH', 'BI', 'BJ', 'BM', 'BO', 'BS', 'BT', 'BW', 'BZ', 'CD', 'CF', 'CG', 'CI', 'CK', 'CL', 'CM', 'CO', 'CR', 'CV', 'DJ', 'DM', 'DO', 'EC', 'EG', 'ER', 'ET', 'FJ', 'FK', 'GA', 'GD', 'GH', 'GI', 'GM', 'GN', 'GQ', 'GT', 'GW', 'GY', 'HK', 'HN', 'HT', 'IE', 'IQ', 'IR', 'JM', 'JO', 'KE', 'KH', 'KI', 'KM', 'KN', 'KP', 'KW', 'KY', 'LA', 'LB', 'LC', 'LK', 'LR', 'LS', 'LY', 'ML', 'MM', 'MO', 'MR', 'MS', 'MT', 'MU', 'MW', 'MZ', 'NA', 'NE', 'NG', 'NI', 'NP', 'NR', 'NU', 'OM', 'PA', 'PE', 'PF', 'PY', 'QA', 'RW', 'SA', 'SB', 'SC', 'SD', 'SL', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SY', 'TC', 'TD', 'TG', 'TL', 'TO', 'TT', 'TV', 'TZ', 'UG', 'UY', 'VC', 'VE', 'VG', 'VN', 'VU', 'WS', 'XA', 'XB', 'XC', 'XE', 'XL', 'XM', 'XN', 'XS', 'YE', 'ZM', 'ZW' );
return in_array( $country_code, $countries, true );
}
/**
* Initializes a purchase unit object.
*
* @param PurchaseUnit $purchase_unit The purchase unit.
* @return void
*/
private function init_purchase_unit( PurchaseUnit $purchase_unit ): void {
if ( $this->sanitizer instanceof PurchaseUnitSanitizer ) {
$purchase_unit->set_sanitizer( $this->sanitizer );
}
}
}

View file

@ -0,0 +1,91 @@
<?php
/**
* The refund factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatusDetails;
/**
* Class RefundFactory
*/
class RefundFactory {
/**
* The Amount factory.
*
* @var AmountFactory
*/
private $amount_factory;
/**
* The SellerPayableBreakdownFactory factory.
*
* @var SellerPayableBreakdownFactory
*/
private $seller_payable_breakdown_factory;
/**
* The RefundPayerFactory factory.
*
* @var RefundPayerFactory
*/
private $refund_payer_factory;
/**
* RefundFactory constructor.
*
* @param AmountFactory $amount_factory The amount factory.
* @param SellerPayableBreakdownFactory $seller_payable_breakdown_factory The payable breakdown factory.
* @param RefundPayerFactory $refund_payer_factory The payer breakdown factory.
*/
public function __construct(
AmountFactory $amount_factory,
SellerPayableBreakdownFactory $seller_payable_breakdown_factory,
RefundPayerFactory $refund_payer_factory
) {
$this->amount_factory = $amount_factory;
$this->seller_payable_breakdown_factory = $seller_payable_breakdown_factory;
$this->refund_payer_factory = $refund_payer_factory;
}
/**
* Returns the refund object based off the PayPal response.
*
* @param \stdClass $data The PayPal response.
*
* @return Refund
*/
public function from_paypal_response( \stdClass $data ) : Refund {
$reason = $data->status_details->reason ?? null;
$seller_payable_breakdown = isset( $data->seller_payable_breakdown ) ?
$this->seller_payable_breakdown_factory->from_paypal_response( $data->seller_payable_breakdown )
: null;
$payer = isset( $data->payer ) ?
$this->refund_payer_factory->from_paypal_response( $data->payer )
: null;
return new Refund(
(string) $data->id,
new RefundStatus(
(string) $data->status,
$reason ? new RefundStatusDetails( $reason ) : null
),
$this->amount_factory->from_paypal_response( $data->amount ),
(string) ( $data->invoice_id ?? '' ),
(string) ( $data->custom_id ?? '' ),
$seller_payable_breakdown,
(string) ( $data->acquirer_reference_number ?? '' ),
(string) ( $data->note_to_payer ?? '' ),
$payer
);
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* The RefundPayerFactory factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Address;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PayerName;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PayerTaxInfo;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Phone;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PhoneWithType;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundPayer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class RefundPayerFactory
*/
class RefundPayerFactory {
/**
* Returns a Refund Payer object based off a PayPal Response.
*
* @param \stdClass $data The JSON object.
*
* @return RefundPayer
*/
public function from_paypal_response( \stdClass $data ): RefundPayer {
return new RefundPayer(
isset( $data->email_address ) ? $data->email_address : '',
isset( $data->merchant_id ) ? $data->merchant_id : ''
);
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* The SellerPayableBreakdownFactory Factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PlatformFee;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerPayableBreakdown;
/**
* Class SellerPayableBreakdownFactory
*/
class SellerPayableBreakdownFactory {
/**
* The Money factory.
*
* @var MoneyFactory
*/
private $money_factory;
/**
* The PlatformFee factory.
*
* @var PlatformFeeFactory
*/
private $platform_fee_factory;
/**
* SellerPayableBreakdownFactory constructor.
*
* @param MoneyFactory $money_factory The Money factory.
* @param PlatformFeeFactory $platform_fee_factory The PlatformFee factory.
*/
public function __construct(
MoneyFactory $money_factory,
PlatformFeeFactory $platform_fee_factory
) {
$this->money_factory = $money_factory;
$this->platform_fee_factory = $platform_fee_factory;
}
/**
* Returns a SellerPayableBreakdownFactory object based off a PayPal Response.
*
* @param stdClass $data The JSON object.
*
* @return SellerPayableBreakdown
*/
public function from_paypal_response( stdClass $data ): SellerPayableBreakdown {
$gross_amount = ( isset( $data->gross_amount ) ) ? $this->money_factory->from_paypal_response( $data->gross_amount ) : null;
$paypal_fee = ( isset( $data->paypal_fee ) ) ? $this->money_factory->from_paypal_response( $data->paypal_fee ) : null;
$paypal_fee_in_receivable_currency = ( isset( $data->paypal_fee_in_receivable_currency ) ) ? $this->money_factory->from_paypal_response( $data->paypal_fee_in_receivable_currency ) : null;
$net_amount = ( isset( $data->net_amount ) ) ? $this->money_factory->from_paypal_response( $data->net_amount ) : null;
$net_amount_in_receivable_currency = ( isset( $data->net_amount_in_receivable_currency ) ) ? $this->money_factory->from_paypal_response( $data->net_amount_in_receivable_currency ) : null;
$total_refunded_amount = ( isset( $data->total_refunded_amount ) ) ? $this->money_factory->from_paypal_response( $data->total_refunded_amount ) : null;
$platform_fees = ( isset( $data->platform_fees ) ) ? array_map(
function ( stdClass $fee_data ): PlatformFee {
return $this->platform_fee_factory->from_paypal_response( $fee_data );
},
$data->platform_fees
) : array();
return new SellerPayableBreakdown(
$gross_amount,
$paypal_fee,
$paypal_fee_in_receivable_currency,
$net_amount,
$net_amount_in_receivable_currency,
$total_refunded_amount,
$platform_fees
);
}
}

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Shipping;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ShippingOption;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
@ -24,23 +25,33 @@ class ShippingFactory {
*/
private $address_factory;
/**
* The shipping option factory.
*
* @var ShippingOptionFactory
*/
private $shipping_option_factory;
/**
* ShippingFactory constructor.
*
* @param AddressFactory $address_factory The address factory.
* @param ShippingOptionFactory $shipping_option_factory The shipping option factory.
*/
public function __construct( AddressFactory $address_factory ) {
public function __construct( AddressFactory $address_factory, ShippingOptionFactory $shipping_option_factory ) {
$this->address_factory = $address_factory;
$this->shipping_option_factory = $shipping_option_factory;
}
/**
* Creates a shipping object based off a WooCommerce customer.
*
* @param \WC_Customer $customer The WooCommerce customer.
* @param bool $with_shipping_options Include WC shipping methods.
*
* @return Shipping
*/
public function from_wc_customer( \WC_Customer $customer ): Shipping {
public function from_wc_customer( \WC_Customer $customer, bool $with_shipping_options = false ): Shipping {
// Replicates the Behavior of \WC_Order::get_formatted_shipping_full_name().
$full_name = sprintf(
// translators: %1$s is the first name and %2$s is the second name. wc translation.
@ -51,7 +62,8 @@ class ShippingFactory {
$address = $this->address_factory->from_wc_customer( $customer );
return new Shipping(
$full_name,
$address
$address,
$with_shipping_options ? $this->shipping_option_factory->from_wc_cart() : array()
);
}
@ -91,9 +103,14 @@ class ShippingFactory {
);
}
$address = $this->address_factory->from_paypal_response( $data->address );
$options = array_map(
array( $this->shipping_option_factory, 'from_paypal_response' ),
$data->options ?? array()
);
return new Shipping(
$data->name->full_name,
$address
$address,
$options
);
}
}

View file

@ -0,0 +1,111 @@
<?php
/**
* The shipping options factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WC_Cart;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ShippingOption;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class ShippingOptionFactory
*/
class ShippingOptionFactory {
/**
* The Money factory.
*
* @var MoneyFactory
*/
private $money_factory;
/**
* ShippingOptionFactory constructor.
*
* @param MoneyFactory $money_factory The Money factory.
*/
public function __construct( MoneyFactory $money_factory ) {
$this->money_factory = $money_factory;
}
/**
* Creates an array of ShippingOption objects for the shipping methods available in the cart.
*
* @param WC_Cart|null $cart The cart.
* @return ShippingOption[]
*/
public function from_wc_cart( ?WC_Cart $cart = null ): array {
if ( ! $cart ) {
$cart = WC()->cart ?? new WC_Cart();
}
$cart->calculate_shipping();
$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', array() );
if ( ! is_array( $chosen_shipping_methods ) ) {
$chosen_shipping_methods = array();
}
$packages = WC()->shipping()->get_packages();
$options = array();
foreach ( $packages as $package ) {
$rates = $package['rates'] ?? array();
foreach ( $rates as $rate ) {
if ( ! $rate instanceof \WC_Shipping_Rate ) {
continue;
}
$options[] = new ShippingOption(
$rate->get_id(),
$rate->get_label(),
in_array( $rate->get_id(), $chosen_shipping_methods, true ),
new Money(
(float) $rate->get_cost(),
get_woocommerce_currency()
),
ShippingOption::TYPE_SHIPPING
);
}
}
if ( ! $chosen_shipping_methods && $options ) {
$options[0]->set_selected( true );
}
return $options;
}
/**
* Creates a ShippingOption object from the PayPal JSON object.
*
* @param stdClass $data The JSON object.
*
* @return ShippingOption
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): ShippingOption {
if ( ! isset( $data->id ) ) {
throw new RuntimeException( 'No id was given for shipping option.' );
}
if ( ! isset( $data->amount ) ) {
throw new RuntimeException( 'Shipping option amount not found' );
}
$amount = $this->money_factory->from_paypal_response( $data->amount );
return new ShippingOption(
$data->id,
$data->label ?? '',
isset( $data->selected ) ? (bool) $data->selected : false,
$amount,
$data->type ?? ShippingOption::TYPE_SHIPPING
);
}
}

View file

@ -0,0 +1,94 @@
<?php
/**
* Failure registry.
*
* This class is used to remember API failures.
* Mostly to prevent multiple failed API requests.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
/**
* Class FailureRegistry
*/
class FailureRegistry {
const CACHE_KEY = 'failure_registry';
const CACHE_TIMEOUT = 60 * 60 * 24; // DAY_IN_SECONDS, if necessary we can increase this.
const SELLER_STATUS_KEY = 'seller_status';
/**
* The Cache.
*
* @var Cache
*/
private $cache;
/**
* FailureRegistry constructor.
*
* @param Cache $cache The Cache.
*/
public function __construct( Cache $cache ) {
$this->cache = $cache;
}
/**
* Returns if there was a failure within a given timeframe.
*
* @param string $key The cache key.
* @param int $seconds The timeframe in seconds.
* @return bool
*/
public function has_failure_in_timeframe( string $key, int $seconds ): bool {
$cache_key = $this->cache_key( $key );
$failure_time = $this->cache->get( $cache_key );
if ( ! $failure_time ) {
return false;
}
$expiration = $failure_time + $seconds;
return $expiration > time();
}
/**
* Registers a failure.
*
* @param string $key The cache key.
* @return void
*/
public function add_failure( string $key ) {
$cache_key = $this->cache_key( $key );
$this->cache->set( $cache_key, time(), self::CACHE_TIMEOUT );
}
/**
* Clear a given failure.
*
* @param string $key The cache key.
* @return void
*/
public function clear_failures( string $key ) {
$cache_key = $this->cache_key( $key );
if ( $this->cache->has( $cache_key ) ) {
$this->cache->delete( $cache_key );
}
}
/**
* Build cache key.
*
* @param string $key The cache key.
* @return string
*/
private function cache_key( string $key ): string {
return implode( '_', array( self::CACHE_KEY, $key ) );
}
}

View file

@ -0,0 +1,24 @@
<?php
/**
* PayPal item helper.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
trait ItemTrait {
/**
* Cleanups the description and prepares it for sending to PayPal.
*
* @param string $description Item description.
* @return string
*/
protected function prepare_description( string $description ): string {
$description = strip_shortcodes( wp_strip_all_tags( $description ) );
return substr( $description, 0, 127 ) ?: '';
}
}

View file

@ -33,4 +33,16 @@ class MoneyFormatter {
? (string) round( $value, 0 )
: number_format( $value, 2, '.', '' );
}
/**
* Returns the minimum amount a currency can be incremented or decremented.
*
* @param string $currency The 3-letter currency code.
* @return float
*/
public function minimum_increment( string $currency ): float {
return (float) in_array( $currency, $this->currencies_without_decimals, true )
? 1.00
: 0.01;
}
}

View file

@ -0,0 +1,160 @@
<?php
/**
* PayPal order transient helper.
*
* This class is used to pass transient data between the PayPal order and the WooCommerce order.
* These two orders can be created on different requests and at different times so this transient
* data must be persisted between requests.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
/**
* Class OrderTransient
*/
class OrderTransient {
const CACHE_KEY = 'order_transient';
const CACHE_TIMEOUT = 60 * 60 * 24; // DAY_IN_SECONDS, if necessary we can increase this.
/**
* The Cache.
*
* @var Cache
*/
private $cache;
/**
* The purchase unit sanitizer.
*
* @var PurchaseUnitSanitizer
*/
private $purchase_unit_sanitizer;
/**
* OrderTransient constructor.
*
* @param Cache $cache The Cache.
* @param PurchaseUnitSanitizer $purchase_unit_sanitizer The purchase unit sanitizer.
*/
public function __construct( Cache $cache, PurchaseUnitSanitizer $purchase_unit_sanitizer ) {
$this->cache = $cache;
$this->purchase_unit_sanitizer = $purchase_unit_sanitizer;
}
/**
* Processes the created PayPal order.
*
* @param Order $order The PayPal order.
* @return void
*/
public function on_order_created( Order $order ): void {
$message = $this->purchase_unit_sanitizer->get_last_message();
$this->add_order_note( $order, $message );
}
/**
* Processes the created WooCommerce order.
*
* @param WC_Order $wc_order The WooCommerce order.
* @param Order $order The PayPal order.
* @return void
*/
public function on_woocommerce_order_created( WC_Order $wc_order, Order $order ): void {
$cache_key = $this->cache_key( $order );
if ( ! $cache_key ) {
return;
}
$this->apply_order_notes( $order, $wc_order );
$this->cache->delete( $cache_key );
}
/**
* Adds an order note associated with a PayPal order.
* It can be added to a WooCommerce order associated with this PayPal order in the future.
*
* @param Order $order The PayPal order.
* @param string $message The message to be added to order notes.
* @return void
*/
private function add_order_note( Order $order, string $message ): void {
if ( ! $message ) {
return;
}
$cache_key = $this->cache_key( $order );
if ( ! $cache_key ) {
return;
}
$transient = $this->cache->get( $cache_key );
if ( ! is_array( $transient ) ) {
$transient = array();
}
if ( ! is_array( $transient['notes'] ) ) {
$transient['notes'] = array();
}
$transient['notes'][] = $message;
$this->cache->set( $cache_key, $transient, self::CACHE_TIMEOUT );
}
/**
* Adds an order note associated with a PayPal order.
* It can be added to a WooCommerce order associated with this PayPal order in the future.
*
* @param Order $order The PayPal order.
* @param WC_Order $wc_order The WooCommerce order.
* @return void
*/
private function apply_order_notes( Order $order, WC_Order $wc_order ): void {
$cache_key = $this->cache_key( $order );
if ( ! $cache_key ) {
return;
}
$transient = $this->cache->get( $cache_key );
if ( ! is_array( $transient ) ) {
return;
}
if ( ! is_array( $transient['notes'] ) ) {
return;
}
foreach ( $transient['notes'] as $note ) {
if ( ! is_string( $note ) ) {
continue;
}
$wc_order->add_order_note( $note );
}
}
/**
* Build cache key.
*
* @param Order $order The PayPal order.
* @return string|null
*/
private function cache_key( Order $order ): ?string {
if ( ! $order->id() ) {
return null;
}
return implode( '_', array( self::CACHE_KEY . $order->id() ) );
}
}

View file

@ -0,0 +1,368 @@
<?php
/**
* Class PurchaseUnitSanitizer.
*
* Sanitizes a purchase_unit array to be consumed by PayPal.
*
* All money values send to PayPal can only have 2 decimal points. WooCommerce internally does
* not have this restriction. Therefore, the totals of the cart in WooCommerce and the totals
* of the rounded money values of the items, we send to PayPal, can differ. In those case we either:
* - Add an extra line with roundings.
* - Don't send the line items.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
/**
* Class PurchaseUnitSanitizer
*/
class PurchaseUnitSanitizer {
const MODE_DITCH = 'ditch';
const MODE_EXTRA_LINE = 'extra_line';
const VALID_MODES = array(
self::MODE_DITCH,
self::MODE_EXTRA_LINE,
);
const EXTRA_LINE_NAME = 'Subtotal mismatch';
/**
* The purchase unit data
*
* @var array
*/
private $purchase_unit = array();
/**
* Whether to allow items to be ditched.
*
* @var bool
*/
private $allow_ditch_items = true;
/**
* The working mode
*
* @var string
*/
private $mode;
/**
* The name for the extra line
*
* @var string
*/
private $extra_line_name;
/**
* The last message. To be added to order notes.
*
* @var string
*/
private $last_message = '';
/**
* If the items and breakdown has been ditched.
*
* @var bool
*/
private $has_ditched_items_breakdown = false;
/**
* PurchaseUnitSanitizer constructor.
*
* @param string|null $mode The mismatch handling mode, ditch or extra_line.
* @param string|null $extra_line_name The name of the extra line.
*/
public function __construct( string $mode = null, string $extra_line_name = null ) {
if ( ! in_array( $mode, self::VALID_MODES, true ) ) {
$mode = self::MODE_DITCH;
}
if ( ! $extra_line_name ) {
$extra_line_name = self::EXTRA_LINE_NAME;
}
$this->mode = $mode;
$this->extra_line_name = $extra_line_name;
}
/**
* The purchase_unit amount.
*
* @return array
*/
private function amount(): array {
return $this->purchase_unit['amount'] ?? array();
}
/**
* The purchase_unit currency code.
*
* @return string
*/
private function currency_code(): string {
return (string) ( $this->amount()['currency_code'] ?? '' );
}
/**
* The purchase_unit breakdown.
*
* @return array
*/
private function breakdown(): array {
return $this->amount()['breakdown'] ?? array();
}
/**
* The purchase_unit breakdown.
*
* @param string $key The breakdown element to get the value from.
* @return float
*/
private function breakdown_value( string $key ): float {
if ( ! isset( $this->breakdown()[ $key ] ) ) {
return 0.0;
}
return (float) ( $this->breakdown()[ $key ]['value'] ?? 0.0 );
}
/**
* The purchase_unit items array.
*
* @return array
*/
private function items(): array {
return $this->purchase_unit['items'] ?? array();
}
/**
* The sanitizes the purchase_unit array.
*
* @param array $purchase_unit The purchase_unit array that should be sanitized.
* @param bool $allow_ditch_items Whether to allow items to be ditched.
* @return array
*/
public function sanitize( array $purchase_unit, bool $allow_ditch_items = true ): array {
$this->purchase_unit = $purchase_unit;
$this->allow_ditch_items = $allow_ditch_items;
$this->has_ditched_items_breakdown = false;
$this->sanitize_item_amount_mismatch();
$this->sanitize_item_tax_mismatch();
$this->sanitize_breakdown_mismatch();
return $this->purchase_unit;
}
/**
* The sanitizes the purchase_unit items amount.
*
* @return void
*/
private function sanitize_item_amount_mismatch(): void {
$item_mismatch = $this->calculate_item_mismatch();
if ( $this->mode === self::MODE_EXTRA_LINE ) {
if ( $item_mismatch < 0 ) {
// Do floors on item amounts so item_mismatch is a positive value.
foreach ( $this->purchase_unit['items'] as $index => $item ) {
// Get a more intelligent adjustment mechanism.
$increment = ( new MoneyFormatter() )->minimum_increment( $item['unit_amount']['currency_code'] );
$this->purchase_unit['items'][ $index ]['unit_amount'] = ( new Money(
( (float) $item['unit_amount']['value'] ) - $increment,
$item['unit_amount']['currency_code']
) )->to_array();
}
}
$item_mismatch = $this->calculate_item_mismatch();
if ( $item_mismatch > 0 ) {
// Add extra line item with roundings.
$line_name = $this->extra_line_name;
$roundings_money = new Money( $item_mismatch, $this->currency_code() );
$this->purchase_unit['items'][] = ( new Item( $line_name, $roundings_money, 1 ) )->to_array();
$this->set_last_message(
__( 'Item amount mismatch. Extra line added.', 'woocommerce-paypal-payments' )
);
}
$item_mismatch = $this->calculate_item_mismatch();
}
if ( $item_mismatch !== 0.0 ) {
// Ditch items.
if ( $this->allow_ditch_items && isset( $this->purchase_unit['items'] ) ) {
unset( $this->purchase_unit['items'] );
$this->set_last_message(
__( 'Item amount mismatch. Items ditched.', 'woocommerce-paypal-payments' )
);
}
}
}
/**
* The sanitizes the purchase_unit items tax.
*
* @return void
*/
private function sanitize_item_tax_mismatch(): void {
$tax_mismatch = $this->calculate_tax_mismatch();
if ( $this->allow_ditch_items && $tax_mismatch !== 0.0 ) {
// Unset tax in items.
foreach ( $this->purchase_unit['items'] as $index => $item ) {
if ( isset( $this->purchase_unit['items'][ $index ]['tax'] ) ) {
unset( $this->purchase_unit['items'][ $index ]['tax'] );
}
if ( isset( $this->purchase_unit['items'][ $index ]['tax_rate'] ) ) {
unset( $this->purchase_unit['items'][ $index ]['tax_rate'] );
}
}
}
}
/**
* The sanitizes the purchase_unit breakdown.
*
* @return void
*/
private function sanitize_breakdown_mismatch(): void {
$breakdown_mismatch = $this->calculate_breakdown_mismatch();
if ( $this->allow_ditch_items && $breakdown_mismatch !== 0.0 ) {
// Ditch breakdowns and items.
if ( isset( $this->purchase_unit['items'] ) ) {
unset( $this->purchase_unit['items'] );
}
if ( isset( $this->purchase_unit['amount']['breakdown'] ) ) {
unset( $this->purchase_unit['amount']['breakdown'] );
}
$this->has_ditched_items_breakdown = true;
$this->set_last_message(
__( 'Breakdown mismatch. Items and breakdown ditched.', 'woocommerce-paypal-payments' )
);
}
}
/**
* The calculates amount mismatch of items sums with breakdown.
*
* @return float
*/
private function calculate_item_mismatch(): float {
$item_total = $this->breakdown_value( 'item_total' );
if ( ! $item_total ) {
return 0;
}
$remaining_item_total = array_reduce(
$this->items(),
function ( float $total, array $item ): float {
return $total - (float) $item['unit_amount']['value'] * (float) $item['quantity'];
},
$item_total
);
return round( $remaining_item_total, 2 );
}
/**
* The calculates tax mismatch of items sums with breakdown.
*
* @return float
*/
private function calculate_tax_mismatch(): float {
$tax_total = $this->breakdown_value( 'tax_total' );
$items_with_tax = array_filter(
$this->items(),
function ( array $item ): bool {
return isset( $item['tax'] );
}
);
if ( ! $tax_total || empty( $items_with_tax ) ) {
return 0;
}
$remaining_tax_total = array_reduce(
$this->items(),
function ( float $total, array $item ): float {
$tax = $item['tax'] ?? false;
if ( $tax ) {
$total -= (float) $tax['value'] * (float) $item['quantity'];
}
return $total;
},
$tax_total
);
return round( $remaining_tax_total, 2 );
}
/**
* The calculates mismatch of breakdown sums with total amount.
*
* @return float
*/
private function calculate_breakdown_mismatch(): float {
$breakdown = $this->breakdown();
if ( ! $breakdown ) {
return 0;
}
$amount_total = 0.0;
$amount_total += $this->breakdown_value( 'item_total' );
$amount_total += $this->breakdown_value( 'tax_total' );
$amount_total += $this->breakdown_value( 'shipping' );
$amount_total -= $this->breakdown_value( 'discount' );
$amount_total -= $this->breakdown_value( 'shipping_discount' );
$amount_total += $this->breakdown_value( 'handling' );
$amount_total += $this->breakdown_value( 'insurance' );
$amount_str = $this->amount()['value'] ?? 0;
$amount_total_str = ( new Money( $amount_total, $this->currency_code() ) )->value_str();
return $amount_str - $amount_total_str;
}
/**
* Indicates if the items and breakdown were ditched.
*
* @return bool
*/
public function has_ditched_items_breakdown(): bool {
return $this->has_ditched_items_breakdown;
}
/**
* Returns the last sanitization message.
*
* @return string
*/
public function get_last_message(): string {
return $this->last_message;
}
/**
* Set the last sanitization message.
*
* @param string $message The message.
*/
public function set_last_message( string $message ): void {
$this->last_message = $message;
}
}

View file

@ -38,24 +38,30 @@ class ApplicationContextRepository {
* Returns the current application context.
*
* @param string $shipping_preferences The shipping preferences.
* @param string $user_action The user action.
*
* @return ApplicationContext
*/
public function current_context(
string $shipping_preferences = ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING
string $shipping_preferences = ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING,
string $user_action = ApplicationContext::USER_ACTION_CONTINUE
): ApplicationContext {
$brand_name = $this->settings->has( 'brand_name' ) ? $this->settings->get( 'brand_name' ) : '';
$locale = $this->valid_bcp47_code();
$landingpage = $this->settings->has( 'landing_page' ) ?
$this->settings->get( 'landing_page' ) : ApplicationContext::LANDING_PAGE_NO_PREFERENCE;
$payment_preference = $this->settings->has( 'payee_preferred' ) && $this->settings->get( 'payee_preferred' ) ?
ApplicationContext::PAYMENT_METHOD_IMMEDIATE_PAYMENT_REQUIRED : ApplicationContext::PAYMENT_METHOD_UNRESTRICTED;
$context = new ApplicationContext(
network_home_url( \WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) ),
(string) wc_get_checkout_url(),
(string) $brand_name,
$locale,
(string) $landingpage,
$shipping_preferences
$shipping_preferences,
$user_action,
$payment_preference
);
return $context;
}

View file

@ -127,4 +127,17 @@ class PartnerReferralsData {
)
);
}
/**
* Append the validation token to the return_url
*
* @param array $data The referral data.
* @param string $token The token to be appended.
* @return array
*/
public function append_onboarding_token( array $data, string $token ): array {
$data['partner_config_override']['return_url'] =
add_query_arg( 'ppcpToken', $token, $data['partner_config_override']['return_url'] );
return $data;
}
}

View file

@ -1,112 +0,0 @@
<?php
/**
* The repository for the request IDs.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Repository
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Repository;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
/**
* Class PayPalRequestIdRepository
*/
class PayPalRequestIdRepository {
const KEY = 'ppcp-request-ids';
/**
* Returns a request ID based on the order ID.
*
* @param string $order_id The order ID.
*
* @return string
*/
public function get_for_order_id( string $order_id ): string {
return $this->get( $order_id );
}
/**
* Returns the request ID for an order.
*
* @param Order $order The order.
*
* @return string
*/
public function get_for_order( Order $order ): string {
return $this->get_for_order_id( $order->id() );
}
/**
* Sets a request ID for a specific order.
*
* @param Order $order The order.
* @param string $request_id The ID.
*
* @return bool
*/
public function set_for_order( Order $order, string $request_id ): bool {
$this->set( $order->id(), $request_id );
return true;
}
/**
* Sets a request ID for the given key.
*
* @param string $key The key in the request ID storage.
* @param string $request_id The ID.
*/
public function set( string $key, string $request_id ): void {
$all = $this->all();
$day_in_seconds = 86400;
$all[ $key ] = array(
'id' => $request_id,
'expiration' => time() + 10 * $day_in_seconds,
);
$all = $this->cleanup( $all );
update_option( self::KEY, $all );
}
/**
* Returns a request ID.
*
* @param string $key The key in the request ID storage.
*
* @return string
*/
public function get( string $key ): string {
$all = $this->all();
return isset( $all[ $key ] ) ? (string) $all[ $key ]['id'] : '';
}
/**
* Return all IDs.
*
* @return array
*/
private function all(): array {
return (array) get_option( 'ppcp-request-ids', array() );
}
/**
* Clean up outdated request IDs.
*
* @param array $all All request IDs.
*
* @return array
*/
private function cleanup( array $all ): array {
foreach ( $all as $order_id => $value ) {
if ( time() < $value['expiration'] ) {
continue;
}
unset( $all[ $order_id ] );
}
return $all;
}
}

View file

@ -0,0 +1,14 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3.25.0"
}
],
[
"@babel/preset-react"
]
]
}

3
modules/ppcp-applepay/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
assets/js
assets/css

View file

@ -0,0 +1,17 @@
{
"name": "woocommerce/ppcp-applepay",
"type": "dhii-mod",
"description": "Applepay module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\Applepay\\": "src"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

156
modules/ppcp-applepay/composer.lock generated Normal file
View file

@ -0,0 +1,156 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "30c5bd428bece98b555ddc0b2da044f3",
"packages": [
{
"name": "container-interop/service-provider",
"version": "v0.4.0",
"source": {
"type": "git",
"url": "https://github.com/container-interop/service-provider.git",
"reference": "4969b9e49460690b7430b3f1a87cab07be61418a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/container-interop/service-provider/zipball/4969b9e49460690b7430b3f1a87cab07be61418a",
"reference": "4969b9e49460690b7430b3f1a87cab07be61418a",
"shasum": ""
},
"require": {
"psr/container": "^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Interop\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Promoting container interoperability through standard service providers",
"homepage": "https://github.com/container-interop/service-provider",
"support": {
"issues": "https://github.com/container-interop/service-provider/issues",
"source": "https://github.com/container-interop/service-provider/tree/master"
},
"time": "2017-09-20T14:13:36+00:00"
},
{
"name": "dhii/module-interface",
"version": "v0.3.0-alpha2",
"source": {
"type": "git",
"url": "https://github.com/Dhii/module-interface.git",
"reference": "0e39f167d7ed8990c82f5d2e6084159d1a502a5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Dhii/module-interface/zipball/0e39f167d7ed8990c82f5d2e6084159d1a502a5b",
"reference": "0e39f167d7ed8990c82f5d2e6084159d1a502a5b",
"shasum": ""
},
"require": {
"container-interop/service-provider": "^0.4",
"php": "^7.1 | ^8.0",
"psr/container": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0 | ^8.0 | ^9.0",
"slevomat/coding-standard": "^6.0",
"vimeo/psalm": "^3.11.7 | ^4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-develop": "0.3.x-dev"
}
},
"autoload": {
"psr-4": {
"Dhii\\Modular\\Module\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dhii Team",
"email": "development@dhii.co"
}
],
"description": "Interfaces for modules",
"support": {
"issues": "https://github.com/Dhii/module-interface/issues",
"source": "https://github.com/Dhii/module-interface/tree/v0.3.0-alpha2"
},
"time": "2021-08-23T08:23:01+00:00"
},
{
"name": "psr/container",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/1.1.1"
},
"time": "2021-03-05T17:36:06+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^7.2 | ^8.0"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

View file

@ -0,0 +1,185 @@
<?php
/**
* The Applepay module extensions.
*
* @package WooCommerce\PayPalCommerce\Applepay
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Applepay;
use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
// Eligibility check.
if ( ! $container->has( 'applepay.eligible' ) || ! $container->get( 'applepay.eligible' ) ) {
return $fields;
}
$is_available = $container->get( 'applepay.available' );
$is_referral = $container->get( 'applepay.is_referral' );
$insert_after = function ( array $array, string $key, array $new ): array {
$keys = array_keys( $array );
$index = array_search( $key, $keys, true );
$pos = false === $index ? count( $array ) : $index + 1;
return array_merge( array_slice( $array, 0, $pos ), $new, array_slice( $array, $pos ) );
};
$display_manager = $container->get( 'wcgateway.display-manager' );
assert( $display_manager instanceof DisplayManager );
// Connection tab fields.
$fields = $insert_after(
$fields,
'ppcp_dcc_status',
array(
'applepay_status' => array(
'title' => __( 'Apple Pay Payments', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-text',
'text' => $container->get( 'applepay.settings.connection.status-text' ),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => Settings::CONNECTION_TAB_ID,
),
)
);
if ( ! $is_available && $is_referral ) {
$connection_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway&ppcp-tab=ppcp-connection#field-credentials_feature_onboarding_heading' );
$connection_link = '<a href="' . $connection_url . '" style="pointer-events: auto">';
return $insert_after(
$fields,
'allow_card_button_gateway',
array(
'applepay_button_enabled' => array(
'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'class' => array( 'ppcp-grayed-out-text' ),
'input_class' => array( 'ppcp-disabled-checkbox' ),
'label' => __( 'Enable Apple Pay button', 'woocommerce-paypal-payments' )
. '<p class="description">'
. sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Your PayPal account %1$srequires additional permissions%2$s to enable Apple Pay.', 'woocommerce-paypal-payments' ),
$connection_link,
'</a>'
)
. '</p>',
'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode(
array(
$display_manager
->rule()
->condition_is_true( false )
->action_enable( 'applepay_button_enabled' )
->to_array(),
)
),
),
),
)
);
}
return $insert_after(
$fields,
'allow_card_button_gateway',
array(
'applepay_button_enabled' => array(
'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Enable Apple Pay button', 'woocommerce-paypal-payments' )
. '<p class="description">'
. sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Buyers can use %1$sApple Pay%2$s to make payments on the web using the Safari web browser or an iOS device.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#apple-pay" target="_blank">',
'</a>'
)
. '</p>',
'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
'custom_attributes' => array(
'data-ppcp-display' => wp_json_encode(
array(
$display_manager
->rule()
->condition_element( 'applepay_button_enabled', '1' )
->action_visible( 'applepay_button_color' )
->action_visible( 'applepay_button_type' )
->action_visible( 'applepay_button_language' )
->to_array(),
)
),
),
),
'applepay_button_type' => array(
'title' => str_repeat( '&nbsp;', 6 ) . __( 'Button Label', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'This controls the label of the Apple Pay button.',
'woocommerce-paypal-payments'
),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay',
'options' => PropertiesDictionary::button_types(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_button_color' => array(
'title' => str_repeat( '&nbsp;', 6 ) . __( 'Button Color', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'The Apple Pay Button may appear as a black button with white lettering, white button with black lettering, or a white button with black lettering and a black outline.',
'woocommerce-paypal-payments'
),
'label' => '',
'input_class' => array( 'wc-enhanced-select' ),
'class' => array(),
'default' => 'black',
'options' => PropertiesDictionary::button_colors(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_button_language' => array(
'title' => str_repeat( '&nbsp;', 6 ) . __( 'Button Language', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
'description' => __(
'The language and region used for the displayed Apple Pay button. The default value is the current language and region setting in a browser.',
'woocommerce-paypal-payments'
),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'en',
'options' => PropertiesDictionary::button_languages(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
),
)
);
},
);

View file

@ -0,0 +1,16 @@
<?php
/**
* The Applepay module.
*
* @package WooCommerce\PayPalCommerce\Applepay
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Applepay;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new ApplepayModule();
};

View file

@ -0,0 +1,34 @@
{
"name": "ppcp-applepay",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"browserslist": [
"> 0.5%",
"Safari >= 8",
"Chrome >= 41",
"Firefox >= 43",
"Edge >= 14"
],
"dependencies": {
"@paypal/paypal-js": "^6.0.0",
"core-js": "^3.25.0"
},
"devDependencies": {
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-react": "^7.18.6",
"@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.76",
"webpack-cli": "^4.10"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
}
}

View file

@ -0,0 +1,56 @@
#applepay-container {
--apple-pay-button-height: 45px;
--apple-pay-button-min-height: 40px;
--apple-pay-button-width: 100%;
--apple-pay-button-max-width: 750px;
--apple-pay-button-border-radius: 4px;
--apple-pay-button-overflow: hidden;
--apple-pay-button-margin:7px 0;
&.ppcp-button-pill {
--apple-pay-button-border-radius: 50px;
}
&.ppcp-button-minicart {
--apple-pay-button-display: block;
--apple-pay-button-height: 40px;
}
}
.woocommerce-checkout {
#applepay-container {
margin-top: 0.5em;
--apple-pay-button-border-radius: 4px;
--apple-pay-button-height: 45px;
&.ppcp-button-pill {
--apple-pay-button-border-radius: 50px;
}
}
}
.ppcp-has-applepay-block {
.wp-block-woocommerce-checkout {
#applepay-container {
--apple-pay-button-margin: 0;
--apple-pay-button-height: 40px;
&.ppcp-button-pill {
--apple-pay-button-border-radius: 50px;
}
}
}
.wp-block-woocommerce-cart {
#applepay-container {
--apple-pay-button-margin: 0;
--apple-pay-button-height: 40px;
}
/* Workaround for blocks grid */
.wc-block-components-express-payment__event-buttons {
--apple-pay-button-display: block;
li[id*="express-payment-method-ppcp-"] {
--apple-pay-button-padding-bottom: 0;
}
}
}
}

View file

@ -0,0 +1,588 @@
import ContextHandlerFactory from "./Context/ContextHandlerFactory";
import {createAppleErrors} from "./Helper/applePayError";
import {setVisible} from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
import {setEnabled} from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
import FormValidator from "../../../ppcp-button/resources/js/modules/Helper/FormValidator";
import ErrorHandler from '../../../ppcp-button/resources/js/modules/ErrorHandler';
class ApplepayButton {
constructor(context, externalHandler, buttonConfig, ppcpConfig) {
this.isInitialized = false;
this.context = context;
this.externalHandler = externalHandler;
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.paymentsClient = null;
this.form_saved = false;
this.contextHandler = ContextHandlerFactory.create(
this.context,
this.buttonConfig,
this.ppcpConfig
);
this.updated_contact_info = []
this.selectedShippingMethod = []
this.nonce = document.getElementById('woocommerce-process-checkout-nonce')?.value
this.log = function() {
if ( this.buttonConfig.is_debug ) {
console.log('[ApplePayButton]', ...arguments);
}
}
this.refreshContextData();
}
init(config) {
if (this.isInitialized) {
return;
}
this.log('Init', this.context);
this.initEventHandlers();
this.isInitialized = true;
this.applePayConfig = config;
const isEligible = this.applePayConfig.isEligible;
if (isEligible) {
this.fetchTransactionInfo().then(() => {
const isSubscriptionProduct = this.ppcpConfig.data_client_id.has_subscriptions === true;
if (isSubscriptionProduct) {
return;
}
this.addButton();
const id_minicart = "#apple-" + this.buttonConfig.button.mini_cart_wrapper;
const id = "#apple-" + this.buttonConfig.button.wrapper;
if (this.context === 'mini-cart') {
document.querySelector(id_minicart)?.addEventListener('click', (evt) => {
evt.preventDefault();
this.onButtonClick();
});
} else {
document.querySelector(id)?.addEventListener('click', (evt) => {
evt.preventDefault();
this.onButtonClick();
});
}
// Listen for changes on any input within the WooCommerce checkout form
jQuery('form.checkout').on('change', 'input, select, textarea', () => {
this.fetchTransactionInfo();
});
});
}
}
reinit() {
if (!this.applePayConfig) {
return;
}
this.isInitialized = false;
this.init(this.applePayConfig);
}
async fetchTransactionInfo() {
this.transactionInfo = await this.contextHandler.transactionInfo();
}
/**
* Returns configurations relative to this button context.
*/
contextConfig() {
let config = {
wrapper: this.buttonConfig.button.wrapper,
ppcpStyle: this.ppcpConfig.button.style,
//buttonStyle: this.buttonConfig.button.style,
ppcpButtonWrapper: this.ppcpConfig.button.wrapper
}
if (this.context === 'mini-cart') {
config.wrapper = this.buttonConfig.button.mini_cart_wrapper;
config.ppcpStyle = this.ppcpConfig.button.mini_cart_style;
config.buttonStyle = this.buttonConfig.button.mini_cart_style;
config.ppcpButtonWrapper = this.ppcpConfig.button.mini_cart_wrapper;
}
if (['cart-block', 'checkout-block'].indexOf(this.context) !== -1) {
config.ppcpButtonWrapper = '#express-payment-method-ppcp-gateway';
}
return config;
}
initEventHandlers() {
const { wrapper, ppcpButtonWrapper } = this.contextConfig();
const wrapper_id = '#' + wrapper;
const syncButtonVisibility = () => {
const $ppcpButtonWrapper = jQuery(ppcpButtonWrapper);
setVisible(wrapper_id, $ppcpButtonWrapper.is(':visible'));
setEnabled(wrapper_id, !$ppcpButtonWrapper.hasClass('ppcp-disabled'));
}
jQuery(document).on('ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled', (ev, data) => {
if (jQuery(data.selector).is(ppcpButtonWrapper)) {
syncButtonVisibility();
}
});
syncButtonVisibility();
}
applePaySession(paymentRequest) {
this.log('applePaySession', paymentRequest);
const session = new ApplePaySession(4, paymentRequest)
session.begin()
if (this.buttonConfig.product.needShipping) {
session.onshippingmethodselected = this.onshippingmethodselected(session)
session.onshippingcontactselected = this.onshippingcontactselected(session)
}
session.onvalidatemerchant = this.onvalidatemerchant(session);
session.onpaymentauthorized = this.onpaymentauthorized(session);
return session;
}
/**
* Add a Apple Pay purchase button
*/
addButton() {
const wrapper =
(this.context === 'mini-cart')
? this.buttonConfig.button.mini_cart_wrapper
: this.buttonConfig.button.wrapper;
const shape =
(this.context === 'mini-cart')
? this.ppcpConfig.button.mini_cart_style.shape
: this.ppcpConfig.button.style.shape;
const appleContainer = this.context === 'mini-cart' ? document.getElementById("applepay-container-minicart") : document.getElementById("applepay-container");
const type = this.buttonConfig.button.type;
const language = this.buttonConfig.button.lang;
const color = this.buttonConfig.button.color;
const id = "apple-" + wrapper;
if (appleContainer) {
appleContainer.innerHTML = `<apple-pay-button id="${id}" buttonstyle="${color}" type="${type}" locale="${language}">`;
}
jQuery('#' + wrapper).addClass('ppcp-button-' + shape);
jQuery(wrapper).append(appleContainer);
}
//------------------------
// Button click
//------------------------
/**
* Show Apple Pay payment sheet when Apple Pay payment button is clicked
*/
async onButtonClick() {
this.log('onButtonClick', this.context);
const paymentDataRequest = this.paymentDataRequest();
// trigger woocommerce validation if we are in the checkout page
if (this.context === 'checkout') {
const checkoutFormSelector = 'form.woocommerce-checkout';
const errorHandler = new ErrorHandler(
PayPalCommerceGateway.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper')
);
try {
const formData = new FormData(document.querySelector(checkoutFormSelector));
this.form_saved = Object.fromEntries(formData.entries());
// This line should be reviewed, the paypal.Applepay().confirmOrder fails if we add it.
//this.update_request_data_with_form(paymentDataRequest);
} catch (error) {
console.error(error);
}
const session = this.applePaySession(paymentDataRequest)
const formValidator = PayPalCommerceGateway.early_checkout_validation_enabled ?
new FormValidator(
PayPalCommerceGateway.ajax.validate_checkout.endpoint,
PayPalCommerceGateway.ajax.validate_checkout.nonce,
) : null;
if (formValidator) {
try {
const errors = await formValidator.validate(document.querySelector(checkoutFormSelector));
if (errors.length > 0) {
errorHandler.messages(errors);
jQuery( document.body ).trigger( 'checkout_error' , [ errorHandler.currentHtml() ] );
session.abort();
return;
}
} catch (error) {
console.error(error);
}
}
return;
}
this.applePaySession(paymentDataRequest)
}
update_request_data_with_form(paymentDataRequest) {
paymentDataRequest.billingContact = this.fill_billing_contact(this.form_saved);
paymentDataRequest.applicationData = this.fill_application_data(this.form_saved);
if (!this.buttonConfig.product.needShipping) {
return;
}
paymentDataRequest.shippingContact = this.fill_shipping_contact(this.form_saved);
}
paymentDataRequest() {
const applepayConfig = this.applePayConfig
const buttonConfig = this.buttonConfig
let baseRequest = {
countryCode: applepayConfig.countryCode,
merchantCapabilities: applepayConfig.merchantCapabilities,
supportedNetworks: applepayConfig.supportedNetworks,
requiredShippingContactFields: ["postalAddress", "email", "phone"],
requiredBillingContactFields: ["postalAddress", "email", "phone"],
}
const paymentDataRequest = Object.assign({}, baseRequest);
paymentDataRequest.currencyCode = buttonConfig.shop.currencyCode;
paymentDataRequest.total = {
label: buttonConfig.shop.totalLabel,
type: "final",
amount: this.transactionInfo.totalPrice,
}
return paymentDataRequest
}
refreshContextData() {
switch (this.context) {
case 'product':
// Refresh product data that makes the price change.
this.productQuantity = document.querySelector('input.qty').value;
this.products = this.contextHandler.products();
this.log('Products updated', this.products);
break;
}
}
//------------------------
// Payment process
//------------------------
onvalidatemerchant(session) {
this.log('onvalidatemerchant', this.buttonConfig.ajax_url);
return (applePayValidateMerchantEvent) => {
this.log('onvalidatemerchant call');
paypal.Applepay().validateMerchant({
validationUrl: applePayValidateMerchantEvent.validationURL
})
.then(validateResult => {
this.log('onvalidatemerchant ok');
session.completeMerchantValidation(validateResult.merchantSession);
//call backend to update validation to true
jQuery.ajax({
url: this.buttonConfig.ajax_url,
type: 'POST',
data: {
action: 'ppcp_validate',
validation: true,
'woocommerce-process-checkout-nonce': this.nonce,
}
})
})
.catch(validateError => {
this.log('onvalidatemerchant error', validateError);
console.error(validateError);
//call backend to update validation to false
jQuery.ajax({
url: this.buttonConfig.ajax_url,
type: 'POST',
data: {
action: 'ppcp_validate',
validation: false,
'woocommerce-process-checkout-nonce': this.nonce,
}
})
this.log('onvalidatemerchant session abort');
session.abort();
});
};
}
onshippingmethodselected(session) {
this.log('onshippingmethodselected', this.buttonConfig.ajax_url);
const ajax_url = this.buttonConfig.ajax_url
return (event) => {
this.log('onshippingmethodselected call');
const data = this.getShippingMethodData(event);
jQuery.ajax({
url: ajax_url,
method: 'POST',
data: data,
success: (applePayShippingMethodUpdate, textStatus, jqXHR) => {
this.log('onshippingmethodselected ok');
let response = applePayShippingMethodUpdate.data
if (applePayShippingMethodUpdate.success === false) {
response.errors = createAppleErrors(response.errors)
}
this.selectedShippingMethod = event.shippingMethod
//order the response shipping methods, so that the selected shipping method is the first one
let orderedShippingMethods = response.newShippingMethods.sort((a, b) => {
if (a.label === this.selectedShippingMethod.label) {
return -1
}
return 1
})
//update the response.newShippingMethods with the ordered shipping methods
response.newShippingMethods = orderedShippingMethods
if (applePayShippingMethodUpdate.success === false) {
response.errors = createAppleErrors(response.errors)
}
session.completeShippingMethodSelection(response)
},
error: (jqXHR, textStatus, errorThrown) => {
this.log('onshippingmethodselected error', textStatus);
console.warn(textStatus, errorThrown)
session.abort()
},
})
};
}
onshippingcontactselected(session) {
this.log('onshippingcontactselected', this.buttonConfig.ajax_url);
const ajax_url = this.buttonConfig.ajax_url
return (event) => {
this.log('onshippingcontactselected call');
const data = this.getShippingContactData(event);
jQuery.ajax({
url: ajax_url,
method: 'POST',
data: data,
success: (applePayShippingContactUpdate, textStatus, jqXHR) => {
this.log('onshippingcontactselected ok');
let response = applePayShippingContactUpdate.data
this.updated_contact_info = event.shippingContact
if (applePayShippingContactUpdate.success === false) {
response.errors = createAppleErrors(response.errors)
}
if (response.newShippingMethods) {
this.selectedShippingMethod = response.newShippingMethods[0]
}
session.completeShippingContactSelection(response)
},
error: (jqXHR, textStatus, errorThrown) => {
this.log('onshippingcontactselected error', textStatus);
console.warn(textStatus, errorThrown)
session.abort()
},
})
};
}
getShippingContactData(event) {
const product_id = this.buttonConfig.product.id;
this.refreshContextData();
switch (this.context) {
case 'product':
return {
action: 'ppcp_update_shipping_contact',
product_id: product_id,
products: JSON.stringify(this.products),
caller_page: 'productDetail',
product_quantity: this.productQuantity,
simplified_contact: event.shippingContact,
need_shipping: this.buttonConfig.product.needShipping,
'woocommerce-process-checkout-nonce': this.nonce,
};
case 'cart':
case 'checkout':
case 'cart-block':
case 'checkout-block':
case 'mini-cart':
return {
action: 'ppcp_update_shipping_contact',
simplified_contact: event.shippingContact,
caller_page: 'cart',
need_shipping: this.buttonConfig.product.needShipping,
'woocommerce-process-checkout-nonce': this.nonce,
};
}
}
getShippingMethodData(event) {
const product_id = this.buttonConfig.product.id;
this.refreshContextData();
switch (this.context) {
case 'product': return {
action: 'ppcp_update_shipping_method',
shipping_method: event.shippingMethod,
product_id: product_id,
products: JSON.stringify(this.products),
caller_page: 'productDetail',
product_quantity: this.productQuantity,
simplified_contact: this.updated_contact_info,
'woocommerce-process-checkout-nonce': this.nonce,
}
case 'cart':
case 'checkout':
case 'cart-block':
case 'checkout-block':
case 'mini-cart':
return {
action: 'ppcp_update_shipping_method',
shipping_method: event.shippingMethod,
caller_page: 'cart',
simplified_contact: this.updated_contact_info,
'woocommerce-process-checkout-nonce': this.nonce,
}
}
}
onpaymentauthorized(session) {
this.log('onpaymentauthorized');
return async (event) => {
this.log('onpaymentauthorized call');
function form() {
return document.querySelector('form.cart');
}
const processInWooAndCapture = async (data) => {
return new Promise((resolve, reject) => {
try {
const billingContact = data.billing_contact
const shippingContact = data.shipping_contact
let request_data = {
action: 'ppcp_create_order',
'caller_page': this.context,
'product_id': this.buttonConfig.product.id ?? null,
'products': JSON.stringify(this.products),
'product_quantity': this.productQuantity ?? null,
'shipping_contact': shippingContact,
'billing_contact': billingContact,
'token': event.payment.token,
'shipping_method': this.selectedShippingMethod,
'woocommerce-process-checkout-nonce': this.nonce,
'funding_source': 'applepay',
'_wp_http_referer': '/?wc-ajax=update_order_review',
'paypal_order_id': data.paypal_order_id,
};
this.log('onpaymentauthorized request', this.buttonConfig.ajax_url, data);
jQuery.ajax({
url: this.buttonConfig.ajax_url,
method: 'POST',
data: request_data,
complete: (jqXHR, textStatus) => {
this.log('onpaymentauthorized complete');
},
success: (authorizationResult, textStatus, jqXHR) => {
this.log('onpaymentauthorized ok');
resolve(authorizationResult)
},
error: (jqXHR, textStatus, errorThrown) => {
this.log('onpaymentauthorized error', textStatus);
reject(new Error(errorThrown));
},
})
} catch (error) {
this.log('onpaymentauthorized catch', error);
console.log(error) // handle error
}
});
}
let id = await this.contextHandler.createOrder();
this.log('onpaymentauthorized paypal order ID', id, event.payment.token, event.payment.billingContact);
try {
const confirmOrderResponse = await paypal.Applepay().confirmOrder({
orderId: id,
token: event.payment.token,
billingContact: event.payment.billingContact,
});
this.log('onpaymentauthorized confirmOrderResponse', confirmOrderResponse);
if (confirmOrderResponse && confirmOrderResponse.approveApplePayPayment) {
if (confirmOrderResponse.approveApplePayPayment.status === "APPROVED") {
try {
let data = {
billing_contact: event.payment.billingContact,
shipping_contact: event.payment.shippingContact,
paypal_order_id: id,
};
let authorizationResult = await processInWooAndCapture(data);
if (authorizationResult.result === "success") {
session.completePayment(ApplePaySession.STATUS_SUCCESS)
window.location.href = authorizationResult.redirect
} else {
session.completePayment(ApplePaySession.STATUS_FAILURE)
}
} catch (error) {
session.completePayment(ApplePaySession.STATUS_FAILURE);
session.abort()
console.error(error);
}
} else {
console.error('Error status is not APPROVED');
session.completePayment(ApplePaySession.STATUS_FAILURE);
}
} else {
console.error('Invalid confirmOrderResponse');
session.completePayment(ApplePaySession.STATUS_FAILURE);
}
} catch (error) {
console.error('Error confirming order with applepay token', error);
session.completePayment(ApplePaySession.STATUS_FAILURE);
session.abort()
}
};
}
fill_billing_contact(form_saved) {
return {
givenName: form_saved.billing_first_name ?? '',
familyName: form_saved.billing_last_name ?? '',
emailAddress: form_saved.billing_email ?? '',
phoneNumber: form_saved.billing_phone ?? '',
addressLines: [form_saved.billing_address_1, form_saved.billing_address_2],
locality: form_saved.billing_city ?? '',
postalCode: form_saved.billing_postcode ?? '',
countryCode: form_saved.billing_country ?? '',
administrativeArea: form_saved.billing_state ?? '',
}
}
fill_shipping_contact(form_saved) {
if (form_saved.shipping_first_name === "") {
return this.fill_billing_contact(form_saved)
}
return {
givenName: (form_saved?.shipping_first_name && form_saved.shipping_first_name !== "") ? form_saved.shipping_first_name : form_saved?.billing_first_name,
familyName: (form_saved?.shipping_last_name && form_saved.shipping_last_name !== "") ? form_saved.shipping_last_name : form_saved?.billing_last_name,
emailAddress: (form_saved?.shipping_email && form_saved.shipping_email !== "") ? form_saved.shipping_email : form_saved?.billing_email,
phoneNumber: (form_saved?.shipping_phone && form_saved.shipping_phone !== "") ? form_saved.shipping_phone : form_saved?.billing_phone,
addressLines: [form_saved.shipping_address_1 ?? '', form_saved.shipping_address_2 ?? ''],
locality: (form_saved?.shipping_city && form_saved.shipping_city !== "") ? form_saved.shipping_city : form_saved?.billing_city,
postalCode: (form_saved?.shipping_postcode && form_saved.shipping_postcode !== "") ? form_saved.shipping_postcode : form_saved?.billing_postcode,
countryCode: (form_saved?.shipping_country && form_saved.shipping_country !== "") ? form_saved.shipping_country : form_saved?.billing_country,
administrativeArea: (form_saved?.shipping_state && form_saved.shipping_state !== "") ? form_saved.shipping_state : form_saved?.billing_state,
}
}
fill_application_data(form_saved) {
const jsonString = JSON.stringify(form_saved);
let utf8Str = encodeURIComponent(jsonString).replace(/%([0-9A-F]{2})/g, (match, p1) => {
return String.fromCharCode('0x' + p1);
});
return btoa(utf8Str);
}
}
export default ApplepayButton;

View file

@ -0,0 +1,55 @@
import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher";
import ApplepayButton from "./ApplepayButton";
class ApplepayManager {
constructor(buttonConfig, ppcpConfig) {
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.ApplePayConfig = null;
this.buttons = [];
buttonModuleWatcher.watchContextBootstrap((bootstrap) => {
const button = new ApplepayButton(
bootstrap.context,
bootstrap.handler,
buttonConfig,
ppcpConfig,
);
this.buttons.push(button);
if (this.ApplePayConfig) {
button.init(this.ApplePayConfig);
}
});
}
init() {
(async () => {
await this.config();
for (const button of this.buttons) {
button.init(this.ApplePayConfig);
}
})();
}
reinit() {
for (const button of this.buttons) {
button.reinit();
}
}
/**
* Gets ApplePay configuration of the PayPal merchant.
* @returns {Promise<null>}
*/
async config() {
this.ApplePayConfig = await paypal.Applepay().config();
return this.ApplePayConfig;
}
}
export default ApplepayManager;

View file

@ -0,0 +1,73 @@
import ErrorHandler from "../../../../ppcp-button/resources/js/modules/ErrorHandler";
import CartActionHandler
from "../../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler";
import onApprove
from "../../../../ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue";
class BaseHandler {
constructor(buttonConfig, ppcpConfig) {
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
}
transactionInfo() {
return new Promise((resolve, reject) => {
fetch(
this.ppcpConfig.ajax.cart_script_params.endpoint,
{
method: 'GET',
credentials: 'same-origin',
}
)
.then(result => result.json())
.then(result => {
if (! result.success) {
return;
}
// handle script reload
const data = result.data;
resolve({
countryCode: data.country_code,
currencyCode: data.currency_code,
totalPriceStatus: 'FINAL',
totalPrice: data.total_str
});
});
});
}
createOrder() {
const errorHandler = new ErrorHandler(
this.ppcpConfig.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper')
);
const actionHandler = new CartActionHandler(
this.ppcpConfig,
errorHandler,
);
return actionHandler.configuration().createOrder(null, null);
}
approveOrderForContinue(data, actions) {
const errorHandler = new ErrorHandler(
this.ppcpConfig.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper')
);
let onApproveHandler = onApprove({
config: this.ppcpConfig
}, errorHandler);
return onApproveHandler(data, actions);
}
}
export default BaseHandler;

View file

@ -0,0 +1,7 @@
import BaseHandler from "./BaseHandler";
class CartBlockHandler extends BaseHandler {
}
export default CartBlockHandler;

View file

@ -0,0 +1,7 @@
import BaseHandler from "./BaseHandler";
class CartHandler extends BaseHandler {
}
export default CartHandler;

View file

@ -0,0 +1,7 @@
import BaseHandler from "./BaseHandler";
class CheckoutBlockHandler extends BaseHandler{
}
export default CheckoutBlockHandler;

Some files were not shown because too many files have changed in this diff Show more