diff --git a/inc/Abilities/DiceFmTest.php b/inc/Abilities/DiceFmTest.php index ad79ac9..29423fa 100644 --- a/inc/Abilities/DiceFmTest.php +++ b/inc/Abilities/DiceFmTest.php @@ -201,6 +201,9 @@ static function ( string $level, string $message, array $context = array() ) use 'date_end' => $dice_event['date_end'] ?? '', 'venue' => $dice_event['venue'] ?? '', 'url' => $dice_event['url'] ?? '', + 'price' => $dice_event['price'] ?? null, + 'currency' => $dice_event['currency'] ?? '', + 'ticket_types' => $dice_event['ticket_types'] ?? array(), 'timezone' => $dice_event['timezone'] ?? '', 'location' => $dice_event['location'] ?? null, ); @@ -293,10 +296,64 @@ private function mapEvent( array $dice_event ): array { 'venueCity' => $venue_city, 'venueState' => $venue_state, 'venueCoordinates' => $venue_coordinates, + 'price' => $this->mapPrice( $dice_event ), 'ticketUrl' => $dice_event['url'] ?? '', ); } + /** + * Map Dice price fields to display format. + * + * @param array $dice_event Raw Dice event. + * @return string + */ + private function mapPrice( array $dice_event ): string { + $currency = strtoupper( trim( (string) ( $dice_event['currency'] ?? 'USD' ) ) ); + + if ( isset( $dice_event['price'] ) && is_numeric( $dice_event['price'] ) && (float) $dice_event['price'] > 0 ) { + $amount = (float) $dice_event['price'] / 100; + return \DataMachineEvents\Core\PriceFormatter::formatStructured( $amount, $amount, $currency ); + } + + $ticket_types = $dice_event['ticket_types'] ?? array(); + if ( ! is_array( $ticket_types ) || empty( $ticket_types ) ) { + return ''; + } + + $face_values = array(); + $total_values = array(); + + foreach ( $ticket_types as $ticket_type ) { + if ( ! is_array( $ticket_type ) || empty( $ticket_type['price'] ) || ! is_array( $ticket_type['price'] ) ) { + continue; + } + + $price_data = $ticket_type['price']; + + if ( isset( $price_data['face_value'] ) && is_numeric( $price_data['face_value'] ) ) { + $face_values[] = (float) $price_data['face_value'] / 100; + } + + if ( isset( $price_data['total'] ) && is_numeric( $price_data['total'] ) ) { + $total_values[] = (float) $price_data['total'] / 100; + } + } + + if ( ! empty( $face_values ) ) { + return \DataMachineEvents\Core\PriceFormatter::formatStructured( min( $face_values ), max( $face_values ), $currency ); + } + + if ( ! empty( $total_values ) ) { + return \DataMachineEvents\Core\PriceFormatter::formatStructured( min( $total_values ), max( $total_values ), $currency ); + } + + if ( isset( $dice_event['price'] ) && is_numeric( $dice_event['price'] ) ) { + return \DataMachineEvents\Core\PriceFormatter::formatStructured( (float) $dice_event['price'] / 100, null, $currency ); + } + + return ''; + } + private function parseDateTimeUtc( string $datetime_utc, string $timezone ): array { if ( empty( $datetime_utc ) ) { return array( diff --git a/inc/Cli/DiceFmTestCommand.php b/inc/Cli/DiceFmTestCommand.php index 94ce500..a5bbb2f 100644 --- a/inc/Cli/DiceFmTestCommand.php +++ b/inc/Cli/DiceFmTestCommand.php @@ -100,6 +100,9 @@ private function outputResult( array $result ): void { \WP_CLI::log( '[RAW] Date: ' . ( $raw['date'] ?? '' ) ); \WP_CLI::log( '[RAW] Date End: ' . ( $raw['date_end'] ?? '' ) ); \WP_CLI::log( '[RAW] Venue: ' . ( $raw['venue'] ?? '' ) ); + \WP_CLI::log( '[RAW] Price: ' . wp_json_encode( $raw['price'] ?? null ) ); + \WP_CLI::log( '[RAW] Currency: ' . ( $raw['currency'] ?? '' ) ); + \WP_CLI::log( '[RAW] Ticket Types: ' . ( isset( $raw['ticket_types'] ) ? wp_json_encode( $raw['ticket_types'] ) : '[]' ) ); \WP_CLI::log( '[RAW] Timezone: ' . ( $raw['timezone'] ?? '' ) ); if ( isset( $raw['location'] ) && is_array( $raw['location'] ) ) { @@ -121,6 +124,7 @@ private function outputResult( array $result ): void { \WP_CLI::log( '[MAPPED] Venue City: ' . ( $mapped['venueCity'] ?? '(none)' ) ); \WP_CLI::log( '[MAPPED] Venue State: ' . ( $mapped['venueState'] ?? '(none)' ) ); \WP_CLI::log( '[MAPPED] Venue Timezone: ' . ( $mapped['venueTimezone'] ?? '(none)' ) ); + \WP_CLI::log( '[MAPPED] Price: ' . ( $mapped['price'] ?? '' ) ); \WP_CLI::log( '[MAPPED] Ticket URL: ' . ( $mapped['ticketUrl'] ?? '' ) ); $issues = $event['issues'] ?? array(); diff --git a/inc/Core/PriceFormatter.php b/inc/Core/PriceFormatter.php index ad4a0d9..49bf89a 100644 --- a/inc/Core/PriceFormatter.php +++ b/inc/Core/PriceFormatter.php @@ -17,6 +17,11 @@ class PriceFormatter { + /** + * Canonical display string for free events. + */ + private const FREE_LABEL = 'Free'; + /** * Format a price range as a display string. * @@ -50,6 +55,56 @@ public static function formatRange( ?float $min, ?float $max = null ): string { return '$' . number_format( $min, 2 ) . ' - $' . number_format( $max, 2 ); } + /** + * Format a structured price payload into a display string. + * + * Treats explicit free flags and all-zero values as free. + * Non-USD currencies are prefixed with the ISO currency code while preserving + * the existing dollar-based numeric formatting behavior. + * + * @param float|null $min Minimum price. + * @param float|null $max Maximum price. + * @param string $currency ISO currency code. + * @param bool|null $is_free Explicit free signal from source data. + * @return string Formatted price string. + */ + public static function formatStructured( ?float $min = null, ?float $max = null, string $currency = 'USD', ?bool $is_free = null ): string { + if ( true === $is_free ) { + return self::formatFree(); + } + + $normalized_min = null !== $min ? (float) $min : null; + $normalized_max = null !== $max ? (float) $max : null; + + if ( self::isZeroOrLess( $normalized_min ) && self::isZeroOrLess( $normalized_max ) ) { + if ( null !== $normalized_min || null !== $normalized_max ) { + return self::formatFree(); + } + return ''; + } + + $formatted = self::formatRange( $normalized_min, $normalized_max ); + if ( '' === $formatted ) { + return ''; + } + + $currency = strtoupper( trim( $currency ) ); + if ( '' === $currency || 'USD' === $currency ) { + return $formatted; + } + + return $currency . ' ' . $formatted; + } + + /** + * Format a free event label. + * + * @return string + */ + public static function formatFree(): string { + return self::FREE_LABEL; + } + /** * Parse a price string and extract numeric values. * @@ -91,4 +146,14 @@ public static function parse( string $raw ): array { public static function isFree( string $raw ): bool { return preg_match( '/^free$/i', trim( $raw ) ) === 1; } + + /** + * Whether the provided numeric value is null or non-positive. + * + * @param float|null $value Numeric value. + * @return bool + */ + private static function isZeroOrLess( ?float $value ): bool { + return null === $value || $value <= 0; + } } diff --git a/inc/Steps/EventImport/Handlers/DiceFm/DiceFm.php b/inc/Steps/EventImport/Handlers/DiceFm/DiceFm.php index b3d5c3d..58f3704 100644 --- a/inc/Steps/EventImport/Handlers/DiceFm/DiceFm.php +++ b/inc/Steps/EventImport/Handlers/DiceFm/DiceFm.php @@ -242,6 +242,7 @@ private function fetch_dice_fm_events( $api_key, $city, $partner_id, ExecutionCo private function convert_dice_fm_event( $event ) { $venue_data = $this->extract_venue_data( $event ); $timezone = $event['timezone'] ?? ''; + $price = $this->extractPrice( $event ); $start_parsed = $this->parseDateTimeUtc( $event['date'] ?? '', $timezone ); $end_parsed = $this->parseDateTimeUtc( $event['date_end'] ?? '', $timezone ); @@ -254,7 +255,7 @@ private function convert_dice_fm_event( $event ) { 'endTime' => $end_parsed['time'], 'venue' => sanitize_text_field( $venue_data['venue_name'] ), 'artist' => '', - 'price' => '', + 'price' => $price, 'ticketUrl' => esc_url_raw( $event['url'] ?? '' ), 'description' => wp_kses_post( $event['description'] ?? '' ), 'venueAddress' => sanitize_text_field( $venue_data['venue_address'] ), @@ -267,6 +268,58 @@ private function convert_dice_fm_event( $event ) { ); } + /** + * Extract and format Dice.fm event pricing. + * + * Supports both top-level cent amount (`price`) and per-ticket pricing + * (`ticket_types[].price`). + * + * @param array $event Raw Dice.fm event data. + * @return string Formatted price string for event-details block. + */ + private function extractPrice( array $event ): string { + $currency = strtoupper( trim( (string) ( $event['currency'] ?? 'USD' ) ) ); + + if ( isset( $event['price'] ) && is_numeric( $event['price'] ) ) { + $top_level_price = (float) $event['price'] / 100; + return $this->formatStructuredPrice( $top_level_price, $top_level_price, $currency ); + } + + $ticket_types = $event['ticket_types'] ?? array(); + if ( ! is_array( $ticket_types ) || empty( $ticket_types ) ) { + return ''; + } + + $face_values = array(); + $total_values = array(); + + foreach ( $ticket_types as $ticket_type ) { + if ( ! is_array( $ticket_type ) || empty( $ticket_type['price'] ) || ! is_array( $ticket_type['price'] ) ) { + continue; + } + + $price_data = $ticket_type['price']; + + if ( isset( $price_data['face_value'] ) && is_numeric( $price_data['face_value'] ) ) { + $face_values[] = (float) $price_data['face_value'] / 100; + } + + if ( isset( $price_data['total'] ) && is_numeric( $price_data['total'] ) ) { + $total_values[] = (float) $price_data['total'] / 100; + } + } + + if ( ! empty( $face_values ) ) { + return $this->formatStructuredPrice( min( $face_values ), max( $face_values ), $currency ); + } + + if ( ! empty( $total_values ) ) { + return $this->formatStructuredPrice( min( $total_values ), max( $total_values ), $currency ); + } + + return ''; + } + /** * Extract venue data from Dice.fm event * diff --git a/inc/Steps/EventImport/Handlers/EventImportHandler.php b/inc/Steps/EventImport/Handlers/EventImportHandler.php index 48a4bfc..9e4b87a 100644 --- a/inc/Steps/EventImport/Handlers/EventImportHandler.php +++ b/inc/Steps/EventImport/Handlers/EventImportHandler.php @@ -193,6 +193,19 @@ protected function formatPriceRange( ?float $min, ?float $max = null ): string { return PriceFormatter::formatRange( $min, $max ); } + /** + * Format structured price data into a display string. + * + * @param float|null $min Minimum price. + * @param float|null $max Maximum price. + * @param string $currency ISO currency code. + * @param bool|null $is_free Explicit free flag. + * @return string + */ + protected function formatStructuredPrice( ?float $min = null, ?float $max = null, string $currency = 'USD', ?bool $is_free = null ): string { + return PriceFormatter::formatStructured( $min, $max, $currency, $is_free ); + } + /** * Store event context (venue + core fields) in engine data. * diff --git a/inc/Steps/EventImport/Handlers/WebScraper/Extractors/BaseExtractor.php b/inc/Steps/EventImport/Handlers/WebScraper/Extractors/BaseExtractor.php index f1814fd..3c03ec7 100644 --- a/inc/Steps/EventImport/Handlers/WebScraper/Extractors/BaseExtractor.php +++ b/inc/Steps/EventImport/Handlers/WebScraper/Extractors/BaseExtractor.php @@ -225,4 +225,17 @@ protected function extractTimeFromText( string $text ): ?string { protected function formatPriceRange( ?float $min, ?float $max = null ): string { return PriceFormatter::formatRange( $min, $max ); } + + /** + * Format structured price data into a display string. + * + * @param float|null $min Minimum price. + * @param float|null $max Maximum price. + * @param string $currency ISO currency code. + * @param bool|null $is_free Explicit free flag. + * @return string + */ + protected function formatStructuredPrice( ?float $min = null, ?float $max = null, string $currency = 'USD', ?bool $is_free = null ): string { + return PriceFormatter::formatStructured( $min, $max, $currency, $is_free ); + } } diff --git a/inc/Steps/EventImport/Handlers/WebScraper/Extractors/DoStuffExtractor.php b/inc/Steps/EventImport/Handlers/WebScraper/Extractors/DoStuffExtractor.php index ae7256d..f406773 100644 --- a/inc/Steps/EventImport/Handlers/WebScraper/Extractors/DoStuffExtractor.php +++ b/inc/Steps/EventImport/Handlers/WebScraper/Extractors/DoStuffExtractor.php @@ -133,7 +133,7 @@ private function parseImage( array &$event, array $raw_event ): void { private function parsePrice( array &$event, array $raw_event ): void { if ( ! empty( $raw_event['is_free'] ) ) { - $event['price'] = 'Free'; + $event['price'] = $this->formatStructuredPrice( null, null, 'USD', true ); } } diff --git a/inc/Steps/EventImport/Handlers/WebScraper/Extractors/DuskFmExtractor.php b/inc/Steps/EventImport/Handlers/WebScraper/Extractors/DuskFmExtractor.php index 99fc816..f8ada81 100644 --- a/inc/Steps/EventImport/Handlers/WebScraper/Extractors/DuskFmExtractor.php +++ b/inc/Steps/EventImport/Handlers/WebScraper/Extractors/DuskFmExtractor.php @@ -249,7 +249,7 @@ private function parseBooking( array $event, array $venue_data ): array { } if ( empty( $price ) && ! empty( $event['isAccessibleForFree'] ) ) { - $price = 'Free'; + $price = $this->formatStructuredPrice( null, null, 'USD', true ); } return array_merge(