From 83ce95cf10fd9e9d581362453c558239d37ae06e Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Mon, 27 Jul 2026 20:46:40 -0300 Subject: Name the municipality when the CEP does not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Answering only "não encontrei" wastes the trip. A CEP that fails is almost never a random number: it is someone who knows the city and guessed the rest, most often at the "-000" that 957 municipalities never received. The API now names that municipality on the 404, so the reply names it too. It stays an answer to a failure. No 📮, no address block, and no CEP suggested from inside the range — the range is sparse (Areal's 118 CEPs span nearly five thousand numbers), so the reply gives the count and says so instead of letting the two ends read as a list of valid CEPs. cep.is/go v0.1.0 discards the 404 body, so internal/lookup reads it with one extra GET, on that path alone and never inside a retry. Everything that can go wrong there — a slow answer, broken JSON, a faixa naming no municipality — falls back to the reply the bot has always given. The wrapper goes away the day the library exposes the field. --- internal/bot/format.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 2 deletions(-) (limited to 'internal/bot/format.go') diff --git a/internal/bot/format.go b/internal/bot/format.go index 9a9a22d..b94dee4 100644 --- a/internal/bot/format.go +++ b/internal/bot/format.go @@ -9,6 +9,8 @@ import ( "time" cepis "cep.is/go" + + "cep.is/telegrambot/internal/lookup" ) // ParseMode is the formatting Telegram applies to every message this bot sends. @@ -152,8 +154,7 @@ func formatRadius(metres int) string { func ErrorMessage(err error) string { switch { case errors.Is(err, cepis.ErrCEPNotFound): - return "🔍 CEP não encontrado na base dos Correios (DNE).\n" + - "Confira os oito dígitos — a base é atualizada a cada quinze dias." + return notFoundMessage(err) case errors.Is(err, cepis.ErrInvalidCEP): return "✋ CEP inválido. Preciso de oito dígitos: 01001000 " + "ou 01001-000." @@ -173,6 +174,97 @@ func ErrorMessage(err error) string { return "⚠️ Não consegui consultar este CEP agora. Tente de novo em alguns minutos." } +// The two halves of the not-found answer, unchanged from the day the bot +// shipped. Whatever is said about the municipality goes between them: the reply +// opens by saying the CEP does not exist and closes by saying what to do, so +// the middle cannot be mistaken for an address. +const ( + notFoundHead = "🔍 CEP não encontrado na base dos Correios (DNE)." + notFoundAdvice = "Confira os oito dígitos — a base é atualizada a cada quinze dias." +) + +// notFoundMessage answers a CEP that does not exist, naming the municipality it +// would belong to when the API told us which one. +// +// It is worth the extra lines. The CEP that fails is almost never a random +// number — it is someone who knows the city and guessed the rest, most often at +// the "-000" that 957 municipalities never received. Telling that person only +// "não encontrei" wastes the trip; telling them the number would have been in +// Areal/RJ says which half of their guess was right. +// +// What it must never do is read like a hit. There is no 📮, no address block +// and no CEP suggested from inside the range: the range is sparse — Areal's 118 +// CEPs are spread over nearly five thousand numbers — so its ends are the +// extremes of what exists, not a list of what is valid, and the reply says so. +func notFoundMessage(err error) string { + faixa := lookup.FaixaOf(err) + if faixa == nil || faixa.Localidade == "" { + return notFoundHead + "\n" + notFoundAdvice + } + + var b strings.Builder + b.WriteString(notFoundHead + "\n") + b.WriteString("Ele cairia em " + esc(faixa.Place()) + "") + + count := faixaCount(faixa) + span := faixaSpan(faixa) + switch { + case count != "" && span != "": + b.WriteString(", que tem " + count + ", " + span + ".\n") + b.WriteString("A faixa é esparsa: nem todo número entre os dois é um CEP válido.\n") + case count != "": + b.WriteString(", que tem " + count + ".\n") + case span != "": + b.WriteString(", com CEPs " + span + ".\n") + b.WriteString("A faixa é esparsa: nem todo número entre os dois é um CEP válido.\n") + default: + b.WriteString(".\n") + } + + b.WriteString(notFoundAdvice) + return b.String() +} + +// faixaCount renders how many CEPs the municipality has, or "" when the API did +// not say. Without it the two ends of the range read as a count of one big +// block, which is the misreading the number exists to prevent. +func faixaCount(faixa *lookup.Faixa) string { + switch { + case faixa.CEPs <= 0: + return "" + case faixa.CEPs == 1: + return "1 CEP" + } + return strconv.Itoa(faixa.CEPs) + " CEPs" +} + +// faixaSpan renders the ends of the range, or "" when they are missing or are +// the same number — "de X a X" is not a range, and printing a single CEP there +// would hand the user a guess to try. +func faixaSpan(faixa *lookup.Faixa) string { + first, last := maskCEP(faixa.CEPIni), maskCEP(faixa.CEPFim) + if first == "" || last == "" || first == last { + return "" + } + return "de " + esc(first) + " a " + esc(last) + "" +} + +// maskCEP puts the mask on eight digits, and returns "" for anything else: a +// CEP the API sent in a shape this bot does not recognise is left out of the +// sentence rather than printed raw. +func maskCEP(cep string) string { + cep = strings.TrimSpace(cep) + if len(cep) != 8 { + return "" + } + for _, r := range cep { + if r < '0' || r > '9' { + return "" + } + } + return cep[:5] + "-" + cep[5:] +} + // RateLimitMessage is what a user held by the local token bucket is told, once // per warning window. func RateLimitMessage(retryAfter time.Duration) string { -- cgit v1.3