summaryrefslogtreecommitdiffstats
path: root/includes
diff options
context:
space:
mode:
Diffstat (limited to 'includes')
-rw-r--r--includes/class-cepis-woo-assets.php21
-rw-r--r--includes/class-cepis-woo-lookup.php135
-rw-r--r--includes/class-cepis-woo-messages.php10
-rw-r--r--includes/class-cepis-woo-transport.php30
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
);
}
}