diff options
Diffstat (limited to 'includes')
| -rw-r--r-- | includes/class-cepis-woo-assets.php | 21 | ||||
| -rw-r--r-- | includes/class-cepis-woo-lookup.php | 135 | ||||
| -rw-r--r-- | includes/class-cepis-woo-messages.php | 10 | ||||
| -rw-r--r-- | includes/class-cepis-woo-transport.php | 30 |
4 files changed, 178 insertions, 18 deletions
diff --git a/includes/class-cepis-woo-assets.php b/includes/class-cepis-woo-assets.php index def6fe2..503441c 100644 --- a/includes/class-cepis-woo-assets.php +++ b/includes/class-cepis-woo-assets.php @@ -27,6 +27,15 @@ final class Cepis_Woo_Assets { private const WIDGET = 'cepis-autofill'; /** + * Script handle for the shared "which municipality was that" helper. + * + * Its own file rather than a copy inside each integration: both checkouts + * need it, there is no build step to share code for them, and one module + * means one cache of what has already been asked. + */ + private const FAIXA = 'cepis-woocommerce-faixa'; + + /** * Hook the enqueue. */ public static function init(): void { @@ -51,10 +60,18 @@ final class Cepis_Woo_Assets { true ); + wp_register_script( + self::FAIXA, + CEPIS_WOO_URL . 'assets/js/cepis-woocommerce-faixa.js', + array(), + CEPIS_WOO_VERSION, + true + ); + $handle = 'cepis-woocommerce-' . $checkout; $dependencies = 'blocks' === $checkout - ? array( self::WIDGET, 'wp-data', 'wc-blocks-data-store' ) - : array( self::WIDGET, 'jquery' ); + ? array( self::WIDGET, self::FAIXA, 'wp-data', 'wc-blocks-data-store' ) + : array( self::WIDGET, self::FAIXA, 'jquery' ); wp_enqueue_script( $handle, diff --git a/includes/class-cepis-woo-lookup.php b/includes/class-cepis-woo-lookup.php index 47e3f16..da2f25a 100644 --- a/includes/class-cepis-woo-lookup.php +++ b/includes/class-cepis-woo-lookup.php @@ -43,6 +43,32 @@ final class Cepis_Woo_Lookup { private const MISS_TTL = DAY_IN_SECONDS; /** + * The fields of a 404's `faixa` object that are passed on. + * + * A whitelist rather than the whole object, so that a field the API grows + * later cannot reach the checkout unreviewed. None of these is ever written + * into a checkout field — see {@see Cepis_Woo_Lookup::faixa()}. + */ + private const FAIXA_FIELDS = array( + 'localidade', + 'uf', + 'estado', + 'regiao', + 'ibge', + 'cep_ini', + 'cep_fim', + 'ceps', + ); + + /** + * The transport the client speaks through, kept so that the body of a 404 + * can be read back after the client has turned it into an exception. + * + * @var Cepis_Woo_Transport|null + */ + private static ?Cepis_Woo_Transport $transport = null; + + /** * Look a CEP up, from cache when possible. * * Never throws: every outcome is described by the returned status and body, @@ -57,21 +83,32 @@ final class Cepis_Woo_Lookup { $cached = get_transient( self::CACHE_PREFIX . $cep ); if ( is_array( $cached ) ) { - return isset( $cached['miss'] ) - ? self::not_found() - : array( - 'status' => 200, - 'body' => $cached, - 'retry_after' => null, - ); + 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 ) { - set_transient( self::CACHE_PREFIX . $cep, array( 'miss' => true ), self::MISS_TTL ); + $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(); + return self::not_found( $faixa ); } catch ( Cepis\InvalidCepException ) { return array( 'status' => 400, @@ -144,7 +181,7 @@ final class Cepis_Woo_Lookup { maxAttempts: 2, totalTimeout: 8.0, userAgent: 'cepis-woocommerce/' . CEPIS_WOO_VERSION . ' (+https://cep.is)', - transport: new Cepis_Woo_Transport(), + transport: self::transport(), ); } @@ -152,18 +189,86 @@ final class Cepis_Woo_Lookup { } /** + * 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<string, int|string>|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<string, int|string>|null $faixa The municipality the CEP + * would have belonged to, when the API named one. + * * @return array{status:int, body:array<string, mixed>, retry_after:?int} */ - private static function not_found(): array { + 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' => array( - 'erro' => 'cep_nao_encontrado', - 'mensagem' => 'CEP não existe na base do DNE', - ), + 'body' => $body, 'retry_after' => null, ); } diff --git a/includes/class-cepis-woo-messages.php b/includes/class-cepis-woo-messages.php index 201fe4e..93d3279 100644 --- a/includes/class-cepis-woo-messages.php +++ b/includes/class-cepis-woo-messages.php @@ -16,6 +16,12 @@ defined( 'ABSPATH' ) || exit; * * Keys are the widget's status kinds and error codes. Anything not listed * keeps the widget's own wording. + * + * `not_found_range` and `not_found_city` are the exception: they are not + * status kinds the widget knows, but templates the plugin's own JavaScript + * fills in when the API says which municipality a missing CEP would have + * belonged to. They live in the same table so that one filter, one text domain + * and one `.po` file cover everything the shopper is told. */ final class Cepis_Woo_Messages { @@ -33,6 +39,10 @@ final class Cepis_Woo_Messages { 'loading' => __( 'Looking the address up…', 'cepis-woocommerce' ), 'found' => __( 'Address found.', 'cepis-woocommerce' ), 'not_found' => __( 'CEP not found. Check the number or fill the address in by hand.', 'cepis-woocommerce' ), + /* translators: 1: municipality, e.g. Areal. 2: state code, e.g. RJ. 3: lowest CEP that exists there, e.g. 25845-001. 4: highest CEP that exists there, e.g. 25849-899. */ + 'not_found_range' => __( 'CEP not found. This number would be in %1$s/%2$s, which has CEPs from %3$s to %4$s. Check the number.', 'cepis-woocommerce' ), + /* translators: 1: municipality, e.g. Areal. 2: state code, e.g. RJ. */ + 'not_found_city' => __( 'CEP not found. This number would be in %1$s/%2$s. Check the number.', 'cepis-woocommerce' ), 'invalid' => __( 'Invalid CEP. Type all 8 digits.', 'cepis-woocommerce' ), 'rate_limited' => __( 'Too many lookups in a short time. Wait a few seconds and try again.', 'cepis-woocommerce' ), 'server' => __( 'The CEP service is unavailable. Fill the address in by hand.', 'cepis-woocommerce' ), diff --git a/includes/class-cepis-woo-transport.php b/includes/class-cepis-woo-transport.php index 888be54..b89a81c 100644 --- a/includes/class-cepis-woo-transport.php +++ b/includes/class-cepis-woo-transport.php @@ -24,6 +24,31 @@ defined( 'ABSPATH' ) || exit; final class Cepis_Woo_Transport implements Cepis\Transport { /** + * Body of the response to the most recent completed request. + * + * The client turns a 404 into a {@see Cepis\CepNotFoundException} and drops + * the body with it, which is the right shape for a library: a CEP that does + * not exist is an error, not a payload. The API nonetheless puts something + * useful in that body — the municipality the number would have belonged to — + * and this is the only place in the plugin that still has it. + * + * Kept here rather than handed to the client so that nothing downstream can + * mistake it for an address. {@see Cepis_Woo_Lookup} reads it, keeps the few + * fields that name a municipality, and passes them on inside the 404. + * + * @var string + */ + private string $last_body = ''; + + /** + * The body of the last response, or an empty string when the last request + * never completed. + */ + public function last_body(): string { + return $this->last_body; + } + + /** * Perform a single GET. * * @param string $url Absolute URL, query string included. @@ -33,6 +58,8 @@ final class Cepis_Woo_Transport implements Cepis\Transport { * @throws Cepis\NetworkException The request never completed. */ public function get( string $url, array $headers, float $timeout ): Cepis\Response { + $this->last_body = ''; + $response = wp_remote_get( $url, array( @@ -48,11 +75,12 @@ final class Cepis_Woo_Transport implements Cepis\Transport { } $response_headers = wp_remote_retrieve_headers( $response ); + $this->last_body = (string) wp_remote_retrieve_body( $response ); return new Cepis\Response( (int) wp_remote_retrieve_response_code( $response ), is_object( $response_headers ) ? $response_headers->getAll() : (array) $response_headers, - (string) wp_remote_retrieve_body( $response ) + $this->last_body ); } } |