/** * Naming the municipality a CEP that does not exist would have belonged to. * * A CEP that fails at the checkout is rarely a random number. It is almost * always a shopper who knows perfectly well which town they live in and got a * digit wrong — most often by trying the `-000`, which 957 municipalities never * received. Areal/RJ, for instance, answers from 25845-001 upwards and never at * 25845-000. The API says so in the body of its own 404, under `faixa`, and * telling the shopper "that number would be in Areal/RJ" turns a dead end into * something they can fix. * * **What comes back from here is a sentence, and only a sentence.** The `faixa` * object never leaves this module, because a CEP that does not exist has no * address: the municipality named in a 404 is a guess about a typo, not a place * to ship to. Filling a city or a state from it would produce a parcel sent to * the wrong town and a chargeback with it. Both checkout integrations therefore * get a string they can only display. * * The body costs one extra request, because the widget — correctly — throws its * 404 away without reading it. That request happens only on the miss path, only * once per CEP however many sections ask for it, and never at all when the CEP * resolves, which is the overwhelming majority of lookups. * * @module */ (function () { /** Eight digits, split where the mask goes. */ var CEP = /^(\d{5})(\d{3})$/; /** Anything that is not a digit. */ var NOT_DIGITS = /\D+/g; /** A `%1$s`-style placeholder, as WordPress writes them. */ var PLACEHOLDER = /%(\d)\$s/g; /** * One promise of a message per CEP, so retyping the same wrong number and the * billing and shipping sections asking at once all cost a single request. * * @type {Record>} */ var asked = {}; /** * Whatever the page was configured with, read late: this file is a dependency * of the integrations and loads before the inline configuration does. * * @returns {Record} */ function settings() { return window.cepisWooCommerce || {}; } /** * The digits of a CEP, however it was typed. * * @param {unknown} value Anything printable. * @returns {string} */ function digits(value) { return String(value == null ? "" : value).replace(NOT_DIGITS, ""); } /** * A CEP with its mask, or an empty string when it is not eight digits. * * @param {unknown} value Anything printable. * @returns {string} */ function mask(value) { var match = CEP.exec(String(value == null ? "" : value)); return match ? match[1] + "-" + match[2] : ""; } /** * Fill a translated template in. * * A placeholder with no value is left as it stands, so a translation with the * wrong number of arguments degrades into visible nonsense rather than * `undefined`. * * @param {string} template Message with `%1$s` placeholders. * @param {string[]} values Values, in order. * @returns {string} */ function fill(template, values) { return template.replace(PLACEHOLDER, function (whole, index) { var value = values[Number(index) - 1]; return "string" === typeof value ? value : whole; }); } /** * A non-empty trimmed string, or "". * * @param {unknown} value Anything. * @returns {string} */ function text(value) { return "string" === typeof value ? value.trim() : ""; } /** * Turn a 404 body into the sentence the shopper gets, or "" when it does not * name a municipality. * * Every step is a guard: `faixa` is absent for a CEP in no municipality's * range at all, and the plugin must behave exactly as it did before whenever * anything is missing or the wrong shape. * * The range is deliberately described as the lowest and the highest CEP that * exist there, never as a list and never as a suggestion. It is sparse — about * 118 real CEPs across nearly five thousand numbers in Areal's case — so a * number picked out of the middle of it would most likely not exist either. * * @param {unknown} body The decoded 404 body. * @returns {string} */ function describe(body) { var messages = settings().messages || {}; var faixa = body && "object" === typeof body && !Array.isArray(body) ? body.faixa : null; if (!faixa || "object" !== typeof faixa || Array.isArray(faixa)) { return ""; } var city = text(faixa.localidade); var uf = text(faixa.uf); if ("" === city || "" === uf) { return ""; } var first = mask(faixa.cep_ini); var last = mask(faixa.cep_fim); var ranged = text(messages.not_found_range); if ("" !== first && "" !== last && "" !== ranged) { return fill(ranged, [city, uf, first, last]); } var plain = text(messages.not_found_city); return "" === plain ? "" : fill(plain, [city, uf]); } /** * Fetch the 404 body for one CEP. * * Resolves with `null` for anything that is not a 404 carrying JSON — * including a CEP that has meanwhile started to exist, a network failure, or * a store whose API answers something else entirely. Never rejects. * * @param {string} cep Eight digits. * @returns {Promise} */ function body(cep) { var config = settings(); var base = String(config.baseUrl || "").replace(/\/+$/, ""); if ("" === base || "function" !== typeof window.fetch) { return Promise.resolve(null); } var controller = "function" === typeof window.AbortController ? new window.AbortController() : null; var timer = controller ? window.setTimeout(function () { controller.abort(); }, config.timeoutMs || 8000) : 0; return window .fetch(base + "/" + cep, { method: "GET", credentials: "omit", headers: { Accept: "application/json" }, signal: controller ? controller.signal : undefined, }) .then(function (response) { return 404 === response.status ? response.text() : ""; }) .then(function (payload) { try { return JSON.parse(payload); } catch (error) { return null; } }) .catch(function () { return null; }) .then(function (decoded) { window.clearTimeout(timer); return decoded; }); } /** * Ask what a CEP that does not exist would have been. * * `done` is always called exactly once, with either a better message or the * fallback unchanged. It is never called with anything but a string, and the * caller is given no way to reach the underlying data. * * @param {string} cep The CEP that was not found. * @param {string} fallback The message already on screen. * @param {(message: string) => void} done Called with what to show. */ function message(cep, fallback, done) { if ("function" !== typeof done) { return; } var number = digits(cep); if (8 !== number.length) { done(fallback); return; } if (!asked[number]) { asked[number] = body(number).then(describe, function () { return ""; }); } asked[number].then( function (better) { done("" === better ? fallback : better); }, function () { done(fallback); }, ); } window.cepisWooCommerceFaixa = { message: message }; })();