Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions src/API/Google/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,13 @@ protected function generate_shipping_settings(): ShippingSettings {
}

/**
* Returns a `[ country => currency ]` map for every non-manual market.
* Returns a `[ country => currency ]` map for every participating,
* non-manual market.
*
* Each market contributes its country and the first entry of its `currency[]`
* array. Manual markets are skipped — they don't get an MC shipping service.
* Markets excluded from syncing while currency conversion is unavailable
* are skipped for the same reason.
*
* @return array<string, string>
*/
Expand All @@ -160,7 +163,7 @@ protected function build_country_currency_map(): array {
$market_service = $this->container->get( MarketService::class );

$map = [];
foreach ( $market_service->get_markets() as $market ) {
foreach ( $market_service->get_participating_markets() as $market ) {
if ( 'manual' === ( $market['shipping_rate'] ?? null ) ) {
continue;
}
Expand Down Expand Up @@ -265,12 +268,33 @@ protected function get_shipping_times(): array {
/**
* Get shipping rate data.
*
* Rows belonging to secondary markets that are currently excluded from
* syncing (non-store currency while conversion is unavailable) are left
* out, so those countries get no Merchant Center shipping service while
* their markets sit out. All other rows pass through untouched.
*
* @return array
*/
protected function get_shipping_rates_from_database(): array {
$rate_query = $this->container->get( ShippingRateQuery::class );
/** @var MarketService $market_service */
$market_service = $this->container->get( MarketService::class );

$excluded_countries = $market_service->get_excluded_market_countries();
$rates = $rate_query->get_results();

return $rate_query->get_results();
if ( empty( $excluded_countries ) ) {
return $rates;
}

return array_values(
array_filter(
$rates,
function ( array $rate ) use ( $excluded_countries ): bool {
return ! in_array( $rate['country'] ?? null, $excluded_countries, true );
}
)
);
}

/**
Expand Down
22 changes: 21 additions & 1 deletion src/Google/GoogleProductService.php
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,29 @@ protected function validate_batch_request_entry( $request_entry, string $method
protected static function get_batch_response_error_messages( GoogleBatchResponseEntry $batch_response_entry ): array {
$errors = [];
foreach ( $batch_response_entry->getErrors()->getErrors() as $error ) {
$errors[ $error->getReason() ] = $error->getMessage();
$errors[ self::normalise_error_reason( (string) $error->getReason() ) ] = $error->getMessage();
}

return $errors;
}

/**
* Normalises a batch error reason to the camelCase form used by the reason
* constants (e.g. `internal_error` becomes `internalError`).
*
* Google has sent the same failure under both spellings, and a reason that
* does not match the constants is invisible to the retry and
* failure-tracking logic keyed off these values.
*
* @param string $reason The error reason as sent by Google.
*
* @return string
*/
protected static function normalise_error_reason( string $reason ): string {
if ( false === strpos( $reason, '_' ) ) {
return $reason;
}

return lcfirst( str_replace( ' ', '', ucwords( str_replace( '_', ' ', $reason ) ) ) );
}
}
13 changes: 13 additions & 0 deletions src/Integration/WPML.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ public function is_active(): bool {
*/
public function init(): void {}

/**
* Returns whether product prices can be converted into another currency.
*
* This is the exact availability condition used by the price conversion
* methods below: WPML active with WCML multi-currency enabled. Markets
* priced in a non-store currency can only be synced when this holds.
*
* @return bool
*/
public function can_convert_currency(): bool {
return $this->is_active() && $this->is_wcml_multi_currency_on();
}

/**
* Returns the site's default WPML language code.
*
Expand Down
2 changes: 1 addition & 1 deletion src/Internal/DependencyManagement/JobServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ public function register(): void {
$this->share_with_tags( PluginUpdate::class, JobRepository::class );

// Share shipping settings syncer job and hooks.
$this->share_action_scheduler_job( UpdateShippingSettings::class, MerchantCenterService::class, GoogleSettings::class, MarketService::class );
$this->share_action_scheduler_job( UpdateShippingSettings::class, MerchantCenterService::class, GoogleSettings::class, MarketService::class, MerchantStatuses::class );
$this->share_with_tags( Shipping\SyncerHooks::class, MerchantCenterService::class, GoogleSettings::class, JobRepository::class );

// Share plugin update jobs
Expand Down
115 changes: 109 additions & 6 deletions src/Jobs/UpdateShippingSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Settings as GoogleSettings;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MarketService;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantStatuses;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use DateTime;
use Exception;

defined( 'ABSPATH' ) || exit;

Expand All @@ -21,7 +27,9 @@
*
* @since 2.1.0
*/
class UpdateShippingSettings extends AbstractActionSchedulerJob {
class UpdateShippingSettings extends AbstractActionSchedulerJob implements OptionsAwareInterface {

use OptionsAwareTrait;

/**
* @var MerchantCenterService
Expand All @@ -38,6 +46,11 @@ class UpdateShippingSettings extends AbstractActionSchedulerJob {
*/
protected $market_service;

/**
* @var MerchantStatuses
*/
protected $merchant_statuses;

/**
* UpdateShippingSettings constructor.
*
Expand All @@ -46,18 +59,40 @@ class UpdateShippingSettings extends AbstractActionSchedulerJob {
* @param MerchantCenterService $merchant_center
* @param GoogleSettings $google_settings
* @param MarketService $market_service
* @param MerchantStatuses $merchant_statuses
*/
public function __construct(
ActionSchedulerInterface $action_scheduler,
ActionSchedulerJobMonitor $monitor,
MerchantCenterService $merchant_center,
GoogleSettings $google_settings,
MarketService $market_service
MarketService $market_service,
MerchantStatuses $merchant_statuses
) {
parent::__construct( $action_scheduler, $monitor );
$this->merchant_center = $merchant_center;
$this->google_settings = $google_settings;
$this->market_service = $market_service;
$this->merchant_center = $merchant_center;
$this->google_settings = $google_settings;
$this->market_service = $market_service;
$this->merchant_statuses = $merchant_statuses;
}

/**
* Initialize the job's hooks.
*
* Alongside the process hook, registers the stored sync failure as a
* Merchant Center issue so the failure is shown in the plugin instead of
* only in Scheduled Actions, where the failure-rate block hides the job
* after repeated failures.
*/
public function init(): void {
parent::init();

add_filter(
'woocommerce_gla_custom_merchant_issues',
[ $this, 'add_shipping_sync_failure_issue' ],
10,
2
);
}

/**
Expand Down Expand Up @@ -86,13 +121,81 @@ public function can_schedule( $args = [] ): bool {
* @param int[] $items An array of job arguments.
*
* @throws JobException If the shipping settings cannot be synced.
* @throws Exception If the shipping settings sync fails. The failure is
* stored first so it can be shown in the plugin as a
* Merchant Center issue.
*/
public function process_items( array $items ) {
if ( ! $this->can_sync_shipping() ) {
throw new JobException( 'Cannot sync shipping settings. Confirm that the merchant center account is connected and the option to automatically sync the shipping settings is selected.' );
}

$this->google_settings->sync_shipping();
try {
$this->google_settings->sync_shipping();
} catch ( Exception $exception ) {
$this->options->update(
OptionsInterface::SHIPPING_SYNC_FAILURE,
[
'message' => $exception->getMessage(),
'failed_at' => gmdate( 'Y-m-d H:i:s' ),
]
);
$this->merchant_statuses->clear_cache();

throw $exception;
}

$this->clear_reported_failure();
}

/**
* Turns the stored sync failure into a Merchant Center issue row.
*
* @hooked woocommerce_gla_custom_merchant_issues
*
* @param array $issues The custom issues collected so far.
* @param DateTime $cache_created_time The issue cache creation time.
*
* @return array
*/
public function add_shipping_sync_failure_issue( array $issues, DateTime $cache_created_time ): array {
$failure = $this->options->get( OptionsInterface::SHIPPING_SYNC_FAILURE );

if ( empty( $failure['message'] ) ) {
return $issues;
}

$issues[] = [
'product_id' => 0,
'product' => 'All products',
'code' => 'shipping_settings_sync_failed',
'issue' => __( 'The shipping settings could not be synced to Google Merchant Center.', 'google-listings-and-ads' ),
'action' => sprintf(
/* translators: %s: the error reported by the failed sync. */
__( 'Resolve the reported problem, then save your shipping settings to retry the sync. Last error: %s', 'google-listings-and-ads' ),
$failure['message']
),
'action_url' => $this->get_settings_url(),
'created_at' => $cache_created_time->format( 'Y-m-d H:i:s' ),
'type' => MerchantStatuses::TYPE_ACCOUNT,
'severity' => 'error',
'source' => 'filter',
];

return $issues;
}

/**
* Clears a previously stored sync failure after a successful sync, and
* refreshes the cached statuses so the issue row disappears promptly.
*/
protected function clear_reported_failure(): void {
if ( empty( $this->options->get( OptionsInterface::SHIPPING_SYNC_FAILURE ) ) ) {
return;
}

$this->options->delete( OptionsInterface::SHIPPING_SYNC_FAILURE );
$this->merchant_statuses->clear_cache();
}

/**
Expand Down
Loading
Loading