summaryrefslogtreecommitdiffstats
path: root/includes/class-cepis-woo-lookup.php
diff options
context:
space:
mode:
Diffstat (limited to 'includes/class-cepis-woo-lookup.php')
-rw-r--r--includes/class-cepis-woo-lookup.php135
1 files changed, 120 insertions, 15 deletions
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,
);
}