Merge branch 'trunk' into PCP-359-customer-details-not-available-i

This commit is contained in:
dinamiko 2021-11-26 15:30:26 +01:00
commit b3b1237247
12 changed files with 468 additions and 44 deletions

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\IdentityToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
@ -39,6 +40,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencySupport;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository;
@ -190,6 +192,13 @@ return array(
$subscription_helper
);
},
'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): BillingAgreementsEndpoint {
return new BillingAgreementsEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.repository.paypal-request-id' => static function( ContainerInterface $container ) : PayPalRequestIdRepository {
return new PayPalRequestIdRepository();
},
@ -302,4 +311,7 @@ return array(
'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies {
return new DccApplies();
},
'api.helpers.currency-support' => static function ( ContainerInterface $container ) : CurrencySupport {
return new CurrencySupport();
},
);

View file

@ -0,0 +1,134 @@
<?php
/**
* The billing agreements endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use Psr\Log\LoggerInterface;
/**
* Class BillingAgreementsEndpoint
*/
class BillingAgreementsEndpoint {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* BillingAgreementsEndpoint constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
}
/**
* Creates a billing agreement token.
*
* @param string $description The description.
* @param string $return_url The return URL.
* @param string $cancel_url The cancel URL.
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function create_token( string $description, string $return_url, string $cancel_url ): stdClass {
$data = array(
'description' => $description,
'payer' => array(
'payment_method' => 'PAYPAL',
),
'plan' => array(
'type' => 'MERCHANT_INITIATED_BILLING',
'merchant_preferences' => array(
'return_url' => $return_url,
'cancel_url' => $cancel_url,
'skip_shipping_address' => true,
),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing-agreements/agreement-tokens';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to create a billing agreement token.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $json;
}
/**
* Checks if reference transactions are enabled in account.
*
* @throws RuntimeException If the request fails (no auth, no connection, etc.).
*/
public function reference_transaction_enabled(): bool {
try {
$this->create_token(
'Checking if reference transactions are enabled',
'https://example.com/return',
'https://example.com/cancel'
);
return true;
} catch ( PayPalApiException $exception ) {
return false;
}
}
}

View file

@ -0,0 +1,70 @@
<?php
/**
* Checks if the current installation uses supported currency.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
/**
* Class CurrencySupport
*/
class CurrencySupport {
/**
* Currencies supported by PayPal.
*
* From https://developer.paypal.com/docs/reports/reference/paypal-supported-currencies/
*
* @var string[]
*/
private $supported_currencies = array(
'AUD',
'BRL',
'CAD',
'CNY',
'CZK',
'DKK',
'EUR',
'HKD',
'HUF',
'ILS',
'JPY',
'MYR',
'MXN',
'TWD',
'NZD',
'NOK',
'PHP',
'PLN',
'GBP',
'RUB',
'SGD',
'SEK',
'CHF',
'THB',
'USD',
);
/**
* Returns whether the given currency is supported.
*
* @param string $currency 3-letter currency code.
* @return bool
*/
public function supports_currency( string $currency ): bool {
return in_array( $currency, $this->supported_currencies, true );
}
/**
* Returns whether the current WC currency is supported.
*
* @return bool
*/
public function supports_wc_currency(): bool {
return $this->supports_currency( get_woocommerce_currency() );
}
}

View file

@ -73,11 +73,11 @@ class ErrorHandler {
clear()
{
if (! this.wrapper.classList.contains('woocommerce-error')) {
if (this.messagesList === null) {
return;
}
this.wrapper.classList.remove('woocommerce-error');
this.wrapper.innerText = '';
this.messagesList.innerHTML = '';
}
}

View file

@ -29,6 +29,15 @@ class PPECHelper {
const PPEC_SETTINGS_OPTION_NAME = 'woocommerce_ppec_paypal_settings';
/**
* Checks if the PayPal Express Checkout plugin was configured previously.
*
* @return bool
*/
public static function is_plugin_configured() {
return is_array( get_option( self::PPEC_SETTINGS_OPTION_NAME ) );
}
/**
* Checks if the PayPal Express Checkout plugin is active.
*

View file

@ -37,9 +37,11 @@ class Renderer {
foreach ( $items as $item ) {
?>
<tr>
<td data-export-label="<?php echo esc_attr( $item['label'] ); ?>"><?php echo esc_attr( $item['label'] ); ?></td>
<td data-export-label="<?php echo esc_attr( $item['exported_label'] ?? $item['label'] ); ?>">
<?php echo esc_attr( $item['label'] ); ?>
</td>
<td class="help"><?php echo wc_help_tip( $item['description'] ); ?></td>
<td><?php echo esc_attr( $item['value'] ); ?></td>
<td><?php echo wp_kses_post( $item['value'] ); ?></td>
</tr>
<?php
}

View file

@ -15,10 +15,14 @@ use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencySupport;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Webhooks\WebhookInfoStorage;
/**
* Class StatusReportModule
@ -44,6 +48,8 @@ class StatusReportModule implements ModuleInterface {
add_action(
'woocommerce_system_status_report',
function () use ( $c ) {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof ContainerInterface );
/* @var State $state The state. */
$state = $c->get( 'onboarding.state' );
@ -51,44 +57,102 @@ class StatusReportModule implements ModuleInterface {
/* @var Bearer $bearer The bearer. */
$bearer = $c->get( 'api.bearer' );
$currency_support = $c->get( 'api.helpers.currency-support' );
assert( $currency_support instanceof CurrencySupport );
/* @var DccApplies $dcc_applies The ddc applies. */
$dcc_applies = $c->get( 'api.helpers.dccapplies' );
/* @var MessagesApply $messages_apply The messages apply. */
$messages_apply = $c->get( 'button.helper.messages-apply' );
$last_webhook_storage = $c->get( 'webhook.last-webhook-storage' );
assert( $last_webhook_storage instanceof WebhookInfoStorage );
$billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' );
assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint );
/* @var Renderer $renderer The renderer. */
$renderer = $c->get( 'status-report.renderer' );
$had_ppec_plugin = PPECHelper::is_plugin_configured();
$items = array(
array(
'label' => esc_html__( 'Onboarded', 'woocommerce-paypal-payments' ),
'exported_label' => 'Onboarded',
'description' => esc_html__( 'Whether PayPal account is correctly configured or not.', 'woocommerce-paypal-payments' ),
'value' => $this->onboarded( $bearer, $state ),
'value' => $this->bool_to_html(
$this->onboarded( $bearer, $state )
),
),
array(
'label' => esc_html__( 'Shop country code', 'woocommerce-paypal-payments' ),
'exported_label' => 'Shop country code',
'description' => esc_html__( 'Country / State value on Settings / General / Store Address.', 'woocommerce-paypal-payments' ),
'value' => wc_get_base_location()['country'],
),
array(
'label' => esc_html__( 'WooCommerce currency supported', 'woocommerce-paypal-payments' ),
'exported_label' => 'WooCommerce currency supported',
'description' => esc_html__( 'Whether PayPal supports the default store currency or not.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$currency_support->supports_wc_currency()
),
),
array(
'label' => esc_html__( 'PayPal card processing available in country', 'woocommerce-paypal-payments' ),
'exported_label' => 'PayPal card processing available in country',
'description' => esc_html__( 'Whether PayPal card processing is available in country or not.', 'woocommerce-paypal-payments' ),
'value' => $dcc_applies->for_country_currency()
? esc_html__( 'Yes', 'woocommerce-paypal-payments' )
: esc_html__( 'No', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$dcc_applies->for_country_currency()
),
),
array(
'label' => esc_html__( 'Pay Later messaging available in country', 'woocommerce-paypal-payments' ),
'exported_label' => 'Pay Later messaging available in country',
'description' => esc_html__( 'Whether Pay Later is available in country or not.', 'woocommerce-paypal-payments' ),
'value' => $messages_apply->for_country()
? esc_html__( 'Yes', 'woocommerce-paypal-payments' )
: esc_html__( 'No', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$messages_apply->for_country()
),
),
array(
'label' => esc_html__( 'Webhook status', 'woocommerce-paypal-payments' ),
'exported_label' => 'Webhook status',
'description' => esc_html__( 'Whether we received webhooks successfully.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html( ! $last_webhook_storage->is_empty() ),
),
array(
'label' => esc_html__( 'Vault enabled', 'woocommerce-paypal-payments' ),
'exported_label' => 'Vault enabled',
'description' => esc_html__( 'Whether vaulting is enabled on PayPal account or not.', 'woocommerce-paypal-payments' ),
'value' => $this->vault_enabled( $bearer ),
'value' => $this->bool_to_html(
$this->vault_enabled( $bearer )
),
),
array(
'label' => esc_html__( 'Logging enabled', 'woocommerce-paypal-payments' ),
'exported_label' => 'Logging enabled',
'description' => esc_html__( 'Whether logging of plugin events and errors is enabled.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$settings->has( 'logging_enabled' ) && $settings->get( 'logging_enabled' )
),
),
array(
'label' => esc_html__( 'Reference Transactions', 'woocommerce-paypal-payments' ),
'exported_label' => 'Reference Transactions',
'description' => esc_html__( 'Whether Reference Transactions are enabled for the connected account', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$this->reference_transaction_enabled( $billing_agreements_endpoint )
),
),
array(
'label' => esc_html__( 'Used PayPal Checkout plugin', 'woocommerce-paypal-payments' ),
'exported_label' => 'Used PayPal Checkout plugin',
'description' => esc_html__( 'Whether the PayPal Checkout Gateway plugin was configured previously or not', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html(
$had_ppec_plugin
),
),
);
@ -112,37 +176,56 @@ class StatusReportModule implements ModuleInterface {
*
* @param Bearer $bearer The bearer.
* @param State $state The state.
* @return string
* @return bool
*/
private function onboarded( $bearer, $state ): string {
private function onboarded( Bearer $bearer, State $state ): bool {
try {
$token = $bearer->bearer();
} catch ( RuntimeException $exception ) {
return esc_html__( 'No', 'woocommerce-paypal-payments' );
return false;
}
$current_state = $state->current_state();
if ( $token->is_valid() && $current_state === $state::STATE_ONBOARDED ) {
return esc_html__( 'Yes', 'woocommerce-paypal-payments' );
}
return esc_html__( 'No', 'woocommerce-paypal-payments' );
return $token->is_valid() && $current_state === $state::STATE_ONBOARDED;
}
/**
* It returns whether vaulting is enabled or not.
*
* @param Bearer $bearer The bearer.
* @return string
* @return bool
*/
private function vault_enabled( $bearer ) {
private function vault_enabled( Bearer $bearer ): bool {
try {
$token = $bearer->bearer();
return $token->vaulting_available()
? esc_html__( 'Yes', 'woocommerce-paypal-payments' )
: esc_html__( 'No', 'woocommerce-paypal-payments' );
return $token->vaulting_available();
} catch ( RuntimeException $exception ) {
return esc_html__( 'No', 'woocommerce-paypal-payments' );
return false;
}
}
/**
* Checks if reference transactions are enabled in account.
*
* @param BillingAgreementsEndpoint $billing_agreements_endpoint The endpoint.
*/
private function reference_transaction_enabled( BillingAgreementsEndpoint $billing_agreements_endpoint ): bool {
try {
return $billing_agreements_endpoint->reference_transaction_enabled();
} catch ( RuntimeException $exception ) {
return false;
}
}
/**
* Converts the bool value to "yes" icon or dash.
*
* @param bool $value The value.
* @return string
*/
private function bool_to_html( bool $value ): string {
return $value
? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>'
: '<mark class="no">&ndash;</mark>';
}
}

View file

@ -32,11 +32,13 @@ return array(
$factory = $container->get( 'api.factory.webhook' );
$endpoint = $container->get( 'api.endpoint.webhook' );
$rest_endpoint = $container->get( 'webhook.endpoint.controller' );
$last_webhook_storage = $container->get( 'webhook.last-webhook-storage' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new WebhookRegistrar(
$factory,
$endpoint,
$rest_endpoint,
$last_webhook_storage,
$logger
);
},
@ -48,6 +50,7 @@ return array(
$verify_request = ! defined( 'PAYPAL_WEBHOOK_REQUEST_VERIFICATION' ) || PAYPAL_WEBHOOK_REQUEST_VERIFICATION;
$webhook_event_factory = $container->get( 'api.factory.webhook-event' );
$simulation = $container->get( 'webhook.status.simulation' );
$last_webhook_storage = $container->get( 'webhook.last-webhook-storage' );
return new IncomingWebhookEndpoint(
$webhook_endpoint,
@ -56,6 +59,7 @@ return array(
$verify_request,
$webhook_event_factory,
$simulation,
$last_webhook_storage,
... $handler
);
},
@ -183,6 +187,13 @@ return array(
);
},
'webhook.last-webhook-storage' => static function ( ContainerInterface $container ): WebhookInfoStorage {
return new WebhookInfoStorage( $container->get( 'webhook.last-webhook-storage.key' ) );
},
'webhook.last-webhook-storage.key' => static function ( ContainerInterface $container ): string {
return 'ppcp-last-webhook';
},
'webhook.module-url' => static function ( ContainerInterface $container ): string {
return plugins_url(
'/modules/ppcp-webhooks/',

View file

@ -76,6 +76,13 @@ class IncomingWebhookEndpoint {
*/
private $simulation;
/**
* The last webhook info storage.
*
* @var WebhookInfoStorage
*/
private $last_webhook_storage;
/**
* IncomingWebhookEndpoint constructor.
*
@ -85,6 +92,7 @@ class IncomingWebhookEndpoint {
* @param bool $verify_request Whether requests need to be verified or not.
* @param WebhookEventFactory $webhook_event_factory The webhook event factory.
* @param WebhookSimulation $simulation The simulation handler.
* @param WebhookInfoStorage $last_webhook_storage The last webhook info storage.
* @param RequestHandler ...$handlers The handlers, which process a request in the end.
*/
public function __construct(
@ -94,6 +102,7 @@ class IncomingWebhookEndpoint {
bool $verify_request,
WebhookEventFactory $webhook_event_factory,
WebhookSimulation $simulation,
WebhookInfoStorage $last_webhook_storage,
RequestHandler ...$handlers
) {
@ -103,6 +112,7 @@ class IncomingWebhookEndpoint {
$this->logger = $logger;
$this->verify_request = $verify_request;
$this->webhook_event_factory = $webhook_event_factory;
$this->last_webhook_storage = $last_webhook_storage;
$this->simulation = $simulation;
}
@ -176,6 +186,8 @@ class IncomingWebhookEndpoint {
public function handle_request( \WP_REST_Request $request ): \WP_REST_Response {
$event = $this->event_from_request( $request );
$this->last_webhook_storage->save( $event );
if ( $this->simulation->is_simulation_event( $event ) ) {
$this->logger->info( 'Received simulated webhook.' );
$this->simulation->receive( $event );

View file

@ -0,0 +1,76 @@
<?php
/**
* Stores the info about webhook events.
*
* @package WooCommerce\PayPalCommerce\Webhooks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks;
use WooCommerce\PayPalCommerce\ApiClient\Entity\WebhookEvent;
/**
* Class WebhookInfoStorage
*/
class WebhookInfoStorage {
/**
* The WP option key.
*
* @var string
*/
private $key;
/**
* WebhookInfoStorage constructor.
*
* @param string $key The WP option key.
*/
public function __construct(
string $key
) {
$this->key = $key;
}
/**
* Saves the info about webhook event.
*
* @param WebhookEvent $webhook_event The webhook event to save.
*/
public function save( WebhookEvent $webhook_event ): void {
$data = array(
'id' => $webhook_event->id(),
'received_time' => time(),
);
update_option( $this->key, $data );
}
/**
* Returns the stored data or null.
*/
public function get_data(): ?array {
$data = get_option( $this->key );
if ( ! $data || ! is_array( $data ) ) {
return null;
}
return $data;
}
/**
* Checks if there is any stored data.
*/
public function is_empty(): bool {
$data = get_option( $this->key );
return ! $data || ! is_array( $data );
}
/**
* Removes the stored data.
*/
public function clear(): void {
delete_option( $this->key );
}
}

View file

@ -44,6 +44,13 @@ class WebhookRegistrar {
*/
private $rest_endpoint;
/**
* The last webhook info storage.
*
* @var WebhookInfoStorage
*/
private $last_webhook_storage;
/**
* The logger.
*
@ -57,18 +64,21 @@ class WebhookRegistrar {
* @param WebhookFactory $webhook_factory The Webhook factory.
* @param WebhookEndpoint $endpoint The Webhook endpoint.
* @param IncomingWebhookEndpoint $rest_endpoint The WordPress Rest API endpoint.
* @param WebhookInfoStorage $last_webhook_storage The last webhook info storage.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
WebhookFactory $webhook_factory,
WebhookEndpoint $endpoint,
IncomingWebhookEndpoint $rest_endpoint,
WebhookInfoStorage $last_webhook_storage,
LoggerInterface $logger
) {
$this->webhook_factory = $webhook_factory;
$this->endpoint = $endpoint;
$this->rest_endpoint = $rest_endpoint;
$this->last_webhook_storage = $last_webhook_storage;
$this->logger = $logger;
}
@ -92,6 +102,7 @@ class WebhookRegistrar {
self::KEY,
$created->to_array()
);
$this->last_webhook_storage->clear();
$this->logger->info( 'Webhooks subscribed.' );
return true;
} catch ( RuntimeException $error ) {
@ -120,6 +131,7 @@ class WebhookRegistrar {
if ( $success ) {
delete_option( self::KEY );
$this->last_webhook_storage->clear();
$this->logger->info( 'Webhooks deleted.' );
}
return $success;

View file

@ -12,20 +12,23 @@ class RendererTest extends TestCase
{
$items = [
[
'label' => 'Foo',
'label' => 'Translated Foo',
'exported_label' => 'Foo',
'description' => 'Bar',
'value' => 'Baz'
],
];
when('esc_attr')->returnArg();
when('wp_kses_post')->returnArg();
when('wc_help_tip')->returnArg();
$testee = new Renderer();
$result = $testee->render('Some title here', $items);
self::assertStringContainsString('<h2>Some title here</h2>', $result);
self::assertStringContainsString('<td data-export-label="Foo">Foo</td>', $result);
self::assertStringContainsString('data-export-label="Foo"', $result);
self::assertStringContainsString('Translated Foo', $result);
self::assertStringContainsString('<td class="help">Bar</td>', $result);
self::assertStringContainsString('<td>Baz</td>', $result);
}