File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/Coupon.tar
CouponHelper.php 0000644 00000027474 15154275264 0007711 0 ustar 00 <?php
declare(strict_types = 1);
namespace Automattic\WooCommerce\GoogleListingsAndAds\Coupon;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\ChannelVisibility;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\NotificationStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\SyncStatus;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications\HelperNotificationInterface;
use WC_Coupon;
defined( 'ABSPATH' ) || exit();
/**
* Class CouponHelper
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Coupon
*/
class CouponHelper implements Service, HelperNotificationInterface {
use PluginHelper;
/**
*
* @var CouponMetaHandler
*/
protected $meta_handler;
/**
*
* @var WC
*/
protected $wc;
/**
*
* @var MerchantCenterService
*/
protected $merchant_center;
/**
* CouponHelper constructor.
*
* @param CouponMetaHandler $meta_handler
* @param WC $wc
* @param MerchantCenterService $merchant_center
*/
public function __construct(
CouponMetaHandler $meta_handler,
WC $wc,
MerchantCenterService $merchant_center
) {
$this->meta_handler = $meta_handler;
$this->wc = $wc;
$this->merchant_center = $merchant_center;
}
/**
* Mark the item as notified.
*
* @param WC_Coupon $coupon
*
* @return void
*/
public function mark_as_notified( $coupon ): void {
$this->meta_handler->update_synced_at( $coupon, time() );
$this->meta_handler->update_sync_status( $coupon, SyncStatus::SYNCED );
$this->update_empty_visibility( $coupon );
}
/**
* Mark a coupon as synced. This function accepts nullable $google_id,
* which guarantees version compatibility for Alpha, Beta and stable verison promtoion APIs.
*
* @param WC_Coupon $coupon
* @param string|null $google_id
* @param string $target_country
*/
public function mark_as_synced(
WC_Coupon $coupon,
?string $google_id,
string $target_country
) {
$this->meta_handler->update_synced_at( $coupon, time() );
$this->meta_handler->update_sync_status( $coupon, SyncStatus::SYNCED );
$this->update_empty_visibility( $coupon );
// merge and update all google ids
$current_google_ids = $this->meta_handler->get_google_ids( $coupon );
$current_google_ids = ! empty( $current_google_ids ) ? $current_google_ids : [];
$google_ids = array_unique(
array_merge(
$current_google_ids,
[
$target_country => $google_id,
]
)
);
$this->meta_handler->update_google_ids( $coupon, $google_ids );
}
/**
*
* @param WC_Coupon $coupon
*/
public function mark_as_unsynced( $coupon ): void {
$this->meta_handler->delete_synced_at( $coupon );
$this->meta_handler->update_sync_status( $coupon, SyncStatus::NOT_SYNCED );
$this->meta_handler->delete_google_ids( $coupon );
$this->meta_handler->delete_errors( $coupon );
$this->meta_handler->delete_failed_sync_attempts( $coupon );
$this->meta_handler->delete_sync_failed_at( $coupon );
}
/**
*
* @param WC_Coupon $coupon
* @param string $target_country
*/
public function remove_google_id_by_country( WC_Coupon $coupon, string $target_country ) {
$google_ids = $this->meta_handler->get_google_ids( $coupon );
if ( empty( $google_ids ) ) {
return;
}
unset( $google_ids[ $target_country ] );
if ( ! empty( $google_ids ) ) {
$this->meta_handler->update_google_ids( $coupon, $google_ids );
} else {
// if there are no Google IDs left then this coupon is no longer considered "synced"
$this->mark_as_unsynced( $coupon );
}
}
/**
* Marks a WooCommerce coupon as invalid and stores the errors in a meta data key.
*
* @param WC_Coupon $coupon
* @param InvalidCouponEntry[] $errors
*/
public function mark_as_invalid( WC_Coupon $coupon, array $errors ) {
// bail if no errors exist
if ( empty( $errors ) ) {
return;
}
$this->meta_handler->update_errors( $coupon, $errors );
$this->meta_handler->update_sync_status( $coupon, SyncStatus::HAS_ERRORS );
$this->update_empty_visibility( $coupon );
// TODO: Update failed sync attempts count in case of internal errors
}
/**
* Marks a WooCommerce coupon as pending synchronization.
*
* @param WC_Coupon $coupon
*/
public function mark_as_pending( WC_Coupon $coupon ) {
$this->meta_handler->update_sync_status( $coupon, SyncStatus::PENDING );
$this->meta_handler->delete_errors( $coupon );
}
/**
* Update empty (NOT EXIST) visibility meta values to SYNC_AND_SHOW.
*
* @param WC_Coupon $coupon
*/
protected function update_empty_visibility( WC_Coupon $coupon ): void {
$visibility = $this->meta_handler->get_visibility( $coupon );
if ( empty( $visibility ) ) {
$this->meta_handler->update_visibility(
$coupon,
ChannelVisibility::SYNC_AND_SHOW
);
}
}
/**
*
* @param WC_Coupon $coupon
*
* @return string[]|null An array of Google IDs stored for each WooCommerce coupon
*/
public function get_synced_google_ids( WC_Coupon $coupon ): ?array {
return $this->meta_handler->get_google_ids( $coupon );
}
/**
* Get WooCommerce coupon
*
* @param int $coupon_id
*
* @return WC_Coupon
*
* @throws InvalidValue If the given ID doesn't reference a valid coupon.
*/
public function get_wc_coupon( int $coupon_id ): WC_Coupon {
$coupon = $this->wc->maybe_get_coupon( $coupon_id );
if ( ! $coupon instanceof WC_Coupon ) {
throw InvalidValue::not_valid_coupon_id( $coupon_id );
}
return $coupon;
}
/**
*
* @param WC_Coupon $coupon
*
* @return bool
*/
public function is_coupon_synced( WC_Coupon $coupon ): bool {
$synced_at = $this->meta_handler->get_synced_at( $coupon );
$google_ids = $this->meta_handler->get_google_ids( $coupon );
return ! empty( $synced_at ) && ! empty( $google_ids );
}
/**
*
* @param WC_Coupon $coupon
*
* @return bool
*/
public function is_sync_ready( WC_Coupon $coupon ): bool {
return ( ChannelVisibility::SYNC_AND_SHOW ===
$this->get_channel_visibility( $coupon ) ) &&
( CouponSyncer::is_coupon_supported( $coupon ) ) &&
( ! $coupon->get_virtual() );
}
/**
* Whether the sync has failed repeatedly for the coupon within the given timeframe.
*
* @param WC_Coupon $coupon
*
* @return bool
*
* @see CouponSyncer::FAILURE_THRESHOLD The number of failed attempts allowed per timeframe
* @see CouponSyncer::FAILURE_THRESHOLD_WINDOW The specified timeframe
*/
public function is_sync_failed_recently( WC_Coupon $coupon ): bool {
$failed_attempts = $this->meta_handler->get_failed_sync_attempts(
$coupon
);
$failed_at = $this->meta_handler->get_sync_failed_at( $coupon );
// if it has failed more times than the specified threshold AND if syncing it has failed within the specified window
return $failed_attempts > CouponSyncer::FAILURE_THRESHOLD &&
$failed_at >
strtotime( sprintf( '-%s', CouponSyncer::FAILURE_THRESHOLD_WINDOW ) );
}
/**
*
* @param WC_Coupon $coupon
*
* @return string
*/
public function get_channel_visibility( WC_Coupon $coupon ): string {
$visibility = $this->meta_handler->get_visibility( $coupon );
if ( empty( $visibility ) ) {
do_action(
'woocommerce_gla_debug_message',
sprintf(
'Channel visibility forced to "%s" for visibility unknown (Post ID: %s).',
ChannelVisibility::DONT_SYNC_AND_SHOW,
$coupon->get_id()
),
__METHOD__
);
return ChannelVisibility::DONT_SYNC_AND_SHOW;
}
return $visibility;
}
/**
* Return a string indicating sync status based on several factors.
*
* @param WC_Coupon $coupon
*
* @return string|null
*/
public function get_sync_status( WC_Coupon $coupon ): ?string {
return $this->meta_handler->get_sync_status( $coupon );
}
/**
* Return the string indicating the coupon status as reported by the Merchant Center.
*
* @param WC_Coupon $coupon
*
* @return string|null
*/
public function get_mc_status( WC_Coupon $coupon ): ?string {
try {
return $this->meta_handler->get_mc_status( $coupon );
} catch ( InvalidValue $exception ) {
do_action(
'woocommerce_gla_debug_message',
sprintf(
'Coupon status returned null for invalid coupon (ID: %s).',
$coupon->get_id()
),
__METHOD__
);
return null;
}
}
/**
* Get validation errors for a specific coupon.
* Combines errors for variable coupons, which have a variation-indexed array of errors.
*
* @param WC_Coupon $coupon
*
* @return array
*/
public function get_validation_errors( WC_Coupon $coupon ): array {
$errors = $this->meta_handler->get_errors( $coupon ) ?: [];
$first_key = array_key_first( $errors );
if ( ! empty( $errors ) && is_array( $errors[ $first_key ] ) ) {
$errors = array_unique( array_merge( ...$errors ) );
}
return $errors;
}
/**
* Indicates if a coupon is ready for sending Notifications.
* A coupon is ready to send notifications if its sync ready and the post status is publish.
*
* @param WC_Coupon $coupon
*
* @return bool
*/
public function is_ready_to_notify( WC_Coupon $coupon ): bool {
$is_ready = $this->is_sync_ready( $coupon ) && $coupon->get_status() === 'publish';
/**
* Allow users to filter if a coupon is ready to notify.
*
* @since 2.8.0
*
* @param bool $value The current filter value.
* @param WC_Coupon $coupon The coupon for the notification.
*/
return apply_filters( 'woocommerce_gla_coupon_is_ready_to_notify', $is_ready, $coupon );
}
/**
* Indicates if a coupon was already notified about its creation.
* Notice we consider synced coupons in MC as notified for creation.
*
* @param WC_Coupon $coupon
*
* @return bool
*/
public function has_notified_creation( WC_Coupon $coupon ): bool {
$valid_has_notified_creation_statuses = [
NotificationStatus::NOTIFICATION_CREATED,
NotificationStatus::NOTIFICATION_UPDATED,
NotificationStatus::NOTIFICATION_PENDING_UPDATE,
NotificationStatus::NOTIFICATION_PENDING_DELETE,
];
return in_array(
$this->meta_handler->get_notification_status( $coupon ),
$valid_has_notified_creation_statuses,
true
) || $this->is_coupon_synced( $coupon );
}
/**
* Set the notification status for a WooCommerce coupon.
*
* @param WC_Coupon $coupon
* @param string $status
*/
public function set_notification_status( $coupon, $status ): void {
$this->meta_handler->update_notification_status( $coupon, $status );
}
/**
* Indicates if a coupon is ready for sending a create Notification.
* A coupon is ready to send create notifications if is ready to notify and has not sent create notification yet.
*
* @param WC_Coupon $coupon
*
* @return bool
*/
public function should_trigger_create_notification( $coupon ): bool {
return $this->is_ready_to_notify( $coupon ) && ! $this->has_notified_creation( $coupon );
}
/**
* Indicates if a coupon is ready for sending an update Notification.
* A coupon is ready to send update notifications if is ready to notify and has sent create notification already.
*
* @param WC_Coupon $coupon
*
* @return bool
*/
public function should_trigger_update_notification( $coupon ): bool {
return $this->is_ready_to_notify( $coupon ) && $this->has_notified_creation( $coupon );
}
/**
* Indicates if a coupon is ready for sending a delete Notification.
* A coupon is ready to send delete notifications if it is not ready to notify and has sent create notification already.
*
* @param WC_Coupon $coupon
*
* @return bool
*/
public function should_trigger_delete_notification( $coupon ): bool {
return ! $this->is_ready_to_notify( $coupon ) && $this->has_notified_creation( $coupon );
}
}
CouponMetaHandler.php 0000644 00000014542 15154275264 0010646 0 ustar 00 <?php
declare(strict_types = 1);
namespace Automattic\WooCommerce\GoogleListingsAndAds\Coupon;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidMeta;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use BadMethodCallException;
use WC_Coupon;
defined( 'ABSPATH' ) || exit();
/**
* Class CouponMetaHandler
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Coupon
*
* @method update_synced_at( WC_Coupon $coupon, $value )
* @method delete_synced_at( WC_Coupon $coupon )
* @method get_synced_at( WC_Coupon $coupon ): int|null
* @method update_google_ids( WC_Coupon $coupon, array $value )
* @method delete_google_ids( WC_Coupon $coupon )
* @method get_google_ids( WC_Coupon $coupon ): array|null
* @method update_visibility( WC_Coupon $coupon, $value )
* @method delete_visibility( WC_Coupon $coupon )
* @method get_visibility( WC_Coupon $coupon ): string|null
* @method update_errors( WC_Coupon $coupon, array $value )
* @method delete_errors( WC_Coupon $coupon )
* @method get_errors( WC_Coupon $coupon ): array|null
* @method update_failed_sync_attempts( WC_Coupon $coupon, int $value )
* @method delete_failed_sync_attempts( WC_Coupon $coupon )
* @method get_failed_sync_attempts( WC_Coupon $coupon ): int|null
* @method update_sync_failed_at( WC_Coupon $coupon, int $value )
* @method delete_sync_failed_at( WC_Coupon $coupon )
* @method get_sync_failed_at( WC_Coupon $coupon ): int|null
* @method update_sync_status( WC_Coupon $coupon, string $value )
* @method delete_sync_status( WC_Coupon $coupon )
* @method get_sync_status( WC_Coupon $coupon ): string|null
* @method update_mc_status( WC_Coupon $coupon, string $value )
* @method delete_mc_status( WC_Coupon $coupon )
* @method get_mc_status( WC_Coupon $coupon ): string|null
* @method update_notification_status( WC_Coupon $coupon, string $value )
* @method delete_notification_status( WC_Coupon $coupon )
* @method get_notification_status( WC_Coupon $coupon ): string|null
*/
class CouponMetaHandler implements Service {
use PluginHelper;
public const KEY_SYNCED_AT = 'synced_at';
public const KEY_GOOGLE_IDS = 'google_ids';
public const KEY_VISIBILITY = 'visibility';
public const KEY_ERRORS = 'errors';
public const KEY_FAILED_SYNC_ATTEMPTS = 'failed_sync_attempts';
public const KEY_SYNC_FAILED_AT = 'sync_failed_at';
public const KEY_SYNC_STATUS = 'sync_status';
public const KEY_MC_STATUS = 'mc_status';
public const KEY_NOTIFICATION_STATUS = 'notification_status';
protected const TYPES = [
self::KEY_SYNCED_AT => 'int',
self::KEY_GOOGLE_IDS => 'array',
self::KEY_VISIBILITY => 'string',
self::KEY_ERRORS => 'array',
self::KEY_FAILED_SYNC_ATTEMPTS => 'int',
self::KEY_SYNC_FAILED_AT => 'int',
self::KEY_SYNC_STATUS => 'string',
self::KEY_MC_STATUS => 'string',
self::KEY_NOTIFICATION_STATUS => 'string',
];
/**
*
* @param string $name
* @param mixed $arguments
*
* @return mixed
*
* @throws BadMethodCallException If the method that's called doesn't exist.
* @throws InvalidMeta If the meta key is invalid.
*/
public function __call( string $name, $arguments ) {
$found_matches = preg_match( '/^([a-z]+)_([\w\d]+)$/i', $name, $matches );
if ( ! $found_matches ) {
throw new BadMethodCallException(
sprintf(
'The method %s does not exist in class CouponMetaHandler',
$name
)
);
}
[
$function_name,
$method,
$key
] = $matches;
// validate the method
if ( ! in_array(
$method,
[
'update',
'delete',
'get',
],
true
) ) {
throw new BadMethodCallException(
sprintf(
'The method %s does not exist in class CouponMetaHandler',
$function_name
)
);
}
// set the value as the third argument if method is `update`
if ( 'update' === $method ) {
$arguments[2] = $arguments[1];
}
// set the key as the second argument
$arguments[1] = $key;
return call_user_func_array(
[
$this,
$method,
],
$arguments
);
}
/**
*
* @param WC_Coupon $coupon
* @param string $key
* @param mixed $value
*
* @throws InvalidMeta If the meta key is invalid.
*/
public function update( WC_Coupon $coupon, string $key, $value ) {
self::validate_meta_key( $key );
if ( isset( self::TYPES[ $key ] ) ) {
if ( in_array(
self::TYPES[ $key ],
[
'bool',
'boolean',
],
true
) ) {
$value = wc_bool_to_string( $value );
} else {
settype( $value, self::TYPES[ $key ] );
}
}
$coupon->update_meta_data( $this->prefix_meta_key( $key ), $value );
$coupon->save_meta_data();
}
/**
*
* @param WC_Coupon $coupon
* @param string $key
*
* @throws InvalidMeta If the meta key is invalid.
*/
public function delete( WC_Coupon $coupon, string $key ) {
self::validate_meta_key( $key );
$coupon->delete_meta_data( $this->prefix_meta_key( $key ) );
$coupon->save_meta_data();
}
/**
*
* @param WC_Coupon $coupon
* @param string $key
*
* @return mixed The value, or null if the meta key doesn't exist.
*
* @throws InvalidMeta If the meta key is invalid.
*/
public function get( WC_Coupon $coupon, string $key ) {
self::validate_meta_key( $key );
$value = null;
if ( $coupon->meta_exists( $this->prefix_meta_key( $key ) ) ) {
$value = $coupon->get_meta( $this->prefix_meta_key( $key ), true );
if ( isset( self::TYPES[ $key ] ) &&
in_array(
self::TYPES[ $key ],
[
'bool',
'boolean',
],
true
) ) {
$value = wc_string_to_bool( $value );
}
}
return $value;
}
/**
*
* @param string $key
*
* @throws InvalidMeta If the meta key is invalid.
*/
protected static function validate_meta_key( string $key ) {
if ( ! self::is_meta_key_valid( $key ) ) {
do_action(
'woocommerce_gla_error',
sprintf( 'Coupon meta key is invalid: %s', $key ),
__METHOD__
);
throw InvalidMeta::invalid_key( $key );
}
}
/**
*
* @param string $key
*
* @return bool Whether the meta key is valid.
*/
public static function is_meta_key_valid( string $key ): bool {
return isset( self::TYPES[ $key ] );
}
/**
* Returns all available meta keys.
*
* @return array
*/
public static function get_all_meta_keys(): array {
return array_keys( self::TYPES );
}
}
CouponSyncer.php 0000644 00000031061 15154275264 0007720 0 ustar 00 <?php
declare(strict_types = 1);
namespace Automattic\WooCommerce\GoogleListingsAndAds\Coupon;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\DeleteCouponEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\GooglePromotionService;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\InvalidCouponEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\TargetAudience;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Exception as GoogleException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Exception;
use WC_Coupon;
defined( 'ABSPATH' ) || exit();
/**
* Class CouponSyncer
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Coupon
*/
class CouponSyncer implements Service {
public const FAILURE_THRESHOLD = 5;
// Number of failed attempts allowed per FAILURE_THRESHOLD_WINDOW
public const FAILURE_THRESHOLD_WINDOW = '3 hours';
/**
*
* @var GooglePromotionService
*/
protected $google_service;
/**
*
* @var CouponHelper
*/
protected $coupon_helper;
/**
*
* @var ValidatorInterface
*/
protected $validator;
/**
*
* @var MerchantCenterService
*/
protected $merchant_center;
/**
*
* @var WC
*/
protected $wc;
/**
* @var TargetAudience
*/
protected $target_audience;
/**
* CouponSyncer constructor.
*
* @param GooglePromotionService $google_service
* @param CouponHelper $coupon_helper
* @param ValidatorInterface $validator
* @param MerchantCenterService $merchant_center
* @param TargetAudience $target_audience
* @param WC $wc
*/
public function __construct(
GooglePromotionService $google_service,
CouponHelper $coupon_helper,
ValidatorInterface $validator,
MerchantCenterService $merchant_center,
TargetAudience $target_audience,
WC $wc
) {
$this->google_service = $google_service;
$this->coupon_helper = $coupon_helper;
$this->validator = $validator;
$this->merchant_center = $merchant_center;
$this->target_audience = $target_audience;
$this->wc = $wc;
}
/**
* Submit a WooCommerce coupon to Google Merchant Center.
*
* @param WC_Coupon $coupon
*
* @throws CouponSyncerException If there are any errors while syncing coupon with Google Merchant Center.
*/
public function update( WC_Coupon $coupon ) {
$this->validate_merchant_center_setup();
if ( ! $this->coupon_helper->is_sync_ready( $coupon ) ) {
do_action(
'woocommerce_gla_debug_message',
sprintf(
'Skipping coupon (ID: %s) because it is not ready to be synced.',
$coupon->get_id()
),
__METHOD__
);
return;
}
$target_country = $this->target_audience->get_main_target_country();
if ( ! $this->merchant_center->is_promotion_supported_country( $target_country ) ) {
do_action(
'woocommerce_gla_debug_message',
sprintf(
'Skipping coupon (ID: %s) because it is not supported in main target country %s.',
$coupon->get_id(),
$target_country
),
__METHOD__
);
return;
}
$adapted_coupon = new WCCouponAdapter(
[
'wc_coupon' => $coupon,
'targetCountry' => $target_country,
]
);
$validation_result = $this->validate_coupon( $adapted_coupon );
if ( $validation_result instanceof InvalidCouponEntry ) {
$this->coupon_helper->mark_as_invalid(
$coupon,
$validation_result->get_errors()
);
do_action(
'woocommerce_gla_debug_message',
sprintf(
'Skipping coupon (ID: %s) because it does not pass validation: %s',
$coupon->get_id(),
wp_json_encode( $validation_result )
),
__METHOD__
);
return;
}
try {
do_action(
'woocommerce_gla_debug_message',
sprintf(
'Start to upload coupon (ID: %s) as promotion structure: %s',
$coupon->get_id(),
wp_json_encode( $adapted_coupon )
),
__METHOD__
);
$response = $this->google_service->create( $adapted_coupon );
$this->coupon_helper->mark_as_synced(
$coupon,
$response->getId(),
$target_country
);
do_action( 'woocommerce_gla_updated_coupon', $adapted_coupon );
do_action(
'woocommerce_gla_debug_message',
sprintf(
"Submitted promotion:\n%s",
wp_json_encode( $adapted_coupon )
),
__METHOD__
);
} catch ( GoogleException $google_exception ) {
$invalid_promotion = new InvalidCouponEntry(
$coupon->get_id(),
[
$google_exception->getCode() => $google_exception->getMessage(),
],
$target_country
);
$this->coupon_helper->mark_as_invalid(
$coupon,
$invalid_promotion->get_errors()
);
$this->handle_update_errors( [ $invalid_promotion ] );
do_action(
'woocommerce_gla_debug_message',
sprintf(
"Promotion failed to sync with Merchant Center:\n%s",
wp_json_encode( $invalid_promotion )
),
__METHOD__
);
} catch ( Exception $exception ) {
do_action( 'woocommerce_gla_exception', $exception, __METHOD__ );
throw new CouponSyncerException(
sprintf(
'Error updating Google promotion: %s',
$exception->getMessage()
),
0,
$exception
);
}
}
/**
*
* @param WCCouponAdapter $coupon
*
* @return InvalidCouponEntry|true
*/
protected function validate_coupon( WCCouponAdapter $coupon ) {
$violations = $this->validator->validate( $coupon );
if ( 0 !== count( $violations ) ) {
$invalid_promotion = new InvalidCouponEntry(
$coupon->get_wc_coupon_id()
);
$invalid_promotion->map_validation_violations( $violations );
return $invalid_promotion;
}
return true;
}
/**
* Delete a WooCommerce coupon from Google Merchant Center.
*
* @param DeleteCouponEntry $coupon
*
* @throws CouponSyncerException If there are any errors while deleting coupon from Google Merchant Center.
*/
public function delete( DeleteCouponEntry $coupon ) {
$this->validate_merchant_center_setup();
$deleted_promotions = [];
$invalid_promotions = [];
$synced_google_ids = $coupon->get_synced_google_ids();
$wc_coupon = $this->wc->maybe_get_coupon(
$coupon->get_wc_coupon_id()
);
$wc_coupon_exist = $wc_coupon instanceof WC_Coupon;
foreach ( $synced_google_ids as $target_country => $google_id ) {
try {
$adapted_coupon = $coupon->get_google_promotion();
$adapted_coupon->setTargetCountry( $target_country );
do_action(
'woocommerce_gla_debug_message',
sprintf(
'Start to delete coupon (ID: %s) as promotion structure: %s',
$coupon->get_wc_coupon_id(),
wp_json_encode( $adapted_coupon )
),
__METHOD__
);
// DeleteCouponEntry is generated with promotion effective date expired
// when WC coupon is able to be deleted.
// To soft-delete the promotion from Google side,
// we will update Google promotion with expired effective date.
$response = $this->google_service->create( $adapted_coupon );
array_push( $deleted_promotions, $response );
if ( $wc_coupon_exist ) {
$this->coupon_helper->remove_google_id_by_country(
$wc_coupon,
$target_country
);
}
} catch ( GoogleException $google_exception ) {
array_push(
$invalid_promotions,
new InvalidCouponEntry(
$coupon->get_wc_coupon_id(),
[
$google_exception->getCode() => $google_exception->getMessage(),
],
$target_country,
$google_id
)
);
} catch ( Exception $exception ) {
do_action( 'woocommerce_gla_exception', $exception, __METHOD__ );
throw new CouponSyncerException(
sprintf(
'Error deleting Google promotion: %s',
$exception->getMessage()
),
0,
$exception
);
}
}
if ( ! empty( $invalid_promotions ) ) {
$this->handle_delete_errors( $invalid_promotions );
do_action(
'woocommerce_gla_debug_message',
sprintf(
"Failed to delete %s promotions from Merchant Center:\n%s",
count( $invalid_promotions ),
wp_json_encode( $invalid_promotions )
),
__METHOD__
);
} elseif ( $wc_coupon_exist ) {
$this->coupon_helper->mark_as_unsynced( $wc_coupon );
}
do_action(
'woocommerce_gla_deleted_promotions',
$deleted_promotions,
$invalid_promotions
);
do_action(
'woocommerce_gla_debug_message',
sprintf(
"Deleted %s promoitons:\n%s",
count( $deleted_promotions ),
wp_json_encode( $deleted_promotions )
),
__METHOD__
);
}
/**
* Return whether coupon is supported as visible on Google.
*
* @param WC_Coupon $coupon
*
* @return bool
*/
public static function is_coupon_supported( WC_Coupon $coupon ): bool {
if ( $coupon->get_virtual() ) {
return false;
}
if ( ! empty( $coupon->get_email_restrictions() ) ) {
return false;
}
if ( ! empty( $coupon->get_exclude_sale_items() ) &&
$coupon->get_exclude_sale_items() ) {
return false;
}
return true;
}
/**
* Return the list of supported coupon types.
*
* @return array
*/
public static function get_supported_coupon_types(): array {
return (array) apply_filters(
'woocommerce_gla_supported_coupon_types',
[ 'percent', 'fixed_cart', 'fixed_product' ]
);
}
/**
* Return the list of coupon types we will hide functionality for (default none).
*
* @since 1.2.0
*
* @return array
*/
public static function get_hidden_coupon_types(): array {
return (array) apply_filters( 'woocommerce_gla_hidden_coupon_types', [] );
}
/**
*
* @param InvalidCouponEntry[] $invalid_coupons
*/
protected function handle_update_errors( array $invalid_coupons ) {
// Get a coupon id to country mappings.
$internal_error_coupon_ids = [];
foreach ( $invalid_coupons as $invalid_coupon ) {
if ( $invalid_coupon->has_error(
GooglePromotionService::INTERNAL_ERROR_CODE
) ) {
$coupon_id = $invalid_coupon->get_wc_coupon_id();
$internal_error_coupon_ids[ $coupon_id ] = $invalid_coupon->get_target_country();
}
}
if ( ! empty( $internal_error_coupon_ids ) &&
apply_filters(
'woocommerce_gla_coupons_update_retry_on_failure',
true,
$internal_error_coupon_ids
) ) {
do_action(
'woocommerce_gla_retry_update_coupons',
$internal_error_coupon_ids
);
do_action(
'woocommerce_gla_error',
sprintf(
'Internal API errors while submitting the following coupons: %s',
join( ', ', $internal_error_coupon_ids )
),
__METHOD__
);
}
}
/**
*
* @param BatchInvalidCouponEntry[] $invalid_coupons
*/
protected function handle_delete_errors( array $invalid_coupons ) {
// Get all wc coupon id to google id mappings that have internal errors.
$internal_error_coupon_ids = [];
foreach ( $invalid_coupons as $invalid_coupon ) {
if ( $invalid_coupon->has_error(
GooglePromotionService::INTERNAL_ERROR_CODE
) ) {
$coupon_id = $invalid_coupon->get_wc_coupon_id();
$internal_error_coupon_ids[ $coupon_id ] = $invalid_coupon->get_google_promotion_id();
}
}
if ( ! empty( $internal_error_coupon_ids ) &&
apply_filters(
'woocommerce_gla_coupons_delete_retry_on_failure',
true,
$internal_error_coupon_ids
) ) {
do_action(
'woocommerce_gla_retry_delete_coupons',
$internal_error_coupon_ids
);
do_action(
'woocommerce_gla_error',
sprintf(
'Internal API errors while deleting the following coupons: %s',
join( ', ', $internal_error_coupon_ids )
),
__METHOD__
);
}
}
/**
* Validates whether Merchant Center is connected and ready for pushing data.
*
* @throws CouponSyncerException If Google Merchant Center is not set up and connected or is not ready for pushing data.
*/
protected function validate_merchant_center_setup(): void {
if ( ! $this->merchant_center->is_ready_for_syncing() ) {
do_action(
'woocommerce_gla_error',
'Cannot sync any coupons before setting up Google Merchant Center.',
__METHOD__
);
throw new CouponSyncerException(
__(
'Google Merchant Center has not been set up correctly. Please review your configuration.',
'google-listings-and-ads'
)
);
}
if ( ! $this->merchant_center->should_push() ) {
do_action(
'woocommerce_gla_error',
'Cannot push any coupons because they are being fetched automatically.',
__METHOD__
);
throw new CouponSyncerException(
__(
'Pushing Coupons will not run if the automatic data fetching is enabled. Please review your configuration in Google Listing and Ads settings.',
'google-listings-and-ads'
)
);
}
}
}
CouponSyncerException.php 0000644 00000000655 15154275264 0011604 0 ustar 00 <?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Coupon;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\GoogleListingsAndAdsException;
use Exception;
defined( 'ABSPATH' ) || exit;
/**
* Class CouponSyncerException
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Coupon
*/
class CouponSyncerException extends Exception implements GoogleListingsAndAdsException {
}
SyncerHooks.php 0000644 00000032770 15154275264 0007550 0 ustar 00 <?php
declare(strict_types = 1);
namespace Automattic\WooCommerce\GoogleListingsAndAds\Coupon;
use Automattic\WooCommerce\GoogleListingsAndAds\Google\DeleteCouponEntry;
use Automattic\WooCommerce\GoogleListingsAndAds\API\WP\NotificationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\JobRepository;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\DeleteCoupon;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\Notifications\CouponNotificationJob;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\UpdateCoupon;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC;
use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\NotificationStatus;
use WC_Coupon;
defined( 'ABSPATH' ) || exit();
/**
* Class SyncerHooks
*
* Hooks to various WooCommerce and WordPress actions to provide automatic coupon sync functionality.
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Coupon
*/
class SyncerHooks implements Service, Registerable {
use PluginHelper;
protected const SCHEDULE_TYPE_UPDATE = 'update';
protected const SCHEDULE_TYPE_DELETE = 'delete';
/**
* Array of strings mapped to coupon IDs indicating that they have been already
* scheduled for update or delete during current request.
* Used to avoid scheduling
* duplicate jobs.
*
* @var string[]
*/
protected $already_scheduled = [];
/**
*
* @var DeleteCouponEntry[][]
*/
protected $delete_requests_map;
/**
*
* @var CouponHelper
*/
protected $coupon_helper;
/**
* @var JobRepository
*/
protected $job_repository;
/**
*
* @var MerchantCenterService
*/
protected $merchant_center;
/**
* @var NotificationsService
*/
protected $notifications_service;
/**
*
* @var WC
*/
protected $wc;
/**
* WP Proxy
*
* @var WP
*/
protected WP $wp;
/**
* SyncerHooks constructor.
*
* @param CouponHelper $coupon_helper
* @param JobRepository $job_repository
* @param MerchantCenterService $merchant_center
* @param NotificationsService $notifications_service
* @param WC $wc
* @param WP $wp
*/
public function __construct(
CouponHelper $coupon_helper,
JobRepository $job_repository,
MerchantCenterService $merchant_center,
NotificationsService $notifications_service,
WC $wc,
WP $wp
) {
$this->coupon_helper = $coupon_helper;
$this->job_repository = $job_repository;
$this->merchant_center = $merchant_center;
$this->notifications_service = $notifications_service;
$this->wc = $wc;
$this->wp = $wp;
}
/**
* Register a service.
*/
public function register(): void {
// only register the hooks if Merchant Center is set up correctly.
if ( ! $this->merchant_center->is_ready_for_syncing() ) {
return;
}
// when a coupon is added / updated, schedule a update job.
add_action( 'woocommerce_new_coupon', [ $this, 'update_by_id' ], 90, 2 );
add_action( 'woocommerce_update_coupon', [ $this, 'update_by_id' ], 90, 2 );
add_action( 'woocommerce_gla_bulk_update_coupon', [ $this, 'update_by_id' ], 90 );
// when a coupon is trashed or removed, schedule a delete job.
add_action( 'wp_trash_post', [ $this, 'pre_delete' ], 90 );
add_action( 'before_delete_post', [ $this, 'pre_delete' ], 90 );
add_action( 'trashed_post', [ $this, 'delete_by_id' ], 90 );
add_action( 'deleted_post', [ $this, 'delete_by_id' ], 90 );
add_action( 'woocommerce_delete_coupon', [ $this, 'delete_by_id' ], 90, 2 );
add_action( 'woocommerce_trash_coupon', [ $this, 'delete_by_id' ], 90, 2 );
// when a coupon is restored from trash, schedule a update job.
add_action( 'untrashed_post', [ $this, 'update_by_id' ], 90 );
// Update coupons when object terms get updated.
add_action( 'set_object_terms', [ $this, 'maybe_update_by_id_when_terms_updated' ], 90, 6 );
}
/**
* Update a coupon by the ID
*
* @param int $coupon_id
*/
public function update_by_id( int $coupon_id ) {
$coupon = $this->wc->maybe_get_coupon( $coupon_id );
if ( $coupon instanceof WC_Coupon ) {
$this->handle_update_coupon( $coupon );
}
}
/**
* Update a coupon by the ID when the terms get updated.
*
* @param int $object_id The object ID.
* @param array $terms An array of object term IDs or slugs.
* @param array $tt_ids An array of term taxonomy IDs.
* @param string $taxonomy The taxonomy slug.
* @param bool $append Whether to append new terms to the old terms.
* @param array $old_tt_ids Old array of term taxonomy IDs.
*/
public function maybe_update_by_id_when_terms_updated( int $object_id, array $terms, array $tt_ids, string $taxonomy, bool $append, array $old_tt_ids ) {
$this->handle_update_coupon_when_product_brands_updated( $taxonomy, $tt_ids, $old_tt_ids );
}
/**
* Delete a coupon by the ID
*
* @param int $coupon_id
*/
public function delete_by_id( int $coupon_id ) {
$this->handle_delete_coupon( $coupon_id );
}
/**
* Pre Delete a coupon by the ID
*
* @param int $coupon_id
*/
public function pre_delete( int $coupon_id ) {
$this->handle_pre_delete_coupon( $coupon_id );
}
/**
* Handle updating of a coupon.
*
* @param WC_Coupon $coupon
* The coupon being saved.
*
* @return void
*/
protected function handle_update_coupon( WC_Coupon $coupon ) {
$coupon_id = $coupon->get_id();
if ( $this->notifications_service->is_ready() ) {
$this->handle_update_coupon_notification( $coupon );
}
// Schedule an update job if product sync is enabled.
if ( $this->coupon_helper->is_sync_ready( $coupon ) ) {
$this->coupon_helper->mark_as_pending( $coupon );
$this->job_repository->get( UpdateCoupon::class )->schedule(
[
[ $coupon_id ],
]
);
} elseif ( $this->coupon_helper->is_coupon_synced( $coupon ) ) {
// Delete the coupon from Google Merchant Center if it's already synced BUT it is not sync ready after the edit.
$coupon_to_delete = new DeleteCouponEntry(
$coupon_id,
$this->get_coupon_to_delete( $coupon ),
$this->coupon_helper->get_synced_google_ids( $coupon )
);
$this->job_repository->get( DeleteCoupon::class )->schedule(
[
$coupon_to_delete,
]
);
do_action(
'woocommerce_gla_debug_message',
sprintf(
'Deleting coupon (ID: %s) from Google Merchant Center because it is not ready to be synced.',
$coupon->get_id()
),
__METHOD__
);
} else {
$this->coupon_helper->mark_as_unsynced( $coupon );
}
}
/**
* Create request entries for the coupon (containing its Google ID),
* so we can schedule a delete job when it is actually trashed / deleted.
*
* @param int $coupon_id
*/
protected function handle_pre_delete_coupon( int $coupon_id ) {
$coupon = $this->wc->maybe_get_coupon( $coupon_id );
if ( $coupon instanceof WC_Coupon &&
$this->coupon_helper->is_coupon_synced( $coupon ) ) {
$this->delete_requests_map[ $coupon_id ] = new DeleteCouponEntry(
$coupon_id,
$this->get_coupon_to_delete( $coupon ),
$this->coupon_helper->get_synced_google_ids( $coupon )
);
}
}
/**
* @param WC_Coupon $coupon
*
* @return WCCouponAdapter
*/
protected function get_coupon_to_delete( WC_Coupon $coupon ): WCCouponAdapter {
$adapted_coupon_to_delete = new WCCouponAdapter(
[
'wc_coupon' => $coupon,
]
);
// Promotion stored in Google can only be soft-deleted to keep historical records.
// Instead of 'delete', we update the promotion with effective dates expired.
// Here we reset an expiring date based on WooCommerce coupon source.
$adapted_coupon_to_delete->disable_promotion( $coupon );
return $adapted_coupon_to_delete;
}
/**
* Handle deleting of a coupon.
*
* @param int $coupon_id
*/
protected function handle_delete_coupon( int $coupon_id ) {
if ( $this->notifications_service->is_ready() ) {
$this->maybe_send_delete_notification( $coupon_id );
}
if ( ! isset( $this->delete_requests_map[ $coupon_id ] ) ) {
return;
}
$coupon_to_delete = $this->delete_requests_map[ $coupon_id ];
if ( ! empty( $coupon_to_delete->get_synced_google_ids() ) &&
! $this->is_already_scheduled_to_delete( $coupon_id ) ) {
$this->job_repository->get( DeleteCoupon::class )->schedule(
[
$coupon_to_delete,
]
);
$this->set_already_scheduled_to_delete( $coupon_id );
}
}
/**
* Send the notification for coupon deletion
*
* @since 2.8.0
* @param int $coupon_id
*/
protected function maybe_send_delete_notification( int $coupon_id ): void {
$coupon = $this->wc->maybe_get_coupon( $coupon_id );
if ( $coupon instanceof WC_Coupon && $this->coupon_helper->should_trigger_delete_notification( $coupon ) ) {
$this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_DELETE );
$this->job_repository->get( CouponNotificationJob::class )->schedule(
[
'item_id' => $coupon->get_id(),
'topic' => NotificationsService::TOPIC_COUPON_DELETED,
]
);
}
}
/**
*
* @param int $coupon_id
* @param string $schedule_type
*
* @return bool
*/
protected function is_already_scheduled(
int $coupon_id,
string $schedule_type
): bool {
return isset( $this->already_scheduled[ $coupon_id ] ) &&
$this->already_scheduled[ $coupon_id ] === $schedule_type;
}
/**
*
* @param int $coupon_id
*
* @return bool
*/
protected function is_already_scheduled_to_update( int $coupon_id ): bool {
return $this->is_already_scheduled(
$coupon_id,
self::SCHEDULE_TYPE_UPDATE
);
}
/**
*
* @param int $coupon_id
*
* @return bool
*/
protected function is_already_scheduled_to_delete( int $coupon_id ): bool {
return $this->is_already_scheduled(
$coupon_id,
self::SCHEDULE_TYPE_DELETE
);
}
/**
*
* @param int $coupon_id
* @param string $schedule_type
*
* @return void
*/
protected function set_already_scheduled(
int $coupon_id,
string $schedule_type
): void {
$this->already_scheduled[ $coupon_id ] = $schedule_type;
}
/**
*
* @param int $coupon_id
*
* @return void
*/
protected function set_already_scheduled_to_update( int $coupon_id ): void {
$this->set_already_scheduled( $coupon_id, self::SCHEDULE_TYPE_UPDATE );
}
/**
*
* @param int $coupon_id
*
* @return void
*/
protected function set_already_scheduled_to_delete( int $coupon_id ): void {
$this->set_already_scheduled( $coupon_id, self::SCHEDULE_TYPE_DELETE );
}
/**
* Schedules notifications for an updated coupon
*
* @param WC_Coupon $coupon
*/
protected function handle_update_coupon_notification( WC_Coupon $coupon ) {
if ( $this->coupon_helper->should_trigger_create_notification( $coupon ) ) {
$this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_CREATE );
$this->job_repository->get( CouponNotificationJob::class )->schedule(
[
'item_id' => $coupon->get_id(),
'topic' => NotificationsService::TOPIC_COUPON_CREATED,
]
);
} elseif ( $this->coupon_helper->should_trigger_update_notification( $coupon ) ) {
$this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_UPDATE );
$this->job_repository->get( CouponNotificationJob::class )->schedule(
[
'item_id' => $coupon->get_id(),
'topic' => NotificationsService::TOPIC_COUPON_UPDATED,
]
);
} elseif ( $this->coupon_helper->should_trigger_delete_notification( $coupon ) ) {
$this->coupon_helper->set_notification_status( $coupon, NotificationStatus::NOTIFICATION_PENDING_DELETE );
$this->job_repository->get( CouponNotificationJob::class )->schedule(
[
'item_id' => $coupon->get_id(),
'topic' => NotificationsService::TOPIC_COUPON_DELETED,
]
);
}
}
/**
* If product to brands relationship is updated, update the coupons that are related to the brands.
*
* @param string $taxonomy The taxonomy slug.
* @param array $tt_ids An array of term taxonomy IDs.
* @param array $old_tt_ids Old array of term taxonomy IDs.
*/
protected function handle_update_coupon_when_product_brands_updated( string $taxonomy, array $tt_ids, array $old_tt_ids ) {
if ( 'product_brand' !== $taxonomy ) {
return;
}
// Convert term taxonomy IDs to integers.
$tt_ids = array_map( 'intval', $tt_ids );
$old_tt_ids = array_map( 'intval', $old_tt_ids );
// Find the difference between the new and old term taxonomy IDs.
$diff1 = array_diff( $tt_ids, $old_tt_ids );
$diff2 = array_diff( $old_tt_ids, $tt_ids );
$diff = array_merge( $diff1, $diff2 );
if ( empty( $diff ) ) {
return;
}
// Serialize the diff to use in the meta query.
// This is needed because the meta value is serialized.
$serialized_diff = maybe_serialize( $diff );
$args = [
'post_type' => 'shop_coupon',
'meta_query' => [
'relation' => 'OR',
[
'key' => 'product_brands',
'value' => $serialized_diff,
'compare' => 'LIKE',
],
[
'key' => 'exclude_product_brands',
'value' => $serialized_diff,
'compare' => 'LIKE',
],
],
];
// Get coupon posts based on the above query args.
$posts = $this->wp->get_posts( $args );
if ( empty( $posts ) ) {
return;
}
foreach ( $posts as $post ) {
$this->update_by_id( $post->ID );
}
}
}
WCCouponAdapter.php 0000644 00000032241 15154275264 0010270 0 ustar 00 <?php
declare(strict_types = 1);
namespace Automattic\WooCommerce\GoogleListingsAndAds\Coupon;
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidValue;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Product\WCProductAdapter;
use Automattic\WooCommerce\GoogleListingsAndAds\Validator\Validatable;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\PriceAmount as GooglePriceAmount;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Promotion as GooglePromotion;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\TimePeriod as GoogleTimePeriod;
use DateInterval;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use WC_DateTime;
use WC_Coupon;
defined( 'ABSPATH' ) || exit();
/**
* Class WCCouponAdapter
*
* This class adapts the WooCommerce coupon class to the Google's Promotion class by mapping their attributes.
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Coupon
*/
class WCCouponAdapter extends GooglePromotion implements Validatable {
use PluginHelper;
public const CHANNEL_ONLINE = 'ONLINE';
public const PRODUCT_APPLICABILITY_ALL_PRODUCTS = 'ALL_PRODUCTS';
public const PRODUCT_APPLICABILITY_SPECIFIC_PRODUCTS = 'SPECIFIC_PRODUCTS';
public const OFFER_TYPE_GENERIC_CODE = 'GENERIC_CODE';
public const PROMOTION_DESTINATION_ADS = 'Shopping_ads';
public const PROMOTION_DESTINATION_FREE_LISTING = 'Free_listings';
public const WC_DISCOUNT_TYPE_PERCENT = 'percent';
public const WC_DISCOUNT_TYPE_FIXED_CART = 'fixed_cart';
public const WC_DISCOUNT_TYPE_FIXED_PRODUCT = 'fixed_product';
public const COUPON_VALUE_TYPE_MONEY_OFF = 'MONEY_OFF';
public const COUPON_VALUE_TYPE_PERCENT_OFF = 'PERCENT_OFF';
protected const DATE_TIME_FORMAT = 'Y-m-d h:i:sa';
public const COUNTRIES_WITH_FREE_SHIPPING_DESTINATION = [ 'BR', 'IT', 'ES', 'JP', 'NL', 'KR', 'US' ];
/**
*
* @var int wc_coupon_id
*/
protected $wc_coupon_id;
/**
* Initialize this object's properties from an array.
*
* @param array $properties Used to seed this object's properties.
*
* @return void
*
* @throws InvalidValue When a WooCommerce coupon is not provided or it is invalid.
*/
public function mapTypes( $properties ) {
if ( empty( $properties['wc_coupon'] ) ||
! $properties['wc_coupon'] instanceof WC_Coupon ) {
throw InvalidValue::not_instance_of( WC_Coupon::class, 'wc_coupon' );
}
$wc_coupon = $properties['wc_coupon'];
$this->wc_coupon_id = $wc_coupon->get_id();
$this->map_woocommerce_coupon( $wc_coupon, $this->get_coupon_destinations( $properties ) );
// Google doesn't expect extra fields, so it's best to remove them
unset( $properties['wc_coupon'] );
parent::mapTypes( $properties );
}
/**
* Map the WooCommerce coupon attributes to the current class.
*
* @param WC_Coupon $wc_coupon
* @param string[] $destinations The destination ID's for the coupon
*
* @return void
*/
protected function map_woocommerce_coupon( WC_Coupon $wc_coupon, array $destinations ) {
$this->setRedemptionChannel( self::CHANNEL_ONLINE );
$this->setPromotionDestinationIds( $destinations );
$content_language = empty( get_locale() ) ? 'en' : strtolower(
substr( get_locale(), 0, 2 )
); // ISO 639-1.
$this->setContentLanguage( $content_language );
$this->map_wc_coupon_id( $wc_coupon )
->map_wc_general_attributes( $wc_coupon )
->map_wc_usage_restriction( $wc_coupon );
}
/**
* Map the WooCommerce coupon ID.
*
* @param WC_Coupon $wc_coupon
*
* @return $this
*/
protected function map_wc_coupon_id( WC_Coupon $wc_coupon ): WCCouponAdapter {
$coupon_id = "{$this->get_slug()}_{$wc_coupon->get_id()}";
$this->setPromotionId( $coupon_id );
return $this;
}
/**
* Map the general WooCommerce coupon attributes.
*
* @param WC_Coupon $wc_coupon
*
* @return $this
*/
protected function map_wc_general_attributes( WC_Coupon $wc_coupon ): WCCouponAdapter {
$this->setOfferType( self::OFFER_TYPE_GENERIC_CODE );
$this->setGenericRedemptionCode( $wc_coupon->get_code() );
$coupon_amount = $wc_coupon->get_amount();
if ( $wc_coupon->is_type( self::WC_DISCOUNT_TYPE_PERCENT ) ) {
$this->setCouponValueType( self::COUPON_VALUE_TYPE_PERCENT_OFF );
$percent_off = round( floatval( $coupon_amount ) );
$this->setPercentOff( $percent_off );
$this->setLongtitle( sprintf( '%d%% off', $percent_off ) );
} elseif ( $wc_coupon->is_type(
[
self::WC_DISCOUNT_TYPE_FIXED_CART,
self::WC_DISCOUNT_TYPE_FIXED_PRODUCT,
]
) ) {
$this->setCouponValueType( self::COUPON_VALUE_TYPE_MONEY_OFF );
$this->setMoneyOffAmount(
$this->map_google_price_amount( $coupon_amount )
);
$this->setLongtitle(
sprintf(
'%d %s off',
$coupon_amount,
get_woocommerce_currency()
)
);
}
$this->setPromotionEffectiveTimePeriod(
$this->get_wc_coupon_effective_dates( $wc_coupon )
);
return $this;
}
/**
* Return the effective time period for the WooCommerce coupon.
*
* @param WC_Coupon $wc_coupon
*
* @return GoogleTimePeriod
*/
protected function get_wc_coupon_effective_dates( WC_Coupon $wc_coupon ): GoogleTimePeriod {
$start_date = $this->get_wc_coupon_start_date( $wc_coupon );
$end_date = $wc_coupon->get_date_expires();
// If there is no expiring date, set to promotion maximumal effective days allowed by Google.\
// Refer to https://support.google.com/merchants/answer/2906014?hl=en
if ( empty( $end_date ) ) {
$end_date = clone $start_date;
$end_date->add( new DateInterval( 'P183D' ) );
}
// If the coupon is already expired. set the coupon expires immediately after start date.
if ( $end_date < $start_date ) {
$end_date = clone $start_date;
$end_date->add( new DateInterval( 'PT1S' ) );
}
return new GoogleTimePeriod(
[
'startTime' => (string) $start_date,
'endTime' => (string) $end_date,
]
);
}
/**
* Return the start date for the WooCommerce coupon.
*
* @param WC_Coupon $wc_coupon
*
* @return WC_DateTime
*/
protected function get_wc_coupon_start_date( $wc_coupon ): WC_DateTime {
new WC_DateTime();
$post_time = get_post_time( self::DATE_TIME_FORMAT, true, $wc_coupon->get_id(), false );
if ( ! empty( $post_time ) ) {
return new WC_DateTime( $post_time );
} else {
return new WC_DateTime();
}
}
/**
* Map the WooCommerce coupon usage restriction.
*
* @param WC_Coupon $wc_coupon
*
* @return $this
*/
protected function map_wc_usage_restriction( WC_Coupon $wc_coupon ): WCCouponAdapter {
$minimal_spend = $wc_coupon->get_minimum_amount();
if ( ! empty( $minimal_spend ) ) {
$this->setMinimumPurchaseAmount(
$this->map_google_price_amount( $minimal_spend )
);
}
$maximal_spend = $wc_coupon->get_maximum_amount();
if ( ! empty( $maximal_spend ) ) {
$this->setLimitValue(
$this->map_google_price_amount( $maximal_spend )
);
}
$has_product_restriction = false;
$get_offer_id = function ( int $product_id ) {
return WCProductAdapter::get_google_product_offer_id( $this->get_slug(), $product_id );
};
$wc_product_ids = $wc_coupon->get_product_ids();
if ( ! empty( $wc_product_ids ) ) {
$google_product_ids = array_map( $get_offer_id, $wc_product_ids );
$has_product_restriction = true;
$this->setItemId( $google_product_ids );
}
// Currently the brand inclusion restriction will override the product inclustion restriction.
// It's align with the current coupon discounts behaviour in WooCommerce.
$wc_product_ids_in_brand = $this->get_product_ids_in_brand( $wc_coupon );
if ( ! empty( $wc_product_ids_in_brand ) ) {
$google_product_ids = array_map( $get_offer_id, $wc_product_ids_in_brand );
$has_product_restriction = true;
$this->setItemId( $google_product_ids );
}
// Get excluded product IDs and excluded product IDs in brand.
$wc_excluded_product_ids = $wc_coupon->get_excluded_product_ids();
$wc_excluded_product_ids_in_brand = $this->get_product_ids_in_brand( $wc_coupon, true );
if ( ! empty( $wc_excluded_product_ids ) || ! empty( $wc_excluded_product_ids_in_brand ) ) {
$google_product_ids = array_merge(
array_map( $get_offer_id, $wc_excluded_product_ids ),
array_map( $get_offer_id, $wc_excluded_product_ids_in_brand )
);
$google_product_ids = array_values( array_unique( $google_product_ids ) );
$has_product_restriction = true;
$this->setItemIdExclusion( $google_product_ids );
}
$wc_product_catetories = $wc_coupon->get_product_categories();
if ( ! empty( $wc_product_catetories ) ) {
$str_product_categories =
WCProductAdapter::convert_product_types( $wc_product_catetories );
$has_product_restriction = true;
$this->setProductType( $str_product_categories );
}
$wc_excluded_product_catetories = $wc_coupon->get_excluded_product_categories();
if ( ! empty( $wc_excluded_product_catetories ) ) {
$str_product_categories =
WCProductAdapter::convert_product_types( $wc_excluded_product_catetories );
$has_product_restriction = true;
$this->setProductTypeExclusion( $str_product_categories );
}
if ( $has_product_restriction ) {
$this->setProductApplicability(
self::PRODUCT_APPLICABILITY_SPECIFIC_PRODUCTS
);
} else {
$this->setProductApplicability(
self::PRODUCT_APPLICABILITY_ALL_PRODUCTS
);
}
return $this;
}
/**
* Map WooCommerce price number to Google price structure.
*
* @param float $wc_amount
*
* @return GooglePriceAmount
*/
protected function map_google_price_amount( $wc_amount ): GooglePriceAmount {
return new GooglePriceAmount(
[
'currency' => get_woocommerce_currency(),
'value' => $wc_amount,
]
);
}
/**
* Disable promotion shared in Google by only updating promotion effective end_date
* to make the promotion expired.
*
* @param WC_Coupon $wc_coupon
*/
public function disable_promotion( WC_Coupon $wc_coupon ) {
$start_date = $this->get_wc_coupon_start_date( $wc_coupon );
// Set promotion to be disabled immediately.
$end_date = new WC_DateTime();
// If this coupon is scheduled in the future, disable it right after start date.
if ( $start_date >= $end_date ) {
$end_date = clone $start_date;
$end_date->add( new DateInterval( 'PT1S' ) );
}
$this->setPromotionEffectiveTimePeriod(
new GoogleTimePeriod(
[
'startTime' => (string) $start_date,
'endTime' => (string) $end_date,
]
)
);
}
/**
*
* @param ClassMetadata $metadata
*/
public static function load_validator_metadata( ClassMetadata $metadata ) {
$metadata->addPropertyConstraint(
'targetCountry',
new Assert\NotBlank()
);
$metadata->addPropertyConstraint(
'promotionId',
new Assert\NotBlank()
);
$metadata->addPropertyConstraint(
'genericRedemptionCode',
new Assert\NotBlank()
);
$metadata->addPropertyConstraint(
'productApplicability',
new Assert\NotBlank()
);
$metadata->addPropertyConstraint(
'offerType',
new Assert\NotBlank()
);
$metadata->addPropertyConstraint(
'redemptionChannel',
new Assert\NotBlank()
);
$metadata->addPropertyConstraint(
'couponValueType',
new Assert\NotBlank()
);
}
/**
*
* @return int $wc_coupon_id
*/
public function get_wc_coupon_id(): int {
return $this->wc_coupon_id;
}
/**
*
* @param string $targetCountry
* phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
*/
public function setTargetCountry( $targetCountry ) {
// set the new target country
parent::setTargetCountry( $targetCountry );
}
/**
* Get the destinations allowed per specific country.
*
* @param array $coupon_data The coupon data to get the allowed destinations.
* @return string[] The destinations country based.
*/
private function get_coupon_destinations( array $coupon_data ): array {
$destinations = [ self::PROMOTION_DESTINATION_ADS ];
if ( isset( $coupon_data['targetCountry'] ) && in_array( $coupon_data['targetCountry'], self::COUNTRIES_WITH_FREE_SHIPPING_DESTINATION, true ) ) {
$destinations[] = self::PROMOTION_DESTINATION_FREE_LISTING;
}
return apply_filters( 'woocommerce_gla_coupon_destinations', $destinations, $coupon_data );
}
/**
* Get the product IDs that belongs to a brand.
*
* @param WC_Coupon $wc_coupon The WC coupon object.
* @param bool $is_exclude If the product IDs are for exclusion.
* @return string[] The product IDs that belongs to a brand.
*/
private function get_product_ids_in_brand( WC_Coupon $wc_coupon, bool $is_exclude = false ) {
$coupon_id = $wc_coupon->get_id();
$meta_key = $is_exclude ? 'exclude_product_brands' : 'product_brands';
// Get the brand term IDs if brand restriction is set.
$brand_term_ids = get_post_meta( $coupon_id, $meta_key );
if ( ! is_array( $brand_term_ids ) ) {
return [];
}
$product_ids = [];
foreach ( $brand_term_ids as $brand_term_id ) {
// Get the product IDs that belongs to the brand.
$object_ids = get_objects_in_term( $brand_term_id, 'product_brand' );
if ( is_wp_error( $object_ids ) ) {
continue;
}
$product_ids = array_merge( $product_ids, $object_ids );
}
return $product_ids;
}
}