provider(), $record->providerItemId() ); $price = null; if ( $record->priceAmount() !== null ) { $observedAt = $record->lastSyncedAt() ?? self::nowUtc(); $price = new PriceInfo( $record->priceAmount(), $record->priceCurrency(), $record->priceLabel(), $observedAt, false, false, null ); } $availabilityStatus = $record->availabilityStatus(); $availability = $availabilityStatus === null ? AvailabilityInfo::unknown() : new AvailabilityInfo( $availabilityStatus, null, true ); $fetchedAt = $record->lastSyncedAt() ?? self::nowUtc(); $primaryImage = $record->imagePrimaryUrl(); if ( $primaryImage === null && $record->provider() === 'mercari' ) { $primaryImage = self::mercariAmbassadorImageUrlFor( $record->detailUrl() ); } return new ProductSnapshot( $record->provider(), $providerProductId, $record->title(), $record->brand(), $record->description(), $record->detailUrl(), $record->affiliateUrl(), $primaryImage, array(), $price, $availability, null, null, array(), array(), $fetchedAt, array() ); } /** * Build a Mercari Ambassador image URL from a Mercari product URL. * * @param string $url Mercari product URL. */ private static function mercariAmbassadorImageUrlFor( string $url ): ?string { // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Application helper is loadable in pure-unit tests without WordPress. $parts = parse_url( $url ); if ( ! is_array( $parts ) || ! isset( $parts['path'] ) || ! is_string( $parts['path'] ) ) { return null; } $endpoint = 'https://ambassador-system.mercari.com/v1/i'; if ( preg_match( '~/shops/product/([^/?#]+)~', $parts['path'], $matches ) === 1 ) { return $endpoint . '?id=' . rawurlencode( $matches[1] ) . '&svc=ms'; } if ( preg_match( '~/item/([^/?#]+)~', $parts['path'], $matches ) === 1 ) { return $endpoint . '?id=' . rawurlencode( $matches[1] ) . '&svc=m'; } return null; } /** * Current UTC instant used as the fallback for the * `observedAt` / `fetchedAt` slots when the record carries * no `lastSyncedAt`. * * Held as a private helper so the two fallback sites use a * single derivation and a future Clock-port migration has a * single point of change. */ private static function nowUtc(): DateTimeImmutable { return new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ); } }