*/ public array $lists = array(); public bool $exclude_sale_items = false; public ?float $minimum_amount = null; public ?float $maximum_amount = null; /** * 'shipping' uses the shipping country and falls back to billing; 'billing' uses billing only. */ public string $country_basis = 'shipping'; public function __construct() { foreach ( self::LISTS as $list ) { $this->lists[ $list ] = array( 'include' => array(), 'exclude' => array(), // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Rule side, not a query argument. ); } } /** * Build from stored or posted data, cleaning every value. * * @param mixed $data Array or JSON. */ public static function from_array( $data ): self { if ( is_string( $data ) && '' !== $data ) { $data = json_decode( $data, true ); } $data = is_array( $data ) ? $data : array(); $rules = new self(); foreach ( self::LISTS as $list ) { foreach ( array( 'include', 'exclude' ) as $side ) { $values = $data[ $list ][ $side ] ?? array(); $rules->lists[ $list ][ $side ] = 'countries' === $list ? self::country_list( $values ) : Campaign::id_list( $values ); } } $rules->exclude_sale_items = ! empty( $data['exclude_sale_items'] ); $rules->minimum_amount = self::amount( $data['minimum_amount'] ?? null ); $rules->maximum_amount = self::amount( $data['maximum_amount'] ?? null ); $rules->country_basis = in_array( $data['country_basis'] ?? '', self::COUNTRY_BASES, true ) ? (string) $data['country_basis'] : 'shipping'; return $rules; } public function to_array(): array { return array( 'products' => $this->lists['products'], 'categories' => $this->lists['categories'], 'tags' => $this->lists['tags'], 'attributes' => $this->lists['attributes'], 'countries' => $this->lists['countries'], 'exclude_sale_items' => $this->exclude_sale_items, 'minimum_amount' => $this->minimum_amount, 'maximum_amount' => $this->maximum_amount, 'country_basis' => $this->country_basis, ); } /** * Whether any rule limits which products are covered. */ public function limits_products(): bool { if ( $this->exclude_sale_items ) { return true; } foreach ( array( 'products', 'categories', 'tags', 'attributes' ) as $list ) { if ( $this->lists[ $list ]['include'] || $this->lists[ $list ]['exclude'] ) { return true; } } return false; } /** * Whether a product is covered. Pure, so it can be tested. * * @param array $facts ids (product and parent), categories, tags, attributes (term IDs), on_sale. */ public function covers_product( array $facts ): bool { $values = array( 'products' => array_map( 'intval', (array) ( $facts['ids'] ?? array() ) ), 'categories' => array_map( 'intval', (array) ( $facts['categories'] ?? array() ) ), 'tags' => array_map( 'intval', (array) ( $facts['tags'] ?? array() ) ), 'attributes' => array_map( 'intval', (array) ( $facts['attributes'] ?? array() ) ), ); if ( $this->exclude_sale_items && ! empty( $facts['on_sale'] ) ) { return false; } foreach ( $values as $list => $ids ) { if ( array_intersect( $ids, $this->lists[ $list ]['exclude'] ) ) { return false; } } if ( array_intersect( $values['products'], $this->lists['products']['include'] ) ) { return true; } $has_include = false; foreach ( array( 'categories', 'tags', 'attributes' ) as $list ) { if ( ! $this->lists[ $list ]['include'] ) { continue; } $has_include = true; if ( ! array_intersect( $values[ $list ], $this->lists[ $list ]['include'] ) ) { return false; } } // Only a product list was set and this product is not on it. if ( ! $has_include && $this->lists['products']['include'] ) { return false; } return true; } /** * Whether the customer's country is allowed. An unknown country ('') is allowed for now: it is * checked again when the order is placed and the address is known. * * @param string $country ISO code. */ public function allows_country( string $country ): bool { $country = strtoupper( $country ); if ( '' === $country ) { return true; } if ( in_array( $country, $this->lists['countries']['exclude'], true ) ) { return false; } return ! $this->lists['countries']['include'] || in_array( $country, $this->lists['countries']['include'], true ); } public function limits_countries(): bool { return (bool) ( $this->lists['countries']['include'] || $this->lists['countries']['exclude'] ); } /** * @param mixed $values List or comma-separated string of ISO codes. * @return string[] */ private static function country_list( $values ): array { $values = is_string( $values ) ? explode( ',', $values ) : (array) $values; $codes = array(); foreach ( $values as $value ) { $code = strtoupper( trim( (string) $value ) ); if ( preg_match( '/^[A-Z]{2}$/', $code ) ) { $codes[] = $code; } } return array_values( array_unique( $codes ) ); } /** * @param mixed $value Amount or empty. */ private static function amount( $value ): ?float { return null === $value || '' === $value ? null : max( 0.0, (float) $value ); } }