, retry_after:?int} */ public static function resolve( string $cep ): array { $cached = get_transient( self::CACHE_PREFIX . $cep ); if ( is_array( $cached ) ) { if ( isset( $cached['miss'] ) ) { $remembered = isset( $cached['faixa'] ) && is_array( $cached['faixa'] ) ? $cached['faixa'] : null; return self::not_found( $remembered ); } return array( 'status' => 200, 'body' => $cached, 'retry_after' => null, ); } try { $address = self::client()->lookup( $cep ); } catch ( Cepis\CepNotFoundException ) { $faixa = self::faixa( self::transport()->last_body() ); $miss = array( 'miss' => true ); if ( null !== $faixa ) { $miss['faixa'] = $faixa; } set_transient( self::CACHE_PREFIX . $cep, $miss, self::MISS_TTL ); return self::not_found( $faixa ); } catch ( Cepis\InvalidCepException ) { return array( 'status' => 400, 'body' => array( 'erro' => 'cep_invalido', 'mensagem' => 'CEP deve ter 8 dígitos', ), 'retry_after' => null, ); } catch ( Cepis\RateLimitedException $e ) { return array( 'status' => 429, 'body' => array( 'erro' => 'limite', 'mensagem' => 'Muitas consultas em pouco tempo', ), 'retry_after' => null === $e->retryAfter ? null : (int) $e->retryAfter, ); } catch ( Cepis\CepisException ) { return array( 'status' => 502, 'body' => array( 'erro' => 'interno', 'mensagem' => 'falha interna', ), 'retry_after' => null, ); } $body = $address->raw; set_transient( self::CACHE_PREFIX . $cep, $body, self::HIT_TTL ); return array( 'status' => 200, 'body' => $body, 'retry_after' => null, ); } /** * Drop every cached answer. * * Used by the settings screen when the API root changes, since answers from * one instance say nothing about another. */ public static function flush(): void { global $wpdb; $like = $wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%'; $names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $like ) ); foreach ( (array) $names as $name ) { delete_transient( substr( (string) $name, strlen( '_transient_' ) ) ); } } /** * The client, built once per request. */ private static function client(): Cepis\Client { static $client = null; if ( null === $client ) { $client = new Cepis\Client( baseUrl: Cepis_Woo_Options::base_url(), timeout: 5.0, maxAttempts: 2, totalTimeout: 8.0, userAgent: 'cepis-woocommerce/' . CEPIS_WOO_VERSION . ' (+https://cep.is)', transport: self::transport(), ); } return $client; } /** * The transport, built once per request. */ private static function transport(): Cepis_Woo_Transport { if ( null === self::$transport ) { self::$transport = new Cepis_Woo_Transport(); } return self::$transport; } /** * The municipality a missing CEP would have belonged to, out of the API's * 404 body. * * **This is context, not an address.** A CEP that does not exist has no * address, and nothing here may ever reach a checkout field: a shipping * address filled from a CEP the Correios do not have is a parcel delivered * somewhere else. It exists only so the shopper can be told what they most * likely mistyped — the classic case being the `-000` that 957 * municipalities never received. * * Returns `null` for anything unexpected, so a body that changed shape, or * is not JSON at all, produces exactly the message this plugin has always * shown. * * @param string $body The response body, as it arrived. * * @return array|null The named fields, or null. */ private static function faixa( string $body ): ?array { if ( '' === $body ) { return null; } $data = json_decode( $body, true ); if ( ! is_array( $data ) || ! isset( $data['faixa'] ) || ! is_array( $data['faixa'] ) ) { return null; } $faixa = array(); foreach ( self::FAIXA_FIELDS as $name ) { if ( ! isset( $data['faixa'][ $name ] ) || ! is_scalar( $data['faixa'][ $name ] ) ) { continue; } $value = $data['faixa'][ $name ]; $faixa[ $name ] = is_int( $value ) ? $value : sanitize_text_field( (string) $value ); } return array() === $faixa ? null : $faixa; } /** * The API's own 404 body, reproduced so the browser cannot tell the two * modes apart. * * `faixa` is **omitted** when there is none, never sent as null, which is * what the API does and what the browser is written to expect. * * @param array|null $faixa The municipality the CEP * would have belonged to, when the API named one. * * @return array{status:int, body:array, retry_after:?int} */ private static function not_found( ?array $faixa = null ): array { $body = array( 'erro' => 'cep_nao_encontrado', 'mensagem' => 'CEP não existe na base do DNE', ); if ( null !== $faixa ) { $body['faixa'] = $faixa; } return array( 'status' => 404, 'body' => $body, 'retry_after' => null, ); } }