sprintf( '%04d-12-01', $year ), 'doors' => $doors, 'mode' => 'email', 'missed' => 'open', 'theme' => 'classic', 'accent' => '#c8102e', 'shape' => 'rounded', 'font' => 'classic', 'snow' => true, 'shuffle' => false, 'background_id' => 0, 'page_id' => 0, 'prefix' => self::DEFAULT_PREFIX, 'consent_text' => '', 'delete_on_uninstall' => false, 'door_list' => array_fill( 1, $doors, self::defaultDoor() ), ]; } public static function defaultDoor(): array { return [ 'type' => 'coupon', 'coupon' => [ 'kind' => 'percent', 'amount' => 10.0, 'validity_hours' => 24, 'min_spend' => 0.0, 'product_ids' => [], 'category_ids' => [], 'individual_use' => false, ], 'content' => [ 'title' => '', 'message' => '', 'image_id' => 0, 'product_id' => 0, 'button_label' => '', ], ]; } /** * @param array $raw Config as submitted or stored. * @param int|null $year Year of the default start date. * @param string[] $kinds Allowed coupon kinds; the first is the default. */ public static function sanitize( array $raw, ?int $year = null, array $kinds = self::COUPON_KINDS ): array { $d = self::defaults( $year ); $doors = self::intIn( $raw['doors'] ?? null, $d['doors'], self::MIN_DOORS, self::MAX_DOORS ); return [ 'start' => self::date( $raw['start'] ?? null, $d['start'] ), 'doors' => $doors, 'mode' => self::oneOf( $raw['mode'] ?? null, self::MODES ), 'missed' => self::oneOf( $raw['missed'] ?? null, self::MISSED ), 'theme' => self::oneOf( $raw['theme'] ?? null, self::THEMES ), 'accent' => self::hex( $raw['accent'] ?? null, $d['accent'] ), 'shape' => self::oneOf( $raw['shape'] ?? null, self::SHAPES ), 'font' => self::oneOf( $raw['font'] ?? null, self::FONTS ), 'snow' => self::bool( $raw['snow'] ?? null, $d['snow'] ), 'shuffle' => self::bool( $raw['shuffle'] ?? null, $d['shuffle'] ), 'background_id' => self::id( $raw['background_id'] ?? null ), 'page_id' => self::id( $raw['page_id'] ?? null ), 'prefix' => self::prefix( $raw['prefix'] ?? null ), 'consent_text' => self::text( $raw['consent_text'] ?? null ), 'delete_on_uninstall' => self::bool( $raw['delete_on_uninstall'] ?? null, $d['delete_on_uninstall'] ), 'door_list' => self::doorList( $raw['door_list'] ?? null, $doors, $kinds ), ]; } /** * Last door's day (Y-m-d) of a sanitised config. */ public static function endDate( array $config ): string { return ( new \DateTimeImmutable( $config['start'] ) ) ->modify( '+' . ( $config['doors'] - 1 ) . ' days' ) ->format( 'Y-m-d' ); } /** * Prefix normalisation, shared with the coupon code generator. */ public static function prefix( $value ): string { $clean = is_string( $value ) ? preg_replace( '/[^A-Z0-9]/', '', strtoupper( $value ) ) : ''; $clean = substr( (string) $clean, 0, self::PREFIX_LENGTH ); return '' === $clean ? self::DEFAULT_PREFIX : $clean; } /** * The submitted door list keyed by door number (1-based), whatever its shape. * * @param mixed $raw Door list as submitted. */ public static function rawDoors( $raw ): array { $raw = is_array( $raw ) ? $raw : []; // A JavaScript array arrives as a zero-based list; door numbers start at 1. if ( [] !== $raw && array_keys( $raw ) === range( 0, count( $raw ) - 1 ) ) { $raw = array_combine( range( 1, count( $raw ) ), $raw ); } return $raw; } private static function doorList( $raw, int $doors, array $kinds ): array { $raw = self::rawDoors( $raw ); $list = []; for ( $n = 1; $n <= $doors; $n++ ) { $list[ $n ] = self::door( $raw[ $n ] ?? null, $kinds ); } return $list; } private static function door( $raw, array $kinds ): array { $d = self::defaultDoor(); $raw = is_array( $raw ) ? $raw : []; $coupon = isset( $raw['coupon'] ) && is_array( $raw['coupon'] ) ? $raw['coupon'] : []; $content = isset( $raw['content'] ) && is_array( $raw['content'] ) ? $raw['content'] : []; $kind = self::oneOf( $coupon['kind'] ?? null, $kinds ); $amount = self::money( $coupon['amount'] ?? null, $d['coupon']['amount'] ); if ( 'percent' === $kind ) { $amount = min( 100.0, $amount ); } $validity = $coupon['validity_hours'] ?? null; $validity = is_numeric( $validity ) && (int) $validity >= 0 ? min( self::MAX_VALIDITY_HOURS, (int) $validity ) : $d['coupon']['validity_hours']; return [ 'type' => self::oneOf( $raw['type'] ?? null, self::DOOR_TYPES ), 'coupon' => [ 'kind' => $kind, 'amount' => $amount, 'validity_hours' => $validity, 'min_spend' => self::money( $coupon['min_spend'] ?? null, $d['coupon']['min_spend'] ), 'product_ids' => self::ids( $coupon['product_ids'] ?? null ), 'category_ids' => self::ids( $coupon['category_ids'] ?? null ), 'individual_use' => self::bool( $coupon['individual_use'] ?? null, $d['coupon']['individual_use'] ), ], 'content' => [ 'title' => self::text( $content['title'] ?? null ), 'message' => is_string( $content['message'] ?? null ) ? $content['message'] : '', 'image_id' => self::id( $content['image_id'] ?? null ), 'product_id' => self::id( $content['product_id'] ?? null ), 'button_label' => self::text( $content['button_label'] ?? null ), ], ]; } /** * First entry of $allowed is the default. */ public static function oneOf( $value, array $allowed ): string { return is_string( $value ) && in_array( $value, $allowed, true ) ? $value : $allowed[0]; } private static function intIn( $value, int $fallback, int $min, int $max ): int { if ( ! is_numeric( $value ) ) { return $fallback; } return max( $min, min( $max, (int) $value ) ); } private static function money( $value, float $fallback ): float { return is_numeric( $value ) ? max( 0.0, round( (float) $value, 2 ) ) : $fallback; } public static function id( $value ): int { return is_numeric( $value ) && (int) $value > 0 ? (int) $value : 0; } private static function ids( $value ): array { if ( ! is_array( $value ) ) { return []; } $ids = array_filter( array_map( [ self::class, 'id' ], $value ) ); return array_values( array_unique( $ids ) ); } public static function bool( $value, bool $fallback ): bool { if ( null === $value ) { return $fallback; } $bool = filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); return null === $bool ? $fallback : $bool; } public static function text( $value ): string { return is_string( $value ) ? trim( $value ) : ''; } private static function hex( $value, string $fallback ): string { return is_string( $value ) && preg_match( '/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value ) ? strtolower( $value ) : $fallback; } private static function date( $value, string $fallback ): string { if ( ! is_string( $value ) ) { return $fallback; } $date = \DateTimeImmutable::createFromFormat( '!Y-m-d', $value ); return $date && $date->format( 'Y-m-d' ) === $value ? $value : $fallback; } }