Merge branch 'trunk' into PCP-3742-disable-gpay-sub

This commit is contained in:
Alex P. 2024-10-22 16:59:16 +03:00
commit ad07c05cb7
No known key found for this signature in database
GPG key ID: 54487A734A204D71
37 changed files with 11765 additions and 301 deletions

View file

@ -90,5 +90,12 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )();
}
if ( apply_filters(
'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled',
getenv( 'PCP_SETTINGS_ENABLED' ) === '1'
) ) {
$modules[] = ( require "$modules_dir/ppcp-settings/module.php" )();
}
return $modules;
};

View file

@ -67,7 +67,7 @@ class BillingSubscriptions {
*/
public function suspend( string $id ):void {
$data = array(
'reason' => 'Suspended by customer',
'reason' => sprintf( 'Suspended by %s.', is_admin() ? 'merchant' : 'customer' ),
);
$bearer = $this->bearer->bearer();
@ -107,7 +107,7 @@ class BillingSubscriptions {
*/
public function activate( string $id ): void {
$data = array(
'reason' => 'Reactivated by customer',
'reason' => sprintf( 'Reactivated by %s.', is_admin() ? 'merchant' : 'customer' ),
);
$bearer = $this->bearer->bearer();
@ -148,7 +148,7 @@ class BillingSubscriptions {
*/
public function cancel( string $id ): void {
$data = array(
'reason' => 'Cancelled by customer',
'reason' => sprintf( 'Cancelled by %s.', is_admin() ? 'merchant' : 'customer' ),
);
$bearer = $this->bearer->bearer();

View file

@ -19,11 +19,9 @@ export function CardFields( {
config,
eventRegistration,
emitResponse,
components,
} ) {
const { onPaymentSetup } = eventRegistration;
const { responseTypes } = emitResponse;
const { PaymentMethodIcons } = components;
const [ cardFieldsForm, setCardFieldsForm ] = useState();
const getCardFieldsForm = ( cardFieldsForm ) => {
@ -95,10 +93,6 @@ export function CardFields( {
} }
>
<PayPalCardFieldsForm />
<PaymentMethodIcons
icons={ config.card_icons }
align="left"
/>
<CheckoutHandler
getCardFieldsForm={ getCardFieldsForm }
getSavePayment={ getSavePayment }

View file

@ -1,19 +1,30 @@
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
import { CardFields } from './Components/card-fields';
import {registerPaymentMethod} from '@woocommerce/blocks-registry';
import {CardFields} from './Components/card-fields';
const config = wc.wcSettings.getSetting( 'ppcp-credit-card-gateway_data' );
const config = wc.wcSettings.getSetting('ppcp-credit-card-gateway_data');
registerPaymentMethod( {
name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <CardFields config={ config } />,
edit: <CardFields config={ config } />,
ariaLabel: config.title,
canMakePayment: () => {
return true;
},
supports: {
showSavedCards: true,
features: config.supports,
},
} );
const Label = ({components, config}) => {
const {PaymentMethodIcons} = components;
return <>
<span dangerouslySetInnerHTML={{__html: config.title}}/>
<PaymentMethodIcons
icons={ config.card_icons }
align="right"
/>
</>
}
registerPaymentMethod({
name: config.id,
label: <Label config={config}/>,
content: <CardFields config={config}/>,
edit: <CardFields config={config}/>,
ariaLabel: config.title,
canMakePayment: () => {
return true;
},
supports: {
showSavedCards: true,
features: config.supports,
},
});

View file

@ -233,6 +233,15 @@ class SingleProductBootstap {
this.form(),
this.errorHandler
);
if (
! this.gateway.vaultingEnabled &&
[ 'subscription', 'variable-subscription' ].includes(
this.gateway.productType
) &&
this.gateway.manualRenewalEnabled !== '1'
) {
return;
}
if (
PayPalCommerceGateway.data_client_id.has_subscriptions &&

View file

@ -1321,8 +1321,26 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
'should_handle_shipping_in_paypal' => $this->should_handle_shipping_in_paypal && ! $this->is_checkout(),
'needShipping' => $this->need_shipping(),
'vaultingEnabled' => $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ),
'productType' => null,
'manualRenewalEnabled' => false,
);
if ( is_product() ) {
$product = wc_get_product( get_the_ID() );
if ( is_a( $product, \WC_Product::class ) ) {
$localize['productType'] = $product->get_type();
}
}
if ( class_exists( '\WCS_Manual_Renewal_Manager' ) ) {
/**
* We verify the existence of the class prior to invoking a static method.
*
* @psalm-suppress UndefinedClass
*/
$localize['manualRenewalEnabled'] = \WCS_Manual_Renewal_Manager::is_manual_renewal_enabled();
}
if ( 'pay-now' === $this->context() ) {
$localize['pay_now'] = $this->pay_now_script_data();
}

View file

@ -124,7 +124,7 @@ class MetaBoxRenderer {
<label for="ppcp-tracking-items"><?php echo esc_html__( 'Select items for this shipment', 'woocommerce-paypal-payments' ); ?></label>
<select multiple class="wc-enhanced-select ppcp-tracking-items" id="ppcp-tracking-items" name="ppcp-tracking[items]">
<?php foreach ( $order_items as $item ) : ?>
<option value="<?php echo intval( $item->get_id() ); ?>"><?php echo esc_html( $item->get_name() ); ?></option>
<option value="<?php echo intval( $item->get_id() ); ?>"><?php echo esc_attr( wp_strip_all_tags( $item->get_name() ) ); ?></option>
<?php endforeach; ?>
</select>
</div>

View file

@ -40,4 +40,13 @@ return array(
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'paypal-subscriptions.renewal-handler' => static function ( ContainerInterface $container ): RenewalHandler {
return new RenewalHandler( $container->get( 'woocommerce.logger.woocommerce' ) );
},
'paypal-subscriptions.status' => static function ( ContainerInterface $container ): SubscriptionStatus {
return new SubscriptionStatus(
$container->get( 'api.endpoint.billing-subscriptions' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -187,6 +187,9 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
30
);
/**
* Executed when updating WC Subscription.
*/
add_action(
'woocommerce_process_shop_subscription_meta',
/**
@ -194,65 +197,41 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
*
* @psalm-suppress MissingClosureParamType
*/
function( $id, $post ) use ( $c ) {
function( $id ) use ( $c ) {
$subscription = wcs_get_subscription( $id );
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
if ( $subscription === false ) {
return;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( ! $subscription_id ) {
return;
}
$subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' );
assert( $subscriptions_endpoint instanceof BillingSubscriptions );
if ( $subscription->get_status() === 'cancelled' ) {
try {
$subscriptions_endpoint->cancel( $subscription_id );
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$subscription_status = $c->get( 'paypal-subscriptions.status' );
assert( $subscription_status instanceof SubscriptionStatus );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not cancel subscription product on PayPal. ' . $error );
}
}
if ( $subscription->get_status() === 'pending-cancel' ) {
try {
$subscriptions_endpoint->suspend( $subscription_id );
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not suspend subscription product on PayPal. ' . $error );
}
}
if ( $subscription->get_status() === 'active' ) {
try {
$current_subscription = $subscriptions_endpoint->subscription( $subscription_id );
if ( $current_subscription->status === 'SUSPENDED' ) {
$subscriptions_endpoint->activate( $subscription_id );
}
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not reactivate subscription product on PayPal. ' . $error );
}
}
$subscription_status->update_status( $subscription->get_status(), $subscription_id );
},
20,
2
20
);
/**
* Update subscription status from WC Subscriptions list page action link.
*/
add_action(
'woocommerce_subscription_status_updated',
function( WC_Subscription $subscription ) use ( $c ) {
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( ! $subscription_id ) {
return;
}
$subscription_status = $c->get( 'paypal-subscriptions.status' );
assert( $subscription_status instanceof SubscriptionStatus );
$subscription_status->update_status( $subscription->get_status(), $subscription_id );
}
);
add_filter(

View file

@ -0,0 +1,107 @@
<?php
/**
* Subscriptions renewal handler.
*
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use Psr\Log\LoggerInterface;
use WC_Data_Exception;
use WC_Order;
use WC_Subscription;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
/**
* Class RenewalHandler
*/
class RenewalHandler {
use TransactionIdHandlingTrait;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* RenewalHandler constructor.
*
* @param LoggerInterface $logger The logger.
*/
public function __construct( LoggerInterface $logger ) {
$this->logger = $logger;
}
/**
* Process subscription renewal.
*
* @param WC_Subscription[] $subscriptions WC Subscriptions.
* @param string $transaction_id PayPal transaction ID.
* @return void
* @throws WC_Data_Exception If something goes wrong while setting payment method.
*/
public function process( array $subscriptions, string $transaction_id ): void {
foreach ( $subscriptions as $subscription ) {
if ( $this->is_for_renewal_order( $subscription ) ) {
$renewal_order = wcs_create_renewal_order( $subscription );
if ( is_a( $renewal_order, WC_Order::class ) ) {
$this->logger->info(
sprintf(
'Processing renewal order #%s for subscription #%s',
$renewal_order->get_id(),
$subscription->get_id()
)
);
$renewal_order->set_payment_method( $subscription->get_payment_method() );
$renewal_order->payment_complete();
$this->update_transaction_id( $transaction_id, $renewal_order, $this->logger );
break;
}
}
$parent_order = wc_get_order( $subscription->get_parent() );
if ( is_a( $parent_order, WC_Order::class ) ) {
$this->logger->info(
sprintf(
'Processing parent order #%s for subscription #%s',
$parent_order->get_id(),
$subscription->get_id()
)
);
$subscription->update_meta_data( '_ppcp_is_subscription_renewal', 'true' );
$subscription->save_meta_data();
$this->update_transaction_id( $transaction_id, $parent_order, $this->logger );
}
}
}
/**
* Checks whether subscription order is for renewal or not.
*
* @param WC_Subscription $subscription WC Subscription.
* @return bool
*/
private function is_for_renewal_order( WC_Subscription $subscription ): bool {
$subscription_renewal_meta = $subscription->get_meta( '_ppcp_is_subscription_renewal' ) ?? '';
if ( $subscription_renewal_meta === 'true' ) {
return true;
}
if (
time() >= $subscription->get_time( 'start' )
&& ( time() - $subscription->get_time( 'start' ) ) <= ( 8 * HOUR_IN_SECONDS )
) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,144 @@
<?php
/**
* Handles PayPal subscription status.
*
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class SubscriptionStatus
*/
class SubscriptionStatus {
/**
* Billing subscriptions endpoint.
*
* @var BillingSubscriptions
*/
private $subscriptions_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* SubscriptionStatus constructor.
*
* @param BillingSubscriptions $subscriptions_endpoint Billing subscriptions endpoint.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
BillingSubscriptions $subscriptions_endpoint,
LoggerInterface $logger
) {
$this->subscriptions_endpoint = $subscriptions_endpoint;
$this->logger = $logger;
}
/**
* Updates PayPal subscription status from the given WC Subscription status.
*
* @param string $subscription_status The WC Subscription status.
* @param string $subscription_id The PayPal Subscription ID.
* @return void
*/
public function update_status( string $subscription_status, string $subscription_id ): void {
if ( $subscription_status === 'pending-cancel' || $subscription_status === 'cancelled' ) {
try {
$current_subscription = $this->subscriptions_endpoint->subscription( $subscription_id );
if ( $current_subscription->status === 'CANCELLED' ) {
return;
}
$this->logger->info(
sprintf(
'Canceling PayPal subscription #%s.',
$subscription_id
)
);
$this->subscriptions_endpoint->cancel( $subscription_id );
} catch ( RuntimeException $exception ) {
$this->logger->error(
sprintf(
'Could not cancel PayPal subscription #%s. %s',
$subscription_id,
$this->get_error( $exception )
)
);
}
}
if ( $subscription_status === 'on-hold' ) {
try {
$this->logger->info(
sprintf(
'Suspending PayPal subscription #%s.',
$subscription_id
)
);
$this->subscriptions_endpoint->suspend( $subscription_id );
} catch ( RuntimeException $exception ) {
$this->logger->error(
sprintf(
'Could not suspend PayPal subscription #%s. %s',
$subscription_id,
$this->get_error( $exception )
)
);
}
}
if ( $subscription_status === 'active' ) {
try {
$current_subscription = $this->subscriptions_endpoint->subscription( $subscription_id );
if ( $current_subscription->status === 'SUSPENDED' ) {
$this->logger->info(
sprintf(
'Activating suspended PayPal subscription #%s.',
$subscription_id
)
);
$this->subscriptions_endpoint->activate( $subscription_id );
}
} catch ( RuntimeException $exception ) {
$this->logger->error(
sprintf(
'Could not reactivate PayPal subscription #%s. %s',
$subscription_id,
$this->get_error( $exception )
)
);
}
}
}
/**
* Get error from exception.
*
* @param RuntimeException $exception The exception.
* @return string
*/
private function get_error( RuntimeException $exception ): string {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
return $error;
}
}

View file

@ -0,0 +1,16 @@
{
"name": "woocommerce/ppcp-settings",
"type": "inpsyde-module",
"description": "Settings module",
"license": "GPL-2.0",
"require": {
"php": "^7.4 | ^8.0"
},
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\Settings\\": "src"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,14 @@
<?php
/**
* The Settings module.
*
* @package WooCommerce\PayPalCommerce\Settings
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings;
return static function () : SettingsModule {
return new SettingsModule();
};

View file

@ -0,0 +1,12 @@
{
"name": "ppcp-settings",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"scripts": {
"watch": "wp-scripts start --webpack-src-dir=resources/js --output-path=assets",
"build": "wp-scripts build --webpack-src-dir=resources/js --output-path=assets"
},
"devDependencies": {
"@wordpress/scripts": "^30.3.0"
}
}

View file

@ -0,0 +1 @@
.red {color:red;}

View file

@ -0,0 +1,3 @@
export function App() {
return <div className="red">App</div>;
}

View file

@ -0,0 +1,7 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
createRoot( document.getElementById( 'ppcp-settings-container' ) ).render(
<App />
);

View file

@ -0,0 +1,26 @@
<?php
/**
* The Settings module services.
*
* @package WooCommerce\PayPalCommerce\Settings
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'settings.url' => static function ( ContainerInterface $container ) : string {
/**
* The path cannot be false.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-settings/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
);

View file

@ -0,0 +1,95 @@
<?php
/**
* The Settings module.
*
* @package WooCommerce\PayPalCommerce\AxoBlock
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Settings;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class SettingsModule
*/
class SettingsModule implements ServiceModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $container ): bool {
add_action(
'admin_enqueue_scripts',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
static function( $hook_suffix ) use ( $container ) {
if ( 'woocommerce_page_wc-settings' !== $hook_suffix ) {
return;
}
/**
* Require resolves.
*
* @psalm-suppress UnresolvableInclude
*/
$script_asset_file = require dirname( realpath( __FILE__ ) ?: '', 2 ) . '/assets/index.asset.php';
$module_url = $container->get( 'settings.url' );
wp_register_script(
'ppcp-admin-settings',
$module_url . '/assets/index.js',
$script_asset_file['dependencies'],
$script_asset_file['version'],
true
);
wp_enqueue_script( 'ppcp-admin-settings' );
/**
* Require resolves.
*
* @psalm-suppress UnresolvableInclude
*/
$style_asset_file = require dirname( realpath( __FILE__ ) ?: '', 2 ) . '/assets/style.asset.php';
wp_register_style(
'ppcp-admin-settings',
$module_url . '/assets/style-style.css',
$style_asset_file['dependencies'],
$style_asset_file['version']
);
wp_enqueue_style( 'ppcp-admin-settings' );
}
);
add_action(
'woocommerce_paypal_payments_gateway_admin_options_wrapper',
static function(): void {
global $hide_save_button;
$hide_save_button = true;
echo '<div id="ppcp-settings-container"></div>';
}
);
return true;
}
}

View file

@ -0,0 +1,12 @@
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
const path = require( 'path' );
module.exports = {
...defaultConfig,
...{
entry: {
index: path.resolve( process.cwd(), 'resources/js', 'index.js' ),
style: path.resolve( process.cwd(), 'resources/css', 'style.scss' ),
},
},
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
import {
hide,
show,
} from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
document.addEventListener( 'DOMContentLoaded', function () {
const refundButton = document.querySelector( 'button.refund-items' );
if ( ! refundButton ) {
return;
}
refundButton.insertAdjacentHTML(
'afterend',
`<button class="button" type="button" id="pcpVoid">${ PcpVoidButton.button_text }</button>`
);
hide( refundButton );
const voidButton = document.querySelector( '#pcpVoid' );
voidButton.addEventListener( 'click', async () => {
if ( ! window.confirm( PcpVoidButton.popup_text ) ) {
return;
}
voidButton.setAttribute( 'disabled', 'disabled' );
const res = await fetch( PcpVoidButton.ajax.void.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify( {
nonce: PcpVoidButton.ajax.void.nonce,
wc_order_id: PcpVoidButton.wc_order_id,
} ),
} );
const data = await res.json();
if ( ! data.success ) {
hide( voidButton );
show( refundButton );
alert( PcpVoidButton.error_text );
throw Error( data.data.message );
}
location.reload();
} );
} );

View file

@ -25,8 +25,10 @@ use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderReauthorizeAction;
use WooCommerce\PayPalCommerce\WcGateway\Assets\VoidButtonAssets;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\VoidOrderEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Helper\FeesUpdater;
use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Factory\SimpleRedirectTaskFactory;
@ -46,7 +48,6 @@ use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways;
use WooCommerce\PayPalCommerce\WcGateway\Cli\SettingsCommand;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNetSessionId;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNetSourceWebsiteId;
use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
@ -118,7 +119,8 @@ return array(
$container->get( 'wcgateway.place-order-button-text' ),
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'vaulting.vault-v3-enabled' ),
$container->get( 'vaulting.wc-payment-tokens' )
$container->get( 'vaulting.wc-payment-tokens' ),
$container->get( 'wcgateway.settings.admin-settings-enabled' )
);
},
'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway {
@ -514,12 +516,20 @@ return array(
);
},
'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor {
$order_endpoint = $container->get( 'api.endpoint.order' );
$payments_endpoint = $container->get( 'api.endpoint.payments' );
$refund_fees_updater = $container->get( 'wcgateway.helper.refund-fees-updater' );
$prefix = $container->get( 'api.prefix' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new RefundProcessor( $order_endpoint, $payments_endpoint, $refund_fees_updater, $prefix, $logger );
return new RefundProcessor(
$container->get( 'api.endpoint.order' ),
$container->get( 'api.endpoint.payments' ),
$container->get( 'wcgateway.helper.refund-fees-updater' ),
$container->get( 'wcgateway.allowed_refund_payment_methods' ),
$container->get( 'api.prefix' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'wcgateway.allowed_refund_payment_methods' => static function ( ContainerInterface $container ): array {
return apply_filters(
'woocommerce_paypal_payments_allowed_refund_payment_methods',
array( PayPalGateway::ID, CreditCardGateway::ID, CardButtonGateway::ID, PayUponInvoiceGateway::ID )
);
},
'wcgateway.processor.authorized-payments' => static function ( ContainerInterface $container ): AuthorizedPaymentsProcessor {
$order_endpoint = $container->get( 'api.endpoint.order' );
@ -1420,7 +1430,10 @@ return array(
return new OXXO(
$container->get( 'wcgateway.checkout-helper' ),
$container->get( 'wcgateway.url' ),
$container->get( 'ppcp.asset-version' )
$container->get( 'ppcp.asset-version' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'api.factory.capture' )
);
},
'wcgateway.oxxo-gateway' => static function( ContainerInterface $container ): OXXOGateway {
@ -1914,4 +1927,26 @@ return array(
return $simple_redirect_tasks;
},
'wcgateway.void-button.assets' => function( ContainerInterface $container ) : VoidButtonAssets {
return new VoidButtonAssets(
$container->get( 'wcgateway.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.allowed_refund_payment_methods' )
);
},
'wcgateway.void-button.endpoint' => function( ContainerInterface $container ) : VoidOrderEndpoint {
return new VoidOrderEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'wcgateway.settings.admin-settings-enabled' => static function( ContainerInterface $container ): bool {
return $container->has( 'settings.url' );
},
);

View file

@ -0,0 +1,171 @@
<?php
/**
* Register and configure assets for the void button.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Assets
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Assets;
use Exception;
use WC_AJAX;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\VoidOrderEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WP_Screen;
/**
* Class VoidButtonAssets
*/
class VoidButtonAssets {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* The methods that can be refunded.
*
* @var array
*/
private $allowed_refund_payment_methods;
/**
* VoidButtonAssets constructor.
*
* @param string $module_url The url of this module.
* @param string $version The assets version.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param array $allowed_refund_payment_methods The methods that can be refunded.
*/
public function __construct(
string $module_url,
string $version,
OrderEndpoint $order_endpoint,
RefundProcessor $refund_processor,
array $allowed_refund_payment_methods
) {
$this->module_url = $module_url;
$this->version = $version;
$this->order_endpoint = $order_endpoint;
$this->refund_processor = $refund_processor;
$this->allowed_refund_payment_methods = $allowed_refund_payment_methods;
}
/**
* Checks if should register assets on the current page.
*/
public function should_register(): bool {
if ( ! is_admin() || wp_doing_ajax() ) {
return false;
}
global $theorder;
if ( ! $theorder instanceof WC_Order ) {
return false;
}
$current_screen = get_current_screen();
if ( ! $current_screen instanceof WP_Screen ) {
return false;
}
if ( $current_screen->post_type !== 'shop_order' ) {
return false;
}
if ( ! in_array( $theorder->get_payment_method(), $this->allowed_refund_payment_methods, true ) ) {
return false;
}
// Skip if there are refunds already, it is probably not voidable anymore + void cannot be partial.
if ( $theorder->get_remaining_refund_amount() !== $theorder->get_total() ) {
return false;
}
$order_id = $theorder->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $order_id ) {
return false;
}
try {
$order = $this->order_endpoint->order( $order_id );
if ( $this->refund_processor->determine_refund_mode( $order ) !== RefundProcessor::REFUND_MODE_VOID ) {
return false;
}
} catch ( Exception $exception ) {
return false;
}
return true;
}
/**
* Enqueues the assets.
*/
public function register(): void {
global $theorder;
assert( $theorder instanceof WC_Order );
wp_enqueue_script(
'ppcp-void-button',
trailingslashit( $this->module_url ) . 'assets/js/void-button.js',
array(),
$this->version,
true
);
wp_localize_script(
'ppcp-void-button',
'PcpVoidButton',
array(
'button_text' => __( 'Void authorization', 'woocommerce-paypal-payments' ),
'popup_text' => __(
'After voiding an authorized transaction, you cannot capture any funds associated with that transaction, and the funds are returned to the customer. Voiding an authorization cancels the entire open amount.',
'woocommerce-paypal-payments'
),
'error_text' => __(
'The operation failed. Use the Refund button if the funds were already captured.',
'woocommerce-paypal-payments'
),
'wc_order_id' => $theorder->get_id(),
'ajax' => array(
'void' => array(
'endpoint' => WC_AJAX::get_endpoint( VoidOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( VoidOrderEndpoint::nonce() ),
),
),
),
);
}
}

View file

@ -112,6 +112,7 @@ class ReturnUrlEndpoint {
}
if ( $wc_order->get_payment_method() === OXXOGateway::ID ) {
$this->session_handler->destroy_session_data();
wp_safe_redirect( wc_get_checkout_url() );
exit();
}

View file

@ -0,0 +1,198 @@
<?php
/**
* The Void Order endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Order_Item_Fee;
use WC_Order_Item_Product;
use WC_Order_Item_Shipping;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
/**
* Class VoidOrderEndpoint
*/
class VoidOrderEndpoint {
const ENDPOINT = 'ppc-void-order';
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/**
* The Request Data Helper.
*
* @var RequestData
*/
private $request_data;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* VoidOrderEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
OrderEndpoint $order_endpoint,
RefundProcessor $refund_processor,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->order_endpoint = $order_endpoint;
$this->refund_processor = $refund_processor;
$this->logger = $logger;
}
/**
* Returns the nonce.
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the incoming request.
*/
public function handle_request(): void {
$request = $this->request_data->read_request( self::nonce() );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error(
array(
'message' => 'Invalid request.',
)
);
return;
}
$wc_order_id = (int) $request['wc_order_id'];
$wc_order = wc_get_order( $wc_order_id );
if ( ! $wc_order instanceof WC_Order ) {
wp_send_json_error(
array(
'message' => 'WC order not found.',
)
);
return;
}
$order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $order_id ) {
wp_send_json_error(
array(
'message' => 'PayPal order ID not found in meta.',
)
);
return;
}
try {
$order = $this->order_endpoint->order( $order_id );
$this->refund_processor->void( $order );
$this->make_refunded( $wc_order );
} catch ( Exception $exception ) {
wp_send_json_error(
array(
'message' => 'Void failed. ' . $exception->getMessage(),
)
);
$this->logger->error( 'Void failed. ' . $exception->getMessage() );
return;
}
wp_send_json_success();
}
/**
* Returns the list of items for the wc_create_refund data,
* making all items refunded (max qty, total, taxes).
*
* @param WC_Order $wc_order The WC order.
*/
protected function refund_items( WC_Order $wc_order ): array {
$refunded_items = array();
foreach ( $wc_order->get_items( array( 'line_item', 'fee', 'shipping' ) ) as $item ) {
// Some methods like get_taxes() are not defined in WC_Order_Item.
if (
! $item instanceof WC_Order_Item_Product
&& ! $item instanceof WC_Order_Item_Fee
&& ! $item instanceof WC_Order_Item_Shipping
) {
continue;
}
$taxes = array();
$item_taxes = $item->get_taxes();
/**
* The type is not really guaranteed in the code.
*
* @psalm-suppress RedundantConditionGivenDocblockType
*/
if ( is_array( $item_taxes ) && isset( $item_taxes['total'] ) ) {
$taxes = $item_taxes['total'];
}
$refunded_items[ $item->get_id() ] = array(
'qty' => $item->get_type() === 'line_item' ? $item->get_quantity() : 0,
'refund_total' => $item->get_total(),
'refund_tax' => $taxes,
);
}
return $refunded_items;
}
/**
* Creates a full refund.
*
* @param WC_Order $wc_order The WC order.
*/
private function make_refunded( WC_Order $wc_order ): void {
wc_create_refund(
array(
'amount' => $wc_order->get_total(),
'reason' => __( 'Voided authorization', 'woocommerce-paypal-payments' ),
'order_id' => $wc_order->get_id(),
'line_items' => $this->refund_items( $wc_order ),
'refund_payment' => false,
'restock_items' => (bool) apply_filters( 'woocommerce_paypal_payments_void_restock_items', true ),
)
);
$wc_order->set_status( 'refunded' );
}
}

View file

@ -10,13 +10,19 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Psr\Log\LoggerInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper;
/**
* Class OXXO.
*/
class OXXO {
public const ID = 'ppcp-oxxo-gateway';
/**
* The checkout helper.
@ -40,21 +46,51 @@ class OXXO {
protected $asset_version;
/**
* OXXO constructor.
* The order endpoint.
*
* @param CheckoutHelper $checkout_helper The checkout helper.
* @param string $module_url The module URL.
* @param string $asset_version The asset version.
* @var OrderEndpoint
*/
protected $order_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* The capture factory.
*
* @var CaptureFactory
*/
protected $capture_factory;
/**
* OXXO constructor
*
* @param CheckoutHelper $checkout_helper The checkout helper.
* @param string $module_url The module URL.
* @param string $asset_version The asset version.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param LoggerInterface $logger The logger.
* @param CaptureFactory $capture_factory The capture factory.
*/
public function __construct(
CheckoutHelper $checkout_helper,
string $module_url,
string $asset_version
string $asset_version,
OrderEndpoint $order_endpoint,
LoggerInterface $logger,
CaptureFactory $capture_factory
) {
$this->checkout_helper = $checkout_helper;
$this->module_url = $module_url;
$this->asset_version = $asset_version;
$this->order_endpoint = $order_endpoint;
$this->logger = $logger;
$this->capture_factory = $capture_factory;
}
/**
@ -199,6 +235,46 @@ class OXXO {
}
}
);
/**
* Process PayPal fees
*/
add_action(
'woocommerce_paypal_payments_payment_capture_completed_webhook_handler',
function ( WC_Order $wc_order, string $order_id ) {
try {
if ( $wc_order->get_payment_method() !== OXXO::ID ) {
return;
}
$order = $this->order_endpoint->order( $order_id );
$payments = $order->purchase_units()[0]->payments();
if ( ! $payments ) {
return;
}
$capture = $payments->captures()[0] ?? null;
if ( $capture ) {
$breakdown = $capture->seller_receivable_breakdown();
if ( $breakdown ) {
$wc_order->update_meta_data( PayPalGateway::FEES_META_KEY, $breakdown->to_array() );
$paypal_fee = $breakdown->paypal_fee();
if ( $paypal_fee ) {
$wc_order->update_meta_data( 'PayPal Transaction Fee', (string) $paypal_fee->value() );
}
$wc_order->save_meta_data();
}
}
} catch ( RuntimeException $exception ) {
$this->logger->error( $exception->getMessage() );
}
},
10,
2
);
}
/**

View file

@ -202,6 +202,13 @@ class PayPalGateway extends \WC_Payment_Gateway {
*/
private $wc_payment_tokens;
/**
* Whether settings module is enabled.
*
* @var bool
*/
private $admin_settings_enabled;
/**
* PayPalGateway constructor.
*
@ -225,6 +232,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param bool $vault_v3_enabled Whether Vault v3 module is enabled.
* @param WooCommercePaymentTokens $wc_payment_tokens WooCommerce payment tokens.
* @param bool $admin_settings_enabled Whether settings module is enabled.
*/
public function __construct(
SettingsRenderer $settings_renderer,
@ -246,7 +254,8 @@ class PayPalGateway extends \WC_Payment_Gateway {
string $place_order_button_text,
PaymentTokensEndpoint $payment_tokens_endpoint,
bool $vault_v3_enabled,
WooCommercePaymentTokens $wc_payment_tokens
WooCommercePaymentTokens $wc_payment_tokens,
bool $admin_settings_enabled
) {
$this->id = self::ID;
$this->settings_renderer = $settings_renderer;
@ -270,6 +279,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
$this->vault_v3_enabled = $vault_v3_enabled;
$this->wc_payment_tokens = $wc_payment_tokens;
$this->admin_settings_enabled = $admin_settings_enabled;
$default_support = array(
'products',
@ -745,6 +755,17 @@ class PayPalGateway extends \WC_Payment_Gateway {
return $ret;
}
/**
* Override the parent admin_options method.
*/
public function admin_options() {
if ( ! $this->admin_settings_enabled ) {
parent::admin_options();
}
do_action( 'woocommerce_paypal_payments_gateway_admin_options_wrapper', $this );
}
/**
* Returns the settings renderer.
*

View file

@ -33,9 +33,9 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
class RefundProcessor {
use RefundMetaTrait;
private const REFUND_MODE_REFUND = 'refund';
private const REFUND_MODE_VOID = 'void';
private const REFUND_MODE_UNKNOWN = 'unknown';
public const REFUND_MODE_REFUND = 'refund';
public const REFUND_MODE_VOID = 'void';
public const REFUND_MODE_UNKNOWN = 'unknown';
/**
* The order endpoint.
@ -72,12 +72,20 @@ class RefundProcessor {
*/
private $refund_fees_updater;
/**
* The methods that can be refunded.
*
* @var array
*/
private $allowed_refund_payment_methods;
/**
* RefundProcessor constructor.
*
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PaymentsEndpoint $payments_endpoint The payments endpoint.
* @param RefundFeesUpdater $refund_fees_updater The refund fees updater.
* @param array $allowed_refund_payment_methods The methods that can be refunded.
* @param string $prefix The prefix.
* @param LoggerInterface $logger The logger.
*/
@ -85,15 +93,17 @@ class RefundProcessor {
OrderEndpoint $order_endpoint,
PaymentsEndpoint $payments_endpoint,
RefundFeesUpdater $refund_fees_updater,
array $allowed_refund_payment_methods,
string $prefix,
LoggerInterface $logger
) {
$this->order_endpoint = $order_endpoint;
$this->payments_endpoint = $payments_endpoint;
$this->refund_fees_updater = $refund_fees_updater;
$this->prefix = $prefix;
$this->logger = $logger;
$this->order_endpoint = $order_endpoint;
$this->payments_endpoint = $payments_endpoint;
$this->refund_fees_updater = $refund_fees_updater;
$this->allowed_refund_payment_methods = $allowed_refund_payment_methods;
$this->prefix = $prefix;
$this->logger = $logger;
}
/**
@ -109,11 +119,7 @@ class RefundProcessor {
*/
public function process( WC_Order $wc_order, float $amount = null, string $reason = '' ) : bool {
try {
$allowed_refund_payment_methods = apply_filters(
'woocommerce_paypal_payments_allowed_refund_payment_methods',
array( PayPalGateway::ID, CreditCardGateway::ID, CardButtonGateway::ID, PayUponInvoiceGateway::ID )
);
if ( ! in_array( $wc_order->get_payment_method(), $allowed_refund_payment_methods, true ) ) {
if ( ! in_array( $wc_order->get_payment_method(), $this->allowed_refund_payment_methods, true ) ) {
return true;
}
@ -134,7 +140,7 @@ class RefundProcessor {
)
);
$mode = $this->determine_refund_mode( $payments );
$mode = $this->determine_refund_mode( $order );
switch ( $mode ) {
case self::REFUND_MODE_REFUND:
@ -226,11 +232,13 @@ class RefundProcessor {
/**
* Determines the refunding mode.
*
* @param Payments $payments The order payments state.
* @param Order $order The order.
*
* @return string One of the REFUND_MODE_ constants.
*/
private function determine_refund_mode( Payments $payments ): string {
public function determine_refund_mode( Order $order ): string {
$payments = $this->get_payments( $order );
$authorizations = $payments->authorizations();
if ( $authorizations ) {
foreach ( $authorizations as $authorization ) {

View file

@ -21,7 +21,9 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\WcGateway\Assets\VoidButtonAssets;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\VoidOrderEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Processor\CreditCardOrderInfoHandlingTrait;
use WC_Order;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
@ -90,6 +92,7 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
$this->register_columns( $c );
$this->register_checkout_paypal_address_preset( $c );
$this->register_wc_tasks( $c );
$this->register_void_button( $c );
add_action(
'woocommerce_sections_checkout',
@ -848,21 +851,19 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
* @return void
*/
protected function register_wc_tasks( ContainerInterface $container ): void {
$simple_redirect_tasks = $container->get( 'wcgateway.settings.wc-tasks.simple-redirect-tasks' );
if ( empty( $simple_redirect_tasks ) ) {
return;
}
$task_registrar = $container->get( 'wcgateway.settings.wc-tasks.task-registrar' );
assert( $task_registrar instanceof TaskRegistrarInterface );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
add_action(
'init',
static function () use ( $simple_redirect_tasks, $task_registrar, $logger ): void {
static function () use ( $container ): void {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
try {
$simple_redirect_tasks = $container->get( 'wcgateway.settings.wc-tasks.simple-redirect-tasks' );
if ( empty( $simple_redirect_tasks ) ) {
return;
}
$task_registrar = $container->get( 'wcgateway.settings.wc-tasks.task-registrar' );
assert( $task_registrar instanceof TaskRegistrarInterface );
$task_registrar->register( $simple_redirect_tasks );
} catch ( Exception $exception ) {
$logger->error( "Failed to create a task in the 'Things to do next' section of WC. " . $exception->getMessage() );
@ -870,4 +871,33 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
},
);
}
/**
* Registers the assets and ajax endpoint for the void button.
*
* @param ContainerInterface $container The container.
*/
protected function register_void_button( ContainerInterface $container ): void {
add_action(
'admin_enqueue_scripts',
static function () use ( $container ) {
$assets = $container->get( 'wcgateway.void-button.assets' );
assert( $assets instanceof VoidButtonAssets );
if ( $assets->should_register() ) {
$assets->register();
}
}
);
add_action(
'wc_ajax_' . VoidOrderEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'wcgateway.void-button.endpoint' );
assert( $endpoint instanceof VoidOrderEndpoint );
$endpoint->handle_request();
}
);
}
}

View file

@ -10,6 +10,7 @@ module.exports = {
'gateway-settings': path.resolve('./resources/js/gateway-settings.js'),
'fraudnet': path.resolve('./resources/js/fraudnet.js'),
'oxxo': path.resolve('./resources/js/oxxo.js'),
'void-button': path.resolve('./resources/js/void-button.js'),
'gateway-settings-style': path.resolve('./resources/css/gateway-settings.scss'),
'common-style': path.resolve('./resources/css/common.scss'),
},

View file

@ -99,7 +99,7 @@ return array(
new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory, $payment_token_helper ),
new VaultPaymentTokenDeleted( $logger ),
new PaymentCapturePending( $logger ),
new PaymentSaleCompleted( $logger ),
new PaymentSaleCompleted( $logger, $container->get( 'paypal-subscriptions.renewal-handler' ) ),
new PaymentSaleRefunded( $logger, $refund_fees_updater ),
new BillingSubscriptionCancelled( $logger ),
new BillingPlanPricingChangeActivated( $logger ),

View file

@ -10,8 +10,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Handler;
use Psr\Log\LoggerInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WC_Data_Exception;
use WooCommerce\PayPalCommerce\PayPalSubscriptions\RenewalHandler;
use WP_REST_Request;
use WP_REST_Response;
@ -20,7 +20,14 @@ use WP_REST_Response;
*/
class PaymentSaleCompleted implements RequestHandler {
use TransactionIdHandlingTrait, RequestHandlerTrait;
use RequestHandlerTrait;
/**
* Renewal handler.
*
* @var RenewalHandler
*/
private $renewal_handler;
/**
* The logger.
@ -33,9 +40,11 @@ class PaymentSaleCompleted implements RequestHandler {
* PaymentSaleCompleted constructor.
*
* @param LoggerInterface $logger The logger.
* @param RenewalHandler $renewal_handler Renewal handler.
*/
public function __construct( LoggerInterface $logger ) {
$this->logger = $logger;
public function __construct( LoggerInterface $logger, RenewalHandler $renewal_handler ) {
$this->logger = $logger;
$this->renewal_handler = $renewal_handler;
}
/**
@ -68,7 +77,7 @@ class PaymentSaleCompleted implements RequestHandler {
*/
public function handle_request( WP_REST_Request $request ): WP_REST_Response {
if ( is_null( $request['resource'] ) ) {
return $this->failure_response();
return $this->failure_response( 'Could not retrieve resource.' );
}
if ( ! function_exists( 'wcs_get_subscriptions' ) ) {
@ -85,7 +94,7 @@ class PaymentSaleCompleted implements RequestHandler {
return $this->failure_response( 'Could not retrieve transaction id for subscription.' );
}
$args = array(
$args = array(
// phpcs:ignore WordPress.DB.SlowDBQuery
'meta_query' => array(
array(
@ -95,24 +104,13 @@ class PaymentSaleCompleted implements RequestHandler {
),
),
);
$subscriptions = wcs_get_subscriptions( $args );
foreach ( $subscriptions as $subscription ) {
$is_renewal = $subscription->get_meta( '_ppcp_is_subscription_renewal' ) ?? '';
if ( $is_renewal ) {
$renewal_order = wcs_create_renewal_order( $subscription );
if ( is_a( $renewal_order, WC_Order::class ) ) {
$renewal_order->set_payment_method( $subscription->get_payment_method() );
$renewal_order->payment_complete();
$this->update_transaction_id( $transaction_id, $renewal_order, $this->logger );
break;
}
}
$parent_order = wc_get_order( $subscription->get_parent() );
if ( is_a( $parent_order, WC_Order::class ) ) {
$subscription->update_meta_data( '_ppcp_is_subscription_renewal', 'true' );
$subscription->save_meta_data();
$this->update_transaction_id( $transaction_id, $parent_order, $this->logger );
$subscriptions = wcs_get_subscriptions( $args );
if ( $subscriptions ) {
try {
$this->renewal_handler->process( $subscriptions, $transaction_id );
} catch ( WC_Data_Exception $exception ) {
return $this->failure_response( 'Could not update payment method.' );
}
}

View file

@ -26,6 +26,7 @@
"install:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn install",
"install:modules:ppcp-card-fields": "cd modules/ppcp-card-fields && yarn install",
"install:modules:ppcp-compat": "cd modules/ppcp-compat && yarn install",
"install:modules:ppcp-settings": "cd modules/ppcp-settings && yarn install",
"install:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn install",
"build:modules:ppcp-admin-notices": "cd modules/ppcp-admin-notices && yarn run build",
"build:modules:ppcp-applepay": "cd modules/ppcp-applepay && yarn run build",
@ -47,6 +48,7 @@
"build:modules:ppcp-card-fields": "cd modules/ppcp-card-fields && yarn run build",
"build:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run build",
"build:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run build",
"build:modules:ppcp-settings": "cd modules/ppcp-settings && yarn run build",
"build:modules": "run-p build:modules:*",
"watch:modules:ppcp-admin-notices": "cd modules/ppcp-admin-notices && yarn run watch",
"watch:modules:ppcp-applepay": "cd modules/ppcp-applepay && yarn run watch",
@ -68,6 +70,7 @@
"watch:modules:ppcp-card-fields": "cd modules/ppcp-card-fields && yarn run watch",
"watch:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run watch",
"watch:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run watch",
"watch:modules:ppcp-settings": "cd modules/ppcp-settings && yarn run watch",
"watch:modules": "run-p watch:modules:*",
"ddev:setup": "ddev start && ddev orchestrate",
"ddev:start": "ddev start",

View file

@ -1,156 +0,0 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Mockery;
use Psr\Log\LoggerInterface;
use Requests_Utility_CaseInsensitiveDictionary;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Token;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PaymentSource;
use function Brain\Monkey\Functions\expect;
use function Brain\Monkey\Functions\when;
class PayUponInvoiceOrderEndpointTest extends TestCase
{
private $bearer;
private $orderFactory;
private $fraudnet;
private $logger;
private $testee;
public function setUp(): void
{
parent::setUp();
$this->bearer = Mockery::mock(Bearer::class);
$token = Mockery::mock(Token::class);
$token->shouldReceive('token')->andReturn('');
$this->bearer->shouldReceive('bearer')->andReturn($token);
$this->orderFactory = Mockery::mock(OrderFactory::class);
$this->fraudnet = Mockery::mock(FraudNet::class);
$this->logger = Mockery::mock(LoggerInterface::class);
$this->testee = new PayUponInvoiceOrderEndpoint(
'',
$this->bearer,
$this->orderFactory,
$this->fraudnet,
$this->logger
);
}
public function testCreateOrder()
{
$this->markTestSkipped('must be revisited.');
list($items, $paymentSource, $headers) = $this->setStubs();
$response = [
'body' => '{"is_correct":true}',
'headers' => $headers,
];
expect('wp_remote_get')->andReturn($response);
expect('wp_remote_retrieve_response_code')->with($response)->andReturn(200);
$this->logger->shouldReceive('debug');
$wc_order = Mockery::mock(WC_Order::class);
$result = $this->testee->create($items, $paymentSource, $wc_order );
$this->assertInstanceOf(Order::class, $result);
}
public function testCreateOrderWpError()
{
$this->markTestSkipped('must be revisited.');
list($items, $paymentSource) = $this->setStubsForError();
$wpError = Mockery::mock(\WP_Error::class);
$wpError->shouldReceive('get_error_messages')->andReturn(['foo']);
$wpError->shouldReceive('get_error_message')->andReturn('foo');
expect('wp_remote_get')->andReturn($wpError);
$this->logger->shouldReceive('debug');
$wc_order = Mockery::mock(WC_Order::class);
$this->expectException(\RuntimeException::class);
$this->testee->create($items, $paymentSource, $wc_order);
}
public function testCreateOrderApiError()
{
$this->markTestSkipped('must be revisited.');
list($items, $paymentSource) = $this->setStubsForError();
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$response = [
'body' => '{"is_correct":true}',
'headers' => $headers,
];
when('get_bloginfo')->justReturn('de-DE');
expect('wp_remote_get')->andReturn($response);
expect('wp_remote_retrieve_response_code')->with($response)->andReturn(500);
$this->logger->shouldReceive('debug');
$wc_order = Mockery::mock(WC_Order::class);
$this->expectException(PayPalApiException::class);
$this->testee->create($items, $paymentSource, $wc_order);
}
/**
* @return array
*/
private function setStubs(): array
{
$order = Mockery::mock(Order::class);
$this->orderFactory
->expects('from_paypal_response')
->andReturnUsing(function (\stdClass $object) use ($order): ?Order {
return ($object->is_correct) ? $order : null;
});
$this->fraudnet->shouldReceive('session_id')->andReturn('');
$purchaseUnit = Mockery::mock(PurchaseUnit::class);
$purchaseUnit->shouldReceive('to_array')->andReturn([
'items' => [],
]);
$items = [$purchaseUnit];
$paymentSource = Mockery::mock(PaymentSource::class);
$paymentSource->shouldReceive('to_array')->andReturn([]);
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
return array($items, $paymentSource, $headers);
}
/**
* @return array
*/
private function setStubsForError(): array
{
$this->fraudnet->shouldReceive('session_id')->andReturn('');
$purchaseUnit = Mockery::mock(PurchaseUnit::class);
$purchaseUnit->shouldReceive('to_array')->andReturn([
'items' => [],
]);
$items = [$purchaseUnit];
$paymentSource = Mockery::mock(PaymentSource::class);
$paymentSource->shouldReceive('to_array')->andReturn([]);
return array($items, $paymentSource);
}
}

View file

@ -123,7 +123,8 @@ class WcGatewayTest extends TestCase
'Pay via PayPal',
$this->paymentTokensEndpoint,
$this->vaultV3Enabled,
$this->wcPaymentTokens
$this->wcPaymentTokens,
false
);
}

View file

@ -0,0 +1,107 @@
<?php
namespace WooCommerce\PayPalCommerce\Tests\E2e;
use WooCommerce\PayPalCommerce\PayPalSubscriptions\RenewalHandler;
class PayPalSubscriptionsRenewalTest extends TestCase
{
public function test_parent_order()
{
$c = $this->getContainer();
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
// Simulates receiving webhook 1 minute after subscription start.
$subscription = $this->createSubscription('-1 minute');
$handler->process([$subscription], 'TRANSACTION-ID');
$renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
$this->assertEquals(count($renewal), 0);
}
public function test_renewal_order()
{
$c = $this->getContainer();
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
// Simulates receiving webhook 9 hours after subscription start.
$subscription = $this->createSubscription('-9 hour');
$handler->process([$subscription], 'TRANSACTION-ID');
$renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
$this->assertEquals(count($renewal), 1);
}
private function createSubscription(string $startDate)
{
$args = [
'method' => 'POST',
'headers' => [
'Authorization' => 'Basic ' . base64_encode( 'admin:admin' ),
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'customer_id' => 1,
'set_paid' => true,
'payment_method' => 'ppcp-gateway',
'billing' => [
'first_name' => 'John',
'last_name' => 'Doe',
'address_1' => '969 Market',
'address_2' => '',
'city' => 'San Francisco',
'state' => 'CA',
'postcode' => '94103',
'country' => 'US',
'email' => 'john.doe@example.com',
'phone' => '(555) 555-5555'
],
'line_items' => [
[
'product_id' => 156,
'quantity' => 1
]
],
]),
];
$response = wp_remote_request(
'https://woocommerce-paypal-payments.ddev.site/wp-json/wc/v3/orders',
$args
);
$body = json_decode( $response['body'] );
$args = [
'method' => 'POST',
'headers' => [
'Authorization' => 'Basic ' . base64_encode( 'admin:admin' ),
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'start_date' => gmdate( 'Y-m-d H:i:s', strtotime($startDate) ),
'parent_id' => $body->id,
'customer_id' => 1,
'status' => 'active',
'billing_period' => 'day',
'billing_interval' => 1,
'payment_method' => 'ppcp-gateway',
'line_items' => [
[
'product_id' => $_ENV['PAYPAL_SUBSCRIPTIONS_PRODUCT_ID'],
'quantity' => 1
]
],
]),
];
$response = wp_remote_request(
'https://woocommerce-paypal-payments.ddev.site/wp-json/wc/v3/subscriptions?per_page=1',
$args
);
$body = json_decode( $response['body'] );
return wcs_get_subscription($body->id);
}
}