<?php declare(strict_types = 1);
namespace MailPoet\EmailEditor\Integrations\MailPoet\Coupons;
if (!defined('ABSPATH')) exit;
use MailPoet\WooCommerce\Helper;
use MailPoet\WP\Functions as WPFunctions;
class CouponBlockValidator {
/** @var Helper */
private $wcHelper;
/** @var WPFunctions */
private $wp;
public function __construct(
Helper $wcHelper,
WPFunctions $wp
) {
$this->wcHelper = $wcHelper;
$this->wp = $wp;
}
public function validate(array $attrs, string $recipientEmail): array {
$discountType = $this->validateDiscountType($attrs['discountType'] ?? null);
$amount = $this->validateRequiredNumber($attrs['amount'] ?? null, 'amount');
if ($discountType === 'percent' && $amount > 100) {
throw new CouponBlockValidationException('Percent coupon amount must be 100 or lower.');
}
$minimumAmount = $this->validateOptionalNumber($attrs['minimumAmount'] ?? null, 'minimumAmount');
$maximumAmount = $this->validateOptionalNumber($attrs['maximumAmount'] ?? null, 'maximumAmount');
if ($minimumAmount !== '' && $maximumAmount !== '' && $maximumAmount < $minimumAmount) {
throw new CouponBlockValidationException('Maximum amount must be greater than or equal to minimum amount.');
}
$emailRestrictions = $this->validateEmailRestrictions($attrs['emailRestrictions'] ?? []);
if (CouponBlockAttributeParser::toBoolean($attrs['restrictToSubscriber'] ?? false)) {
$recipientEmailRestrictions = $this->validateEmailRestrictions([$recipientEmail]);
if (empty($recipientEmailRestrictions)) {
throw new CouponBlockValidationException('Recipient email is required for recipient-restricted coupons.');
}
$emailRestrictions[] = $recipientEmailRestrictions[0];
}
return [
'discountType' => $discountType,
'amount' => $amount,
'expiryDay' => $this->validateOptionalInteger($attrs['expiryDay'] ?? null, 'expiryDay'),
'freeShipping' => CouponBlockAttributeParser::toBoolean($attrs['freeShipping'] ?? false),
'individualUse' => CouponBlockAttributeParser::toBoolean($attrs['individualUse'] ?? false),
'excludeSaleItems' => CouponBlockAttributeParser::toBoolean($attrs['excludeSaleItems'] ?? false),
'usageLimit' => $this->validateOptionalInteger($attrs['usageLimit'] ?? null, 'usageLimit'),
'usageLimitPerUser' => $this->validateOptionalInteger($attrs['usageLimitPerUser'] ?? null, 'usageLimitPerUser'),
'minimumAmount' => $minimumAmount,
'maximumAmount' => $maximumAmount,
'productIds' => $this->validateProductIds($attrs['productIds'] ?? []),
'excludedProductIds' => $this->validateProductIds($attrs['excludedProductIds'] ?? []),
'productCategoryIds' => $this->validateProductCategoryIds($attrs['productCategoryIds'] ?? []),
'excludedProductCategoryIds' => $this->validateProductCategoryIds($attrs['excludedProductCategoryIds'] ?? []),
'emailRestrictions' => array_values(array_unique($emailRestrictions)),
];
}
private function validateDiscountType($discountType): string {
if (!is_string($discountType) || $discountType === '') {
throw new CouponBlockValidationException('Discount type is required.');
}
if (!in_array($discountType, array_keys($this->wcHelper->wcGetCouponTypes()), true)) {
throw new CouponBlockValidationException('Invalid discount type.');
}
return $discountType;
}
private function validateRequiredNumber($value, string $field): float {
if ($value === null || $value === '' || !is_numeric($value)) {
throw new CouponBlockValidationException(sprintf('%s must be numeric.', $field));
}
$value = (float)$value;
if ($value < 0) {
throw new CouponBlockValidationException(sprintf('%s must be greater than or equal to 0.', $field));
}
return $value;
}
/**
* @return float|''
*/
private function validateOptionalNumber($value, string $field) {
if ($value === null || $value === '') {
return '';
}
return $this->validateRequiredNumber($value, $field);
}
private function validateOptionalInteger($value, string $field): int {
if ($value === null || $value === '') {
return 0;
}
if (filter_var($value, FILTER_VALIDATE_INT) === false) {
throw new CouponBlockValidationException(sprintf('%s must be an integer.', $field));
}
$value = (int)$value;
if ($value < 0) {
throw new CouponBlockValidationException(sprintf('%s must be greater than or equal to 0.', $field));
}
return $value;
}
private function validateProductIds($items): array {
$ids = $this->extractItemIds($items, 'product');
foreach ($ids as $id) {
if (!$this->wcHelper->wcGetProduct($id)) {
throw new CouponBlockValidationException('Invalid product ID.');
}
}
return $ids;
}
private function validateProductCategoryIds($items): array {
$ids = $this->extractItemIds($items, 'product category');
foreach ($ids as $id) {
$term = $this->wp->getTerm($id, 'product_cat');
if (!$term || $this->wp->isWpError($term)) {
throw new CouponBlockValidationException('Invalid product category ID.');
}
}
return $ids;
}
private function extractItemIds($items, string $label): array {
if ($items === null || $items === '') {
return [];
}
if (!is_array($items)) {
throw new CouponBlockValidationException(sprintf('Invalid %s IDs.', $label));
}
$ids = [];
foreach ($items as $item) {
$id = null;
if (is_array($item)) {
$id = $item['id'] ?? null;
} elseif (is_object($item) && isset($item->id)) {
$id = $item->id;
}
if (filter_var($id, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]) === false) {
throw new CouponBlockValidationException(sprintf('Invalid %s ID.', $label));
}
$ids[] = (int)$id;
}
return array_values(array_unique($ids));
}
private function validateEmailRestrictions($raw): array {
if ($raw === null || $raw === '') {
return [];
}
if (is_string($raw)) {
$raw = explode(',', $raw);
}
if (!is_array($raw)) {
throw new CouponBlockValidationException('Invalid email restrictions.');
}
$emails = [];
foreach ($raw as $email) {
if (!is_string($email)) {
throw new CouponBlockValidationException('Invalid email restriction.');
}
$email = strtolower(trim($this->wp->sanitizeEmail($email)));
if (!$email) {
continue;
}
if (!$this->wp->isEmail($email)) {
throw new CouponBlockValidationException('Invalid email restriction.');
}
$emails[] = $email;
}
return array_values(array_unique($emails));
}
}