Merge trunk and fix conflicts

This commit is contained in:
dinamiko 2022-05-17 17:05:00 +02:00
commit 4ca2e637ed
58 changed files with 1174 additions and 785 deletions

28
.ddev/bin/share Executable file
View file

@ -0,0 +1,28 @@
#!/usr/bin/env bash
PROJECT_HOST=$(ddev wp option get home | tr -d '\r' | awk -F'^http[s]?://' '{print $2}')
ddev share >/dev/null &
NGROK_PID=$!
ngrok-url(){
curl -s localhost:4040/api/tunnels | jq -r .tunnels[0].public_url | awk -F'^http[s]?://' '{print $2}'
}
wp-replace(){
echo "Replacing ${1} with ${2}"
ddev wp search-replace "${1}" "${2}" --all-tables --precise --recurse-objects
}
kill-ngrok(){
echo "Killing ngrok process ${NGROK_PID}"
pkill "${NGROK_PID}"
wp-replace "${NGROK_HOST}" "${PROJECT_HOST}"
}
echo "ngrok started with PID ${NGROK_PID}"
echo "Waiting a few seconds for the service to come up"
sleep 3
NGROK_HOST=$(ngrok-url)
wp-replace "${PROJECT_HOST}" "${NGROK_HOST}"
echo "Your site is now reachable at https://${NGROK_HOST}"
echo "ctrl-c to stop sharing and revert database changes"
trap kill-ngrok INT
wait -f "${NGROK_PID}"

42
.ddev/commands/web/orchestrate Executable file
View file

@ -0,0 +1,42 @@
#!/bin/bash
set -x
## Description: Set up the development environment
## Usage: orchestrate
## Example: "ddev orchestrate"
mkdir -p "${DDEV_DOCROOT}"
pushd "${DDEV_DOCROOT}"
PLUGIN_FOLDER="${DDEV_DOCROOT}/wp-content/plugins/${PLUGIN_NAME:-$DDEV_PROJECT}"
VALID_ARGS=$(getopt -o fp: --long force,plugin: -- "$@")
if [[ $? -ne 0 ]]; then
exit 1;
fi
eval set -- "$VALID_ARGS"
while [ : ]; do
case "$1" in
-f | --force)
echo "Removing WordPress installation"
shift
export RECREATE_ENV=1;
popd
find "${DDEV_DOCROOT}" -mindepth 1 ! -regex "^${PLUGIN_FOLDER}\(/.*\)?" -delete
pushd "${DDEV_DOCROOT}"
;;
-p | --plugin)
echo "Processing 'plugin' option. Input argument is '$2'"
shift 2
;;
--) shift;
break
;;
esac
done
# Execute all fragments from orchestrate.d
if [ -d "${0}.d" ]; then
for FN in ${0}.d/*.sh ; do
echo $FN
source "${FN}"
done
fi

View file

@ -0,0 +1,6 @@
#!/bin/bash
if ! wp core download --version="${WP_VERSION:-latest}"; then
echo 'WordPress is already installed.'
exit
fi

View file

@ -0,0 +1,6 @@
#!/bin/bash
cat << 'EOF' > ".gitignore"
# ignores everything in the docroot (docroot in config.yaml), this path may not be included in the standard .ddev/.gitignore.
*
EOF

View file

@ -0,0 +1,22 @@
#!/bin/bash
# Create wp-config.php
# the PHP snippet avoids some notices during CLI calls
# https://make.wordpress.org/cli/handbook/guides/common-issues/#php-notice-undefined-index-on-_server-superglobal
printf '
if ( defined( "WP_CLI" ) && WP_CLI && ! isset( $_SERVER["HTTP_HOST"] ) ) {
$_SERVER["HTTP_HOST"] = "%s";
}
' "${DDEV_HOSTNAME}" | wp config create \
--dbname=db \
--dbuser=db \
--dbpass=db \
--dbhost=db \
--force \
--extra-php
wp config set WP_DEBUG true --raw
wp config set WP_DEBUG_DISPLAY true --raw
wp config set WP_DEBUG_LOG true --raw
wp config set WP_SITEURL '(isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] ? "https://" : "http://") . $_SERVER["HTTP_HOST"]' --raw
wp config set WP_HOME '(isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] ? "https://" : "http://") . $_SERVER["HTTP_HOST"]' --raw

View file

@ -0,0 +1,32 @@
#!/bin/bash
if [ ! -z "${RECREATE_ENV}" ]; then
echo "Deleting database before creating a new one"
wp db clean --yes
fi
if [ "${WP_MULTISITE}" = "true" ]; then
wp core multisite-install \
--title="${WP_TITLE}" \
--admin_user="${ADMIN_USER}" \
--admin_password="${ADMIN_PASS}" \
--url="${DDEV_PRIMARY_URL}" \
--admin_email="${ADMIN_EMAIL}" \
--skip-email
readarray -d , -t slugs <<< "${WP_MULTISITE_SLUGS},"; unset "slugs[-1]";
for slug in "${slugs[@]}"; do
if [ ! -z "${slug}" ]; then
wp site create --slug="${slug}"
fi
done
else
wp core install \
--title="${WP_TITLE}" \
--admin_user="${ADMIN_USER}" \
--admin_password="${ADMIN_PASS}" \
--url="${DDEV_PRIMARY_URL}" \
--admin_email="${ADMIN_EMAIL}" \
--skip-email
fi

View file

@ -0,0 +1,16 @@
#!/bin/bash
cat << 'EOF' >> ".htaccess"
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
RewriteRule . index.php [L]
EOF

View file

@ -0,0 +1,4 @@
#!/bin/bash
wp plugin is-installed akismet && wp plugin uninstall akismet
wp plugin is-installed hello && wp plugin uninstall hello

View file

@ -0,0 +1,6 @@
#!/bin/bash
popd
composer install
yarn run build:dev

View file

@ -0,0 +1,10 @@
#!/bin/bash
pushd "${DDEV_DOCROOT}"
flags=""
if [ "${WP_MULTISITE}" = "true" ]; then
flags+=" --network"
fi
wp plugin activate "${PLUGIN_NAME:-$DDEV_PROJECT}" $flags

View file

@ -0,0 +1,9 @@
#!/bin/bash
flags=""
if [ "${WP_MULTISITE}" = "true" ]; then
flags+=" --network"
fi
wp plugin install woocommerce --version="${WC_VERSION}"
wp plugin activate woocommerce $flags

View file

@ -0,0 +1,4 @@
#!/bin/bash
wp rewrite structure '/%postname%'
wp rewrite flush

View file

@ -0,0 +1,3 @@
#!/bin/bash
wp theme install storefront --activate

7
.ddev/commands/web/wp Executable file
View file

@ -0,0 +1,7 @@
#!/bin/bash
## Description: Run WordPress CLI inside the web container. DDEV core only provides this for the "wordpress" project type for some reason
## Usage: wp [flags] [args]
## Example: "ddev wp core version" or "ddev wp plugin install user-switching --activate"
## ProjectTypes: php
wp "$@" --path=.ddev/wordpress

209
.ddev/config.yaml Normal file
View file

@ -0,0 +1,209 @@
name: woocommerce-paypal-payments
type: php
docroot: .ddev/wordpress
php_version: "7.1"
webserver_type: apache-fpm
router_http_port: "80"
router_https_port: "443"
xdebug_enabled: false
additional_hostnames: ['wc-pp']
additional_fqdns: []
mariadb_version: "10.3"
mysql_version: ""
nfs_mount_enabled: false
mutagen_enabled: false
use_dns_when_possible: true
composer_version: "1"
hooks:
pre-start:
- exec-host: "mkdir -p .ddev/wordpress/wp-content/plugins/${DDEV_PROJECT}"
web_environment:
- WP_VERSION=5.9.3
- WP_LOCALE=en_US
- WP_TITLE=WooCommerce PayPal Payments
- WP_MULTISITE=true
- WP_MULTISITE_SLUGS=de,es
- ADMIN_USER=admin
- ADMIN_PASS=admin
- ADMIN_EMAIL=admin@example.com
- WC_VERSION=6.1.0
# Key features of ddev's config.yaml:
# name: <projectname> # Name of the project, automatically provides
# http://projectname.ddev.site and https://projectname.ddev.site
# type: <projecttype> # drupal6/7/8, backdrop, typo3, wordpress, php
# docroot: <relative_path> # Relative path to the directory containing index.php.
# php_version: "7.4" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"
# You can explicitly specify the webimage, dbimage, dbaimage lines but this
# is not recommended, as the images are often closely tied to ddev's' behavior,
# so this can break upgrades.
# webimage: <docker_image> # nginx/php docker image.
# dbimage: <docker_image> # mariadb docker image.
# dbaimage: <docker_image>
# mariadb_version and mysql_version
# ddev can use many versions of mariadb and mysql
# However these directives are mutually exclusive
# mariadb_version: 10.2
# mysql_version: 8.0
# router_http_port: <port> # Port to be used for http (defaults to port 80)
# router_https_port: <port> # Port for https (defaults to 443)
# xdebug_enabled: false # Set to true to enable xdebug and "ddev start" or "ddev restart"
# Note that for most people the commands
# "ddev xdebug" to enable xdebug and "ddev xdebug off" to disable it work better,
# as leaving xdebug enabled all the time is a big performance hit.
# xhprof_enabled: false # Set to true to enable xhprof and "ddev start" or "ddev restart"
# Note that for most people the commands
# "ddev xhprof" to enable xhprof and "ddev xhprof off" to disable it work better,
# as leaving xhprof enabled all the time is a big performance hit.
# webserver_type: nginx-fpm # or apache-fpm
# timezone: Europe/Berlin
# This is the timezone used in the containers and by PHP;
# it can be set to any valid timezone,
# see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# For example Europe/Dublin or MST7MDT
# composer_version: "2"
# if composer_version:"2" it will use the most recent composer v2
# It can also be set to "1", to get most recent composer v1
# or "" for the default v2 created at release time.
# It can be set to any existing specific composer version.
# After first project 'ddev start' this will not be updated until it changes
# additional_hostnames:
# - somename
# - someothername
# would provide http and https URLs for "somename.ddev.site"
# and "someothername.ddev.site".
# additional_fqdns:
# - example.com
# - sub1.example.com
# would provide http and https URLs for "example.com" and "sub1.example.com"
# Please take care with this because it can cause great confusion.
# upload_dir: custom/upload/dir
# would set the destination path for ddev import-files to <docroot>/custom/upload/dir
# working_dir:
# web: /var/www/html
# db: /home
# would set the default working directory for the web and db services.
# These values specify the destination directory for ddev ssh and the
# directory in which commands passed into ddev exec are run.
# omit_containers: [db, dba, ddev-ssh-agent]
# Currently only these containers are supported. Some containers can also be
# omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit
# the "db" container, several standard features of ddev that access the
# database container will be unusable. In the global configuration it is also
# possible to omit ddev-router, but not here.
# nfs_mount_enabled: false
# Great performance improvement but requires host configuration first.
# See https://ddev.readthedocs.io/en/stable/users/performance/#using-nfs-to-mount-the-project-into-the-container
# mutagen_enabled: false
# Experimental performance improvement using mutagen asynchronous updates.
# See https://ddev.readthedocs.io/en/latest/users/performance/#using-mutagen
# fail_on_hook_fail: False
# Decide whether 'ddev start' should be interrupted by a failing hook
# host_https_port: "59002"
# The host port binding for https can be explicitly specified. It is
# dynamic unless otherwise specified.
# This is not used by most people, most people use the *router* instead
# of the localhost port.
# host_webserver_port: "59001"
# The host port binding for the ddev-webserver can be explicitly specified. It is
# dynamic unless otherwise specified.
# This is not used by most people, most people use the *router* instead
# of the localhost port.
# host_db_port: "59002"
# The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic
# unless explicitly specified.
# phpmyadmin_port: "8036"
# phpmyadmin_https_port: "8037"
# The PHPMyAdmin ports can be changed from the default 8036 and 8037
# host_phpmyadmin_port: "8036"
# The phpmyadmin (dba) port is not normally bound on the host at all, instead being routed
# through ddev-router, but it can be specified and bound.
# mailhog_port: "8025"
# mailhog_https_port: "8026"
# The MailHog ports can be changed from the default 8025 and 8026
# host_mailhog_port: "8025"
# The mailhog port is not normally bound on the host at all, instead being routed
# through ddev-router, but it can be bound directly to localhost if specified here.
# webimage_extra_packages: [php7.4-tidy, php-bcmath]
# Extra Debian packages that are needed in the webimage can be added here
# dbimage_extra_packages: [telnet,netcat]
# Extra Debian packages that are needed in the dbimage can be added here
# use_dns_when_possible: true
# If the host has internet access and the domain configured can
# successfully be looked up, DNS will be used for hostname resolution
# instead of editing /etc/hosts
# Defaults to true
# project_tld: ddev.site
# The top-level domain used for project URLs
# The default "ddev.site" allows DNS lookup via a wildcard
# If you prefer you can change this to "ddev.local" to preserve
# pre-v1.9 behavior.
# ngrok_args: --subdomain mysite --auth username:pass
# Provide extra flags to the "ngrok http" command, see
# https://ngrok.com/docs#http or run "ngrok http -h"
# disable_settings_management: false
# If true, ddev will not create CMS-specific settings files like
# Drupal's settings.php/settings.ddev.php or TYPO3's AdditionalConfiguration.php
# In this case the user must provide all such settings.
# You can inject environment variables into the web container with:
# web_environment:
# - SOMEENV=somevalue
# - SOMEOTHERENV=someothervalue
# no_project_mount: false
# (Experimental) If true, ddev will not mount the project into the web container;
# the user is responsible for mounting it manually or via a script.
# This is to enable experimentation with alternate file mounting strategies.
# For advanced users only!
# bind_all_interfaces: false
# If true, host ports will be bound on all network interfaces,
# not just the localhost interface. This means that ports
# will be available on the local network if the host firewall
# allows it.
# Many ddev commands can be extended to run tasks before or after the
# ddev command is executed, for example "post-start", "post-import-db",
# "pre-composer", "post-composer"
# See https://ddev.readthedocs.io/en/stable/users/extending-commands/ for more
# information on the commands that can be extended and the tasks you can define
# for them. Example:
#hooks:
# Un-comment to emit the WP CLI version after ddev start.
# post-start:
# - exec: wp cli version

View file

@ -0,0 +1,5 @@
version: '3.6'
services:
web:
volumes:
- ../:/var/www/html/.ddev/wordpress/wp-content/plugins/${DDEV_PROJECT}:ro

View file

@ -0,0 +1,39 @@
source /usr/lib/git-core/git-sh-prompt
# ignore duplicate commands, ignore commands starting with a space
# keep the last 5000 entries
export HISTCONTROL=erasedups
export HISTSIZE=5000
# Add global and "project root level" composer executables to the PATH
export PATH="$HOME/.composer/vendor/bin:$PATH"
export PATH="/var/www/html/vendor/bin:$PATH"
# append to the history instead of overwriting (good for multiple connections)
shopt -s histappend
export PS1="\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$\[$(tput sgr0)\] "
export PHP_IDE_CONFIG='serverName=woocommerce-paypal-payments.ddev.site'
#export XDEBUG_CONFIG="mode=debug client_host=127.0.0.1 client_port=9003 start_with_request=yes"
alias ls='ls --group-directories-first'
alias cp='cp -aiv'
alias grep='grep --color=always'
alias tgz='tar -pczf'
profile(){
XDEBUG_MODE=profile "$1"
}
# all files in homeadditions/bashrc.d/*.sh will be
# interpreted as shell script so you can use these to
# customize your bash
if [ -d "${HOME}/bashrc.d" ]; then
for FN in ${HOME}/bashrc.d/*.sh ; do
source "${FN}"
done
fi
cd "${DDEV_DOCROOT}"

View file

@ -1,30 +0,0 @@
PLUGIN_NAME=woocommerce-paypal-payments
BASE_PATH=./
PROJECT_MOUNT_PATH=/var/www/html/wp-content/plugins/woocommerce-paypal-payments
DOCROOT_PATH=/var/www/html
BUILD_ROOT_PATH=/app/
PROJECT_NAME=inpsyde_woocommerce-paypal-payments
HOST_IP_ADDRESS=172.17.0.1
WP_DOMAIN=wc-pp.myhost
WP_TITLE="WooCommerce PayPal Payments"
WP_VERSION=5.3
WC_VERSION=4.7.0
PHP_BUILD_VERSION=7.1
PHP_TEST_VERSION=7.1
PHP_DEPS_VERSION=7.1
DB_ROOT_PASSWORD=root
DB_NAME=wordpress
DB_USER_NAME=wordpress
DB_USER_PASSWORD=J(P*@%OSJiaifN2
ADMIN_USER=admin
ADMIN_PASS=admin
ADMIN_EMAIL=me@my.com
#NGROK_HOST=mydomain.ngrok.io
COMPOSER_MEMORY_LIMIT=1.5G

4
.gitattributes vendored
View file

@ -1,6 +1,6 @@
/.github/ export-ignore
/.psalm/ export-ignore
/docker/ export-ignore
/.ddev/ export-ignore
/tests/ export-ignore
/.idea/ export-ignore
.env export-ignore
@ -10,8 +10,6 @@ auth.json export-ignore
composer.json export-ignore
composer.lock export-ignore
DEVELOPMENT.md export-ignore
docker-compose.yml export-ignore
grumphp.yml export-ignore
gulpfile.js export-ignore
package.json export-ignore
phpcs.xml export-ignore

View file

@ -38,38 +38,41 @@ follow these steps:
```
$ yarn run build
```
or if using the Docker setup:
or if using the DDEV setup:
```
$ yarn run docker:build-package
$ yarn run ddev:build-package
```
## Setup
You can install WooCommerce PayPal Payments locally using the dev environment of your preference, or you can use the Docker environment which includes WP, WC and all developments tools.
You can install WooCommerce PayPal Payments locally using the dev environment of your preference, or you can use the DDEV setup provided in this repository which includes WP, WC and all developments tools.
To set up the Docker environment, follow these steps:
To set up the DDEV environment, follow these steps:
0. Install Docker and Docker Compose.
1. `$ cp .env.example .env` and edit the configuration in the `.env` file if needed.
2. `$ yarn run docker:build` (or copy the commands from [package.json](/package.json) if you do not have `yarn`).
3. `$ yarn run docker:install`
4. `$ yarn run docker:start`
5. Add `127.0.0.1 wc-pp.myhost` to your `hosts` file and open http://wc-pp.myhost (the default value of `WP_DOMAIN` in `.env`).
0. Install Docker and [DDEV](https://ddev.readthedocs.io/en/stable/).
1. Edit the configuration in the [`.ddev/config.yml`](.ddev/config.yaml) file if needed.
2. `$ ddev start`
3. `$ ddev orchestrate` to install WP/WC.
4. Open https://wc-pp.ddev.site
### Running tests in the Docker environment
Use `$ ddev orchestrate -f` for reinstalattion (will destroy all site data).
You may also need `$ ddev restart` to apply the config changes.
### Running tests and other tasks in the DDEV environment
Tests and code style:
- `$ yarn run docker:test`
- `$ yarn run docker:lint`
After some changes in `.env` (such as PHP, WP versions) you may need to rebuild the Docker image:
1. `$ yarn run docker:destroy` (all data will be lost)
2. `$ yarn run docker:build`
- `$ yarn ddev:test`
- `$ yarn ddev:lint`
- `$ yarn ddev:fix-lint` - PHPCBF to fix basic code style issued
See [package.json](/package.json) for other useful commands.
For debugging, see [the DDEV docs](https://ddev.readthedocs.io/en/stable/users/step-debugging/).
Enable xdebug via `$ ddev xdebug`, and press `Start Listening for PHP Debug Connections` in PHPStorm.
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).
## Test account setup
You will need a PayPal sandbox merchant and customer accounts to configure the plugin and make test purchases with it.
@ -82,20 +85,21 @@ For testing webhooks locally, follow these steps to set up ngrok:
0. Install [ngrok](https://ngrok.com/).
1. Run
```
ngrok http -host-header=rewrite wc-pp.myhost
```
1.
- If using DDEV, run our wrapper Bash script which will start `ddev share` and replace the URLs in the WP database:
```
$ .ddev/bin/share
```
2. In your environment variables (accessible to the web server), add `NGROK_HOST` with the host that you got from `ngrok`, like `abcd1234.ngrok.io`.
- For the Docker environment: set `NGROK_HOST` in the `.env` file and restart the web server. (`yarn run docker:stop && yarn run docker:start`)
3. Complete onboarding or resubscribe webhooks on the Webhooks Status page.
Currently, ngrok is used only for the webhook listening URL.
- For other environments, run
```
$ ngrok http -host-header=rewrite wc-pp.myhost
```
and in your environment variables (accessible to the web server) add `NGROK_HOST` with the host that you got from `ngrok`, like `abcd1234.ngrok.io`. ngrok will be used only for the webhook listening URL.
The URLs displayed on the WordPress pages, used in redirects, etc. will still remain local.
2. Complete onboarding or resubscribe webhooks on the Webhooks Status page.
## License
[GPL-2.0 License](LICENSE)

View file

@ -1,120 +0,0 @@
version: '3.4'
services:
wp_dev:
build:
context: ./
dockerfile: docker/Dockerfile_wp
target: dev
args:
PHP_BUILD_VERSION: $PHP_BUILD_VERSION
PHP_TEST_VERSION: $PHP_TEST_VERSION
PHP_DEPS_VERSION: $PHP_DEPS_VERSION
PROJECT_MOUNT_PATH: $PROJECT_MOUNT_PATH
BUILD_ROOT_PATH: $BUILD_ROOT_PATH
DOCROOT_PATH: $DOCROOT_PATH
WP_DOMAIN: ${WP_DOMAIN}
WP_VERSION: $WP_VERSION
container_name: "${PROJECT_NAME}_wp_dev"
depends_on:
- db
restart: unless-stopped
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: $DB_NAME
WORDPRESS_DB_USER: $DB_USER_NAME
WORDPRESS_DB_PASSWORD: $DB_USER_PASSWORD
WORDPRESS_DEBUG: 1
DOCROOT_PATH: ${DOCROOT_PATH}
PLUGIN_NAME: ${PLUGIN_NAME}
ADMIN_USER: ${ADMIN_USER}
ADMIN_PASS: ${ADMIN_PASS}
ADMIN_EMAIL: ${ADMIN_EMAIL}
WP_DOMAIN: ${WP_DOMAIN}
WP_TITLE: ${WP_TITLE}
WC_VERSION: $WC_VERSION
NGROK_HOST: ${NGROK_HOST}
volumes:
- wordpress:${DOCROOT_PATH}
- ${BASE_PATH}:${PROJECT_MOUNT_PATH}
ports:
- 80:80
db:
image: mariadb:latest
container_name: "${PROJECT_NAME}_db"
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: $DB_ROOT_PASSWORD
MYSQL_DATABASE: $DB_NAME
MYSQL_USER: $DB_USER_NAME
MYSQL_PASSWORD: $DB_USER_PASSWORD
ports:
- 3306:3306
volumes:
- db:/var/lib/mysql
db_admin:
image: phpmyadmin/phpmyadmin:latest
container_name: "${PROJECT_NAME}_db_admin"
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: $DB_ROOT_PASSWORD
depends_on:
- db
ports:
- 1234:80
# volumes:
# - db_admin
composer:
build:
context: ./
dockerfile: docker/Dockerfile_wp
target: composer
args:
PHP_DEPS_VERSION: $PHP_DEPS_VERSION
BUILD_ROOT_PATH: $BUILD_ROOT_PATH
container_name: "${PROJECT_NAME}_composer"
working_dir: ${BUILD_ROOT_PATH}
volumes:
- ${BASE_PATH}:${BUILD_ROOT_PATH}
environment:
- COMPOSER_MEMORY_LIMIT=${COMPOSER_MEMORY_LIMIT}
build:
build:
context: ./
dockerfile: docker/Dockerfile_wp
target: build
args:
PHP_DEPS_VERSION: $PHP_DEPS_VERSION
PHP_BUILD_VERSION: $PHP_BUILD_VERSION
BUILD_ROOT_PATH: $BUILD_ROOT_PATH
WP_VERSION: $WP_VERSION
container_name: "${PROJECT_NAME}_build"
working_dir: ${BUILD_ROOT_PATH}
volumes:
- ${BASE_PATH}:${BUILD_ROOT_PATH}
test:
extra_hosts:
- "host.docker.internal:${HOST_IP_ADDRESS}"
build:
context: ./
dockerfile: docker/Dockerfile_wp
target: test
args:
PHP_DEPS_VERSION: $PHP_DEPS_VERSION
BUILD_ROOT_PATH: $BUILD_ROOT_PATH
PHP_BUILD_VERSION: $PHP_BUILD_VERSION
PHP_TEST_VERSION: $PHP_TEST_VERSION
container_name: "${PROJECT_NAME}_test"
working_dir: ${BUILD_ROOT_PATH}
volumes:
- ${BASE_PATH}:${BUILD_ROOT_PATH}
volumes:
wordpress:
db:
db_admin:

View file

@ -1,159 +0,0 @@
ARG PHP_BUILD_VERSION
ARG PHP_TEST_VERSION
ARG PHP_DEPS_VERSION
ARG WP_VERSION
# Composer on correct PHP version
FROM php:${PHP_DEPS_VERSION}-cli as composer
ARG BUILD_ROOT_PATH
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
RUN echo 'memory_limit = 128M' >> /usr/local/etc/php/conf.d/docker-php-memlimit.ini;
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php --install-dir=/usr/bin --filename=composer --version=1.10.22 && \
php -r "unlink('composer-setup.php');"
RUN apt-get update
RUN apt-get install -y \
ssh \
zip \
unzip \
curl \
git \
# These are for extensions
zlib1g-dev \
libicu-dev
RUN mkdir -p ~/.ssh
RUN ssh-keyscan -H github.com >> ~/.ssh/known_hosts
RUN docker-php-ext-install intl json && \
docker-php-ext-enable intl json
WORKDIR ${BUILD_ROOT_PATH}
# Composer on correct PHP version
FROM php:${PHP_BUILD_VERSION}-cli as build
ARG BUILD_ROOT_PATH
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
RUN echo 'memory_limit = 256M' >> /usr/local/etc/php/conf.d/docker-php-memlimit.ini;
RUN apt-get update
RUN apt-get install -y gnupg apt-transport-https ca-certificates
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update
RUN apt-get install -y \
ssh \
zip \
unzip \
curl \
git \
yarn \
ca-certificates \
# These are for extensions
zlib1g-dev \
libicu-dev \
g++ \
# For installing things from URL
wget
# https://github.com/nodesource/distributions/issues/1266
RUN update-ca-certificates
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php --install-dir=/usr/bin --filename=composer --version=1.10.22 && \
php -r "unlink('composer-setup.php');"
RUN mkdir -p ~/.ssh
RUN ssh-keyscan -H github.com >> ~/.ssh/known_hosts
RUN docker-php-ext-install intl json && \
docker-php-ext-enable intl json
# Install Node
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash -
RUN apt-get install -y nodejs
WORKDIR ${BUILD_ROOT_PATH}
COPY . ./
FROM php:${PHP_TEST_VERSION}-cli as test
ARG BUILD_ROOT_PATH
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
RUN apt-get update
RUN apt-get install -y \
# These are for extensions
zlib1g-dev \
libicu-dev \
g++
RUN pecl install xdebug-2.6.1
RUN docker-php-ext-install pcntl posix intl json
WORKDIR ${BUILD_ROOT_PATH}
COPY --from=build ${BUILD_ROOT_PATH} ${BUILD_ROOT_PATH}
# Install PHP dev dependencies
FROM build as vendor-dev
ARG BUILD_ROOT_PATH
WORKDIR ${BUILD_ROOT_PATH}
COPY --from=build ${BUILD_ROOT_PATH} ${BUILD_ROOT_PATH}
RUN composer config discard-changes true && composer install --no-dev --no-scripts
# WordPress for development
FROM wordpress:${WP_VERSION}-php${PHP_TEST_VERSION}-apache as dev
ARG PROJECT_MOUNT_PATH
ARG BUILD_ROOT_PATH
ARG DOCROOT_PATH
ARG WP_DOMAIN
COPY docker/wp-entrypoint.sh /usr/local/bin
COPY docker/wait-for-it.sh /usr/local/bin
RUN chmod +x /usr/local/bin/wp-entrypoint.sh /usr/local/bin/wait-for-it.sh
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
&& chmod +x wp-cli.phar \
&& mv wp-cli.phar /usr/local/bin/wp
RUN sed -i "s|#ServerName www.example.com|ServerName ${WP_DOMAIN}|" /etc/apache2/sites-available/*.conf
RUN sed -i "s|#ServerName www.example.com|ServerName ${WP_DOMAIN}|" /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
RUN apt-get update
RUN apt-get install -y \
zip \
unzip \
curl \
# These are for extensions
zlib1g-dev \
libicu-dev \
g++
RUN docker-php-ext-install pcntl posix intl json
RUN apt-get remove -y \
# These are for extensions
zlib1g-dev \
libicu-dev \
g++
WORKDIR ${DOCROOT_PATH}
COPY --from=vendor-dev ${BUILD_ROOT_PATH} ${PROJECT_MOUNT_PATH}
ENTRYPOINT ["wp-entrypoint.sh"]
CMD ["apache2-foreground"]

View file

@ -1,182 +0,0 @@
#!/usr/bin/env bash
# Use this script to test if a given TCP host/port are available
WAITFORIT_cmdname=${0##*/}
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
usage()
{
cat << USAGE >&2
Usage:
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Don't output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}
wait_for()
{
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
else
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
fi
WAITFORIT_start_ts=$(date +%s)
while :
do
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
WAITFORIT_result=$?
else
(echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
WAITFORIT_result=$?
fi
if [[ $WAITFORIT_result -eq 0 ]]; then
WAITFORIT_end_ts=$(date +%s)
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
break
fi
sleep 1
done
return $WAITFORIT_result
}
wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
else
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
fi
WAITFORIT_PID=$!
trap "kill -INT -$WAITFORIT_PID" INT
wait $WAITFORIT_PID
WAITFORIT_RESULT=$?
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
fi
return $WAITFORIT_RESULT
}
# process arguments
while [[ $# -gt 0 ]]
do
case "$1" in
*:* )
WAITFORIT_hostport=(${1//:/ })
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
shift 1
;;
--child)
WAITFORIT_CHILD=1
shift 1
;;
-q | --quiet)
WAITFORIT_QUIET=1
shift 1
;;
-s | --strict)
WAITFORIT_STRICT=1
shift 1
;;
-h)
WAITFORIT_HOST="$2"
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
WAITFORIT_HOST="${1#*=}"
shift 1
;;
-p)
WAITFORIT_PORT="$2"
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
WAITFORIT_PORT="${1#*=}"
shift 1
;;
-t)
WAITFORIT_TIMEOUT="$2"
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
WAITFORIT_TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
WAITFORIT_CLI=("$@")
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
# Check to see if timeout is from busybox?
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
WAITFORIT_BUSYTIMEFLAG=""
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
WAITFORIT_ISBUSY=1
# Check if busybox timeout uses -t flag
# (recent Alpine versions don't support -t anymore)
if timeout &>/dev/stdout | grep -q -e '-t '; then
WAITFORIT_BUSYTIMEFLAG="-t"
fi
else
WAITFORIT_ISBUSY=0
fi
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
wait_for
WAITFORIT_RESULT=$?
exit $WAITFORIT_RESULT
else
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
wait_for_wrapper
WAITFORIT_RESULT=$?
else
wait_for
WAITFORIT_RESULT=$?
fi
fi
if [[ $WAITFORIT_CLI != "" ]]; then
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
exit $WAITFORIT_RESULT
fi
exec "${WAITFORIT_CLI[@]}"
else
exit $WAITFORIT_RESULT
fi

View file

@ -1,45 +0,0 @@
#!/bin/bash
set -e
if wait-for-it.sh "${WORDPRESS_DB_HOST}" -t 60; then
docker-entrypoint.sh apache2 -v
wp core multisite-install \
--allow-root \
--title="${WP_TITLE}" \
--admin_user="${ADMIN_USER}" \
--admin_password="${ADMIN_PASS}" \
--url="${WP_DOMAIN}" \
--admin_email="${ADMIN_EMAIL}" \
--skip-email
cat << 'EOF' > "${DOCROOT_PATH}/.htaccess"
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
RewriteRule . index.php [L]
EOF
wp site create \
--allow-root \
--slug="de" \
|| true # allow failure if already exists
wp plugin is-installed akismet --allow-root && wp plugin uninstall akismet --allow-root --path="${DOCROOT_PATH}"
wp plugin is-installed hello --allow-root && wp plugin uninstall hello --allow-root --path="${DOCROOT_PATH}"
wp plugin install woocommerce --version="${WC_VERSION}" --allow-root --path="${DOCROOT_PATH}" \
&& wp plugin activate woocommerce --network --allow-root --path="${DOCROOT_PATH}"
wp plugin activate "${PLUGIN_NAME}" --network --allow-root --path="${DOCROOT_PATH}"
fi
exec "$@"

View file

@ -27,6 +27,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ApplicationContextFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExchangeRateFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\FraudProcessorResponseFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ItemFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\MoneyFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
@ -257,7 +258,8 @@ return array(
$amount_factory = $container->get( 'api.factory.amount' );
return new CaptureFactory(
$amount_factory,
$container->get( 'api.factory.seller-receivable-breakdown' )
$container->get( 'api.factory.seller-receivable-breakdown' ),
$container->get( 'api.factory.fraud-processor-response' )
);
},
'api.factory.purchase-unit' => static function ( ContainerInterface $container ): PurchaseUnitFactory {
@ -354,6 +356,9 @@ return array(
$container->get( 'api.factory.platform-fee' )
);
},
'api.factory.fraud-processor-response' => static function ( ContainerInterface $container ): FraudProcessorResponseFactory {
return new FraudProcessorResponseFactory();
},
'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies {
return new DccApplies(
$container->get( 'api.dcc-supported-country-currency-matrix' ),

View file

@ -198,11 +198,11 @@ class PaymentsEndpoint {
*
* @param Refund $refund The refund to be processed.
*
* @return void
* @return string Refund ID.
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function refund( Refund $refund ) : void {
public function refund( Refund $refund ) : string {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/payments/captures/' . $refund->for_capture()->id() . '/refund';
$args = array(
@ -216,19 +216,21 @@ class PaymentsEndpoint {
);
$response = $this->request( $url, $args );
$json = json_decode( $response['body'] );
if ( is_wp_error( $response ) ) {
throw new RuntimeException( 'Could not refund payment.' );
}
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
$json = json_decode( $response['body'] );
if ( 201 !== $status_code || ! is_object( $json ) ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $json->id;
}
/**

View file

@ -58,6 +58,13 @@ class Capture {
*/
private $seller_receivable_breakdown;
/**
* The fraud processor response (AVS, CVV ...).
*
* @var FraudProcessorResponse|null
*/
protected $fraud_processor_response;
/**
* The invoice id.
*
@ -83,6 +90,7 @@ class Capture {
* @param string $invoice_id The invoice id.
* @param string $custom_id The custom id.
* @param SellerReceivableBreakdown|null $seller_receivable_breakdown The detailed breakdown of the capture activity (fees, ...).
* @param FraudProcessorResponse|null $fraud_processor_response The fraud processor response (AVS, CVV ...).
*/
public function __construct(
string $id,
@ -92,7 +100,8 @@ class Capture {
string $seller_protection,
string $invoice_id,
string $custom_id,
?SellerReceivableBreakdown $seller_receivable_breakdown
?SellerReceivableBreakdown $seller_receivable_breakdown,
?FraudProcessorResponse $fraud_processor_response
) {
$this->id = $id;
@ -103,6 +112,7 @@ class Capture {
$this->invoice_id = $invoice_id;
$this->custom_id = $custom_id;
$this->seller_receivable_breakdown = $seller_receivable_breakdown;
$this->fraud_processor_response = $fraud_processor_response;
}
/**
@ -177,6 +187,15 @@ class Capture {
return $this->seller_receivable_breakdown;
}
/**
* Returns the fraud processor response (AVS, CVV ...).
*
* @return FraudProcessorResponse|null
*/
public function fraud_processor_response() : ?FraudProcessorResponse {
return $this->fraud_processor_response;
}
/**
* Returns the entity as array.
*
@ -199,6 +218,9 @@ class Capture {
if ( $this->seller_receivable_breakdown ) {
$data['seller_receivable_breakdown'] = $this->seller_receivable_breakdown->to_array();
}
if ( $this->fraud_processor_response ) {
$data['fraud_processor_response'] = $this->fraud_processor_response->to_array();
}
return $data;
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* The FraudProcessorResponse object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class FraudProcessorResponse
*/
class FraudProcessorResponse {
/**
* The AVS response code.
*
* @var string|null
*/
protected $avs_code;
/**
* The CVV response code.
*
* @var string|null
*/
protected $cvv_code;
/**
* FraudProcessorResponse constructor.
*
* @param string|null $avs_code The AVS response code.
* @param string|null $cvv_code The CVV response code.
*/
public function __construct( ?string $avs_code, ?string $cvv_code ) {
$this->avs_code = $avs_code;
$this->cvv_code = $cvv_code;
}
/**
* Returns the AVS response code.
*
* @return string|null
*/
public function avs_code(): ?string {
return $this->avs_code;
}
/**
* Returns the CVV response code.
*
* @return string|null
*/
public function cvv_code(): ?string {
return $this->cvv_code;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array(
'avs_code' => $this->avs_code() ?: '',
'address_match' => $this->avs_code() === 'M' ? 'Y' : 'N',
'postal_match' => $this->avs_code() === 'M' ? 'Y' : 'N',
'cvv_match' => $this->cvv_code() === 'M' ? 'Y' : 'N',
);
}
}

View file

@ -175,6 +175,15 @@ class PurchaseUnit {
return $this->shipping;
}
/**
* Sets shipping info.
*
* @param Shipping|null $shipping The value to set.
*/
public function set_shipping( ?Shipping $shipping ): void {
$this->shipping = $shipping;
}
/**
* Returns the reference id.
*

View file

@ -119,4 +119,13 @@ class PayPalApiException extends RuntimeException {
public function issues(): array {
return $this->response->issues ?? array();
}
/**
* The HTTP status code.
*
* @return int
*/
public function status_code(): int {
return $this->status_code;
}
}

View file

@ -124,10 +124,31 @@ class AmountFactory {
$currency = $order->get_currency();
$items = $this->item_factory->from_wc_order( $order );
$discount_value = array_sum(
array(
(float) $order->get_total_discount( false ), // Only coupons.
$this->discounts_from_items( $items ),
)
);
$discount = null;
if ( $discount_value ) {
$discount = new Money(
(float) $discount_value,
$currency
);
}
$items = array_filter(
$items,
function ( Item $item ): bool {
return $item->unit_amount()->value() > 0;
}
);
$total_value = (float) $order->get_total();
if ( (
CreditCardGateway::ID === $order->get_payment_method()
|| ( PayPalGateway::ID === $order->get_payment_method() && 'card' === $order->get_meta( PayPalGateway::ORDER_PAYMENT_SOURCE ) )
|| ( PayPalGateway::ID === $order->get_payment_method() && 'card' === $order->get_meta( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY ) )
)
&& $this->is_free_trial_order( $order )
) {
@ -160,14 +181,6 @@ class AmountFactory {
$currency
);
$discount = null;
if ( (float) $order->get_total_discount( false ) ) {
$discount = new Money(
(float) $order->get_total_discount( false ),
$currency
);
}
$breakdown = new AmountBreakdown(
$item_total,
$shipping,
@ -251,4 +264,29 @@ class AmountFactory {
return new AmountBreakdown( ...$money );
}
/**
* Returns the sum of items with negative amount;
*
* @param Item[] $items PayPal order items.
* @return float
*/
private function discounts_from_items( array $items ): float {
$discounts = array_filter(
$items,
function ( Item $item ): bool {
return $item->unit_amount()->value() < 0;
}
);
return abs(
array_sum(
array_map(
function ( Item $item ): float {
return (float) $item->quantity() * $item->unit_amount()->value();
},
$discounts
)
)
);
}
}

View file

@ -32,19 +32,29 @@ class CaptureFactory {
*/
private $seller_receivable_breakdown_factory;
/**
* The FraudProcessorResponseFactory factory.
*
* @var FraudProcessorResponseFactory
*/
protected $fraud_processor_response_factory;
/**
* CaptureFactory constructor.
*
* @param AmountFactory $amount_factory The amount factory.
* @param SellerReceivableBreakdownFactory $seller_receivable_breakdown_factory The SellerReceivableBreakdown factory.
* @param FraudProcessorResponseFactory $fraud_processor_response_factory The FraudProcessorResponseFactory factory.
*/
public function __construct(
AmountFactory $amount_factory,
SellerReceivableBreakdownFactory $seller_receivable_breakdown_factory
SellerReceivableBreakdownFactory $seller_receivable_breakdown_factory,
FraudProcessorResponseFactory $fraud_processor_response_factory
) {
$this->amount_factory = $amount_factory;
$this->seller_receivable_breakdown_factory = $seller_receivable_breakdown_factory;
$this->fraud_processor_response_factory = $fraud_processor_response_factory;
}
/**
@ -55,12 +65,15 @@ class CaptureFactory {
* @return Capture
*/
public function from_paypal_response( \stdClass $data ) : Capture {
$reason = $data->status_details->reason ?? null;
$seller_receivable_breakdown = isset( $data->seller_receivable_breakdown ) ?
$this->seller_receivable_breakdown_factory->from_paypal_response( $data->seller_receivable_breakdown )
: null;
$fraud_processor_response = isset( $data->processor_response ) ?
$this->fraud_processor_response_factory->from_paypal_response( $data->processor_response )
: null;
return new Capture(
(string) $data->id,
new CaptureStatus(
@ -72,7 +85,8 @@ class CaptureFactory {
(string) $data->seller_protection->status,
(string) $data->invoice_id,
(string) $data->custom_id,
$seller_receivable_breakdown
$seller_receivable_breakdown,
$fraud_processor_response
);
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* The FraudProcessorResponseFactory Factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\FraudProcessorResponse;
/**
* Class FraudProcessorResponseFactory
*/
class FraudProcessorResponseFactory {
/**
* Returns a FraudProcessorResponse object based off a PayPal Response.
*
* @param stdClass $data The JSON object.
*
* @return FraudProcessorResponse
*/
public function from_paypal_response( stdClass $data ): FraudProcessorResponse {
$avs_code = $data->avs_code ?: null;
$cvv_code = $data->cvv_code ?: null;
return new FraudProcessorResponse( $avs_code, $cvv_code );
}
}

View file

@ -142,7 +142,7 @@ class ItemFactory {
mb_substr( $product->get_name(), 0, 127 ),
new Money( $price_without_tax_rounded, $currency ),
$quantity,
mb_substr( wp_strip_all_tags( $product->get_description() ), 0, 127 ),
substr( wp_strip_all_tags( $product->get_description() ), 0, 127 ) ?: '',
$tax,
$product->get_sku(),
( $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS

View file

@ -107,7 +107,12 @@ class PurchaseUnitFactory {
*/
public function from_wc_order( \WC_Order $order ): PurchaseUnit {
$amount = $this->amount_factory->from_wc_order( $order );
$items = $this->item_factory->from_wc_order( $order );
$items = array_filter(
$this->item_factory->from_wc_order( $order ),
function ( Item $item ): bool {
return $item->unit_amount()->value() > 0;
}
);
$shipping = $this->shipping_factory->from_wc_order( $order );
if (
! $this->shipping_needed( ... array_values( $items ) ) ||
@ -153,7 +158,12 @@ class PurchaseUnitFactory {
*/
public function from_wc_cart( \WC_Cart $cart ): PurchaseUnit {
$amount = $this->amount_factory->from_wc_cart( $cart );
$items = $this->item_factory->from_wc_cart( $cart );
$items = array_filter(
$this->item_factory->from_wc_cart( $cart ),
function ( Item $item ): bool {
return $item->unit_amount()->value() > 0;
}
);
$shipping = null;
$customer = \WC()->customer;

View file

@ -209,12 +209,6 @@ class CreditCardRenderer {
const firstName = document.getElementById('billing_first_name') ? document.getElementById('billing_first_name').value : '';
const lastName = document.getElementById('billing_last_name') ? document.getElementById('billing_last_name').value : '';
if (!firstName || !lastName) {
this.spinner.unblock();
this.errorHandler.message(this.defaultConfig.hosted_fields.labels.cardholder_name_required);
return;
}
hostedFieldsData.cardholderName = firstName + ' ' + lastName;
}

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Button\Assets;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Product;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
@ -220,16 +221,15 @@ class SmartButton implements SmartButtonInterface {
* @throws \WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException When a setting was not found.
*/
public function render_wrapper(): bool {
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return false;
}
if ( $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ) ) {
$this->render_button_wrapper_registrar();
$this->render_message_wrapper_registrar();
}
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return false;
}
if (
$this->settings->has( 'dcc_enabled' )
&& $this->settings->get( 'dcc_enabled' )
@ -430,6 +430,10 @@ class SmartButton implements SmartButtonInterface {
add_action(
$this->mini_cart_button_renderer_hook(),
function () {
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return;
}
if ( $this->is_cart_price_total_zero() || $this->is_free_trial_cart() ) {
return;
}
@ -512,13 +516,16 @@ class SmartButton implements SmartButtonInterface {
* Renders the HTML for the buttons.
*/
public function button_renderer() {
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return;
}
$product = wc_get_product();
if (
! is_checkout() && is_a( $product, \WC_Product::class )
&& (
$product->is_type( array( 'external', 'grouped' ) )
|| ! $product->is_in_stock()
)
! is_checkout() && is_a( $product, WC_Product::class )
&& ! $this->product_supports_payment( $product )
) {
return;
@ -538,7 +545,22 @@ class SmartButton implements SmartButtonInterface {
/**
* Renders the HTML for the credit messaging.
*/
public function message_renderer():void {
public function message_renderer() {
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return false;
}
$product = wc_get_product();
if (
! is_checkout() && is_a( $product, WC_Product::class )
/**
* The filter returning true if PayPal buttons can be rendered, or false otherwise.
*/
&& ! $this->product_supports_payment( $product )
) {
return;
}
echo '<div id="ppcp-messages" data-partner-attribution-id="Woo_PPCP"></div>';
}
@ -559,7 +581,7 @@ class SmartButton implements SmartButtonInterface {
}
$placement = 'product';
$product = wc_get_product();
$amount = ( is_a( $product, \WC_Product::class ) ) ? wc_get_price_including_tax( $product ) : 0;
$amount = ( is_a( $product, WC_Product::class ) ) ? wc_get_price_including_tax( $product ) : 0;
$layout = $this->settings->has( 'message_product_layout' ) ?
$this->settings->get( 'message_product_layout' ) : 'text';
$logo_type = $this->settings->has( 'message_product_logo' ) ?
@ -1219,6 +1241,24 @@ class SmartButton implements SmartButtonInterface {
return WC()->cart && WC()->cart->get_total( 'numeric' ) == 0;
}
/**
* Checks if PayPal buttons/messages can be rendered for the given product.
*
* @param WC_Product $product The product.
*
* @return bool
*/
protected function product_supports_payment( WC_Product $product ): bool {
/**
* The filter returning true if PayPal buttons/messages can be rendered for this product, or false otherwise.
*/
return apply_filters(
'woocommerce_paypal_payments_product_supports_payment_request_button',
! $product->is_type( array( 'external', 'grouped' ) ) && $product->is_in_stock(),
$product
);
}
/**
* Retrieves all payment tokens for the user, via API or cached if already queried.
*

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
@ -242,6 +243,13 @@ class CreateOrderEndpoint implements EndpointInterface {
}
$order = $this->create_paypal_order( $wc_order );
if ( 'pay-now' === $data['context'] && is_a( $wc_order, \WC_Order::class ) ) {
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
$wc_order->save_meta_data();
}
wp_send_json_success( $order->to_array() );
return true;
@ -317,19 +325,51 @@ class CreateOrderEndpoint implements EndpointInterface {
* @return Order Created PayPal order.
*
* @throws RuntimeException If create order request fails.
* @throws PayPalApiException If create order request fails.
* phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
*/
private function create_paypal_order( \WC_Order $wc_order = null ): Order {
$needs_shipping = WC()->cart instanceof \WC_Cart && WC()->cart->needs_shipping();
$shipping_address_is_fix = $needs_shipping && 'checkout' === $this->parsed_request_data['context'];
return $this->api_endpoint->create(
$this->purchase_units,
$this->payer( $this->parsed_request_data, $wc_order ),
null,
$this->payment_method(),
'',
$shipping_address_is_fix
);
try {
return $this->api_endpoint->create(
$this->purchase_units,
$this->payer( $this->parsed_request_data, $wc_order ),
null,
$this->payment_method(),
'',
$shipping_address_is_fix
);
} catch ( PayPalApiException $exception ) {
// Looks like currently there is no proper way to validate the shipping address for PayPal,
// so we cannot make some invalid addresses null in PurchaseUnitFactory,
// which causes failure e.g. for guests using the button on products pages when the country does not have postal codes.
if ( 422 === $exception->status_code()
&& array_filter(
$exception->details(),
function ( stdClass $detail ): bool {
return isset( $detail->field ) && str_contains( (string) $detail->field, 'shipping/address' );
}
) ) {
$this->logger->info( 'Invalid shipping address for order creation, retrying without it.' );
foreach ( $this->purchase_units as $purchase_unit ) {
$purchase_unit->set_shipping( null );
}
return $this->api_endpoint->create(
$this->purchase_units,
$this->payer( $this->parsed_request_data, $wc_order ),
null,
$this->payment_method(),
'',
$shipping_address_is_fix
);
}
throw $exception;
}
}
/**

View file

@ -119,7 +119,7 @@ class PaymentTokenChecker {
try {
if ( $this->is_free_trial_order( $wc_order ) ) {
if ( CreditCardGateway::ID === $wc_order->get_payment_method()
|| ( PayPalGateway::ID === $wc_order->get_payment_method() && 'card' === $wc_order->get_meta( PayPalGateway::ORDER_PAYMENT_SOURCE ) )
|| ( PayPalGateway::ID === $wc_order->get_payment_method() && 'card' === $wc_order->get_meta( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY ) )
) {
$order = $this->order_repository->for_wc_order( $wc_order );
$this->authorized_payments_processor->void_authorizations( $order );

View file

@ -9,6 +9,18 @@
"#ppcp-vault_enabled"
)
const payLaterEnabledLabels = document.querySelectorAll(
".ppcp-pay-later-enabled-label"
)
const payLaterDisabledLabels = document.querySelectorAll(
".ppcp-pay-later-disabled-label"
)
const disabledCheckboxes = document.querySelectorAll(
'.ppcp-disabled-checkbox'
)
function atLeastOneChecked(checkboxesNodeList) {
return Array.prototype.slice.call(checkboxesNodeList).filter(node => !node.disabled && node.checked).length > 0
}
@ -21,18 +33,41 @@
nodeList.forEach(node => node.removeAttribute('disabled'))
}
function updateCheckboxes() {
atLeastOneChecked(payLaterMessagingCheckboxes) ? disableAll(vaultingCheckboxes) : enableAll(vaultingCheckboxes)
atLeastOneChecked(vaultingCheckboxes) ? disableAll(payLaterMessagingCheckboxes) : enableAll(payLaterMessagingCheckboxes)
if(typeof PayPalCommerceGatewaySettings === 'undefined' || PayPalCommerceGatewaySettings.vaulting_features_available !== '1' ) {
disableAll(vaultingCheckboxes)
}
function hideAll(nodeList) {
nodeList.forEach(node => node.style.display = 'none')
}
updateCheckboxes()
function displayAll(nodeList) {
nodeList.forEach(node => node.style.display = '')
}
payLaterMessagingCheckboxes.forEach(node => node.addEventListener('change', updateCheckboxes))
vaultingCheckboxes.forEach(node => node.addEventListener('change', updateCheckboxes));
function uncheckAll(nodeList){
nodeList.forEach(node => {
node.checked = false
node.dispatchEvent(new Event('change'))
})
}
function disablePayLater() {
uncheckAll(payLaterMessagingCheckboxes)
disableAll(payLaterMessagingCheckboxes)
hideAll(payLaterEnabledLabels)
displayAll(payLaterDisabledLabels)
}
function enablePayLater() {
enableAll(payLaterMessagingCheckboxes)
displayAll(payLaterEnabledLabels)
hideAll(payLaterDisabledLabels)
}
function togglePayLater() {
atLeastOneChecked(vaultingCheckboxes) ? disablePayLater() : enablePayLater()
}
disableAll( disabledCheckboxes )
togglePayLater()
vaultingCheckboxes.forEach(node => node.addEventListener('change', togglePayLater));
}
);

View file

@ -13,6 +13,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesDisclaimers;
@ -783,15 +784,7 @@ return array(
'title' => __( 'Vaulting', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'desc_tip' => true,
'label' => sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Enable saved cards and subscription features on your store. To use vaulting features, you must %1$senable vaulting on your account%2$s.', 'woocommerce-paypal-payments' ),
'<a
href="https://docs.woocommerce.com/document/woocommerce-paypal-payments/#enable-vaulting-on-your-live-account"
target="_blank"
>',
'</a>'
),
'label' => $container->get( 'button.helper.vaulting-label' ),
'description' => __( 'Allow registered buyers to save PayPal and Credit Card accounts. Allow Subscription renewals.', 'woocommerce-paypal-payments' ),
'default' => false,
'screens' => array(
@ -799,6 +792,7 @@ return array(
),
'requirements' => array(),
'gateway' => array( 'paypal', 'dcc' ),
'input_class' => $container->get( 'wcgateway.helper.vaulting-scope' ) ? array() : array( 'ppcp-disabled-checkbox' ),
),
'logging_enabled' => array(
'title' => __( 'Logging', 'woocommerce-paypal-payments' ),
@ -989,7 +983,7 @@ return array(
'message_enabled' => array(
'title' => __( 'Enable message on Checkout', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Enable on Checkout', 'woocommerce-paypal-payments' ),
'label' => sprintf( $container->get( 'wcgateway.settings.fields.pay-later-label' ), __( 'Enable on Checkout', 'woocommerce-paypal-payments' ) ),
'default' => true,
'screens' => array(
State::STATE_START,
@ -1295,7 +1289,7 @@ return array(
'message_product_enabled' => array(
'title' => __( 'Enable message on Single Product', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Enable on Single Product', 'woocommerce-paypal-payments' ),
'label' => sprintf( $container->get( 'wcgateway.settings.fields.pay-later-label' ), __( 'Enable on Single Product', 'woocommerce-paypal-payments' ) ),
'default' => true,
'screens' => array(
State::STATE_START,
@ -1601,7 +1595,7 @@ return array(
'message_cart_enabled' => array(
'title' => __( 'Enable message on Cart', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __( 'Enable on Cart', 'woocommerce-paypal-payments' ),
'label' => sprintf( $container->get( 'wcgateway.settings.fields.pay-later-label' ), __( 'Enable on Cart', 'woocommerce-paypal-payments' ) ),
'default' => true,
'screens' => array(
State::STATE_START,
@ -2047,17 +2041,6 @@ return array(
$fields['disable_cards']['options'] = $card_options;
$fields['card_icons']['options'] = array_merge( $dark_versions, $card_options );
/**
* Display vault message on Pay Later label if vault is enabled.
*/
$settings = $container->get( 'wcgateway.settings' );
if ( $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ) ) {
$message = __( "You have PayPal vaulting enabled, that's why Pay Later Messaging options are unavailable now. You cannot use both features at the same time.", 'woocommerce-paypal-payments' );
$fields['message_enabled']['label'] = $message;
$fields['message_product_enabled']['label'] = $message;
$fields['message_cart_enabled']['label'] = $message;
}
return $fields;
},
@ -2156,4 +2139,44 @@ return array(
$settings->has( 'logging_enabled' ) && $settings->get( 'logging_enabled' )
);
},
'wcgateway.helper.vaulting-scope' => static function ( ContainerInterface $container ): bool {
try {
$token = $container->get( 'api.bearer' )->bearer();
return $token->vaulting_available();
} catch ( RuntimeException $exception ) {
return false;
}
},
'button.helper.vaulting-label' => static function ( ContainerInterface $container ): string {
$vaulting_label = __( 'Enable saved cards and subscription features on your store.', 'woocommerce-paypal-payments' );
if ( ! $container->get( 'wcgateway.helper.vaulting-scope' ) ) {
$vaulting_label .= sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( ' To use vaulting features, you must %1$senable vaulting on your account%2$s.', 'woocommerce-paypal-payments' ),
'<a
href="https://docs.woocommerce.com/document/woocommerce-paypal-payments/#enable-vaulting-on-your-live-account"
target="_blank"
>',
'</a>'
);
}
$vaulting_label .= '<p class="description">';
$vaulting_label .= __( 'This will disable all Pay Later messaging on your site.', 'woocommerce-paypal-payments' );
$vaulting_label .= '</p>';
return $vaulting_label;
},
'wcgateway.settings.fields.pay-later-label' => static function ( ContainerInterface $container ): string {
$pay_later_label = '<span class="ppcp-pay-later-enabled-label">%s</span>';
$pay_later_label .= '<span class="ppcp-pay-later-disabled-label">';
$pay_later_label .= __( "You have PayPal vaulting enabled, that's why Pay Later Messaging options are unavailable now. You cannot use both features at the same time.", 'woocommerce-paypal-payments' );
$pay_later_label .= '</span>';
return $pay_later_label;
},
);

View file

@ -9,7 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
@ -31,34 +30,24 @@ class SettingsPageAssets {
*/
private $version;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* Assets constructor.
*
* @param string $module_url The url of this module.
* @param string $version The assets version.
* @param Bearer $bearer The bearer.
*/
public function __construct( string $module_url, string $version, Bearer $bearer ) {
public function __construct( string $module_url, string $version ) {
$this->module_url = $module_url;
$this->version = $version;
$this->bearer = $bearer;
}
/**
* Register assets provided by this module.
*/
public function register_assets() {
$bearer = $this->bearer;
add_action(
'admin_enqueue_scripts',
function() use ( $bearer ) {
function() {
if ( ! is_admin() || wp_doing_ajax() ) {
return;
}
@ -67,7 +56,7 @@ class SettingsPageAssets {
return;
}
$this->register_admin_assets( $bearer );
$this->register_admin_assets();
}
);
@ -98,10 +87,8 @@ class SettingsPageAssets {
/**
* Register assets for admin pages.
*
* @param Bearer $bearer The bearer.
*/
private function register_admin_assets( Bearer $bearer ) {
private function register_admin_assets() {
wp_enqueue_script(
'ppcp-gateway-settings',
trailingslashit( $this->module_url ) . 'assets/js/gateway-settings.js',
@ -109,18 +96,5 @@ class SettingsPageAssets {
$this->version,
true
);
try {
$token = $bearer->bearer();
wp_localize_script(
'ppcp-gateway-settings',
'PayPalCommerceGatewaySettings',
array(
'vaulting_features_available' => $token->vaulting_available(),
)
);
} catch ( RuntimeException $exception ) {
return;
}
}
}

View file

@ -69,7 +69,7 @@ class DisableGateways {
unset( $methods[ CreditCardGateway::ID ] );
}
if ( $this->settings->has( 'button_enabled' ) && ! $this->settings->get( 'button_enabled' ) && ! $this->session_handler->order() ) {
if ( $this->settings->has( 'button_enabled' ) && ! $this->settings->get( 'button_enabled' ) && ! $this->session_handler->order() && is_checkout() ) {
unset( $methods[ PayPalGateway::ID ] );
}

View file

@ -33,12 +33,13 @@ class PayPalGateway extends \WC_Payment_Gateway {
use ProcessPaymentTrait;
const ID = 'ppcp-gateway';
const INTENT_META_KEY = '_ppcp_paypal_intent';
const ORDER_ID_META_KEY = '_ppcp_paypal_order_id';
const ORDER_PAYMENT_MODE_META_KEY = '_ppcp_paypal_payment_mode';
const ORDER_PAYMENT_SOURCE = '_ppcp_paypal_payment_source';
const FEES_META_KEY = '_ppcp_paypal_fees';
const ID = 'ppcp-gateway';
const INTENT_META_KEY = '_ppcp_paypal_intent';
const ORDER_ID_META_KEY = '_ppcp_paypal_order_id';
const ORDER_PAYMENT_MODE_META_KEY = '_ppcp_paypal_payment_mode';
const ORDER_PAYMENT_SOURCE_META_KEY = '_ppcp_paypal_payment_source';
const FEES_META_KEY = '_ppcp_paypal_fees';
const REFUNDS_META_KEY = '_ppcp_refunds';
/**
* The Settings Renderer.

View file

@ -39,7 +39,7 @@ trait OrderMetaTrait {
);
$payment_source = $this->get_payment_source( $order );
if ( $payment_source ) {
$wc_order->update_meta_data( PayPalGateway::ORDER_PAYMENT_SOURCE, $payment_source );
$wc_order->update_meta_data( PayPalGateway::ORDER_PAYMENT_SOURCE_META_KEY, $payment_source );
}
}

View file

@ -150,7 +150,8 @@ class OrderProcessor {
* @return bool
*/
public function process( \WC_Order $wc_order ): bool {
$order = $this->session_handler->order();
$order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
$order = $this->session_handler->order() ?? $this->order_endpoint->order( $order_id );
if ( ! $order ) {
$this->last_error = __( 'No PayPal order found in the current WooCommerce session.', 'woocommerce-paypal-payments' );
return false;

View file

@ -0,0 +1,47 @@
<?php
/**
* Operations with refund metadata.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Processor
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use WC_Order;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Trait RefundMetaTrait.
*/
trait RefundMetaTrait {
/**
* Adds a refund ID to the order metadata.
*
* @param WC_Order $wc_order The WC order to which metadata will be added.
* @param string $refund_id The refund ID to be added.
*/
protected function add_refund_to_meta( WC_Order $wc_order, string $refund_id ): void {
$refunds = $this->get_refunds_meta( $wc_order );
$refunds[] = $refund_id;
$wc_order->update_meta_data( PayPalGateway::REFUNDS_META_KEY, $refunds );
$wc_order->save();
}
/**
* Returns refund IDs from the order metadata.
*
* @param WC_Order $wc_order The WC order.
*
* @return string[]
*/
protected function get_refunds_meta( WC_Order $wc_order ): array {
$refunds = $wc_order->get_meta( PayPalGateway::REFUNDS_META_KEY );
if ( ! is_array( $refunds ) ) {
$refunds = array();
}
return $refunds;
}
}

View file

@ -26,6 +26,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
* Class RefundProcessor
*/
class RefundProcessor {
use RefundMetaTrait;
private const REFUND_MODE_REFUND = 'refund';
private const REFUND_MODE_VOID = 'void';
@ -113,8 +114,8 @@ class RefundProcessor {
throw new RuntimeException( 'No capture.' );
}
$capture = $captures[0];
$refund = new Refund(
$capture = $captures[0];
$refund = new Refund(
$capture,
$capture->invoice_id(),
$reason,
@ -122,7 +123,10 @@ class RefundProcessor {
new Money( $amount, $wc_order->get_currency() )
)
);
$this->payments_endpoint->refund( $refund );
$refund_id = $this->payments_endpoint->refund( $refund );
$this->add_refund_to_meta( $wc_order, $refund_id );
break;
case self::REFUND_MODE_VOID:
$voidable_authorizations = array_filter(

View file

@ -167,7 +167,7 @@ class SettingsListener {
* Prevent enabling both Pay Later messaging and PayPal vaulting
*/
public function listen_for_vaulting_enabled() {
if ( ! $this->is_valid_site_request() ) {
if ( ! $this->is_valid_site_request() || State::STATE_ONBOARDED !== $this->state->current_state() ) {
return;
}
@ -208,10 +208,6 @@ class SettingsListener {
$this->settings->set( 'message_product_enabled', false );
$this->settings->set( 'message_cart_enabled', false );
$this->settings->persist();
$redirect_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=ppcp-gateway' );
wp_safe_redirect( $redirect_url, 302 );
exit;
}
/**
@ -441,19 +437,10 @@ class SettingsListener {
*/
private function is_valid_site_request() : bool {
/**
* No nonce needed at this point.
*
* phpcs:disable WordPress.Security.NonceVerification.Missing
* phpcs:disable WordPress.Security.NonceVerification.Recommended
*/
if ( empty( $this->page_id ) ) {
return false;
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return false;
}

View file

@ -124,26 +124,6 @@ class SettingsRenderer {
$messages = array();
if ( $this->can_display_vaulting_admin_message() ) {
$vaulting_title = __( 'PayPal vaulting', 'woocommerce-paypal-payments' );
$pay_later_messages_title = __( 'Pay Later Messaging', 'woocommerce-paypal-payments' );
$enabled = $this->paypal_vaulting_is_enabled() ? $vaulting_title : $pay_later_messages_title;
$disabled = $this->settings_status->pay_later_messaging_is_enabled() ? $vaulting_title : $pay_later_messages_title;
$pay_later_messages_or_vaulting_text = sprintf(
// translators: %1$s and %2$s is translated PayPal vaulting and Pay Later Messaging strings.
__(
'You have %1$s enabled, that\'s why %2$s options are unavailable now. You cannot use both features at the same time.',
'woocommerce-paypal-payments'
),
$enabled,
$disabled
);
$messages[] = new Message( $pay_later_messages_or_vaulting_text, 'warning' );
}
//phpcs:disable WordPress.Security.NonceVerification.Recommended
//phpcs:disable WordPress.Security.NonceVerification.Missing
if ( ! isset( $_GET['ppcp-onboarding-error'] ) || ! empty( $_POST ) ) {

View file

@ -81,6 +81,48 @@ class WCGatewayModule implements ModuleInterface {
if ( $breakdown ) {
$wc_order->update_meta_data( PayPalGateway::FEES_META_KEY, $breakdown->to_array() );
$wc_order->save_meta_data();
$paypal_fee = $breakdown->paypal_fee();
if ( $paypal_fee ) {
update_post_meta( $wc_order->get_id(), 'PayPal Transaction Key', $paypal_fee->value() );
}
}
$fraud = $capture->fraud_processor_response();
if ( $fraud ) {
$fraud_responses = $fraud->to_array();
$avs_response_order_note_title = __( 'Address Verification Result', 'woocommerce-paypal-payments' );
/* translators: %1$s is AVS order note title, %2$s is AVS order note result markup */
$avs_response_order_note_format = __( '%1$s %2$s', 'woocommerce-paypal-payments' );
$avs_response_order_note_result_format = '<ul class="ppcp_avs_result">
<li>%1$s</li>
<ul class="ppcp_avs_result_inner">
<li>%2$s</li>
<li>%3$s</li>
</ul>
</ul>';
$avs_response_order_note_result = sprintf(
$avs_response_order_note_result_format,
/* translators: %s is fraud AVS code */
sprintf( __( 'AVS: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['avs_code'] ) ),
/* translators: %s is fraud AVS address match */
sprintf( __( 'Address Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['address_match'] ) ),
/* translators: %s is fraud AVS postal match */
sprintf( __( 'Postal Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['postal_match'] ) )
);
$avs_response_order_note = sprintf(
$avs_response_order_note_format,
esc_html( $avs_response_order_note_title ),
wp_kses_post( $avs_response_order_note_result )
);
$wc_order->add_order_note( $avs_response_order_note );
$cvv_response_order_note_format = '<ul class="ppcp_cvv_result"><li>%1$s</li></ul>';
$cvv_response_order_note = sprintf(
$cvv_response_order_note_format,
/* translators: %s is fraud CVV match */
sprintf( __( 'CVV2 Match: %s', 'woocommerce-paypal-payments' ), esc_html( $fraud_responses['cvv_match'] ) )
);
$wc_order->add_order_note( $cvv_response_order_note );
}
},
10,
@ -106,8 +148,7 @@ class WCGatewayModule implements ModuleInterface {
if ( $c->has( 'wcgateway.url' ) ) {
$assets = new SettingsPageAssets(
$c->get( 'wcgateway.url' ),
$c->get( 'ppcp.asset-version' ),
$c->get( 'api.bearer' )
$c->get( 'ppcp.asset-version' )
);
$assets->register_assets();
}

View file

@ -10,14 +10,17 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Handler;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WP_REST_Request;
use WP_REST_Response;
/**
* Class PaymentCaptureRefunded
*/
class PaymentCaptureRefunded implements RequestHandler {
use PrefixTrait, TransactionIdHandlingTrait;
use PrefixTrait, TransactionIdHandlingTrait, RefundMetaTrait;
/**
* The logger.
@ -49,25 +52,26 @@ class PaymentCaptureRefunded implements RequestHandler {
/**
* Whether a handler is responsible for a given request or not.
*
* @param \WP_REST_Request $request The request.
* @param WP_REST_Request $request The request.
*
* @return bool
*/
public function responsible_for_request( \WP_REST_Request $request ): bool {
public function responsible_for_request( WP_REST_Request $request ): bool {
return in_array( $request['event_type'], $this->event_types(), true );
}
/**
* Responsible for handling the request.
*
* @param \WP_REST_Request $request The request.
* @param WP_REST_Request $request The request.
*
* @return \WP_REST_Response
* @return WP_REST_Response
*/
public function handle_request( \WP_REST_Request $request ): \WP_REST_Response {
$response = array( 'success' => false );
$order_id = isset( $request['resource']['custom_id'] ) ?
public function handle_request( WP_REST_Request $request ): WP_REST_Response {
$response = array( 'success' => false );
$order_id = isset( $request['resource']['custom_id'] ) ?
$this->sanitize_custom_id( $request['resource']['custom_id'] ) : 0;
$refund_id = (string) ( $request['resource']['id'] ?? '' );
if ( ! $order_id ) {
$message = sprintf(
// translators: %s is the PayPal webhook Id.
@ -85,7 +89,7 @@ class PaymentCaptureRefunded implements RequestHandler {
)
);
$response['message'] = $message;
return rest_ensure_response( $response );
return new WP_REST_Response( $response );
}
$wc_order = wc_get_order( $order_id );
@ -93,7 +97,7 @@ class PaymentCaptureRefunded implements RequestHandler {
$message = sprintf(
// translators: %s is the PayPal refund Id.
__( 'Order for PayPal refund %s not found.', 'woocommerce-paypal-payments' ),
isset( $request['resource']['id'] ) ? $request['resource']['id'] : ''
$refund_id
);
$this->logger->log(
'warning',
@ -103,7 +107,13 @@ class PaymentCaptureRefunded implements RequestHandler {
)
);
$response['message'] = $message;
return rest_ensure_response( $response );
return new WP_REST_Response( $response );
}
$already_added_refunds = $this->get_refunds_meta( $wc_order );
if ( in_array( $refund_id, $already_added_refunds, true ) ) {
$this->logger->info( "Refund {$refund_id} is already handled." );
return new WP_REST_Response( $response );
}
/**
@ -132,7 +142,7 @@ class PaymentCaptureRefunded implements RequestHandler {
);
$response['message'] = $refund->get_error_message();
return rest_ensure_response( $response );
return new WP_REST_Response( $response );
}
$this->logger->log(
@ -152,11 +162,12 @@ class PaymentCaptureRefunded implements RequestHandler {
)
);
if ( is_array( $request['resource'] ) && isset( $request['resource']['id'] ) ) {
$this->update_transaction_id( $request['resource']['id'], $wc_order, $this->logger );
if ( $refund_id ) {
$this->update_transaction_id( $refund_id, $wc_order, $this->logger );
$this->add_refund_to_meta( $wc_order, $refund_id );
}
$response['success'] = true;
return rest_ensure_response( $response );
return new WP_REST_Response( $response );
}
}

View file

@ -18,27 +18,28 @@
"build:modules": "yarn run build:modules:ppcp-button && yarn build:modules:ppcp-wc-gateway && yarn build:modules:ppcp-webhooks && yarn build:modules:ppcp-vaulting",
"build:dev": "yarn run install:modules && yarn run build:modules",
"docker:build": "docker-compose build",
"docker:start": "docker-compose up -d wp_dev",
"docker:stop": "docker-compose down",
"docker:destroy": "docker-compose down -v",
"docker:logs": "docker-compose logs",
"docker:shell": "docker-compose run --rm wp_dev bash",
"docker:install": "docker-compose run --rm composer composer install && yarn run docker:build-js",
"docker:build-js": "docker-compose run --rm build yarn run build:dev",
"docker:composer-update": "docker-compose run --rm composer composer update && docker-compose run --rm composer composer update --lock",
"docker:test": "docker-compose run --rm test vendor/bin/phpunit",
"docker:lint": "docker-compose run --rm test sh -c 'vendor/bin/phpcs --parallel=8 -s && vendor/bin/psalm --show-info=false --threads=8 --diff'",
"docker:fix-lint": "docker-compose run --rm test vendor/bin/phpcbf",
"docker:build-package": "docker-compose run --rm build yarn run build",
"ddev:setup": "ddev start && ddev orchestrate",
"ddev:start": "ddev start",
"ddev:stop": "ddev stop",
"ddev:reset": "ddev orchestrate -f",
"ddev:install": "ddev composer install && yarn run ddev:build-js",
"ddev:build-js": "ddev yarn build:dev",
"ddev:composer-update": "ddev composer update && ddev composer update --lock",
"ddev:test": "ddev exec phpunit",
"ddev:lint": "yarn ddev:phpcs && yarn ddev:psalm",
"ddev:phpcs": "ddev exec phpcs --parallel=8 -s",
"ddev:psalm": "ddev exec psalm --show-info=false --threads=8 --diff",
"ddev:fix-lint": "ddev exec phpcbf",
"ddev:xdebug-on": "ddev xdebug",
"ddev:xdebug-off": "ddev xdebug",
"ddev:build-package": "ddev yarn build",
"prebuild": "rm -rf ./vendor && find . -name 'node_modules' -type d -maxdepth 3 -exec rm -rf {} +",
"build": "composer install --no-dev && yarn run build:dev && yarn run archive",
"prearchive": "rm -rf $npm_package_name.zip",
"archive": "zip -r $npm_package_name.zip . -x **.git/\\* **node_modules/\\*",
"postarchive": "yarn run archive:cleanup && rm -rf $npm_package_name && unzip $npm_package_name.zip -d $npm_package_name && rm $npm_package_name.zip && zip -r $npm_package_name.zip $npm_package_name && rm -rf $npm_package_name",
"archive:cleanup": "zip -d $npm_package_name.zip .env* docker/\\* docker-compose.yml .editorconfig tests/\\* .github/\\* .psalm/\\* wordpress_org_assets/\\* \\*.DS_Store README.md .gitattributes .gitignore .travis.yml composer.json composer.lock package.json package-lock.json patchwork.json yarn.lock phpunit.xml.dist .phpunit.result.cache phpcs.xml.dist modules/ppcp-button/.babelrc modules/ppcp-button/package.json modules/ppcp-button/webpack.config.js modules/ppcp-button/yarn.lock vendor/\\*/.idea/\\* vendor/\\*/.gitignore vendor/\\*/.gitattributes vendor/\\*/.travis.yml"
"archive:cleanup": "zip -d $npm_package_name.zip .env* .ddev/\\* .editorconfig tests/\\* .github/\\* .psalm/\\* wordpress_org_assets/\\* \\*.DS_Store README.md .gitattributes .gitignore .travis.yml composer.json composer.lock package.json package-lock.json patchwork.json yarn.lock phpunit.xml.dist .phpunit.result.cache phpcs.xml.dist modules/ppcp-button/.babelrc modules/ppcp-button/package.json modules/ppcp-button/webpack.config.js modules/ppcp-button/yarn.lock vendor/\\*/.idea/\\* vendor/\\*/.gitignore vendor/\\*/.gitattributes vendor/\\*/.travis.yml"
},
"config": {
"wp_org_slug": "woocommerce-paypal-payments"

View file

@ -151,5 +151,11 @@
<MixedStringOffsetAssignment errorLevel="info"/>
<ParamNameMismatch errorLevel="info"/>
<RedundantCastGivenDocblockType errorLevel="info"/>
<TooManyArguments>
<errorLevel type="suppress">
<referencedFunction name="apply_filters" />
</errorLevel>
</TooManyArguments>
</issueHandlers>
</psalm>

View file

@ -300,7 +300,7 @@ class ItemFactoryTest extends TestCase
* @var Item $item
*/
$this->assertEquals(mb_substr($name, 0, 127), $item->name());
$this->assertEquals(mb_substr($description, 0, 127), $item->description());
$this->assertEquals(substr($description, 0, 127), $item->description());
}
public function testFromPayPalResponse()

View file

@ -6,6 +6,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Address;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payee;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
@ -21,7 +22,19 @@ class PurchaseUnitFactoryTest extends TestCase
private $wcOrderId = 1;
private $wcOrderNumber = '100000';
public function testWcOrderDefault()
private $item;
public function setUp(): void
{
parent::setUp();
$this->item = Mockery::mock(Item::class, [
'category' => Item::PHYSICAL_GOODS,
'unit_amount' => new Money(42.5, 'USD'),
]);
}
public function testWcOrderDefault()
{
$wcOrder = Mockery::mock(\WC_Order::class);
$wcOrder->expects('get_order_number')->andReturn($this->wcOrderNumber);
@ -37,13 +50,11 @@ class PurchaseUnitFactoryTest extends TestCase
$payee = Mockery::mock(Payee::class);
$payeeRepository
->shouldReceive('payee')->andReturn($payee);
$item = Mockery::mock(Item::class);
$item->shouldReceive('category')->andReturn(Item::PHYSICAL_GOODS);
$itemFactory = Mockery::mock(ItemFactory::class);
$itemFactory
->shouldReceive('from_wc_order')
->with($wcOrder)
->andReturn([$item]);
->andReturn([$this->item]);
$address = Mockery::mock(Address::class);
$address
@ -79,11 +90,67 @@ class PurchaseUnitFactoryTest extends TestCase
$this->assertEquals($this->wcOrderId, $unit->custom_id());
$this->assertEquals('', $unit->soft_descriptor());
$this->assertEquals('WC-' . $this->wcOrderNumber, $unit->invoice_id());
$this->assertEquals([$item], $unit->items());
$this->assertEquals([$this->item], $unit->items());
$this->assertEquals($amount, $unit->amount());
$this->assertEquals($shipping, $unit->shipping());
}
public function testWcOrderWithNegativeFees()
{
$wcOrder = Mockery::mock(\WC_Order::class);
$wcOrder->expects('get_order_number')->andReturn($this->wcOrderNumber);
$wcOrder->expects('get_id')->andReturn($this->wcOrderId);
$amount = Mockery::mock(Amount::class);
$amountFactory = Mockery::mock(AmountFactory::class);
$amountFactory
->shouldReceive('from_wc_order')
->with($wcOrder)
->andReturn($amount);
$payeeFactory = Mockery::mock(PayeeFactory::class);
$payeeRepository = Mockery::mock(PayeeRepository::class);
$payee = Mockery::mock(Payee::class);
$payeeRepository
->shouldReceive('payee')->andReturn($payee);
$fee = Mockery::mock(Item::class, [
'category' => Item::DIGITAL_GOODS,
'unit_amount' => new Money(10.0, 'USD'),
]);
$discount = Mockery::mock(Item::class, [
'unit_amount' => new Money(-5, 'USD'),
]);
$itemFactory = Mockery::mock(ItemFactory::class);
$itemFactory
->shouldReceive('from_wc_order')
->with($wcOrder)
->andReturn([$this->item, $fee, $discount]);
$address = Mockery::mock(Address::class);
$address->shouldReceive('country_code')->andReturn('DE');
$address->shouldReceive('postal_code')->andReturn('12345');
$shipping = Mockery::mock(Shipping::class);
$shipping->shouldReceive('address')->andReturn($address);
$shippingFactory = Mockery::mock(ShippingFactory::class);
$shippingFactory
->shouldReceive('from_wc_order')
->with($wcOrder)
->andReturn($shipping);
$paymentsFacory = Mockery::mock(PaymentsFactory::class);
$testee = new PurchaseUnitFactory(
$amountFactory,
$payeeRepository,
$payeeFactory,
$itemFactory,
$shippingFactory,
$paymentsFacory
);
$unit = $testee->from_wc_order($wcOrder);
$this->assertTrue(is_a($unit, PurchaseUnit::class));
$this->assertEquals([$this->item, $fee], $unit->items());
}
public function testWcOrderShippingGetsDroppedWhenNoPostalCode()
{
$wcOrder = Mockery::mock(\WC_Order::class);
@ -100,12 +167,11 @@ class PurchaseUnitFactoryTest extends TestCase
$payee = Mockery::mock(Payee::class);
$payeeRepository
->expects('payee')->andReturn($payee);
$item = Mockery::mock(Item::class, ['category' => Item::PHYSICAL_GOODS]);
$itemFactory = Mockery::mock(ItemFactory::class);
$itemFactory
->expects('from_wc_order')
->with($wcOrder)
->andReturn([$item]);
->andReturn([$this->item]);
$address = Mockery::mock(Address::class);
$address
@ -155,12 +221,11 @@ class PurchaseUnitFactoryTest extends TestCase
$payee = Mockery::mock(Payee::class);
$payeeRepository
->expects('payee')->andReturn($payee);
$item = Mockery::mock(Item::class, ['category' => Item::PHYSICAL_GOODS]);
$itemFactory = Mockery::mock(ItemFactory::class);
$itemFactory
->expects('from_wc_order')
->with($wcOrder)
->andReturn([$item]);
->andReturn([$this->item]);
$address = Mockery::mock(Address::class);
$address
@ -208,13 +273,11 @@ class PurchaseUnitFactoryTest extends TestCase
$payeeRepository
->expects('payee')->andReturn($payee);
$item = Mockery::mock(Item::class);
$item->shouldReceive('category')->andReturn(Item::PHYSICAL_GOODS);
$itemFactory = Mockery::mock(ItemFactory::class);
$itemFactory
->expects('from_wc_cart')
->with($wcCart)
->andReturn([$item]);
->andReturn([$this->item]);
$address = Mockery::mock(Address::class);
$address
@ -251,7 +314,7 @@ class PurchaseUnitFactoryTest extends TestCase
$this->assertEquals('', $unit->custom_id());
$this->assertEquals('', $unit->soft_descriptor());
$this->assertEquals('', $unit->invoice_id());
$this->assertEquals([$item], $unit->items());
$this->assertEquals([$this->item], $unit->items());
$this->assertEquals($amount, $unit->amount());
$this->assertEquals($shipping, $unit->shipping());
}
@ -273,13 +336,12 @@ class PurchaseUnitFactoryTest extends TestCase
$payee = Mockery::mock(Payee::class);
$payeeRepository
->expects('payee')->andReturn($payee);
$item = Mockery::mock(Item::class);
$item->shouldReceive('category')->andReturn(Item::PHYSICAL_GOODS);
$itemFactory = Mockery::mock(ItemFactory::class);
$itemFactory
->expects('from_wc_cart')
->with($wcCart)
->andReturn([$item]);
->andReturn([$this->item]);
$shippingFactory = Mockery::mock(ShippingFactory::class);
$paymentsFacory = Mockery::mock(PaymentsFactory::class);
$testee = new PurchaseUnitFactory(
@ -312,12 +374,11 @@ class PurchaseUnitFactoryTest extends TestCase
$payee = Mockery::mock(Payee::class);
$payeeRepository
->expects('payee')->andReturn($payee);
$item = Mockery::mock(Item::class, ['category' => Item::PHYSICAL_GOODS]);
$itemFactory = Mockery::mock(ItemFactory::class);
$itemFactory
->expects('from_wc_cart')
->with($wcCart)
->andReturn([$item]);
->andReturn([$this->item]);
$address = Mockery::mock(Address::class);
$address
@ -359,8 +420,7 @@ class PurchaseUnitFactoryTest extends TestCase
$payeeFactory->expects('from_paypal_response')->with($rawPayee)->andReturn($payee);
$payeeRepository = Mockery::mock(PayeeRepository::class);
$itemFactory = Mockery::mock(ItemFactory::class);
$item = Mockery::mock(Item::class, ['category' => Item::PHYSICAL_GOODS]);
$itemFactory->expects('from_paypal_response')->with($rawItem)->andReturn($item);
$itemFactory->expects('from_paypal_response')->with($rawItem)->andReturn($this->item);
$shippingFactory = Mockery::mock(ShippingFactory::class);
$shipping = Mockery::mock(Shipping::class);
$shippingFactory->expects('from_paypal_response')->with($rawShipping)->andReturn($shipping);
@ -394,7 +454,7 @@ class PurchaseUnitFactoryTest extends TestCase
$this->assertEquals('customId', $unit->custom_id());
$this->assertEquals('softDescriptor', $unit->soft_descriptor());
$this->assertEquals('invoiceId', $unit->invoice_id());
$this->assertEquals([$item], $unit->items());
$this->assertEquals([$this->item], $unit->items());
$this->assertEquals($amount, $unit->amount());
$this->assertEquals($shipping, $unit->shipping());
}
@ -411,8 +471,7 @@ class PurchaseUnitFactoryTest extends TestCase
$payeeFactory = Mockery::mock(PayeeFactory::class);
$payeeRepository = Mockery::mock(PayeeRepository::class);
$itemFactory = Mockery::mock(ItemFactory::class);
$item = Mockery::mock(Item::class, ['category' => Item::PHYSICAL_GOODS]);
$itemFactory->expects('from_paypal_response')->with($rawItem)->andReturn($item);
$itemFactory->expects('from_paypal_response')->with($rawItem)->andReturn($this->item);
$shippingFactory = Mockery::mock(ShippingFactory::class);
$shipping = Mockery::mock(Shipping::class);
$shippingFactory->expects('from_paypal_response')->with($rawShipping)->andReturn($shipping);
@ -454,8 +513,7 @@ class PurchaseUnitFactoryTest extends TestCase
$payeeFactory->expects('from_paypal_response')->with($rawPayee)->andReturn($payee);
$payeeRepository = Mockery::mock(PayeeRepository::class);
$itemFactory = Mockery::mock(ItemFactory::class);
$item = Mockery::mock(Item::class, ['category' => Item::PHYSICAL_GOODS]);
$itemFactory->expects('from_paypal_response')->with($rawItem)->andReturn($item);
$itemFactory->expects('from_paypal_response')->with($rawItem)->andReturn($this->item);
$shippingFactory = Mockery::mock(ShippingFactory::class);
$paymentsFacory = Mockery::mock(PaymentsFactory::class);
$testee = new PurchaseUnitFactory(

View file

@ -13,9 +13,8 @@ class SettingsPagesAssetsTest extends TestCase
{
$moduleUrl = 'http://example.com/wp-content/plugins/woocommerce-paypal-payments/modules/ppcp-wc-gateway';
$modulePath = '/var/www/html/wp-content/plugins/woocommerce-paypal-payments/modules/ppcp-wc-gateway';
$bearer = Mockery::mock(Bearer::class);
$testee = new SettingsPageAssets($moduleUrl, $modulePath, $bearer);
$testee = new SettingsPageAssets($moduleUrl, $modulePath);
when('is_admin')
->justReturn(true);

View file

@ -89,6 +89,11 @@ class OrderProcessorTest extends TestCase
->shouldReceive('payment_source')
->andReturn(null);
$wcOrder
->shouldReceive('get_meta')
->with(PayPalGateway::ORDER_ID_META_KEY)
->andReturn(1);
$sessionHandler = Mockery::mock(SessionHandler::class);
$sessionHandler
->expects('order')
@ -208,6 +213,12 @@ class OrderProcessorTest extends TestCase
$currentOrder
->shouldReceive('payment_source')
->andReturn(null);
$wcOrder
->shouldReceive('get_meta')
->with(PayPalGateway::ORDER_ID_META_KEY)
->andReturn(1);
$sessionHandler = Mockery::mock(SessionHandler::class);
$sessionHandler
->expects('order')
@ -313,6 +324,12 @@ class OrderProcessorTest extends TestCase
$currentOrder
->shouldReceive('purchase_units')
->andReturn([$purchaseUnit]);
$wcOrder
->shouldReceive('get_meta')
->with(PayPalGateway::ORDER_ID_META_KEY)
->andReturn(1);
$sessionHandler = Mockery::mock(SessionHandler::class);
$sessionHandler
->expects('order')