diff --git a/.ddev/commands/web/orchestrate.d/00_download_wordpress.sh b/.ddev/commands/web/orchestrate.d/00_download_wordpress.sh index 6ff769fd3..b6f261f30 100644 --- a/.ddev/commands/web/orchestrate.d/00_download_wordpress.sh +++ b/.ddev/commands/web/orchestrate.d/00_download_wordpress.sh @@ -1,6 +1,6 @@ #!/bin/bash -if ! wp core download; then +if ! wp core download --version="${WP_VERSION:-latest}"; then echo 'WordPress is already installed.' exit fi diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index a8226819f..24bcd1a21 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -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' ), diff --git a/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php index e470dba46..2a39a4b9b 100644 --- a/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/PaymentsEndpoint.php @@ -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; } /** diff --git a/modules/ppcp-api-client/src/Entity/Capture.php b/modules/ppcp-api-client/src/Entity/Capture.php index 2c6d55fbc..a462f6998 100644 --- a/modules/ppcp-api-client/src/Entity/Capture.php +++ b/modules/ppcp-api-client/src/Entity/Capture.php @@ -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; } } diff --git a/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php new file mode 100644 index 000000000..2cc7c5480 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/FraudProcessorResponse.php @@ -0,0 +1,74 @@ +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', + ); + } + +} diff --git a/modules/ppcp-api-client/src/Factory/AmountFactory.php b/modules/ppcp-api-client/src/Factory/AmountFactory.php index bf9637c32..d0079f1a9 100644 --- a/modules/ppcp-api-client/src/Factory/AmountFactory.php +++ b/modules/ppcp-api-client/src/Factory/AmountFactory.php @@ -127,7 +127,7 @@ class AmountFactory { $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 ) ) { diff --git a/modules/ppcp-api-client/src/Factory/CaptureFactory.php b/modules/ppcp-api-client/src/Factory/CaptureFactory.php index 7b3b5d82c..0c833fca6 100644 --- a/modules/ppcp-api-client/src/Factory/CaptureFactory.php +++ b/modules/ppcp-api-client/src/Factory/CaptureFactory.php @@ -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 ); } } diff --git a/modules/ppcp-api-client/src/Factory/FraudProcessorResponseFactory.php b/modules/ppcp-api-client/src/Factory/FraudProcessorResponseFactory.php new file mode 100644 index 000000000..e48d7fd3e --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/FraudProcessorResponseFactory.php @@ -0,0 +1,33 @@ +avs_code ?: null; + $cvv_code = $data->cvv_code ?: null; + + return new FraudProcessorResponse( $avs_code, $cvv_code ); + } +} diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index b06edf9bb..65e343ecb 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -13,7 +13,6 @@ class CreditCardRenderer { } render(wrapper, contextConfig) { - if ( ( this.defaultConfig.context !== 'checkout' @@ -42,6 +41,9 @@ class CreditCardRenderer { } const gateWayBox = document.querySelector('.payment_box.payment_method_ppcp-credit-card-gateway'); + if(! gateWayBox) { + return + } const oldDisplayStyle = gateWayBox.style.display; gateWayBox.style.display = 'block'; diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 61a5c21ed..37f4c2a14 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -221,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' ) @@ -433,6 +432,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; } @@ -446,28 +449,21 @@ class SmartButton implements SmartButtonInterface { ); } - if ( $this->is_cart_price_total_zero() && ! $this->is_free_trial_cart() ) { - return false; - } + add_action( $this->checkout_button_renderer_hook(), array( $this, 'button_renderer' ), 10 ); $not_enabled_on_cart = $this->settings->has( 'button_cart_enabled' ) && ! $this->settings->get( 'button_cart_enabled' ); - if ( - is_cart() - && ! $not_enabled_on_cart - && ! $this->is_free_trial_cart() - ) { - add_action( - $this->proceed_to_checkout_button_renderer_hook(), - array( - $this, - 'button_renderer', - ), - 20 - ); - } + add_action( + $this->proceed_to_checkout_button_renderer_hook(), + function() use ( $not_enabled_on_cart ) { + if ( ! is_cart() || $not_enabled_on_cart || $this->is_free_trial_cart() || $this->is_cart_price_total_zero() ) { + return; + } - add_action( $this->checkout_button_renderer_hook(), array( $this, 'button_renderer' ), 10 ); + $this->button_renderer(); + }, + 20 + ); return true; } @@ -522,6 +518,11 @@ 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 ( @@ -547,6 +548,10 @@ class SmartButton implements SmartButtonInterface { * Renders the HTML for the credit messaging. */ public function message_renderer() { + if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) { + return false; + } + $product = wc_get_product(); if ( @@ -1231,10 +1236,11 @@ class SmartButton implements SmartButtonInterface { * Check if cart product price total is 0. * * @return bool true if is 0, otherwise false. + * @psalm-suppress RedundantConditionGivenDocblockType */ protected function is_cart_price_total_zero(): bool { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - return WC()->cart->get_cart_contents_total() == 0; + return WC()->cart && WC()->cart->get_total( 'numeric' ) == 0; } /** diff --git a/modules/ppcp-vaulting/src/PaymentTokenChecker.php b/modules/ppcp-vaulting/src/PaymentTokenChecker.php index 45cdae20c..604c7cc37 100644 --- a/modules/ppcp-vaulting/src/PaymentTokenChecker.php +++ b/modules/ppcp-vaulting/src/PaymentTokenChecker.php @@ -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 ); diff --git a/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php b/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php index 64520b38c..33fbcef6e 100644 --- a/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php +++ b/modules/ppcp-wc-gateway/src/Checkout/DisableGateways.php @@ -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 ] ); } diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 8cc34ef51..4af143dee 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -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. diff --git a/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php b/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php index 1992e60bb..815f8d73e 100644 --- a/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php +++ b/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php @@ -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 ); } $wc_order->save(); diff --git a/modules/ppcp-wc-gateway/src/Processor/RefundMetaTrait.php b/modules/ppcp-wc-gateway/src/Processor/RefundMetaTrait.php new file mode 100644 index 000000000..c847bc7a7 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Processor/RefundMetaTrait.php @@ -0,0 +1,47 @@ +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; + } +} diff --git a/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php b/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php index 1d588124c..16f9b4698 100644 --- a/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/RefundProcessor.php @@ -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( diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 1e8dc49bb..72bbd9c5e 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -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 = '