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.php170
1 files changed, 170 insertions, 0 deletions
diff --git a/includes/class-cepis-woo-lookup.php b/includes/class-cepis-woo-lookup.php
new file mode 100644
index 0000000..47e3f16
--- /dev/null
+++ b/includes/class-cepis-woo-lookup.php
@@ -0,0 +1,170 @@
+<?php
+/**
+ * Server-side CEP lookups, cached.
+ *
+ * @package CepisWooCommerce
+ */
+
+declare(strict_types=1);
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Resolves a CEP with the bundled `Cepis\Client` and remembers the answer in a
+ * transient.
+ *
+ * Only the proxy mode goes through here. In the default direct mode the
+ * browser talks to cep.is itself and no PHP runs at all.
+ */
+final class Cepis_Woo_Lookup {
+
+ /**
+ * Prefix for the transient holding one CEP's answer.
+ */
+ private const CACHE_PREFIX = 'cepis_woo_';
+
+ /**
+ * How long a resolved address is kept.
+ *
+ * The DNE database behind the API is regenerated fortnightly, so a week is
+ * half a refresh cycle: an address can be at most one release behind, while
+ * a CEP a store sees repeatedly still costs one outbound request a week.
+ */
+ private const HIT_TTL = WEEK_IN_SECONDS;
+
+ /**
+ * How long a "this CEP does not exist" is kept.
+ *
+ * Much shorter than a hit, and for a different reason: a CEP absent today
+ * can appear in the next fortnightly base, and telling a shopper their
+ * brand-new street does not exist for two weeks is worse than making the
+ * request again tomorrow.
+ */
+ private const MISS_TTL = DAY_IN_SECONDS;
+
+ /**
+ * Look a CEP up, from cache when possible.
+ *
+ * Never throws: every outcome is described by the returned status and body,
+ * in exactly the shapes the public API uses, so the browser sees the same
+ * thing whichever mode the store is in.
+ *
+ * @param string $cep Eight digits, already validated by the caller.
+ *
+ * @return array{status:int, body:array<string, mixed>, retry_after:?int}
+ */
+ public static function resolve( string $cep ): array {
+ $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,
+ );
+ }
+
+ try {
+ $address = self::client()->lookup( $cep );
+ } catch ( Cepis\CepNotFoundException ) {
+ set_transient( self::CACHE_PREFIX . $cep, array( 'miss' => true ), self::MISS_TTL );
+
+ return self::not_found();
+ } 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: new Cepis_Woo_Transport(),
+ );
+ }
+
+ return $client;
+ }
+
+ /**
+ * The API's own 404 body, reproduced so the browser cannot tell the two
+ * modes apart.
+ *
+ * @return array{status:int, body:array<string, mixed>, retry_after:?int}
+ */
+ private static function not_found(): array {
+ return array(
+ 'status' => 404,
+ 'body' => array(
+ 'erro' => 'cep_nao_encontrado',
+ 'mensagem' => 'CEP não existe na base do DNE',
+ ),
+ 'retry_after' => null,
+ );
+ }
+}