diff --git a/.psalm/stubs.php b/.psalm/stubs.php index 9f0cdaf55..00ece2e4e 100644 --- a/.psalm/stubs.php +++ b/.psalm/stubs.php @@ -2,3 +2,6 @@ if (!defined('EP_PAGES')) { define('EP_PAGES', 4096); } +if (!defined('MONTH_IN_SECONDS')) { + define('MONTH_IN_SECONDS', 30 * DAY_IN_SECONDS); +} diff --git a/changelog.txt b/changelog.txt index d96d61df3..10b2f8a1f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,14 +1,26 @@ *** Changelog *** -= 1.8.2 - TBD = += 1.9.1 - TBD = +* Fix - ITEM_TOTAL_MISMATCH error when checking out with multiple products #721 +* Fix - Unable to purchase a product with Credit card button in pay for order page #718 +* Fix - Pay Later messaging only displayed when smart button is active on the same page #283 +* Fix - Pay Later messaging displayed for out of stock variable products or with no variation selected #667 +* Fix - Placeholders and card type detection not working for PayPal Card Processing (260) #685 +* Fix - PUI gateway is displayed with unsupported store currency #711 +* Enhancement - Missing PayPal fee in WC order details for PUI purchase #714 +* Enhancement - Skip loading of PUI js file on all pages where PUI gateway is not displayed #723 +* Enhancement - PUI feature capitalization not consistent #724 + += 1.9.0 - 2022-07-04 = +* Add - New Feature - Pay Upon Invoice (Germany only) #608 * Fix - Order not approved: payment via vaulted PayPal account fails #677 * Fix - Cant' refund : "ERROR Refund failed: No country given for address." #639 -* Fix - Something went wrong error in Virtual products when using vaulted payment #673 +* Fix - Something went wrong error in Virtual products when using vaulted payment #673 * Fix - PayPal smart buttons are not displayed for product variations when parent product is set to out of stock #669 -* Fix - Pay Later messaging displayed for out of stock variable products or with no variation selected #667 -* Fix - "Capture Virtual-Only Orders" intent sets virtual+downloadable product orders to "Processing" instead of "Completed" #665 -* Fix - Free trial period causing incorrerct disable-funding parameters with DCC disabled #661 -* Fix - Smart button not visible on single product page when product price is below 1 and decimal is "," #654 +* Fix - Pay Later messaging displayed for out of stock variable products or with no variation selected #667 +* Fix - "Capture Virtual-Only Orders" intent sets virtual+downloadable product orders to "Processing" instead of "Completed" #665 +* Fix - Free trial period causing incorrerct disable-funding parameters with DCC disabled #661 +* Fix - Smart button not visible on single product page when product price is below 1 and decimal is "," #654 * Fix - Checkout using an email address containing a + symbol results in a "[INVALID_REQUEST]" error #523 * Fix - Order details are sometimes empty in PayPal dashboard #689 * Fix - Incorrect TAX details on PayPal order overview #541 @@ -35,8 +47,8 @@ = 1.8.0 - 2022-05-03 = * Add - Allow free trial subscriptions #580 * Fix - The Card Processing does not appear as an available payment method when manually creating an order #562 -* Fix - Express buttons & Pay Later visible on variable Subscription products /w disabled vaulting #281 -* Fix - Pay for order (guest) failing when no email address available #535 +* Fix - Express buttons & Pay Later visible on variable Subscription products /w disabled vaulting #281 +* Fix - Pay for order (guest) failing when no email address available #535 * Fix - Emoji in product description causing INVALID_STRING_LENGTH error #491 * Enhancement - Change cart total amount that is sent to PayPal gateway #486 * Enhancement - Include dark Visa and Mastercard gateway icon list for PayPal Card Processing #566 diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 6be5355be..f34ac93da 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -43,13 +43,13 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerReceivableBreakdownFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper; use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository; -use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; @@ -221,10 +221,6 @@ return array( $dcc_applies = $container->get( 'api.helpers.dccapplies' ); return new PartnerReferralsData( $dcc_applies ); }, - 'api.repository.cart' => static function ( ContainerInterface $container ): CartRepository { - $factory = $container->get( 'api.factory.purchase-unit' ); - return new CartRepository( $factory ); - }, 'api.repository.payee' => static function ( ContainerInterface $container ): PayeeRepository { $merchant_email = $container->get( 'api.merchant_email' ); $merchant_id = $container->get( 'api.merchant_id' ); @@ -298,6 +294,9 @@ return array( $address_factory = $container->get( 'api.factory.address' ); return new ShippingFactory( $address_factory ); }, + 'api.factory.shipping-preference' => static function ( ContainerInterface $container ): ShippingPreferenceFactory { + return new ShippingPreferenceFactory(); + }, 'api.factory.amount' => static function ( ContainerInterface $container ): AmountFactory { $item_factory = $container->get( 'api.factory.item' ); return new AmountFactory( diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index b12d1a025..41448a542 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -163,57 +163,23 @@ class OrderEndpoint { * Creates an order. * * @param PurchaseUnit[] $items The purchase unit items for the order. + * @param string $shipping_preference One of ApplicationContext::SHIPPING_PREFERENCE_ values. * @param Payer|null $payer The payer off the order. * @param PaymentToken|null $payment_token The payment token. * @param PaymentMethod|null $payment_method The payment method. * @param string $paypal_request_id The paypal request id. - * @param bool $shipping_address_is_fixed Whether the shipping address is changeable or not. * * @return Order * @throws RuntimeException If the request fails. */ public function create( array $items, + string $shipping_preference, Payer $payer = null, PaymentToken $payment_token = null, PaymentMethod $payment_method = null, - string $paypal_request_id = '', - bool $shipping_address_is_fixed = false + string $paypal_request_id = '' ): Order { - - $contains_physical_goods = false; - $items = array_filter( - $items, - static function ( $item ) use ( &$contains_physical_goods ): bool { - $is_purchase_unit = is_a( $item, PurchaseUnit::class ); - /** - * A purchase unit. - * - * @var PurchaseUnit $item - */ - if ( $is_purchase_unit && $item->contains_physical_goods() ) { - $contains_physical_goods = true; - } - - return $is_purchase_unit; - } - ); - - $shipping_preference = ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING; - if ( $contains_physical_goods ) { - if ( $shipping_address_is_fixed ) { - // Checkout + no address given? Probably something weird happened, like no form validation? - // Also note that $items currently always seems to be an array with one item. - if ( $this->has_items_without_shipping( $items ) ) { - $shipping_preference = ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING; - } else { - $shipping_preference = ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS; - } - } else { - $shipping_preference = ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE; - } - } - $bearer = $this->bearer->bearer(); $data = array( 'intent' => ( $this->subscription_helper->cart_contains_subscription() || $this->subscription_helper->current_product_is_subscription() ) ? 'AUTHORIZE' : $this->intent, @@ -598,20 +564,4 @@ class OrderEndpoint { $new_order = $this->order( $order_to_update->id() ); return $new_order; } - - /** - * Checks if there is at least one item without shipping. - * - * @param PurchaseUnit[] $items The items. - * @return bool Whether items contains shipping or not. - */ - private function has_items_without_shipping( array $items ): bool { - foreach ( $items as $item ) { - if ( ! $item->shipping() ) { - return true; - } - } - - return false; - } } diff --git a/modules/ppcp-api-client/src/Endpoint/PayUponInvoiceOrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/PayUponInvoiceOrderEndpoint.php new file mode 100644 index 000000000..41e5563ec --- /dev/null +++ b/modules/ppcp-api-client/src/Endpoint/PayUponInvoiceOrderEndpoint.php @@ -0,0 +1,258 @@ +host = $host; + $this->bearer = $bearer; + $this->order_factory = $order_factory; + $this->logger = $logger; + $this->fraudnet = $fraudnet; + } + + /** + * Creates an order. + * + * @param PurchaseUnit[] $items The purchase unit items for the order. + * @param PaymentSource $payment_source The payment source. + * @return Order + * @throws RuntimeException When there is a problem with the payment source. + * @throws PayPalApiException When there is a problem creating the order. + */ + public function create( array $items, PaymentSource $payment_source ): Order { + + $data = array( + 'intent' => 'CAPTURE', + 'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL', + 'purchase_units' => array_map( + static function ( PurchaseUnit $item ): array { + return $item->to_array(); + }, + $items + ), + 'payment_source' => array( + 'pay_upon_invoice' => $payment_source->to_array(), + ), + ); + + $data = $this->ensure_tax( $data ); + $data = $this->ensure_tax_rate( $data ); + $data = $this->ensure_shipping( $data, $payment_source->to_array() ); + + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v2/checkout/orders'; + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + 'PayPal-Client-Metadata-Id' => $this->fraudnet->session_id(), + 'PayPal-Request-Id' => uniqid( 'ppcp-', true ), + ), + 'body' => wp_json_encode( $data ), + ); + + $response = $this->request( $url, $args ); + if ( $response instanceof WP_Error ) { + throw new RuntimeException( $response->get_error_message() ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( ! in_array( $status_code, array( 200, 201 ), true ) ) { + $issue = $json->details[0]->issue ?? null; + + $site_country_code = explode( '-', get_bloginfo( 'language' ) )[0] ?? ''; + if ( 'PAYMENT_SOURCE_INFO_CANNOT_BE_VERIFIED' === $issue ) { + if ( 'de' === $site_country_code ) { + throw new RuntimeException( 'Die Kombination aus Ihrem Namen und Ihrer Anschrift konnte nicht validiert werden. Bitte korrigieren Sie Ihre Daten und versuchen Sie es erneut. Weitere Informationen finden Sie in den Ratepay Datenschutzbestimmungen oder nutzen Sie das Ratepay Kontaktformular.' ); + } else { + throw new RuntimeException( 'The combination of your name and address could not be validated. Please correct your data and try again. You can find further information in the Ratepay Data Privacy Statement or you can contact Ratepay using this contact form.' ); + } + } + if ( 'PAYMENT_SOURCE_DECLINED_BY_PROCESSOR' === $issue ) { + if ( 'de' === $site_country_code ) { + throw new RuntimeException( 'Die gewählte Zahlungsart kann nicht genutzt werden. Diese Entscheidung basiert auf einem automatisierten Datenverarbeitungsverfahren. Weitere Informationen finden Sie in den Ratepay Datenschutzbestimmungen oder nutzen Sie das Ratepay Kontaktformular.' ); + } else { + throw new RuntimeException( 'It is not possible to use the selected payment method. This decision is based on automated data processing. You can find further information in the Ratepay Data Privacy Statement or you can contact Ratepay using this contact form.' ); + } + } + + throw new PayPalApiException( $json, $status_code ); + } + + return $this->order_factory->from_paypal_response( $json ); + } + + /** + * Get PayPal order as object. + * + * @param string $id The PayPal order ID. + * @return stdClass + * @throws RuntimeException When there is a problem getting the order. + * @throws PayPalApiException When there is a problem getting the order. + */ + public function order( string $id ): stdClass { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $id; + $args = array( + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'PayPal-Request-Id' => uniqid( 'ppcp-', true ), + ), + ); + + $response = $this->request( $url, $args ); + if ( $response instanceof WP_Error ) { + throw new RuntimeException( $response->get_error_message() ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 200 !== $status_code ) { + throw new PayPalApiException( $json, $status_code ); + } + + return $json; + } + + /** + * Ensures items contains tax. + * + * @param array $data The data. + * @return array + */ + private function ensure_tax( array $data ): array { + $items_count = count( $data['purchase_units'][0]['items'] ); + + for ( $i = 0; $i < $items_count; $i++ ) { + if ( ! isset( $data['purchase_units'][0]['items'][ $i ]['tax'] ) ) { + $data['purchase_units'][0]['items'][ $i ]['tax'] = array( + 'currency_code' => 'EUR', + 'value' => '0.00', + ); + } + } + + return $data; + } + + /** + * Ensures items contains tax rate. + * + * @param array $data The data. + * @return array + */ + private function ensure_tax_rate( array $data ): array { + $items_count = count( $data['purchase_units'][0]['items'] ); + + for ( $i = 0; $i < $items_count; $i++ ) { + if ( ! isset( $data['purchase_units'][0]['items'][ $i ]['tax_rate'] ) ) { + $data['purchase_units'][0]['items'][ $i ]['tax_rate'] = '0.00'; + } + } + + return $data; + } + + /** + * Ensures purchase units contains shipping by using payment source data. + * + * @param array $data The data. + * @param array $payment_source The payment source. + * @return array + */ + private function ensure_shipping( array $data, array $payment_source ): array { + if ( isset( $data['purchase_units'][0]['shipping'] ) ) { + return $data; + } + + $given_name = $payment_source['name']['given_name'] ?? ''; + $surname = $payment_source['name']['surname'] ?? ''; + $address = $payment_source['billing_address'] ?? array(); + + $data['purchase_units'][0]['shipping']['name'] = array( 'full_name' => $given_name . ' ' . $surname ); + $data['purchase_units'][0]['shipping']['address'] = $address; + + return $data; + } +} diff --git a/modules/ppcp-api-client/src/Endpoint/RequestTrait.php b/modules/ppcp-api-client/src/Endpoint/RequestTrait.php index c4cbb2a45..369dc9c1e 100644 --- a/modules/ppcp-api-client/src/Endpoint/RequestTrait.php +++ b/modules/ppcp-api-client/src/Endpoint/RequestTrait.php @@ -87,9 +87,11 @@ trait RequestTrait { if ( isset( $response['response'] ) ) { $output .= 'Response: ' . wc_print_r( $response['response'], true ) . "\n"; - if ( isset( $response['body'] ) + if ( + isset( $response['body'] ) && isset( $response['response']['code'] ) - && ! in_array( $response['response']['code'], array( 200, 201, 202, 204 ), true ) ) { + && ! in_array( $response['response']['code'], array( 200, 201, 202, 204 ), true ) + ) { $output .= 'Response Body: ' . wc_print_r( $response['body'], true ) . "\n"; } } diff --git a/modules/ppcp-api-client/src/Entity/Amount.php b/modules/ppcp-api-client/src/Entity/Amount.php index 27ec10581..0b7c022c6 100644 --- a/modules/ppcp-api-client/src/Entity/Amount.php +++ b/modules/ppcp-api-client/src/Entity/Amount.php @@ -64,6 +64,15 @@ class Amount { return $this->money->value(); } + /** + * The value formatted as string for API requests. + * + * @return string + */ + public function value_str(): string { + return $this->money->value_str(); + } + /** * Returns the breakdown. * @@ -79,12 +88,7 @@ class Amount { * @return array */ public function to_array(): array { - $amount = array( - 'currency_code' => $this->currency_code(), - 'value' => in_array( $this->currency_code(), $this->currencies_without_decimals, true ) - ? round( $this->value(), 0 ) - : number_format( $this->value(), 2, '.', '' ), - ); + $amount = $this->money->to_array(); if ( $this->breakdown() && count( $this->breakdown()->to_array() ) ) { $amount['breakdown'] = $this->breakdown()->to_array(); } diff --git a/modules/ppcp-api-client/src/Entity/Item.php b/modules/ppcp-api-client/src/Entity/Item.php index 218bf3fa3..374c2efcb 100644 --- a/modules/ppcp-api-client/src/Entity/Item.php +++ b/modules/ppcp-api-client/src/Entity/Item.php @@ -14,7 +14,6 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Entity; */ class Item { - const PHYSICAL_GOODS = 'PHYSICAL_GOODS'; const DIGITAL_GOODS = 'DIGITAL_GOODS'; @@ -67,6 +66,13 @@ class Item { */ private $category; + /** + * The tax rate. + * + * @var float + */ + protected $tax_rate; + /** * Item constructor. * @@ -77,6 +83,7 @@ class Item { * @param Money|null $tax The tax. * @param string $sku The SKU. * @param string $category The category. + * @param float $tax_rate The tax rate. */ public function __construct( string $name, @@ -85,7 +92,8 @@ class Item { string $description = '', Money $tax = null, string $sku = '', - string $category = 'PHYSICAL_GOODS' + string $category = 'PHYSICAL_GOODS', + float $tax_rate = 0 ) { $this->name = $name; @@ -94,8 +102,9 @@ class Item { $this->description = $description; $this->tax = $tax; $this->sku = $sku; - $this->category = ( self::DIGITAL_GOODS === $category ) ? - self::DIGITAL_GOODS : self::PHYSICAL_GOODS; + $this->category = ( self::DIGITAL_GOODS === $category ) ? self::DIGITAL_GOODS : self::PHYSICAL_GOODS; + $this->category = $category; + $this->tax_rate = $tax_rate; } /** @@ -161,6 +170,15 @@ class Item { return $this->category; } + /** + * Returns the tax rate. + * + * @return float + */ + public function tax_rate():float { + return round( (float) $this->tax_rate, 2 ); + } + /** * Returns the object as array. * @@ -180,6 +198,10 @@ class Item { $item['tax'] = $this->tax()->to_array(); } + if ( $this->tax_rate() ) { + $item['tax_rate'] = (string) $this->tax_rate(); + } + return $item; } } diff --git a/modules/ppcp-api-client/src/Entity/Money.php b/modules/ppcp-api-client/src/Entity/Money.php index f1f3f848f..5ba49dc43 100644 --- a/modules/ppcp-api-client/src/Entity/Money.php +++ b/modules/ppcp-api-client/src/Entity/Money.php @@ -9,6 +9,8 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Entity; +use WooCommerce\PayPalCommerce\ApiClient\Helper\MoneyFormatter; + /** * Class Money */ @@ -29,11 +31,11 @@ class Money { private $value; /** - * Currencies that does not support decimals. + * The MoneyFormatter. * - * @var array + * @var MoneyFormatter */ - private $currencies_without_decimals = array( 'HUF', 'JPY', 'TWD' ); + private $money_formatter; /** * Money constructor. @@ -44,6 +46,8 @@ class Money { public function __construct( float $value, string $currency_code ) { $this->value = $value; $this->currency_code = $currency_code; + + $this->money_formatter = new MoneyFormatter(); } /** @@ -55,6 +59,15 @@ class Money { return $this->value; } + /** + * The value formatted as string for API requests. + * + * @return string + */ + public function value_str(): string { + return $this->money_formatter->format( $this->value, $this->currency_code ); + } + /** * The currency code. * @@ -72,9 +85,7 @@ class Money { public function to_array(): array { return array( 'currency_code' => $this->currency_code(), - 'value' => in_array( $this->currency_code(), $this->currencies_without_decimals, true ) - ? round( $this->value(), 0 ) - : number_format( $this->value(), 2, '.', '' ), + 'value' => $this->value_str(), ); } } diff --git a/modules/ppcp-api-client/src/Entity/OrderStatus.php b/modules/ppcp-api-client/src/Entity/OrderStatus.php index e8e33cba8..e2dec053b 100644 --- a/modules/ppcp-api-client/src/Entity/OrderStatus.php +++ b/modules/ppcp-api-client/src/Entity/OrderStatus.php @@ -15,21 +15,21 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; * Class OrderStatus */ class OrderStatus { - - - const INTERNAL = 'INTERNAL'; - const CREATED = 'CREATED'; - const SAVED = 'SAVED'; - const APPROVED = 'APPROVED'; - const VOIDED = 'VOIDED'; - const COMPLETED = 'COMPLETED'; - const VALID_STATI = array( + const INTERNAL = 'INTERNAL'; + const CREATED = 'CREATED'; + const SAVED = 'SAVED'; + const APPROVED = 'APPROVED'; + const VOIDED = 'VOIDED'; + const COMPLETED = 'COMPLETED'; + const PENDING_APPROVAL = 'PENDING_APPROVAL'; + const VALID_STATUS = array( self::INTERNAL, self::CREATED, self::SAVED, self::APPROVED, self::VOIDED, self::COMPLETED, + self::PENDING_APPROVAL, ); /** @@ -46,7 +46,7 @@ class OrderStatus { * @throws RuntimeException When the status is not valid. */ public function __construct( string $status ) { - if ( ! in_array( $status, self::VALID_STATI, true ) ) { + if ( ! in_array( $status, self::VALID_STATUS, true ) ) { throw new RuntimeException( sprintf( // translators: %s is the current status. diff --git a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php index 4f4c8b785..fd7973738 100644 --- a/modules/ppcp-api-client/src/Entity/PurchaseUnit.php +++ b/modules/ppcp-api-client/src/Entity/PurchaseUnit.php @@ -331,9 +331,9 @@ class PurchaseUnit { $remaining_item_total = array_reduce( $items, function ( float $total, Item $item ): float { - return $total - $item->unit_amount()->value() * (float) $item->quantity(); + return $total - (float) $item->unit_amount()->value_str() * (float) $item->quantity(); }, - $item_total->value() + (float) $item_total->value_str() ); $remaining_item_total = round( $remaining_item_total, 2 ); @@ -356,11 +356,11 @@ class PurchaseUnit { function ( float $total, Item $item ): float { $tax = $item->tax(); if ( $tax ) { - $total -= $tax->value() * (float) $item->quantity(); + $total -= (float) $tax->value_str() * (float) $item->quantity(); } return $total; }, - $tax_total->value() + (float) $tax_total->value_str() ); $remaining_tax_total = round( $remaining_tax_total, 2 ); @@ -378,29 +378,29 @@ class PurchaseUnit { $amount_total = 0.0; if ( $shipping ) { - $amount_total += $shipping->value(); + $amount_total += (float) $shipping->value_str(); } if ( $item_total ) { - $amount_total += $item_total->value(); + $amount_total += (float) $item_total->value_str(); } if ( $discount ) { - $amount_total -= $discount->value(); + $amount_total -= (float) $discount->value_str(); } if ( $tax_total ) { - $amount_total += $tax_total->value(); + $amount_total += (float) $tax_total->value_str(); } if ( $shipping_discount ) { - $amount_total -= $shipping_discount->value(); + $amount_total -= (float) $shipping_discount->value_str(); } if ( $handling ) { - $amount_total += $handling->value(); + $amount_total += (float) $handling->value_str(); } if ( $insurance ) { - $amount_total += $insurance->value(); + $amount_total += (float) $insurance->value_str(); } - $amount_str = (string) $amount->to_array()['value']; - $amount_total_str = (string) ( new Money( $amount_total, $amount->currency_code() ) )->to_array()['value']; + $amount_str = $amount->value_str(); + $amount_total_str = ( new Money( $amount_total, $amount->currency_code() ) )->value_str(); $needs_to_ditch = $amount_str !== $amount_total_str; return $needs_to_ditch; } diff --git a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php index 303b7bc6a..c2667fc92 100644 --- a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php +++ b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php @@ -152,11 +152,15 @@ class PurchaseUnitFactory { /** * Creates a PurchaseUnit based off a WooCommerce cart. * - * @param \WC_Cart $cart The cart. + * @param \WC_Cart|null $cart The cart. * * @return PurchaseUnit */ - public function from_wc_cart( \WC_Cart $cart ): PurchaseUnit { + public function from_wc_cart( ?\WC_Cart $cart = null ): PurchaseUnit { + if ( ! $cart ) { + $cart = WC()->cart ?? new \WC_Cart(); + } + $amount = $this->amount_factory->from_wc_cart( $cart ); $items = array_filter( $this->item_factory->from_wc_cart( $cart ), diff --git a/modules/ppcp-api-client/src/Factory/ShippingPreferenceFactory.php b/modules/ppcp-api-client/src/Factory/ShippingPreferenceFactory.php new file mode 100644 index 000000000..0b433d6c4 --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/ShippingPreferenceFactory.php @@ -0,0 +1,64 @@ +contains_physical_goods(); + if ( ! $contains_physical_goods ) { + return ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING; + } + + $has_shipping = null !== $purchase_unit->shipping(); + $needs_shipping = $cart && $cart->needs_shipping(); + $shipping_address_is_fixed = $needs_shipping && 'checkout' === $context; + + if ( $shipping_address_is_fixed ) { + // Checkout + no address given? Probably something weird happened, like no form validation? + if ( ! $has_shipping ) { + return ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING; + } + + return ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS; + } + + if ( 'card' === $funding_source ) { + if ( ! $has_shipping ) { + return ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING; + } + // Looks like GET_FROM_FILE does not work for the vaulted card button. + return ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS; + } + + return ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE; + } +} diff --git a/modules/ppcp-api-client/src/Helper/Cache.php b/modules/ppcp-api-client/src/Helper/Cache.php index e31dd14a3..c5b79dc5c 100644 --- a/modules/ppcp-api-client/src/Helper/Cache.php +++ b/modules/ppcp-api-client/src/Helper/Cache.php @@ -67,10 +67,11 @@ class Cache { * * @param string $key The key under which the value should be cached. * @param mixed $value The value to cache. + * @param int $expiration Time until expiration in seconds. * * @return bool */ - public function set( string $key, $value ): bool { - return (bool) set_transient( $this->prefix . $key, $value ); + public function set( string $key, $value, int $expiration = 0 ): bool { + return (bool) set_transient( $this->prefix . $key, $value, $expiration ); } } diff --git a/modules/ppcp-api-client/src/Helper/MoneyFormatter.php b/modules/ppcp-api-client/src/Helper/MoneyFormatter.php new file mode 100644 index 000000000..447ba0a0e --- /dev/null +++ b/modules/ppcp-api-client/src/Helper/MoneyFormatter.php @@ -0,0 +1,36 @@ +currencies_without_decimals, true ) + ? (string) round( $value, 0 ) + : number_format( $value, 2, '.', '' ); + } +} diff --git a/modules/ppcp-api-client/src/Repository/CartRepository.php b/modules/ppcp-api-client/src/Repository/CartRepository.php deleted file mode 100644 index 226f50d91..000000000 --- a/modules/ppcp-api-client/src/Repository/CartRepository.php +++ /dev/null @@ -1,45 +0,0 @@ -factory = $factory; - } - - /** - * Returns all Pur of the WooCommerce cart. - * - * @return PurchaseUnit[] - */ - public function all(): array { - $cart = WC()->cart ?? new \WC_Cart(); - return array( $this->factory->from_wc_cart( $cart ) ); - } -} diff --git a/modules/ppcp-api-client/src/Repository/PartnerReferralsData.php b/modules/ppcp-api-client/src/Repository/PartnerReferralsData.php index e2a13c949..d1ea64f64 100644 --- a/modules/ppcp-api-client/src/Repository/PartnerReferralsData.php +++ b/modules/ppcp-api-client/src/Repository/PartnerReferralsData.php @@ -72,53 +72,60 @@ class PartnerReferralsData { * @return array */ public function data(): array { - return array( - 'partner_config_override' => array( - 'partner_logo_url' => 'https://connect.woocommerce.com/images/woocommerce_logo.png', - /** - * Returns the URL which will be opened at the end of onboarding. - */ - 'return_url' => apply_filters( - 'woocommerce_paypal_payments_partner_config_override_return_url', - admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ) + /** + * Returns the partners referrals data. + */ + return apply_filters( + 'ppcp_partner_referrals_data', + array( + 'partner_config_override' => array( + 'partner_logo_url' => 'https://connect.woocommerce.com/images/woocommerce_logo.png', + /** + * Returns the URL which will be opened at the end of onboarding. + */ + 'return_url' => apply_filters( + 'woocommerce_paypal_payments_partner_config_override_return_url', + admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ) + ), + /** + * Returns the description of the URL which will be opened at the end of onboarding. + */ + 'return_url_description' => apply_filters( + 'woocommerce_paypal_payments_partner_config_override_return_url_description', + __( 'Return to your shop.', 'woocommerce-paypal-payments' ) + ), + 'show_add_credit_card' => true, ), - /** - * Returns the description of the URL which will be opened at the end of onboarding. - */ - 'return_url_description' => apply_filters( - 'woocommerce_paypal_payments_partner_config_override_return_url_description', - __( 'Return to your shop.', 'woocommerce-paypal-payments' ) + 'products' => $this->products, + 'legal_consents' => array( + array( + 'type' => 'SHARE_DATA_CONSENT', + 'granted' => true, + ), ), - 'show_add_credit_card' => true, - ), - 'products' => $this->products, - 'legal_consents' => array( - array( - 'type' => 'SHARE_DATA_CONSENT', - 'granted' => true, - ), - ), - 'operations' => array( - array( - 'operation' => 'API_INTEGRATION', - 'api_integration_preference' => array( - 'rest_api_integration' => array( - 'integration_method' => 'PAYPAL', - 'integration_type' => 'FIRST_PARTY', - 'first_party_details' => array( - 'features' => array( - 'PAYMENT', - 'FUTURE_PAYMENT', - 'REFUND', - 'ADVANCED_TRANSACTIONS_SEARCH', - 'VAULT', + 'operations' => array( + array( + 'operation' => 'API_INTEGRATION', + 'api_integration_preference' => array( + 'rest_api_integration' => array( + 'integration_method' => 'PAYPAL', + 'integration_type' => 'FIRST_PARTY', + 'first_party_details' => array( + 'features' => array( + 'PAYMENT', + 'FUTURE_PAYMENT', + 'REFUND', + 'ADVANCED_TRANSACTIONS_SEARCH', + 'VAULT', + 'TRACKING_SHIPMENT_READWRITE', + ), + 'seller_nonce' => $this->nonce(), ), - 'seller_nonce' => $this->nonce(), ), ), ), ), - ), + ) ); } } diff --git a/modules/ppcp-api-client/src/Repository/PurchaseUnitRepositoryInterface.php b/modules/ppcp-api-client/src/Repository/PurchaseUnitRepositoryInterface.php deleted file mode 100644 index 6edbb1bbf..000000000 --- a/modules/ppcp-api-client/src/Repository/PurchaseUnitRepositoryInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - { const onSmartButtonClick = (data, actions) => { window.ppcpFundingSource = data.fundingSource; - // TODO: quick fix to get the error about empty form before attempting PayPal order - // it should solve #513 for most of the users, but proper solution should be implemented later. - const requiredFields = jQuery('form.woocommerce-checkout .validate-required:visible :input'); - requiredFields.each((i, input) => { - jQuery(input).trigger('validate'); - }); - if (jQuery('form.woocommerce-checkout .woocommerce-invalid:visible').length) { - errorHandler.clear(); - errorHandler.message(PayPalCommerceGateway.labels.error.js_validation); + if (PayPalCommerceGateway.basic_checkout_validation_enabled) { + // TODO: quick fix to get the error about empty form before attempting PayPal order + // it should solve #513 for most of the users, but proper solution should be implemented later. + const requiredFields = jQuery('form.woocommerce-checkout .validate-required:visible :input'); + requiredFields.each((i, input) => { + jQuery(input).trigger('validate'); + }); + if (jQuery('form.woocommerce-checkout .validate-required.woocommerce-invalid:visible').length) { + errorHandler.clear(); + errorHandler.message(PayPalCommerceGateway.labels.error.js_validation); - return actions.reject(); + return actions.reject(); + } } const form = document.querySelector('form.woocommerce-checkout'); diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js index f1dbd5477..6da17f4e9 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -41,7 +41,7 @@ class SingleProductBootstap { } - priceAmountIsZero() { + priceAmount() { let priceText = "0"; if (document.querySelector('form.cart ins .woocommerce-Price-amount')) { @@ -55,9 +55,12 @@ class SingleProductBootstap { } priceText = priceText.replace(/,/g, '.'); - const amount = parseFloat(priceText.replace(/([^\d,\.\s]*)/g, '')); - return amount === 0; + return parseFloat(priceText.replace(/([^\d,\.\s]*)/g, '')); + } + + priceAmountIsZero() { + return this.priceAmount() === 0; } render() { @@ -70,19 +73,12 @@ class SingleProductBootstap { () => { this.renderer.showButtons(this.gateway.button.wrapper); this.renderer.showButtons(this.gateway.hosted_fields.wrapper); - let priceText = "0"; - if (document.querySelector('form.cart ins .woocommerce-Price-amount')) { - priceText = document.querySelector('form.cart ins .woocommerce-Price-amount').innerText; - } - else if (document.querySelector('form.cart .woocommerce-Price-amount')) { - priceText = document.querySelector('form.cart .woocommerce-Price-amount').innerText; - } - const amount = parseInt(priceText.replace(/([^\d,\.\s]*)/g, '')); - this.messages.renderWithAmount(amount) + this.messages.renderWithAmount(this.priceAmount()) }, () => { this.renderer.hideButtons(this.gateway.button.wrapper); this.renderer.hideButtons(this.gateway.hosted_fields.wrapper); + this.messages.hideMessages(); }, document.querySelector('form.cart'), new ErrorHandler(this.gateway.labels.error.generic), diff --git a/modules/ppcp-button/resources/js/modules/Helper/DccInputFactory.js b/modules/ppcp-button/resources/js/modules/Helper/DccInputFactory.js index c01969269..c5a2034fc 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/DccInputFactory.js +++ b/modules/ppcp-button/resources/js/modules/Helper/DccInputFactory.js @@ -1,9 +1,12 @@ const dccInputFactory = (original) => { const styles = window.getComputedStyle(original); const newElement = document.createElement('span'); + newElement.setAttribute('id', original.id); + newElement.setAttribute('class', original.className); + Object.values(styles).forEach( (prop) => { - if (! styles[prop] || ! isNaN(prop) ) { + if (! styles[prop] || ! isNaN(prop) || prop === 'background-image' ) { return; } newElement.style.setProperty(prop,'' + styles[prop]); @@ -11,4 +14,4 @@ const dccInputFactory = (original) => { return newElement; } -export default dccInputFactory; \ No newline at end of file +export default dccInputFactory; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js index e2c08a483..7e0b5a235 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/CreditCardRenderer.js @@ -1,5 +1,6 @@ import dccInputFactory from "../Helper/DccInputFactory"; import {show} from "../Helper/Hiding"; +import Product from "../Entity/Product"; class CreditCardRenderer { @@ -117,11 +118,23 @@ class CreditCardRenderer { } const validCards = this.defaultConfig.hosted_fields.valid_cards; this.cardValid = validCards.indexOf(event.cards[0].type) !== -1; + + const className = this._cardNumberFiledCLassNameByCardType(event.cards[0].type); + this._recreateElementClassAttribute(cardNumber, cardNumberField.className); + if (event.fields.number.isValid) { + cardNumber.classList.add(className); + } }) hostedFields.on('validityChange', (event) => { const formValid = Object.keys(event.fields).every(function (key) { return event.fields[key].isValid; }); + + const className = this._cardNumberFiledCLassNameByCardType(event.cards[0].type); + event.fields.number.isValid + ? cardNumber.classList.add(className) + : this._recreateElementClassAttribute(cardNumber, cardNumberField.className); + this.formValid = formValid; }); @@ -230,5 +243,14 @@ class CreditCardRenderer { this.errorHandler.message(message); } } + + _cardNumberFiledCLassNameByCardType(cardType) { + return cardType === 'american-express' ? 'amex' : cardType.replace('-', ''); + } + + _recreateElementClassAttribute(element, newClassName) { + element.removeAttribute('class') + element.setAttribute('class', newClassName); + } } export default CreditCardRenderer; diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index f8b6c415e..3b4b710fd 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -27,7 +27,7 @@ use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; return array( - 'button.client_id' => static function ( ContainerInterface $container ): string { + 'button.client_id' => static function ( ContainerInterface $container ): string { $settings = $container->get( 'wcgateway.settings' ); $client_id = $settings->has( 'client_id' ) ? $settings->get( 'client_id' ) : ''; @@ -45,7 +45,7 @@ return array( return $env->current_environment_is( Environment::SANDBOX ) ? CONNECT_WOO_SANDBOX_CLIENT_ID : CONNECT_WOO_CLIENT_ID; }, - 'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface { + 'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface { $state = $container->get( 'onboarding.state' ); /** @@ -88,33 +88,33 @@ return array( $settings_status, $currency, $container->get( 'wcgateway.all-funding-sources' ), + $container->get( 'button.basic-checkout-validation-enabled' ), $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'button.url' => static function ( ContainerInterface $container ): string { + 'button.url' => static function ( ContainerInterface $container ): string { return plugins_url( '/modules/ppcp-button/', dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ); }, - 'button.request-data' => static function ( ContainerInterface $container ): RequestData { + 'button.request-data' => static function ( ContainerInterface $container ): RequestData { return new RequestData(); }, - 'button.endpoint.change-cart' => static function ( ContainerInterface $container ): ChangeCartEndpoint { + 'button.endpoint.change-cart' => static function ( ContainerInterface $container ): ChangeCartEndpoint { if ( ! \WC()->cart ) { throw new RuntimeException( 'cant initialize endpoint at this moment' ); } $cart = WC()->cart; $shipping = WC()->shipping(); $request_data = $container->get( 'button.request-data' ); - $repository = $container->get( 'api.repository.cart' ); + $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' ); $data_store = \WC_Data_Store::load( 'product' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); - return new ChangeCartEndpoint( $cart, $shipping, $request_data, $repository, $data_store, $logger ); + return new ChangeCartEndpoint( $cart, $shipping, $request_data, $purchase_unit_factory, $data_store, $logger ); }, - 'button.endpoint.create-order' => static function ( ContainerInterface $container ): CreateOrderEndpoint { + 'button.endpoint.create-order' => static function ( ContainerInterface $container ): CreateOrderEndpoint { $request_data = $container->get( 'button.request-data' ); - $cart_repository = $container->get( 'api.repository.cart' ); $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' ); $order_endpoint = $container->get( 'api.endpoint.order' ); $payer_factory = $container->get( 'api.factory.payer' ); @@ -125,8 +125,8 @@ return array( $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new CreateOrderEndpoint( $request_data, - $cart_repository, $purchase_unit_factory, + $container->get( 'api.factory.shipping-preference' ), $order_endpoint, $payer_factory, $session_handler, @@ -136,7 +136,7 @@ return array( $logger ); }, - 'button.helper.early-order-handler' => static function ( ContainerInterface $container ) : EarlyOrderHandler { + 'button.helper.early-order-handler' => static function ( ContainerInterface $container ) : EarlyOrderHandler { $state = $container->get( 'onboarding.state' ); $order_processor = $container->get( 'wcgateway.order-processor' ); @@ -144,7 +144,7 @@ return array( $prefix = $container->get( 'api.prefix' ); return new EarlyOrderHandler( $state, $order_processor, $session_handler, $prefix ); }, - 'button.endpoint.approve-order' => static function ( ContainerInterface $container ): ApproveOrderEndpoint { + 'button.endpoint.approve-order' => static function ( ContainerInterface $container ): ApproveOrderEndpoint { $request_data = $container->get( 'button.request-data' ); $order_endpoint = $container->get( 'api.endpoint.order' ); $session_handler = $container->get( 'session.handler' ); @@ -164,7 +164,7 @@ return array( $logger ); }, - 'button.endpoint.data-client-id' => static function( ContainerInterface $container ) : DataClientIdEndpoint { + 'button.endpoint.data-client-id' => static function( ContainerInterface $container ) : DataClientIdEndpoint { $request_data = $container->get( 'button.request-data' ); $identity_token = $container->get( 'api.endpoint.identity-token' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); @@ -174,31 +174,39 @@ return array( $logger ); }, - 'button.endpoint.vault-paypal' => static function( ContainerInterface $container ) : StartPayPalVaultingEndpoint { + 'button.endpoint.vault-paypal' => static function( ContainerInterface $container ) : StartPayPalVaultingEndpoint { return new StartPayPalVaultingEndpoint( $container->get( 'button.request-data' ), $container->get( 'api.endpoint.payment-token' ), $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure { + 'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure { $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new ThreeDSecure( $logger ); }, - 'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply { + 'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply { return new MessagesApply( $container->get( 'api.shop.country' ) ); }, - 'button.is-logged-in' => static function ( ContainerInterface $container ): bool { + 'button.is-logged-in' => static function ( ContainerInterface $container ): bool { return is_user_logged_in(); }, - 'button.registration-required' => static function ( ContainerInterface $container ): bool { + 'button.registration-required' => static function ( ContainerInterface $container ): bool { return WC()->checkout()->is_registration_required(); }, - 'button.current-user-must-register' => static function ( ContainerInterface $container ): bool { + 'button.current-user-must-register' => static function ( ContainerInterface $container ): bool { return ! $container->get( 'button.is-logged-in' ) && $container->get( 'button.registration-required' ); }, + + 'button.basic-checkout-validation-enabled' => static function ( ContainerInterface $container ): bool { + /** + * The filter allowing to disable the basic client-side validation of the checkout form + * when the PayPal button is clicked. + */ + return (bool) apply_filters( 'woocommerce_paypal_payments_basic_checkout_validation_enabled', true ); + }, ); diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 37a0955bf..cfeaf6798 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -144,6 +144,13 @@ class SmartButton implements SmartButtonInterface { */ private $all_funding_sources; + /** + * Whether the basic JS validation of the form iss enabled. + * + * @var bool + */ + private $basic_checkout_validation_enabled; + /** * The logger. * @@ -176,6 +183,7 @@ class SmartButton implements SmartButtonInterface { * @param SettingsStatus $settings_status The Settings status helper. * @param string $currency 3-letter currency code of the shop. * @param array $all_funding_sources All existing funding sources. + * @param bool $basic_checkout_validation_enabled Whether the basic JS validation of the form iss enabled. * @param LoggerInterface $logger The logger. */ public function __construct( @@ -194,25 +202,27 @@ class SmartButton implements SmartButtonInterface { SettingsStatus $settings_status, string $currency, array $all_funding_sources, + bool $basic_checkout_validation_enabled, LoggerInterface $logger ) { - $this->module_url = $module_url; - $this->version = $version; - $this->session_handler = $session_handler; - $this->settings = $settings; - $this->payer_factory = $payer_factory; - $this->client_id = $client_id; - $this->request_data = $request_data; - $this->dcc_applies = $dcc_applies; - $this->subscription_helper = $subscription_helper; - $this->messages_apply = $messages_apply; - $this->environment = $environment; - $this->payment_token_repository = $payment_token_repository; - $this->settings_status = $settings_status; - $this->currency = $currency; - $this->all_funding_sources = $all_funding_sources; - $this->logger = $logger; + $this->module_url = $module_url; + $this->version = $version; + $this->session_handler = $session_handler; + $this->settings = $settings; + $this->payer_factory = $payer_factory; + $this->client_id = $client_id; + $this->request_data = $request_data; + $this->dcc_applies = $dcc_applies; + $this->subscription_helper = $subscription_helper; + $this->messages_apply = $messages_apply; + $this->environment = $environment; + $this->payment_token_repository = $payment_token_repository; + $this->settings_status = $settings_status; + $this->currency = $currency; + $this->all_funding_sources = $all_funding_sources; + $this->basic_checkout_validation_enabled = $basic_checkout_validation_enabled; + $this->logger = $logger; } /** @@ -761,17 +771,17 @@ class SmartButton implements SmartButtonInterface { $this->request_data->enqueue_nonce_fix(); $localize = array( - 'script_attributes' => $this->attributes(), - 'data_client_id' => array( + 'script_attributes' => $this->attributes(), + 'data_client_id' => array( 'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(), 'endpoint' => \WC_AJAX::get_endpoint( DataClientIdEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( DataClientIdEndpoint::nonce() ), 'user' => get_current_user_id(), 'has_subscriptions' => $this->has_subscriptions(), ), - 'redirect' => wc_get_checkout_url(), - 'context' => $this->context(), - 'ajax' => array( + 'redirect' => wc_get_checkout_url(), + 'context' => $this->context(), + 'ajax' => array( 'change_cart' => array( 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), @@ -789,13 +799,13 @@ class SmartButton implements SmartButtonInterface { 'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ), ), ), - 'enforce_vault' => $this->has_subscriptions(), - 'can_save_vault_token' => $this->can_save_vault_token(), - 'is_free_trial_cart' => $is_free_trial_cart, - 'vaulted_paypal_email' => ( is_checkout() && $is_free_trial_cart ) ? $this->get_vaulted_paypal_email() : '', - 'bn_codes' => $this->bn_codes(), - 'payer' => $this->payerData(), - 'button' => array( + 'enforce_vault' => $this->has_subscriptions(), + 'can_save_vault_token' => $this->can_save_vault_token(), + 'is_free_trial_cart' => $is_free_trial_cart, + 'vaulted_paypal_email' => ( is_checkout() && $is_free_trial_cart ) ? $this->get_vaulted_paypal_email() : '', + 'bn_codes' => $this->bn_codes(), + 'payer' => $this->payerData(), + 'button' => array( 'wrapper' => '#ppc-button', 'mini_cart_wrapper' => '#ppc-button-minicart', 'cancel_wrapper' => '#ppcp-cancel', @@ -816,7 +826,7 @@ class SmartButton implements SmartButtonInterface { 'tagline' => $this->style_for_context( 'tagline', $this->context() ), ), ), - 'hosted_fields' => array( + 'hosted_fields' => array( 'wrapper' => '#ppcp-hosted-fields', 'mini_cart_wrapper' => '#ppcp-hosted-fields-mini-cart', 'labels' => array( @@ -836,8 +846,8 @@ class SmartButton implements SmartButtonInterface { 'valid_cards' => $this->dcc_applies->valid_cards(), 'contingency' => $this->get_3ds_contingency(), ), - 'messages' => $this->message_values(), - 'labels' => array( + 'messages' => $this->message_values(), + 'labels' => array( 'error' => array( 'generic' => __( 'Something went wrong. Please try again or choose another payment source.', @@ -849,9 +859,10 @@ class SmartButton implements SmartButtonInterface { ), ), ), - 'order_id' => 'pay-now' === $this->context() ? absint( $wp->query_vars['order-pay'] ) : 0, - 'single_product_buttons_enabled' => $this->settings->has( 'button_product_enabled' ) && $this->settings->get( 'button_product_enabled' ), - 'mini_cart_buttons_enabled' => $this->settings->has( 'button_mini-cart_enabled' ) && $this->settings->get( 'button_mini-cart_enabled' ), + 'order_id' => 'pay-now' === $this->context() ? absint( $wp->query_vars['order-pay'] ) : 0, + 'single_product_buttons_enabled' => $this->settings->has( 'button_product_enabled' ) && $this->settings->get( 'button_product_enabled' ), + 'mini_cart_buttons_enabled' => $this->settings->has( 'button_mini-cart_enabled' ) && $this->settings->get( 'button_mini-cart_enabled' ), + 'basic_checkout_validation_enabled' => $this->basic_checkout_validation_enabled, ); if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) { @@ -1036,6 +1047,7 @@ class SmartButton implements SmartButtonInterface { $this->context() === 'product' && $this->settings->has( 'button_product_enabled' ) && $this->settings->get( 'button_product_enabled' ) + || $this->settings->has( 'message_product_enabled' ) ) { $load_buttons = true; } @@ -1049,6 +1061,7 @@ class SmartButton implements SmartButtonInterface { $this->context() === 'cart' && $this->settings->has( 'button_cart_enabled' ) && $this->settings->get( 'button_cart_enabled' ) + || $this->settings->has( 'message_product_enabled' ) ) { $load_buttons = true; } diff --git a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php index 1ad4b451d..1c64ddb0d 100644 --- a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php @@ -13,7 +13,7 @@ use Exception; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; -use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository; +use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException; /** @@ -46,11 +46,11 @@ class ChangeCartEndpoint implements EndpointInterface { private $request_data; /** - * Contains purchase units based off the current WC cart. + * The PurchaseUnit factory. * - * @var CartRepository + * @var PurchaseUnitFactory */ - private $repository; + private $purchase_unit_factory; /** * The product data store. @@ -69,28 +69,28 @@ class ChangeCartEndpoint implements EndpointInterface { /** * ChangeCartEndpoint constructor. * - * @param \WC_Cart $cart The current WC cart object. - * @param \WC_Shipping $shipping The current WC shipping object. - * @param RequestData $request_data The request data helper. - * @param CartRepository $repository The repository for the current purchase items. - * @param \WC_Data_Store $product_data_store The data store for products. - * @param LoggerInterface $logger The logger. + * @param \WC_Cart $cart The current WC cart object. + * @param \WC_Shipping $shipping The current WC shipping object. + * @param RequestData $request_data The request data helper. + * @param PurchaseUnitFactory $purchase_unit_factory The PurchaseUnit factory. + * @param \WC_Data_Store $product_data_store The data store for products. + * @param LoggerInterface $logger The logger. */ public function __construct( \WC_Cart $cart, \WC_Shipping $shipping, RequestData $request_data, - CartRepository $repository, + PurchaseUnitFactory $purchase_unit_factory, \WC_Data_Store $product_data_store, LoggerInterface $logger ) { - $this->cart = $cart; - $this->shipping = $shipping; - $this->request_data = $request_data; - $this->repository = $repository; - $this->product_data_store = $product_data_store; - $this->logger = $logger; + $this->cart = $cart; + $this->shipping = $shipping; + $this->request_data = $request_data; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->product_data_store = $product_data_store; + $this->logger = $logger; } /** @@ -292,11 +292,7 @@ class ChangeCartEndpoint implements EndpointInterface { * @return array */ private function generate_purchase_units(): array { - return array_map( - static function ( PurchaseUnit $line_item ): array { - return $line_item->to_array(); - }, - $this->repository->all() - ); + $pu = $this->purchase_unit_factory->from_wc_cart(); + return array( $pu->to_array() ); } } diff --git a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php index 98e990b3c..c0045c8c4 100644 --- a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php @@ -23,7 +23,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; -use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; @@ -48,13 +48,6 @@ class CreateOrderEndpoint implements EndpointInterface { */ private $request_data; - /** - * The cart repository. - * - * @var CartRepository - */ - private $cart_repository; - /** * The PurchaseUnit factory. * @@ -62,6 +55,13 @@ class CreateOrderEndpoint implements EndpointInterface { */ private $purchase_unit_factory; + /** + * The shipping_preference factory. + * + * @var ShippingPreferenceFactory + */ + private $shipping_preference_factory; + /** * The order endpoint. * @@ -105,11 +105,11 @@ class CreateOrderEndpoint implements EndpointInterface { private $parsed_request_data; /** - * The array of purchase units for order. + * The purchase unit for order. * - * @var PurchaseUnit[] + * @var PurchaseUnit|null */ - private $purchase_units; + private $purchase_unit; /** * Whether a new user must be registered during checkout. @@ -128,21 +128,21 @@ class CreateOrderEndpoint implements EndpointInterface { /** * CreateOrderEndpoint constructor. * - * @param RequestData $request_data The RequestData object. - * @param CartRepository $cart_repository The CartRepository object. - * @param PurchaseUnitFactory $purchase_unit_factory The Purchaseunit factory. - * @param OrderEndpoint $order_endpoint The OrderEndpoint object. - * @param PayerFactory $payer_factory The PayerFactory object. - * @param SessionHandler $session_handler The SessionHandler object. - * @param Settings $settings The Settings object. - * @param EarlyOrderHandler $early_order_handler The EarlyOrderHandler object. - * @param bool $registration_needed Whether a new user must be registered during checkout. - * @param LoggerInterface $logger The logger. + * @param RequestData $request_data The RequestData object. + * @param PurchaseUnitFactory $purchase_unit_factory The PurchaseUnit factory. + * @param ShippingPreferenceFactory $shipping_preference_factory The shipping_preference factory. + * @param OrderEndpoint $order_endpoint The OrderEndpoint object. + * @param PayerFactory $payer_factory The PayerFactory object. + * @param SessionHandler $session_handler The SessionHandler object. + * @param Settings $settings The Settings object. + * @param EarlyOrderHandler $early_order_handler The EarlyOrderHandler object. + * @param bool $registration_needed Whether a new user must be registered during checkout. + * @param LoggerInterface $logger The logger. */ public function __construct( RequestData $request_data, - CartRepository $cart_repository, PurchaseUnitFactory $purchase_unit_factory, + ShippingPreferenceFactory $shipping_preference_factory, OrderEndpoint $order_endpoint, PayerFactory $payer_factory, SessionHandler $session_handler, @@ -152,16 +152,16 @@ class CreateOrderEndpoint implements EndpointInterface { LoggerInterface $logger ) { - $this->request_data = $request_data; - $this->cart_repository = $cart_repository; - $this->purchase_unit_factory = $purchase_unit_factory; - $this->api_endpoint = $order_endpoint; - $this->payer_factory = $payer_factory; - $this->session_handler = $session_handler; - $this->settings = $settings; - $this->early_order_handler = $early_order_handler; - $this->registration_needed = $registration_needed; - $this->logger = $logger; + $this->request_data = $request_data; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->api_endpoint = $order_endpoint; + $this->payer_factory = $payer_factory; + $this->session_handler = $session_handler; + $this->settings = $settings; + $this->early_order_handler = $early_order_handler; + $this->registration_needed = $registration_needed; + $this->logger = $logger; } /** @@ -198,9 +198,9 @@ class CreateOrderEndpoint implements EndpointInterface { ) ); } - $this->purchase_units = array( $this->purchase_unit_factory->from_wc_order( $wc_order ) ); + $this->purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); } else { - $this->purchase_units = $this->cart_repository->all(); + $this->purchase_unit = $this->purchase_unit_factory->from_wc_cart(); // The cart does not have any info about payment method, so we must handle free trial here. if ( ( @@ -209,10 +209,10 @@ class CreateOrderEndpoint implements EndpointInterface { ) && $this->is_free_trial_cart() ) { - $this->purchase_units[0]->set_amount( + $this->purchase_unit->set_amount( new Amount( - new Money( 1.0, $this->purchase_units[0]->amount()->currency_code() ), - $this->purchase_units[0]->amount()->breakdown() + new Money( 1.0, $this->purchase_unit->amount()->currency_code() ), + $this->purchase_unit->amount()->breakdown() ) ); } @@ -329,17 +329,22 @@ class CreateOrderEndpoint implements EndpointInterface { * 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']; + assert( $this->purchase_unit instanceof PurchaseUnit ); + + $shipping_preference = $this->shipping_preference_factory->from_state( + $this->purchase_unit, + $this->parsed_request_data['context'], + WC()->cart, + $this->parsed_request_data['funding_source'] ?? '' + ); try { return $this->api_endpoint->create( - $this->purchase_units, + array( $this->purchase_unit ), + $shipping_preference, $this->payer( $this->parsed_request_data, $wc_order ), null, - $this->payment_method(), - '', - $shipping_address_is_fix + $this->payment_method() ); } catch ( PayPalApiException $exception ) { // Looks like currently there is no proper way to validate the shipping address for PayPal, @@ -354,17 +359,14 @@ class CreateOrderEndpoint implements EndpointInterface { ) ) { $this->logger->info( 'Invalid shipping address for order creation, retrying without it.' ); - foreach ( $this->purchase_units as $purchase_unit ) { - $purchase_unit->set_shipping( null ); - } + $this->purchase_unit->set_shipping( null ); return $this->api_endpoint->create( - $this->purchase_units, + array( $this->purchase_unit ), + $shipping_preference, $this->payer( $this->parsed_request_data, $wc_order ), null, - $this->payment_method(), - '', - $shipping_address_is_fix + $this->payment_method() ); } @@ -449,12 +451,11 @@ class CreateOrderEndpoint implements EndpointInterface { /** * Checks whether the terms input field is checked. * - * @param string $form_values The form values. + * @param array $form_fields The form fields. * @throws \RuntimeException When field is not checked. */ - private function validate_paynow_form( string $form_values ) { - $parsed_values = wp_parse_args( $form_values ); - if ( isset( $parsed_values['terms-field'] ) && ! isset( $parsed_values['terms'] ) ) { + private function validate_paynow_form( array $form_fields ) { + if ( isset( $form_fields['terms-field'] ) && ! isset( $form_fields['terms'] ) ) { throw new \RuntimeException( __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce-paypal-payments' ) ); diff --git a/modules/ppcp-onboarding/assets/js/onboarding.js b/modules/ppcp-onboarding/assets/js/onboarding.js index 0753ad943..5671938e4 100644 --- a/modules/ppcp-onboarding/assets/js/onboarding.js +++ b/modules/ppcp-onboarding/assets/js/onboarding.js @@ -70,6 +70,38 @@ const ppcp_onboarding = { }, 1000 ); + + const onboard_pui = document.querySelector('#ppcp-onboarding-pui'); + onboard_pui?.addEventListener('click', (event) => { + event.preventDefault(); + buttons.forEach((element) => { + element.removeAttribute('href'); + }); + + fetch(PayPalCommerceGatewayOnboarding.pui_endpoint, { + method: 'POST', + body: JSON.stringify({ + nonce: PayPalCommerceGatewayOnboarding.pui_nonce, + checked: onboard_pui.checked + }) + }).then((res)=>{ + return res.json(); + }).then((data)=>{ + if (!data.success) { + alert('Could not update signup buttons: ' + JSON.stringify(data)); + return; + } + + buttons.forEach((element) => { + for (let [key, value] of Object.entries(data.data.signup_links)) { + key = 'connect-to' + key.replace(/-/g, ''); + if(key === element.id) { + element.setAttribute('href', value); + } + } + }); + }); + }) }, loginSeller: function(env, authCode, sharedId) { diff --git a/modules/ppcp-onboarding/services.php b/modules/ppcp-onboarding/services.php index 5bbbbd6a9..82f26e26c 100644 --- a/modules/ppcp-onboarding/services.php +++ b/modules/ppcp-onboarding/services.php @@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets; use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint; +use WooCommerce\PayPalCommerce\Onboarding\Endpoint\PayUponInvoiceEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController; @@ -186,6 +187,16 @@ return array( $logger ); }, + 'onboarding.endpoint.pui' => static function( ContainerInterface $container ) : PayUponInvoiceEndpoint { + return new PayUponInvoiceEndpoint( + $container->get( 'wcgateway.settings' ), + $container->get( 'button.request-data' ), + $container->get( 'onboarding.signup-link-cache' ), + $container->get( 'onboarding.render' ), + $container->get( 'onboarding.signup-link-ids' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'api.endpoint.partner-referrals-sandbox' => static function ( ContainerInterface $container ) : PartnerReferrals { return new PartnerReferrals( @@ -202,23 +213,36 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, + 'onboarding.signup-link-cache' => static function( ContainerInterface $container ): Cache { + return new Cache( 'ppcp-paypal-signup-link' ); + }, + 'onboarding.signup-link-ids' => static function ( ContainerInterface $container ): array { + return array( + 'production-ppcp', + 'production-express_checkout', + 'sandbox-ppcp', + 'sandbox-express_checkout', + ); + }, 'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer { - $partner_referrals = $container->get( 'api.endpoint.partner-referrals-production' ); $partner_referrals_sandbox = $container->get( 'api.endpoint.partner-referrals-sandbox' ); $partner_referrals_data = $container->get( 'api.repository.partner-referrals-data' ); $settings = $container->get( 'wcgateway.settings' ); + $signup_link_cache = $container->get( 'onboarding.signup-link-cache' ); return new OnboardingRenderer( $settings, $partner_referrals, $partner_referrals_sandbox, - $partner_referrals_data + $partner_referrals_data, + $signup_link_cache ); }, 'onboarding.render-options' => static function ( ContainerInterface $container ) : OnboardingOptionsRenderer { return new OnboardingOptionsRenderer( $container->get( 'onboarding.url' ), - $container->get( 'api.shop.country' ) + $container->get( 'api.shop.country' ), + $container->get( 'wcgateway.settings' ) ); }, 'onboarding.rest' => static function( $container ) : OnboardingRESTController { diff --git a/modules/ppcp-onboarding/src/Assets/OnboardingAssets.php b/modules/ppcp-onboarding/src/Assets/OnboardingAssets.php index 6e73c3bac..8c65cc803 100644 --- a/modules/ppcp-onboarding/src/Assets/OnboardingAssets.php +++ b/modules/ppcp-onboarding/src/Assets/OnboardingAssets.php @@ -145,6 +145,8 @@ class OnboardingAssets { 'error_messages' => array( 'no_credentials' => __( 'API credentials must be entered to save the settings.', 'woocommerce-paypal-payments' ), ), + 'pui_endpoint' => \WC_AJAX::get_endpoint( 'ppc-pui' ), + 'pui_nonce' => wp_create_nonce( 'ppc-pui' ), ); } diff --git a/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php b/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php index b254d6947..b7a6cdddb 100644 --- a/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php +++ b/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php @@ -127,6 +127,7 @@ class LoginSellerEndpoint implements EndpointInterface { $is_sandbox = isset( $data['env'] ) && 'sandbox' === $data['env']; $this->settings->set( 'sandbox_on', $is_sandbox ); $this->settings->set( 'products_dcc_enabled', null ); + $this->settings->set( 'products_pui_enabled', null ); $this->settings->persist(); $endpoint = $is_sandbox ? $this->login_seller_sandbox : $this->login_seller_production; @@ -144,6 +145,7 @@ class LoginSellerEndpoint implements EndpointInterface { } $this->settings->set( 'client_secret', $credentials->client_secret ); $this->settings->set( 'client_id', $credentials->client_id ); + $this->settings->persist(); $accept_cards = (bool) ( $data['acceptCards'] ?? true ); $funding_sources = array(); diff --git a/modules/ppcp-onboarding/src/Endpoint/PayUponInvoiceEndpoint.php b/modules/ppcp-onboarding/src/Endpoint/PayUponInvoiceEndpoint.php new file mode 100644 index 000000000..695e54b83 --- /dev/null +++ b/modules/ppcp-onboarding/src/Endpoint/PayUponInvoiceEndpoint.php @@ -0,0 +1,143 @@ +settings = $settings; + $this->request_data = $request_data; + $this->signup_link_cache = $signup_link_cache; + $this->onboarding_renderer = $onboarding_renderer; + $this->logger = $logger; + $this->signup_link_ids = $signup_link_ids; + } + + /** + * The nonce. + * + * @return string + */ + public static function nonce(): string { + return 'ppc-pui'; + } + + /** + * Handles the request. + * + * @return bool + * @throws NotFoundException When order not found or handling failed. + */ + public function handle_request(): bool { + $signup_links = array(); + + try { + $data = $this->request_data->read_request( $this->nonce() ); + $this->settings->set( 'ppcp-onboarding-pui', $data['checked'] ); + $this->settings->persist(); + + foreach ( $this->signup_link_ids as $key ) { + if ( $this->signup_link_cache->has( $key ) ) { + $this->signup_link_cache->delete( $key ); + } + } + + foreach ( $this->signup_link_ids as $key ) { + $parts = explode( '-', $key ); + $is_production = 'production' === $parts[0]; + $products = 'ppcp' === $parts[1] ? array( 'PPCP' ) : array( 'EXPRESS_CHECKOUT' ); + $signup_links[ $key ] = $this->onboarding_renderer->get_signup_link( $is_production, $products ); + } + } catch ( Exception $exception ) { + $this->logger->error( $exception->getMessage() ); + } + + wp_send_json_success( + array( + 'onboarding_pui' => $this->settings->get( 'ppcp-onboarding-pui' ), + 'signup_links' => $signup_links, + ) + ); + + return true; + } +} + diff --git a/modules/ppcp-onboarding/src/OnboardingModule.php b/modules/ppcp-onboarding/src/OnboardingModule.php index 0960c7b6c..767d47839 100644 --- a/modules/ppcp-onboarding/src/OnboardingModule.php +++ b/modules/ppcp-onboarding/src/OnboardingModule.php @@ -96,6 +96,14 @@ class OnboardingModule implements ModuleInterface { } ); + add_action( + 'wc_ajax_ppc-pui', + static function () use ( $c ) { + $endpoint = $c->get( 'onboarding.endpoint.pui' ); + $endpoint->handle_request(); + } + ); + // Initialize REST routes at the appropriate time. $rest_controller = $c->get( 'onboarding.rest' ); add_action( 'rest_api_init', array( $rest_controller, 'register_routes' ) ); diff --git a/modules/ppcp-onboarding/src/OnboardingRESTController.php b/modules/ppcp-onboarding/src/OnboardingRESTController.php index 58d506315..9fd59e473 100644 --- a/modules/ppcp-onboarding/src/OnboardingRESTController.php +++ b/modules/ppcp-onboarding/src/OnboardingRESTController.php @@ -236,6 +236,7 @@ class OnboardingRESTController { } $settings->set( 'products_dcc_enabled', null ); + $settings->set( 'products_pui_enabled', null ); if ( ! $settings->persist() ) { return new \WP_Error( diff --git a/modules/ppcp-onboarding/src/Render/OnboardingOptionsRenderer.php b/modules/ppcp-onboarding/src/Render/OnboardingOptionsRenderer.php index ce0d20b3f..c760b2f2c 100644 --- a/modules/ppcp-onboarding/src/Render/OnboardingOptionsRenderer.php +++ b/modules/ppcp-onboarding/src/Render/OnboardingOptionsRenderer.php @@ -9,6 +9,9 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Onboarding\Render; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; +use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; + /** * Class OnboardingRenderer */ @@ -27,15 +30,24 @@ class OnboardingOptionsRenderer { */ private $country; + /** + * The settings. + * + * @var Settings + */ + protected $settings; + /** * OnboardingOptionsRenderer constructor. * - * @param string $module_url The module url (for assets). - * @param string $country 2-letter country code of the shop. + * @param string $module_url The module url (for assets). + * @param string $country 2-letter country code of the shop. + * @param Settings $settings The settings. */ - public function __construct( string $module_url, string $country ) { + public function __construct( string $module_url, string $country, Settings $settings ) { $this->module_url = $module_url; $this->country = $country; + $this->settings = $settings; } /** @@ -56,8 +68,29 @@ class OnboardingOptionsRenderer { __( 'Securely accept all major credit & debit cards on the strength of the PayPal network', 'woocommerce-paypal-payments' ) . ' -
  • ' . $this->render_dcc( $is_shop_supports_dcc ) . '
  • -'; +
  • ' . $this->render_dcc( $is_shop_supports_dcc ) . '
  • ' . + $this->render_pui_option() + . ''; + } + + /** + * Renders pui option. + * + * @return string + * @throws NotFoundException When setting is not found. + */ + private function render_pui_option(): string { + if ( 'DE' === $this->country ) { + $checked = 'checked'; + if ( $this->settings->has( 'ppcp-onboarding-pui' ) && $this->settings->get( 'ppcp-onboarding-pui' ) !== '1' ) { + $checked = ''; + } + return '
  • '; + } + + return ''; } /** diff --git a/modules/ppcp-onboarding/src/Render/OnboardingRenderer.php b/modules/ppcp-onboarding/src/Render/OnboardingRenderer.php index 95c0c46a4..8bbbc238a 100644 --- a/modules/ppcp-onboarding/src/Render/OnboardingRenderer.php +++ b/modules/ppcp-onboarding/src/Render/OnboardingRenderer.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Onboarding\Render; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; @@ -47,6 +48,13 @@ class OnboardingRenderer { */ private $partner_referrals_data; + /** + * The cache + * + * @var Cache + */ + protected $cache; + /** * OnboardingRenderer constructor. * @@ -54,17 +62,20 @@ class OnboardingRenderer { * @param PartnerReferrals $production_partner_referrals The PartnerReferrals for production. * @param PartnerReferrals $sandbox_partner_referrals The PartnerReferrals for sandbox. * @param PartnerReferralsData $partner_referrals_data The default partner referrals data. + * @param Cache $cache The cache. */ public function __construct( Settings $settings, PartnerReferrals $production_partner_referrals, PartnerReferrals $sandbox_partner_referrals, - PartnerReferralsData $partner_referrals_data + PartnerReferralsData $partner_referrals_data, + Cache $cache ) { $this->settings = $settings; $this->production_partner_referrals = $production_partner_referrals; $this->sandbox_partner_referrals = $sandbox_partner_referrals; $this->partner_referrals_data = $partner_referrals_data; + $this->cache = $cache; } /** @@ -83,9 +94,17 @@ class OnboardingRenderer { ->with_products( $products ) ->data(); + $environment = $is_production ? 'production' : 'sandbox'; + $product = 'PPCP' === $data['products'][0] ? 'ppcp' : 'express_checkout'; + if ( $this->cache->has( $environment . '-' . $product ) ) { + return $this->cache->get( $environment . '-' . $product ); + } + $url = $is_production ? $this->production_partner_referrals->signup_link( $data ) : $this->sandbox_partner_referrals->signup_link( $data ); $url = add_query_arg( $args, $url ); + $this->cache->set( $environment . '-' . $product, $url, 3 * MONTH_IN_SECONDS ); + return $url; } diff --git a/modules/ppcp-subscription/services.php b/modules/ppcp-subscription/services.php index b4f25c52d..026d26965 100644 --- a/modules/ppcp-subscription/services.php +++ b/modules/ppcp-subscription/services.php @@ -29,6 +29,7 @@ return array( $repository, $endpoint, $purchase_unit_factory, + $container->get( 'api.factory.shipping-preference' ), $payer_factory, $environment ); diff --git a/modules/ppcp-subscription/src/RenewalHandler.php b/modules/ppcp-subscription/src/RenewalHandler.php index 2103e443d..eebd76533 100644 --- a/modules/ppcp-subscription/src/RenewalHandler.php +++ b/modules/ppcp-subscription/src/RenewalHandler.php @@ -13,6 +13,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use Psr\Log\LoggerInterface; @@ -58,6 +59,13 @@ class RenewalHandler { */ private $purchase_unit_factory; + /** + * The shipping_preference factory. + * + * @var ShippingPreferenceFactory + */ + private $shipping_preference_factory; + /** * The payer factory. * @@ -75,28 +83,31 @@ class RenewalHandler { /** * RenewalHandler constructor. * - * @param LoggerInterface $logger The logger. - * @param PaymentTokenRepository $repository The payment token repository. - * @param OrderEndpoint $order_endpoint The order endpoint. - * @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory. - * @param PayerFactory $payer_factory The payer factory. - * @param Environment $environment The environment. + * @param LoggerInterface $logger The logger. + * @param PaymentTokenRepository $repository The payment token repository. + * @param OrderEndpoint $order_endpoint The order endpoint. + * @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory. + * @param ShippingPreferenceFactory $shipping_preference_factory The shipping_preference factory. + * @param PayerFactory $payer_factory The payer factory. + * @param Environment $environment The environment. */ public function __construct( LoggerInterface $logger, PaymentTokenRepository $repository, OrderEndpoint $order_endpoint, PurchaseUnitFactory $purchase_unit_factory, + ShippingPreferenceFactory $shipping_preference_factory, PayerFactory $payer_factory, Environment $environment ) { - $this->logger = $logger; - $this->repository = $repository; - $this->order_endpoint = $order_endpoint; - $this->purchase_unit_factory = $purchase_unit_factory; - $this->payer_factory = $payer_factory; - $this->environment = $environment; + $this->logger = $logger; + $this->repository = $repository; + $this->order_endpoint = $order_endpoint; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->payer_factory = $payer_factory; + $this->environment = $environment; } /** @@ -133,7 +144,7 @@ class RenewalHandler { * * @throws \Exception If customer cannot be read/found. */ - private function process_order( \WC_Order $wc_order ) { + private function process_order( \WC_Order $wc_order ): void { $user_id = (int) $wc_order->get_customer_id(); $customer = new \WC_Customer( $user_id ); @@ -141,11 +152,16 @@ class RenewalHandler { if ( ! $token ) { return; } - $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); - $payer = $this->payer_factory->from_customer( $customer ); + $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); + $payer = $this->payer_factory->from_customer( $customer ); + $shipping_preference = $this->shipping_preference_factory->from_state( + $purchase_unit, + 'renewal' + ); $order = $this->order_endpoint->create( array( $purchase_unit ), + $shipping_preference, $payer, $token ); diff --git a/modules/ppcp-wc-gateway/resources/js/pay-upon-invoice.js b/modules/ppcp-wc-gateway/resources/js/pay-upon-invoice.js new file mode 100644 index 000000000..dcf30dc03 --- /dev/null +++ b/modules/ppcp-wc-gateway/resources/js/pay-upon-invoice.js @@ -0,0 +1,55 @@ +window.addEventListener('load', function() { + + function _loadBeaconJS(options) { + var script = document.createElement('script'); + script.src = options.fnUrl; + document.body.appendChild(script); + } + + function _injectConfig() { + var script = document.querySelector("[fncls='fnparams-dede7cc5-15fd-4c75-a9f4-36c430ee3a99']"); + if (script) { + if (script.parentNode) { + script.parentNode.removeChild(script); + } + } + + script = document.createElement('script'); + script.id = 'fconfig'; + script.type = 'application/json'; + script.setAttribute('fncls', 'fnparams-dede7cc5-15fd-4c75-a9f4-36c430ee3a99'); + + var configuration = { + 'f': FraudNetConfig.f, + 's': FraudNetConfig.s + }; + if(FraudNetConfig.sandbox === '1') { + configuration.sandbox = true; + } + + script.text = JSON.stringify(configuration); + document.body.appendChild(script); + + const payForOrderForm = document.forms.order_review; + if(payForOrderForm) { + const puiPayForOrderSessionId = document.createElement('input'); + puiPayForOrderSessionId.setAttribute('type', 'hidden'); + puiPayForOrderSessionId.setAttribute('name', 'pui_pay_for_order_session_id'); + puiPayForOrderSessionId.setAttribute('value', FraudNetConfig.f); + payForOrderForm.appendChild(puiPayForOrderSessionId); + } + + _loadBeaconJS({fnUrl: "https://c.paypal.com/da/r/fb.js"}) + } + + document.addEventListener('hosted_fields_loaded', (event) => { + if (PAYPAL.asyncData && typeof PAYPAL.asyncData.initAndCollect === 'function') { + PAYPAL.asyncData.initAndCollect() + } + + _injectConfig(); + }); + + _injectConfig(); +}) + diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index ed6d3699a..a9e8f44a0 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway; use Psr\Container\ContainerInterface; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PayUponInvoiceOrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; @@ -30,8 +31,16 @@ use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\FraudNet; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\FraudNetSessionId; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\FraudNetSourceWebsiteId; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PaymentSourceFactory; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoice; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider; use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus; +use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper; +use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; @@ -46,7 +55,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage; return array( - 'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway { + 'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway { $order_processor = $container->get( 'wcgateway.order-processor' ); $settings_renderer = $container->get( 'wcgateway.settings.render' ); $funding_source_renderer = $container->get( 'wcgateway.funding-source.renderer' ); @@ -63,6 +72,7 @@ return array( $order_endpoint = $container->get( 'api.endpoint.order' ); $environment = $container->get( 'onboarding.environment' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); + $api_shop_country = $container->get( 'api.shop.country' ); return new PayPalGateway( $settings_renderer, $funding_source_renderer, @@ -77,12 +87,14 @@ return array( $page_id, $environment, $payment_token_repository, + $container->get( 'api.factory.shipping-preference' ), $logger, $payments_endpoint, - $order_endpoint + $order_endpoint, + $api_shop_country ); }, - 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { + 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { $order_processor = $container->get( 'wcgateway.order-processor' ); $settings_renderer = $container->get( 'wcgateway.settings.render' ); $authorized_payments = $container->get( 'wcgateway.processor.authorized-payments' ); @@ -112,6 +124,7 @@ return array( $transaction_url_provider, $payment_token_repository, $purchase_unit_factory, + $container->get( 'api.factory.shipping-preference' ), $payer_factory, $order_endpoint, $subscription_helper, @@ -120,27 +133,27 @@ return array( $payments_endpoint ); }, - 'wcgateway.disabler' => static function ( ContainerInterface $container ): DisableGateways { + 'wcgateway.disabler' => static function ( ContainerInterface $container ): DisableGateways { $session_handler = $container->get( 'session.handler' ); $settings = $container->get( 'wcgateway.settings' ); return new DisableGateways( $session_handler, $settings ); }, - 'wcgateway.is-wc-payments-page' => static function ( ContainerInterface $container ): bool { + 'wcgateway.is-wc-payments-page' => static function ( ContainerInterface $container ): bool { $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; return 'wc-settings' === $page && 'checkout' === $tab; }, - 'wcgateway.is-ppcp-settings-page' => static function ( ContainerInterface $container ): bool { + 'wcgateway.is-ppcp-settings-page' => static function ( ContainerInterface $container ): bool { if ( ! $container->get( 'wcgateway.is-wc-payments-page' ) ) { return false; } $section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : ''; - return in_array( $section, array( PayPalGateway::ID, CreditCardGateway::ID, WebhooksStatusPage::ID ), true ); + return in_array( $section, array( PayPalGateway::ID, CreditCardGateway::ID, WebhooksStatusPage::ID, PayUponInvoiceGateway::ID ), true ); }, - 'wcgateway.current-ppcp-settings-page-id' => static function ( ContainerInterface $container ): string { + 'wcgateway.current-ppcp-settings-page-id' => static function ( ContainerInterface $container ): string { if ( ! $container->get( 'wcgateway.is-ppcp-settings-page' ) ) { return ''; } @@ -151,33 +164,36 @@ return array( return $ppcp_tab ? $ppcp_tab : $section; }, - 'wcgateway.settings' => static function ( ContainerInterface $container ): Settings { + 'wcgateway.settings' => static function ( ContainerInterface $container ): Settings { return new Settings(); }, - 'wcgateway.notice.connect' => static function ( ContainerInterface $container ): ConnectAdminNotice { + 'wcgateway.notice.connect' => static function ( ContainerInterface $container ): ConnectAdminNotice { $state = $container->get( 'onboarding.state' ); $settings = $container->get( 'wcgateway.settings' ); return new ConnectAdminNotice( $state, $settings ); }, - 'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): DccWithoutPayPalAdminNotice { + 'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): DccWithoutPayPalAdminNotice { $state = $container->get( 'onboarding.state' ); $settings = $container->get( 'wcgateway.settings' ); $is_payments_page = $container->get( 'wcgateway.is-wc-payments-page' ); $is_ppcp_settings_page = $container->get( 'wcgateway.is-ppcp-settings-page' ); return new DccWithoutPayPalAdminNotice( $state, $settings, $is_payments_page, $is_ppcp_settings_page ); }, - 'wcgateway.notice.authorize-order-action' => + 'wcgateway.notice.authorize-order-action' => static function ( ContainerInterface $container ): AuthorizeOrderActionNotice { return new AuthorizeOrderActionNotice(); }, - 'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer { - return new SectionsRenderer( $container->get( 'wcgateway.current-ppcp-settings-page-id' ) ); + 'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer { + return new SectionsRenderer( + $container->get( 'wcgateway.current-ppcp-settings-page-id' ), + $container->get( 'api.shop.country' ) + ); }, - 'wcgateway.settings.status' => static function ( ContainerInterface $container ): SettingsStatus { + 'wcgateway.settings.status' => static function ( ContainerInterface $container ): SettingsStatus { $settings = $container->get( 'wcgateway.settings' ); return new SettingsStatus( $settings ); }, - 'wcgateway.settings.render' => static function ( ContainerInterface $container ): SettingsRenderer { + 'wcgateway.settings.render' => static function ( ContainerInterface $container ): SettingsRenderer { $settings = $container->get( 'wcgateway.settings' ); $state = $container->get( 'onboarding.state' ); $fields = $container->get( 'wcgateway.settings.fields' ); @@ -197,7 +213,7 @@ return array( $page_id ); }, - 'wcgateway.settings.listener' => static function ( ContainerInterface $container ): SettingsListener { + 'wcgateway.settings.listener' => static function ( ContainerInterface $container ): SettingsListener { $settings = $container->get( 'wcgateway.settings' ); $fields = $container->get( 'wcgateway.settings.fields' ); $webhook_registrar = $container->get( 'webhook.registrar' ); @@ -205,9 +221,21 @@ return array( $cache = new Cache( 'ppcp-paypal-bearer' ); $bearer = $container->get( 'api.bearer' ); $page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' ); - return new SettingsListener( $settings, $fields, $webhook_registrar, $cache, $state, $bearer, $page_id ); + $signup_link_cache = $container->get( 'onboarding.signup-link-cache' ); + $signup_link_ids = $container->get( 'onboarding.signup-link-ids' ); + return new SettingsListener( + $settings, + $fields, + $webhook_registrar, + $cache, + $state, + $bearer, + $page_id, + $signup_link_cache, + $signup_link_ids + ); }, - 'wcgateway.order-processor' => static function ( ContainerInterface $container ): OrderProcessor { + 'wcgateway.order-processor' => static function ( ContainerInterface $container ): OrderProcessor { $session_handler = $container->get( 'session.handler' ); $order_endpoint = $container->get( 'api.endpoint.order' ); @@ -232,13 +260,13 @@ return array( $order_helper ); }, - 'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor { + 'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor { $order_endpoint = $container->get( 'api.endpoint.order' ); $payments_endpoint = $container->get( 'api.endpoint.payments' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new RefundProcessor( $order_endpoint, $payments_endpoint, $logger ); }, - 'wcgateway.processor.authorized-payments' => static function ( ContainerInterface $container ): AuthorizedPaymentsProcessor { + 'wcgateway.processor.authorized-payments' => static function ( ContainerInterface $container ): AuthorizedPaymentsProcessor { $order_endpoint = $container->get( 'api.endpoint.order' ); $payments_endpoint = $container->get( 'api.endpoint.payments' ); $logger = $container->get( 'woocommerce.logger.woocommerce' ); @@ -254,23 +282,23 @@ return array( $subscription_helper ); }, - 'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction { + 'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction { $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); return new RenderAuthorizeAction( $column ); }, - 'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail { + 'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail { $column = $container->get( 'wcgateway.admin.orders-payment-status-column' ); return new PaymentStatusOrderDetail( $column ); }, - 'wcgateway.admin.orders-payment-status-column' => static function ( ContainerInterface $container ): OrderTablePaymentStatusColumn { + 'wcgateway.admin.orders-payment-status-column' => static function ( ContainerInterface $container ): OrderTablePaymentStatusColumn { $settings = $container->get( 'wcgateway.settings' ); return new OrderTablePaymentStatusColumn( $settings ); }, - 'wcgateway.admin.fees-renderer' => static function ( ContainerInterface $container ): FeesRenderer { + 'wcgateway.admin.fees-renderer' => static function ( ContainerInterface $container ): FeesRenderer { return new FeesRenderer(); }, - 'wcgateway.settings.fields' => static function ( ContainerInterface $container ): array { + 'wcgateway.settings.fields' => static function ( ContainerInterface $container ): array { $state = $container->get( 'onboarding.state' ); assert( $state instanceof State ); @@ -2046,7 +2074,7 @@ return array( return $fields; }, - 'wcgateway.all-funding-sources' => static function( ContainerInterface $container ): array { + 'wcgateway.all-funding-sources' => static function( ContainerInterface $container ): array { return array( 'card' => _x( 'Credit or debit cards', 'Name of payment method', 'woocommerce-paypal-payments' ), 'credit' => _x( 'Pay Later', 'Name of payment method', 'woocommerce-paypal-payments' ), @@ -2064,28 +2092,28 @@ return array( ); }, - 'wcgateway.checkout.address-preset' => static function( ContainerInterface $container ): CheckoutPayPalAddressPreset { + 'wcgateway.checkout.address-preset' => static function( ContainerInterface $container ): CheckoutPayPalAddressPreset { return new CheckoutPayPalAddressPreset( $container->get( 'session.handler' ) ); }, - 'wcgateway.url' => static function ( ContainerInterface $container ): string { + 'wcgateway.url' => static function ( ContainerInterface $container ): string { return plugins_url( $container->get( 'wcgateway.relative-path' ), dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ); }, - 'wcgateway.relative-path' => static function( ContainerInterface $container ): string { + 'wcgateway.relative-path' => static function( ContainerInterface $container ): string { return 'modules/ppcp-wc-gateway/'; }, - 'wcgateway.absolute-path' => static function( ContainerInterface $container ): string { + 'wcgateway.absolute-path' => static function( ContainerInterface $container ): string { return plugin_dir_path( dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ) . $container->get( 'wcgateway.relative-path' ); }, - 'wcgateway.endpoint.return-url' => static function ( ContainerInterface $container ) : ReturnUrlEndpoint { + 'wcgateway.endpoint.return-url' => static function ( ContainerInterface $container ) : ReturnUrlEndpoint { $gateway = $container->get( 'wcgateway.paypal-gateway' ); $endpoint = $container->get( 'api.endpoint.order' ); $prefix = $container->get( 'api.prefix' ); @@ -2096,41 +2124,103 @@ return array( ); }, - 'wcgateway.transaction-url-sandbox' => static function ( ContainerInterface $container ): string { + 'wcgateway.transaction-url-sandbox' => static function ( ContainerInterface $container ): string { return 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; }, - 'wcgateway.transaction-url-live' => static function ( ContainerInterface $container ): string { + 'wcgateway.transaction-url-live' => static function ( ContainerInterface $container ): string { return 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; }, - 'wcgateway.transaction-url-provider' => static function ( ContainerInterface $container ): TransactionUrlProvider { + 'wcgateway.transaction-url-provider' => static function ( ContainerInterface $container ): TransactionUrlProvider { $sandbox_url_base = $container->get( 'wcgateway.transaction-url-sandbox' ); $live_url_base = $container->get( 'wcgateway.transaction-url-live' ); return new TransactionUrlProvider( $sandbox_url_base, $live_url_base ); }, - 'wcgateway.helper.dcc-product-status' => static function ( ContainerInterface $container ) : DCCProductStatus { + 'wcgateway.helper.dcc-product-status' => static function ( ContainerInterface $container ) : DCCProductStatus { $settings = $container->get( 'wcgateway.settings' ); $partner_endpoint = $container->get( 'api.endpoint.partners' ); return new DCCProductStatus( $settings, $partner_endpoint ); }, - 'button.helper.messages-disclaimers' => static function ( ContainerInterface $container ): MessagesDisclaimers { + 'button.helper.messages-disclaimers' => static function ( ContainerInterface $container ): MessagesDisclaimers { return new MessagesDisclaimers( $container->get( 'api.shop.country' ) ); }, - 'wcgateway.funding-source.renderer' => function ( ContainerInterface $container ) : FundingSourceRenderer { + 'wcgateway.funding-source.renderer' => function ( ContainerInterface $container ) : FundingSourceRenderer { return new FundingSourceRenderer( $container->get( 'wcgateway.settings' ) ); }, - - 'wcgateway.logging.is-enabled' => function ( ContainerInterface $container ) : bool { + 'wcgateway.pay-upon-invoice-order-endpoint' => static function ( ContainerInterface $container ): PayUponInvoiceOrderEndpoint { + return new PayUponInvoiceOrderEndpoint( + $container->get( 'api.host' ), + $container->get( 'api.bearer' ), + $container->get( 'api.factory.order' ), + $container->get( 'wcgateway.pay-upon-invoice-fraudnet' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'wcgateway.pay-upon-invoice-payment-source-factory' => static function ( ContainerInterface $container ): PaymentSourceFactory { + return new PaymentSourceFactory(); + }, + 'wcgateway.pay-upon-invoice-gateway' => static function ( ContainerInterface $container ): PayUponInvoiceGateway { + return new PayUponInvoiceGateway( + $container->get( 'wcgateway.pay-upon-invoice-order-endpoint' ), + $container->get( 'api.factory.purchase-unit' ), + $container->get( 'wcgateway.pay-upon-invoice-payment-source-factory' ), + $container->get( 'onboarding.environment' ), + $container->get( 'wcgateway.transaction-url-provider' ), + $container->get( 'woocommerce.logger.woocommerce' ), + $container->get( 'wcgateway.pay-upon-invoice-helper' ) + ); + }, + 'wcgateway.pay-upon-invoice-fraudnet-session-id' => static function ( ContainerInterface $container ): FraudNetSessionId { + return new FraudNetSessionId(); + }, + 'wcgateway.pay-upon-invoice-fraudnet-source-website-id' => static function ( ContainerInterface $container ): FraudNetSourceWebsiteId { + return new FraudNetSourceWebsiteId( $container->get( 'api.merchant_id' ) ); + }, + 'wcgateway.pay-upon-invoice-fraudnet' => static function ( ContainerInterface $container ): FraudNet { + $session_id = $container->get( 'wcgateway.pay-upon-invoice-fraudnet-session-id' ); + $source_website_id = $container->get( 'wcgateway.pay-upon-invoice-fraudnet-source-website-id' ); + return new FraudNet( + (string) $session_id(), + (string) $source_website_id() + ); + }, + 'wcgateway.pay-upon-invoice-helper' => static function( ContainerInterface $container ): PayUponInvoiceHelper { + return new PayUponInvoiceHelper(); + }, + 'wcgateway.pay-upon-invoice-product-status' => static function( ContainerInterface $container ): PayUponInvoiceProductStatus { + return new PayUponInvoiceProductStatus( + $container->get( 'wcgateway.settings' ), + $container->get( 'api.endpoint.partners' ) + ); + }, + 'wcgateway.pay-upon-invoice' => static function ( ContainerInterface $container ): PayUponInvoice { + return new PayUponInvoice( + $container->get( 'wcgateway.url' ), + $container->get( 'wcgateway.pay-upon-invoice-fraudnet' ), + $container->get( 'wcgateway.pay-upon-invoice-order-endpoint' ), + $container->get( 'woocommerce.logger.woocommerce' ), + $container->get( 'wcgateway.settings' ), + $container->get( 'onboarding.environment' ), + $container->get( 'ppcp.asset-version' ), + $container->get( 'onboarding.state' ), + $container->get( 'wcgateway.is-ppcp-settings-page' ), + $container->get( 'wcgateway.current-ppcp-settings-page-id' ), + $container->get( 'wcgateway.pay-upon-invoice-product-status' ), + $container->get( 'wcgateway.pay-upon-invoice-helper' ), + $container->get( 'api.factory.capture' ) + ); + }, + 'wcgateway.logging.is-enabled' => function ( ContainerInterface $container ) : bool { $settings = $container->get( 'wcgateway.settings' ); /** @@ -2142,7 +2232,7 @@ return array( ); }, - 'wcgateway.helper.vaulting-scope' => static function ( ContainerInterface $container ): bool { + 'wcgateway.helper.vaulting-scope' => static function ( ContainerInterface $container ): bool { try { $token = $container->get( 'api.bearer' )->bearer(); return $token->vaulting_available(); @@ -2151,7 +2241,7 @@ return array( } }, - 'button.helper.vaulting-label' => static function ( ContainerInterface $container ): string { + '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' ) ) { @@ -2173,7 +2263,7 @@ return array( return $vaulting_label; }, - 'wcgateway.settings.fields.pay-later-label' => static function ( ContainerInterface $container ): string { + 'wcgateway.settings.fields.pay-later-label' => static function ( ContainerInterface $container ): string { $pay_later_label = '%s'; $pay_later_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' ); diff --git a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php index c39d03745..b11116878 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php @@ -14,6 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; @@ -111,6 +112,13 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { */ private $purchase_unit_factory; + /** + * The shipping_preference factory. + * + * @var ShippingPreferenceFactory + */ + private $shipping_preference_factory; + /** * The payer factory. * @@ -167,6 +175,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { * @param TransactionUrlProvider $transaction_url_provider Service able to provide view transaction url base. * @param PaymentTokenRepository $payment_token_repository The payment token repository. * @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory. + * @param ShippingPreferenceFactory $shipping_preference_factory The shipping_preference factory. * @param PayerFactory $payer_factory The payer factory. * @param OrderEndpoint $order_endpoint The order endpoint. * @param SubscriptionHelper $subscription_helper The subscription helper. @@ -186,6 +195,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { TransactionUrlProvider $transaction_url_provider, PaymentTokenRepository $payment_token_repository, PurchaseUnitFactory $purchase_unit_factory, + ShippingPreferenceFactory $shipping_preference_factory, PayerFactory $payer_factory, OrderEndpoint $order_endpoint, SubscriptionHelper $subscription_helper, @@ -252,16 +262,17 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { ) ); - $this->module_url = $module_url; - $this->payment_token_repository = $payment_token_repository; - $this->purchase_unit_factory = $purchase_unit_factory; - $this->payer_factory = $payer_factory; - $this->order_endpoint = $order_endpoint; - $this->transaction_url_provider = $transaction_url_provider; - $this->subscription_helper = $subscription_helper; - $this->logger = $logger; - $this->payments_endpoint = $payments_endpoint; - $this->state = $state; + $this->module_url = $module_url; + $this->payment_token_repository = $payment_token_repository; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->shipping_preference_factory = $shipping_preference_factory; + $this->payer_factory = $payer_factory; + $this->order_endpoint = $order_endpoint; + $this->transaction_url_provider = $transaction_url_provider; + $this->subscription_helper = $subscription_helper; + $this->logger = $logger; + $this->payments_endpoint = $payments_endpoint; + $this->state = $state; } /** diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 9770f7c28..1aabbfc5a 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -12,13 +12,14 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint; -use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; @@ -118,6 +119,13 @@ class PayPalGateway extends \WC_Payment_Gateway { */ protected $payment_token_repository; + /** + * The shipping_preference factory. + * + * @var ShippingPreferenceFactory + */ + private $shipping_preference_factory; + /** * The payments endpoint * @@ -160,6 +168,13 @@ class PayPalGateway extends \WC_Payment_Gateway { */ private $logger; + /** + * The api shop country. + * + * @var string + */ + protected $api_shop_country; + /** * PayPalGateway constructor. * @@ -176,9 +191,11 @@ class PayPalGateway extends \WC_Payment_Gateway { * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. * @param Environment $environment The environment. * @param PaymentTokenRepository $payment_token_repository The payment token repository. + * @param ShippingPreferenceFactory $shipping_preference_factory The shipping_preference factory. * @param LoggerInterface $logger The logger. * @param PaymentsEndpoint $payments_endpoint The payments endpoint. * @param OrderEndpoint $order_endpoint The order endpoint. + * @param string $api_shop_country The api shop country. */ public function __construct( SettingsRenderer $settings_renderer, @@ -194,9 +211,11 @@ class PayPalGateway extends \WC_Payment_Gateway { string $page_id, Environment $environment, PaymentTokenRepository $payment_token_repository, + ShippingPreferenceFactory $shipping_preference_factory, LoggerInterface $logger, PaymentsEndpoint $payments_endpoint, - OrderEndpoint $order_endpoint + OrderEndpoint $order_endpoint, + string $api_shop_country ) { $this->id = self::ID; @@ -214,6 +233,7 @@ class PayPalGateway extends \WC_Payment_Gateway { $this->id = self::ID; $this->order_processor = $order_processor; $this->authorized_payments = $authorized_payments_processor; + $this->shipping_preference_factory = $shipping_preference_factory; $this->settings_renderer = $settings_renderer; $this->config = $config; $this->session_handler = $session_handler; @@ -277,6 +297,7 @@ class PayPalGateway extends \WC_Payment_Gateway { $this->payments_endpoint = $payments_endpoint; $this->order_endpoint = $order_endpoint; $this->state = $state; + $this->api_shop_country = $api_shop_country; } /** @@ -342,6 +363,10 @@ class PayPalGateway extends \WC_Payment_Gateway { if ( $this->is_paypal_tab() ) { return __( 'PayPal Checkout', 'woocommerce-paypal-payments' ); } + if ( $this->is_pui_tab() ) { + return __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ); + } + return __( 'PayPal', 'woocommerce-paypal-payments' ); } @@ -390,6 +415,19 @@ class PayPalGateway extends \WC_Payment_Gateway { } + /** + * Whether we are on the PUI tab. + * + * @return bool + */ + private function is_pui_tab():bool { + if ( 'DE' !== $this->api_shop_country ) { + return false; + } + + return is_admin() && PayUponInvoiceGateway::ID === $this->page_id; + } + /** * Whether we are on the Webhooks Status tab. * diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/FraudNet.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/FraudNet.php new file mode 100644 index 000000000..49fa9aad8 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/FraudNet.php @@ -0,0 +1,59 @@ +session_id = $session_id; + $this->source_website_id = $source_website_id; + } + + /** + * Returns the session ID. + * + * @return string + */ + public function session_id(): string { + return $this->session_id; + } + + /** + * Returns the source website id. + * + * @return string + */ + public function source_website_id(): string { + return $this->source_website_id; + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/FraudNetSessionId.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/FraudNetSessionId.php new file mode 100644 index 000000000..358c50958 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/FraudNetSessionId.php @@ -0,0 +1,47 @@ +session === null ) { + return ''; + } + + if ( WC()->session->get( 'ppcp_fraudnet_session_id' ) ) { + return WC()->session->get( 'ppcp_fraudnet_session_id' ); + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['pay_for_order'] ) && 'true' === $_GET['pay_for_order'] ) { + $pui_pay_for_order_session_id = filter_input( INPUT_POST, 'pui_pay_for_order_session_id', FILTER_SANITIZE_STRING ); + if ( $pui_pay_for_order_session_id && '' !== $pui_pay_for_order_session_id ) { + return $pui_pay_for_order_session_id; + } + } + + $session_id = bin2hex( random_bytes( 16 ) ); + WC()->session->set( 'ppcp_fraudnet_session_id', $session_id ); + + return $session_id; + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/FraudNetSourceWebsiteId.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/FraudNetSourceWebsiteId.php new file mode 100644 index 000000000..6ce1453f3 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/FraudNetSourceWebsiteId.php @@ -0,0 +1,41 @@ +api_merchant_id = $api_merchant_id; + } + + /** + * Returns the source website ID. + * + * @return string + */ + public function __invoke() { + return "{$this->api_merchant_id}_checkout-page"; + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php new file mode 100644 index 000000000..87f89997a --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php @@ -0,0 +1,538 @@ +module_url = $module_url; + $this->fraud_net = $fraud_net; + $this->pui_order_endpoint = $pui_order_endpoint; + $this->logger = $logger; + $this->settings = $settings; + $this->environment = $environment; + $this->asset_version = $asset_version; + $this->state = $state; + $this->is_ppcp_settings_page = $is_ppcp_settings_page; + $this->current_ppcp_settings_page_id = $current_ppcp_settings_page_id; + $this->pui_product_status = $pui_product_status; + $this->pui_helper = $pui_helper; + $this->capture_factory = $capture_factory; + } + + /** + * Initializes PUI integration. + * + * @throws NotFoundException When setting is not found. + */ + public function init(): void { + add_filter( + 'ppcp_partner_referrals_data', + function ( array $data ): array { + if ( $this->settings->has( 'ppcp-onboarding-pui' ) && $this->settings->get( 'ppcp-onboarding-pui' ) !== '1' ) { + return $data; + } + + $data['business_entity'] = array( + 'business_type' => array( + 'type' => 'PRIVATE_CORPORATION', + ), + 'addresses' => array( + array( + 'address_line_1' => WC()->countries->get_base_address(), + 'admin_area_1' => WC()->countries->get_base_city(), + 'postal_code' => WC()->countries->get_base_postcode(), + 'country_code' => WC()->countries->get_base_country(), + 'type' => 'WORK', + ), + ), + ); + + if ( in_array( 'PPCP', $data['products'], true ) ) { + $data['products'][] = 'PAYMENT_METHODS'; + } elseif ( in_array( 'EXPRESS_CHECKOUT', $data['products'], true ) ) { + $data['products'][0] = 'PAYMENT_METHODS'; + } + $data['capabilities'][] = 'PAY_UPON_INVOICE'; + + return $data; + } + ); + + add_action( + 'wp_enqueue_scripts', + array( $this, 'register_assets' ) + ); + + add_action( + 'ppcp_payment_capture_completed_webhook_handler', + function ( WC_Order $wc_order, string $order_id ) { + try { + $order = $this->pui_order_endpoint->order( $order_id ); + + $payment_instructions = array( + $order->payment_source->pay_upon_invoice->payment_reference, + $order->payment_source->pay_upon_invoice->deposit_bank_details, + ); + $wc_order->update_meta_data( + 'ppcp_ratepay_payment_instructions_payment_reference', + $payment_instructions + ); + $wc_order->save_meta_data(); + $this->logger->info( "Ratepay payment instructions added to order #{$wc_order->get_id()}." ); + + $capture = $this->capture_factory->from_paypal_response( $order->purchase_units[0]->payments->captures[0] ); + $breakdown = $capture->seller_receivable_breakdown(); + if ( $breakdown ) { + $wc_order->update_meta_data( PayPalGateway::FEES_META_KEY, $breakdown->to_array() ); + $wc_order->save_meta_data(); + } + } catch ( RuntimeException $exception ) { + $this->logger->error( $exception->getMessage() ); + } + }, + 10, + 2 + ); + + add_action( + 'woocommerce_email_before_order_table', + function( WC_Order $order, bool $sent_to_admin ) { + if ( ! $sent_to_admin && PayUponInvoiceGateway::ID === $order->get_payment_method() && $order->has_status( 'processing' ) ) { + $this->logger->info( "Adding Ratepay payment instructions to email for order #{$order->get_id()}." ); + + $instructions = $order->get_meta( 'ppcp_ratepay_payment_instructions_payment_reference' ); + + $gateway_settings = get_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings' ); + $merchant_name = $gateway_settings['brand_name'] ?? ''; + + $order_date = $order->get_date_created(); + if ( null === $order_date ) { + $this->logger->error( 'Could not get WC order date for Ratepay payment instructions.' ); + return; + } + + $order_purchase_date = $order_date->date( 'd-m-Y' ); + $order_time = $order_date->date( 'H:i:s' ); + $order_date = $order_date->date( 'd-m-Y H:i:s' ); + + $thirty_days_date = strtotime( $order_date . ' +30 days' ); + if ( false === $thirty_days_date ) { + $this->logger->error( 'Could not create +30 days date from WC order date.' ); + return; + } + $order_date_30d = gmdate( 'd-m-Y', $thirty_days_date ); + + $payment_reference = $instructions[0] ?? ''; + $bic = $instructions[1]->bic ?? ''; + $bank_name = $instructions[1]->bank_name ?? ''; + $iban = $instructions[1]->iban ?? ''; + $account_holder_name = $instructions[1]->account_holder_name ?? ''; + + echo wp_kses_post( "

    Für Ihre Bestellung #{$order->get_id()} ({$order_purchase_date} $order_time) bei {$merchant_name} haben Sie die Zahlung mittels “Rechnungskauf mit Ratepay“ gewählt." ); + echo '
    Bitte benutzen Sie die folgenden Informationen für Ihre Überweisung:
    '; + echo wp_kses_post( "

    Bitte überweisen Sie den Betrag in Höhe von {$order->get_currency()}{$order->get_total()} bis zum {$order_date_30d} auf das unten angegebene Konto. Wichtig: Bitte geben Sie unbedingt als Verwendungszweck {$payment_reference} an, sonst kann die Zahlung nicht zugeordnet werden.

    " ); + + echo ''; + + echo wp_kses_post( "

    {$merchant_name} hat die Forderung gegen Sie an die PayPal (Europe) S.à r.l. et Cie, S.C.A. abgetreten, die wiederum die Forderung an Ratepay GmbH abgetreten hat. Zahlungen mit schuldbefreiender Wirkung können nur an die Ratepay GmbH geleistet werden.

    " ); + + echo '

    Mit freundlichen Grüßen'; + echo '
    '; + echo wp_kses_post( "{$merchant_name}

    " ); + } + }, + 10, + 3 + ); + + add_filter( + 'woocommerce_gateway_description', + function( string $description, string $id ): string { + if ( PayUponInvoiceGateway::ID === $id ) { + ob_start(); + + $site_country_code = explode( '-', get_bloginfo( 'language' ) )[0] ?? ''; + + echo '
    '; + + woocommerce_form_field( + 'billing_birth_date', + array( + 'type' => 'date', + 'label' => 'de' === $site_country_code ? 'Geburtsdatum' : 'Birth date', + 'class' => array( 'form-row-wide' ), + 'required' => true, + 'clear' => true, + ) + ); + + echo '
    '; + + // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch + $button_text = apply_filters( 'woocommerce_order_button_text', __( 'Place order', 'woocommerce' ) ); + + if ( 'de' === $site_country_code ) { + echo wp_kses_post( + 'Mit Klicken auf ' . $button_text . ' akzeptieren Sie die Ratepay Zahlungsbedingungen und erklären sich mit der Durchführung einer Risikoprüfung durch Ratepay, unseren Partner, einverstanden. Sie akzeptieren auch PayPals Datenschutzerklärung. Falls Ihre Transaktion per Kauf auf Rechnung erfolgreich abgewickelt werden kann, wird der Kaufpreis an Ratepay abgetreten und Sie dürfen nur an Ratepay überweisen, nicht an den Händler.' + ); + } else { + echo wp_kses_post( + 'By clicking on ' . $button_text . ', you agree to the terms of payment and performance of a risk check from the payment partner, Ratepay. You also agree to PayPal’s privacy statement. If your request to purchase upon invoice is accepted, the purchase price claim will be assigned to Ratepay, and you may only pay Ratepay, not the merchant.' + ); + } + echo '
    '; + + $description .= ob_get_clean() ?: ''; + } + + return $description; + }, + 10, + 2 + ); + + add_action( + 'woocommerce_after_checkout_validation', + function( array $fields, WP_Error $errors ) { + $payment_method = filter_input( INPUT_POST, 'payment_method', FILTER_SANITIZE_STRING ); + if ( PayUponInvoiceGateway::ID !== $payment_method ) { + return; + } + + if ( 'DE' !== $fields['billing_country'] ) { + $errors->add( 'validation', __( 'Billing country not available.', 'woocommerce-paypal-payments' ) ); + } + + $birth_date = filter_input( INPUT_POST, 'billing_birth_date', FILTER_SANITIZE_STRING ); + if ( ( $birth_date && ! $this->pui_helper->validate_birth_date( $birth_date ) ) || $birth_date === '' ) { + $errors->add( 'validation', __( 'Invalid birth date.', 'woocommerce-paypal-payments' ) ); + } + + $national_number = filter_input( INPUT_POST, 'billing_phone', FILTER_SANITIZE_STRING ); + if ( $national_number ) { + $numeric_phone_number = preg_replace( '/[^0-9]/', '', $national_number ); + if ( $numeric_phone_number && ! preg_match( '/^[0-9]{1,14}?$/', $numeric_phone_number ) ) { + $errors->add( 'validation', __( 'Phone number size must be between 1 and 14', 'woocommerce-paypal-payments' ) ); + } + } + }, + 10, + 2 + ); + + add_filter( + 'woocommerce_available_payment_gateways', + function ( array $methods ): array { + if ( State::STATE_ONBOARDED !== $this->state->current_state() ) { + return $methods; + } + + if ( + ! $this->pui_product_status->pui_is_active() + || ! $this->pui_helper->is_checkout_ready_for_pui() + ) { + unset( $methods[ PayUponInvoiceGateway::ID ] ); + } + + return $methods; + } + ); + + add_action( + 'woocommerce_settings_checkout', + function () { + if ( + PayUponInvoiceGateway::ID === $this->current_ppcp_settings_page_id + && ! $this->pui_product_status->pui_is_active() + ) { + $gateway_settings = get_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings' ); + $gateway_enabled = $gateway_settings['enabled'] ?? ''; + if ( 'yes' === $gateway_enabled ) { + $gateway_settings['enabled'] = 'no'; + update_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings', $gateway_settings ); + $redirect_url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-pay-upon-invoice-gateway' ); + wp_safe_redirect( $redirect_url ); + exit; + } + + printf( + '

    %1$s

    ', + esc_html__( 'Could not enable gateway because the connected PayPal account is not activated for Pay upon Invoice. Reconnect your account while Onboard with Pay upon Invoice is selected to try again.', 'woocommerce-paypal-payments' ) + ); + } + } + ); + + add_action( + 'woocommerce_update_options_checkout_ppcp-pay-upon-invoice-gateway', + function () { + $customer_service_instructions = filter_input( INPUT_POST, 'woocommerce_ppcp-pay-upon-invoice-gateway_customer_service_instructions', FILTER_SANITIZE_STRING ); + if ( '' === $customer_service_instructions ) { + $gateway_settings = get_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings' ); + $gateway_enabled = $gateway_settings['enabled'] ?? ''; + if ( 'yes' === $gateway_enabled ) { + $gateway_settings['enabled'] = 'no'; + update_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings', $gateway_settings ); + + $redirect_url = admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-pay-upon-invoice-gateway' ); + wp_safe_redirect( $redirect_url ); + exit; + } + } + } + ); + + add_action( + 'woocommerce_settings_checkout', + function() { + if ( + PayUponInvoiceGateway::ID === $this->current_ppcp_settings_page_id + && $this->pui_product_status->pui_is_active() + ) { + $error_messages = array(); + $pui_gateway = WC()->payment_gateways->payment_gateways()[ PayUponInvoiceGateway::ID ]; + if ( $pui_gateway->get_option( 'logo_url' ) === '' ) { + $error_messages[] = esc_html__( 'Could not enable gateway because "Logo URL" field is empty.', 'woocommerce-paypal-payments' ); + } + if ( $pui_gateway->get_option( 'customer_service_instructions' ) === '' ) { + $error_messages[] = esc_html__( 'Could not enable gateway because "Customer service instructions" field is empty.', 'woocommerce-paypal-payments' ); + } + if ( count( $error_messages ) > 0 ) { ?> +
    + ' . $message . '

    '; + }, + $error_messages + ) + ?> +
    + get_payment_method() === 'ppcp-pay-upon-invoice-gateway' ) { + $instructions = $order->get_meta( 'ppcp_ratepay_payment_instructions_payment_reference' ); + if ( $instructions ) { + add_meta_box( + 'ppcp_pui_ratepay_payment_instructions', + __( 'RatePay payment instructions', 'woocommerce-paypal-payments' ), + function() use ( $instructions ) { + $payment_reference = $instructions[0] ?? ''; + $bic = $instructions[1]->bic ?? ''; + $bank_name = $instructions[1]->bank_name ?? ''; + $iban = $instructions[1]->iban ?? ''; + $account_holder_name = $instructions[1]->account_holder_name ?? ''; + + echo ''; + }, + $post_type, + 'side', + 'high' + ); + } + } + } + } + ); + } + + /** + * Registers PUI assets. + */ + public function register_assets(): void { + $gateway_settings = get_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings' ); + $gateway_enabled = $gateway_settings['enabled'] ?? ''; + if ( $gateway_enabled === 'yes' && ( is_checkout() || is_checkout_pay_page() ) ) { + wp_enqueue_script( + 'ppcp-pay-upon-invoice', + trailingslashit( $this->module_url ) . 'assets/js/pay-upon-invoice.js', + array(), + $this->asset_version, + true + ); + + wp_localize_script( + 'ppcp-pay-upon-invoice', + 'FraudNetConfig', + array( + 'f' => $this->fraud_net->session_id(), + 's' => $this->fraud_net->source_website_id(), + 'sandbox' => $this->environment->current_environment_is( Environment::SANDBOX ), + ) + ); + } + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php new file mode 100644 index 000000000..fe9d53d38 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoiceGateway.php @@ -0,0 +1,274 @@ +id = self::ID; + + $this->method_title = __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ); + $this->method_description = __( 'Pay upon Invoice is an invoice payment method in Germany. It is a local buy now, pay later payment method that allows the buyer to place an order, receive the goods, try them, verify they are in good order, and then pay the invoice within 30 days.', 'woocommerce-paypal-payments' ); + + $gateway_settings = get_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings' ); + $this->title = $gateway_settings['title'] ?? $this->method_title; + $this->description = $gateway_settings['description'] ?? __( 'Once you place an order, pay within 30 days. Our payment partner Ratepay will send you payment instructions.', 'woocommerce-paypal-payments' ); + + $this->init_form_fields(); + $this->init_settings(); + + add_action( + 'woocommerce_update_options_payment_gateways_' . $this->id, + array( + $this, + 'process_admin_options', + ) + ); + + $this->order_endpoint = $order_endpoint; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->payment_source_factory = $payment_source_factory; + $this->logger = $logger; + $this->environment = $environment; + $this->transaction_url_provider = $transaction_url_provider; + $this->pui_helper = $pui_helper; + } + + /** + * Initialize the form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'label' => __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ), + 'default' => 'no', + 'desc_tip' => true, + 'description' => __( 'Enable/Disable Pay upon Invoice payment gateway.', 'woocommerce-paypal-payments' ), + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->title, + 'desc_tip' => true, + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => $this->description, + 'desc_tip' => true, + 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce-paypal-payments' ), + ), + 'experience_context' => array( + 'title' => __( 'Experience Context', 'woocommerce-paypal-payments' ), + 'type' => 'title', + 'description' => __( "Specify brand name, logo and customer service instructions to be presented on Ratepay's payment instructions.", 'woocommerce-paypal-payments' ), + ), + 'brand_name' => array( + 'title' => __( 'Brand name', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => get_bloginfo( 'name' ) ?? '', + 'desc_tip' => true, + 'description' => __( 'Merchant name displayed in Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ), + ), + 'logo_url' => array( + 'title' => __( 'Logo URL', 'woocommerce-paypal-payments' ), + 'type' => 'url', + 'default' => '', + 'desc_tip' => true, + 'description' => __( 'Logo to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ), + ), + 'customer_service_instructions' => array( + 'title' => __( 'Customer service instructions', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'default' => '', + 'desc_tip' => true, + 'description' => __( 'Customer service instructions to be presented on Ratepay\'s payment instructions.', 'woocommerce-paypal-payments' ), + ), + ); + } + + /** + * Processes the order. + * + * @param int $order_id The WC order ID. + * @return array + */ + public function process_payment( $order_id ) { + $wc_order = wc_get_order( $order_id ); + $birth_date = filter_input( INPUT_POST, 'billing_birth_date', FILTER_SANITIZE_STRING ) ?? ''; + + $pay_for_order = filter_input( INPUT_GET, 'pay_for_order', FILTER_SANITIZE_STRING ); + if ( 'true' === $pay_for_order ) { + if ( ! $this->pui_helper->validate_birth_date( $birth_date ) ) { + wc_add_notice( 'Invalid birth date.', 'error' ); + return array( + 'result' => 'failure', + ); + } + } + + $wc_order->update_status( 'on-hold', __( 'Awaiting Pay upon Invoice payment.', 'woocommerce-paypal-payments' ) ); + $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); + $payment_source = $this->payment_source_factory->from_wc_order( $wc_order, $birth_date ); + + try { + $order = $this->order_endpoint->create( array( $purchase_unit ), $payment_source ); + $this->add_paypal_meta( $wc_order, $order, $this->environment ); + + as_schedule_single_action( + time() + ( 5 * MINUTE_IN_SECONDS ), + 'woocommerce_paypal_payments_check_pui_payment_captured', + array( + 'wc_order_id' => $order_id, + 'order_id' => $order->id(), + ) + ); + + WC()->cart->empty_cart(); + + return array( + 'result' => 'success', + 'redirect' => $this->get_return_url( $wc_order ), + ); + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + + if ( is_a( $exception, PayPalApiException::class ) && is_array( $exception->details() ) ) { + $details = ''; + foreach ( $exception->details() as $detail ) { + $issue = $detail->issue ?? ''; + $field = $detail->field ?? ''; + $description = $detail->description ?? ''; + $details .= $issue . ' ' . $field . ' ' . $description . '
    '; + } + + $error = $details; + } + + $this->logger->error( $error ); + wc_add_notice( $error, 'error' ); + + $wc_order->update_status( + 'failed', + $error + ); + + return array( + 'result' => 'failure', + 'redirect' => wc_get_checkout_url(), + ); + } + } + + /** + * Return transaction url for this gateway and given order. + * + * @param WC_Order $order WC order to get transaction url by. + * + * @return string + */ + public function get_transaction_url( $order ): string { + $this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order ); + + return parent::get_transaction_url( $order ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PaymentSource.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PaymentSource.php new file mode 100644 index 000000000..bac9791cb --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PaymentSource.php @@ -0,0 +1,322 @@ +given_name = $given_name; + $this->surname = $surname; + $this->email = $email; + $this->birth_date = $birth_date; + $this->national_number = $national_number; + $this->phone_country_code = $phone_country_code; + $this->address_line_1 = $address_line_1; + $this->admin_area_2 = $admin_area_2; + $this->postal_code = $postal_code; + $this->country_code = $country_code; + $this->locale = $locale; + $this->brand_name = $brand_name; + $this->logo_url = $logo_url; + $this->customer_service_instructions = $customer_service_instructions; + } + + /** + * Returns the given name. + * + * @return string + */ + public function given_name(): string { + return $this->given_name; + } + + /** + * Returns the surname. + * + * @return string + */ + public function surname(): string { + return $this->surname; + } + + /** + * Returns the email. + * + * @return string + */ + public function email(): string { + return $this->email; + } + + /** + * Returns the birth date. + * + * @return string + */ + public function birth_date(): string { + return $this->birth_date; + } + + /** + * Returns the national number. + * + * @return string + */ + public function national_number(): string { + return $this->national_number; + } + + /** + * Returns the phone country code. + * + * @return string + */ + public function phone_country_code(): string { + return $this->phone_country_code; + } + + /** + * Returns the address line 1. + * + * @return string + */ + public function address_line_1(): string { + return $this->address_line_1; + } + + /** + * Returns the admin area 2. + * + * @return string + */ + public function admin_area_2(): string { + return $this->admin_area_2; + } + + /** + * Returns the postal code. + * + * @return string + */ + public function postal_code(): string { + return $this->postal_code; + } + + /** + * Returns the country code. + * + * @return string + */ + public function country_code(): string { + return $this->country_code; + } + + /** + * Returns the locale. + * + * @return string + */ + public function locale(): string { + return $this->locale; + } + + /** + * Returns the brand name. + * + * @return string + */ + public function brand_name(): string { + return $this->brand_name; + } + + /** + * The logo URL. + * + * @return string + */ + public function logo_url(): string { + return $this->logo_url; + } + + /** + * Returns the customer service instructions. + * + * @return array + */ + public function customer_service_instructions(): array { + return $this->customer_service_instructions; + } + + /** + * Returns payment source as array. + * + * @return array + */ + public function to_array(): array { + return array( + 'name' => array( + 'given_name' => $this->given_name(), + 'surname' => $this->surname(), + ), + 'email' => $this->email(), + 'birth_date' => $this->birth_date(), + 'phone' => array( + 'national_number' => $this->national_number(), + 'country_code' => $this->phone_country_code(), + ), + 'billing_address' => array( + 'address_line_1' => $this->address_line_1(), + 'admin_area_2' => $this->admin_area_2(), + 'postal_code' => $this->postal_code(), + 'country_code' => $this->country_code(), + ), + 'experience_context' => array( + 'locale' => $this->locale(), + 'brand_name' => $this->brand_name(), + 'logo_url' => $this->logo_url(), + 'customer_service_instructions' => $this->customer_service_instructions(), + ), + ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PaymentSourceFactory.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PaymentSourceFactory.php new file mode 100644 index 000000000..d4dc82562 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PaymentSourceFactory.php @@ -0,0 +1,59 @@ +get_address(); + + $phone_country_code = WC()->countries->get_country_calling_code( $address['country'] ); + $phone_country_code = is_array( $phone_country_code ) && ! empty( $phone_country_code ) ? $phone_country_code[0] : $phone_country_code; + if ( is_string( $phone_country_code ) && '' !== $phone_country_code ) { + $phone_country_code = substr( $phone_country_code, strlen( '+' ) ) ?: ''; + } else { + $phone_country_code = ''; + } + + $gateway_settings = get_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings' ); + $merchant_name = $gateway_settings['brand_name'] ?? ''; + $logo_url = $gateway_settings['logo_url'] ?? ''; + $customer_service_instructions = $gateway_settings['customer_service_instructions'] ?? ''; + + return new PaymentSource( + $address['first_name'] ?? '', + $address['last_name'] ?? '', + $address['email'] ?? '', + $birth_date, + preg_replace( '/[^0-9]/', '', $address['phone'] ) ?? '', + $phone_country_code, + $address['address_1'] ?? '', + $address['city'] ?? '', + $address['postcode'] ?? '', + $address['country'] ?? '', + 'en-DE', + $merchant_name, + $logo_url, + array( $customer_service_instructions ) + ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index fa425c6f3..10e0e1313 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -82,9 +82,16 @@ trait ProcessPaymentTrait { $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); $payer = $this->payer_factory->from_customer( $customer ); + + $shipping_preference = $this->shipping_preference_factory->from_state( + $purchase_unit, + '' + ); + try { $order = $this->order_endpoint->create( array( $purchase_unit ), + $shipping_preference, $payer, $selected_token ); diff --git a/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceHelper.php b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceHelper.php new file mode 100644 index 000000000..32391bb0a --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceHelper.php @@ -0,0 +1,140 @@ +format( $format ) ) { + return false; + } + + $date_time = strtotime( $date ); + if ( $date_time && time() < strtotime( '+18 years', $date_time ) ) { + return false; + } + + return true; + } + + /** + * Ensures product is ready for PUI. + * + * @param WC_Product $product WC product. + * @return bool + */ + public function product_ready_for_pui( WC_Product $product ):bool { + if ( $product->is_downloadable() || $product->is_virtual() ) { + return false; + } + + if ( is_a( $product, WC_Product_Variable::class ) ) { + foreach ( $product->get_available_variations( 'object' ) as $variation ) { + if ( is_a( $variation, WC_Product_Variation::class ) ) { + if ( true === $variation->is_downloadable() || true === $variation->is_virtual() ) { + return false; + } + } + } + } + + return true; + } + + /** + * Checks whether checkout is ready for PUI. + * + * @return bool + */ + public function is_checkout_ready_for_pui(): bool { + $gateway_settings = get_option( 'woocommerce_ppcp-pay-upon-invoice-gateway_settings' ); + if ( $gateway_settings && '' === $gateway_settings['customer_service_instructions'] ) { + return false; + } + + $billing_country = filter_input( INPUT_POST, 'country', FILTER_SANITIZE_STRING ) ?? null; + if ( $billing_country && 'DE' !== $billing_country ) { + return false; + } + + if ( 'EUR' !== get_woocommerce_currency() ) { + return false; + } + + $cart = WC()->cart ?? null; + if ( $cart && ! is_checkout_pay_page() ) { + $cart_total = (float) $cart->get_total( 'numeric' ); + if ( $cart_total < 5 || $cart_total > 2500 ) { + return false; + } + + $items = $cart->get_cart_contents(); + foreach ( $items as $item ) { + $product = wc_get_product( $item['product_id'] ); + if ( is_a( $product, WC_Product::class ) && ! $this->product_ready_for_pui( $product ) ) { + return false; + } + } + } + + if ( is_wc_endpoint_url( 'order-pay' ) ) { + /** + * Needed for WordPress `query_vars`. + * + * @psalm-suppress InvalidGlobal + */ + global $wp; + + if ( isset( $wp->query_vars['order-pay'] ) && absint( $wp->query_vars['order-pay'] ) > 0 ) { + $order_id = absint( $wp->query_vars['order-pay'] ); + $order = wc_get_order( $order_id ); + if ( is_a( $order, WC_Order::class ) ) { + $order_total = (float) $order->get_total(); + if ( $order_total < 5 || $order_total > 2500 ) { + return false; + } + + foreach ( $order->get_items() as $item_id => $item ) { + if ( is_a( $item, WC_Order_Item_Product::class ) ) { + $product = wc_get_product( $item->get_product_id() ); + if ( is_a( $product, WC_Product::class ) && ! $this->product_ready_for_pui( $product ) ) { + return false; + } + } + } + } + } + } + + return true; + } +} diff --git a/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceProductStatus.php b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceProductStatus.php new file mode 100644 index 000000000..60b72dc7f --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Helper/PayUponInvoiceProductStatus.php @@ -0,0 +1,106 @@ +settings = $settings; + $this->partners_endpoint = $partners_endpoint; + } + + /** + * Whether the active/subscribed products support PUI. + * + * @return bool + */ + public function pui_is_active() : bool { + if ( is_bool( $this->current_status_cache ) ) { + return $this->current_status_cache; + } + if ( $this->settings->has( 'products_pui_enabled' ) && $this->settings->get( 'products_pui_enabled' ) ) { + $this->current_status_cache = true; + return true; + } + + try { + $seller_status = $this->partners_endpoint->seller_status(); + } catch ( RuntimeException $error ) { + $this->current_status_cache = false; + return false; + } + + foreach ( $seller_status->products() as $product ) { + if ( $product->name() !== 'PAYMENT_METHODS' ) { + continue; + } + + if ( ! in_array( + $product->vetting_status(), + array( + SellerStatusProduct::VETTING_STATUS_APPROVED, + SellerStatusProduct::VETTING_STATUS_SUBSCRIBED, + ), + true + ) + ) { + continue; + } + + if ( in_array( 'PAY_UPON_INVOICE', $product->capabilities(), true ) ) { + $this->settings->set( 'products_pui_enabled', true ); + $this->settings->persist(); + $this->current_status_cache = true; + return true; + } + } + + $this->current_status_cache = false; + return false; + } +} diff --git a/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php b/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php index 3a9049bbd..815f8d73e 100644 --- a/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php +++ b/modules/ppcp-wc-gateway/src/Processor/OrderMetaTrait.php @@ -41,6 +41,8 @@ trait OrderMetaTrait { if ( $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/Settings/SectionsRenderer.php b/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php index 2bddabe1b..24faa5201 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/SectionsRenderer.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Settings; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; use WooCommerce\PayPalCommerce\Webhooks\Status\WebhooksStatusPage; /** @@ -27,13 +28,22 @@ class SectionsRenderer { */ protected $page_id; + /** + * The api shop country. + * + * @var string + */ + protected $api_shop_country; + /** * SectionsRenderer constructor. * * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. + * @param string $api_shop_country The api shop country. */ - public function __construct( string $page_id ) { - $this->page_id = $page_id; + public function __construct( string $page_id, string $api_shop_country ) { + $this->page_id = $page_id; + $this->api_shop_country = $api_shop_country; } /** @@ -54,17 +64,25 @@ class SectionsRenderer { } $sections = array( - PayPalGateway::ID => __( 'PayPal Checkout', 'woocommerce-paypal-payments' ), - CreditCardGateway::ID => __( 'PayPal Card Processing', 'woocommerce-paypal-payments' ), - WebhooksStatusPage::ID => __( 'Webhooks Status', 'woocommerce-paypal-payments' ), + PayPalGateway::ID => __( 'PayPal Checkout', 'woocommerce-paypal-payments' ), + CreditCardGateway::ID => __( 'PayPal Card Processing', 'woocommerce-paypal-payments' ), + PayUponInvoiceGateway::ID => __( 'Pay upon Invoice', 'woocommerce-paypal-payments' ), + WebhooksStatusPage::ID => __( 'Webhooks Status', 'woocommerce-paypal-payments' ), ); + if ( 'DE' !== $this->api_shop_country ) { + unset( $sections[ PayUponInvoiceGateway::ID ] ); + } + echo '