summaryrefslogtreecommitdiffstats
path: root/src/cepis-lookup.cob
blob: 90f1502bdf6aceea609a40dcdac53e4d4589532c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
IDENTIFICATION DIVISION.
PROGRAM-ID. CEPIS-LOOKUP.

*> ---------------------------------------------------------------------
*> Look up one CEP.
*>
*>     CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT
*>                               CEPIS-ADDRESS CEPIS-STATUS
*>
*> The CEP may be masked or not. It is validated locally first, so a
*> seven-digit CEP costs no round trip: CEPIS-INVALID-CEP comes back
*> without a socket ever being opened.
*>
*> On CEPIS-OK the address is filled. On anything else CEPIS-ADDRESS is
*> cleared, so a caller that forgets to test the status reads spaces
*> rather than the previous CEP's street.
*>
*> ADDR-GEO-PRESENT says whether the coordinate exists -- about 57% of
*> CEPs have one. When it does, read ADDR-GEO-PRECISAO before measuring
*> any distance with it: "logradouro" means the point is a stretch of
*> street, with ADDR-GEO-RAIO-M around 100-150 m, while "localidade"
*> means the CEP is a whole municipality and the point is its centroid,
*> with a radius of 7 to 40 km. Computing shipping from the second is
*> wrong by tens of kilometres.
*>
*> A CEPIS-NOT-FOUND may come back with CEPIS-FAIXA filled: the
*> municipality the CEP would have belonged to. A CEP that fails is
*> rarely a random number -- it is usually someone who knows the city and
*> guessed the digits, and the classic guess is the -000 that 957
*> municipalities never received. The lookup has still failed and the
*> address is still empty; CEPIS-FAIXA only names where the guess landed.
*> ---------------------------------------------------------------------

DATA DIVISION.
WORKING-STORAGE SECTION.
COPY "cepis-http.cpy".
COPY "cepis-json.cpy".

01  WS-CEP                          PIC X(8).
01  WS-GEO-FROM                     PIC 9(9).
01  WS-GEO-TO                       PIC 9(9).
01  WS-GEO-OK                       PIC X.
01  WS-NUM-OK                       PIC X.
01  WS-LAT                          PIC S9(3)V9(6).
01  WS-LON                          PIC S9(3)V9(6).
01  WS-RAIO                         PIC 9(9).
01  WS-PRECISAO                     PIC X(16).
01  WS-FAIXA-FROM                   PIC 9(9).
01  WS-FAIXA-TO                     PIC 9(9).
01  WS-FAIXA-OK                     PIC X.
01  WS-F-LOCALIDADE                 PIC X(100).
01  WS-F-UF                         PIC X(2).
01  WS-F-ESTADO                     PIC X(60).
01  WS-F-REGIAO                     PIC X(30).
01  WS-F-IBGE                       PIC X(10).
01  WS-F-CEP-INI                    PIC X(8).
01  WS-F-CEP-FIM                    PIC X(8).
01  WS-F-CEPS                       PIC 9(9).
01  WS-I                            PIC 9(4).
01  WS-C                            PIC X.
01  WS-DOTS                         PIC 9(4).
01  WS-JUNK                         PIC 9(4).
01  WS-DIGITS                       PIC 9(4).

LINKAGE SECTION.
COPY "cepis.cpy".

PROCEDURE DIVISION USING CEPIS-CONFIG CEPIS-CEP-INPUT
                         CEPIS-ADDRESS CEPIS-STATUS.

MAIN-START.
    PERFORM CLEAR-ADDRESS

    CALL "CEPIS-NORMALIZE-CEP"
        USING CEPIS-CEP-INPUT WS-CEP CEPIS-STATUS
    END-CALL
    IF CEPIS-FAILED
        GOBACK
    END-IF

    MOVE SPACES TO HTTPQ-PATH
    STRING "/" WS-CEP DELIMITED BY SIZE INTO HTTPQ-PATH
    END-STRING
    MOVE 9 TO HTTPQ-PATH-LEN
    MOVE "N" TO HTTPQ-ALLOW-503

    CALL "CEPIS-REQUEST" USING CEPIS-CONFIG CEPIS-HTTP-REQUEST
                               CEPIS-HTTP-RESPONSE CEPIS-STATUS
    END-CALL
    IF CEPIS-FAILED
        GOBACK
    END-IF

    EVALUATE HTTP-STATUS
        WHEN 200
            PERFORM PARSE-ADDRESS
        WHEN 404
            SET CEPIS-NOT-FOUND TO TRUE
            MOVE SPACES TO CEPIS-STATUS-MESSAGE
            STRING "CEP " WS-CEP " does not exist in the DNE database"
                DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE
            END-STRING
            PERFORM PARSE-FAIXA
        WHEN 400
            SET CEPIS-INVALID-CEP TO TRUE
            PERFORM API-MESSAGE
        WHEN OTHER
            SET CEPIS-INVALID-RESPONSE TO TRUE
            MOVE SPACES TO CEPIS-STATUS-MESSAGE
            STRING "unexpected status " HTTP-STATUS
                   " for CEP " WS-CEP
                DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE
            END-STRING
    END-EVALUATE
    GOBACK.

*> -------------------------------------------------------------------
CLEAR-ADDRESS.
    INITIALIZE CEPIS-ADDRESS
    SET ADDR-NOT-TRUNCATED TO TRUE
    SET ADDR-GEO-ABSENT TO TRUE
    MOVE SPACES TO ADDR-GEO-PRECISAO.

*> -------------------------------------------------------------------
*> The API's own 400 carries a message this client does not have: the
*> format already passed local validation, so the server knows something
*> we do not. Surface its "mensagem" when there is one.
API-MESSAGE.
    MOVE SPACES TO CEPIS-STATUS-MESSAGE
    MOVE "mensagem" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-TYPE = "S" AND JS-VALUE-LEN > 0
        MOVE JS-VALUE TO CEPIS-STATUS-MESSAGE
    ELSE
        STRING "CEP " WS-CEP " rejected by the API"
            DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE
        END-STRING
    END-IF.

*> -------------------------------------------------------------------
GET-MEMBER.
    MOVE 1 TO JS-FROM
    MOVE HTTP-BODY-LEN TO JS-TO
    CALL "CEPIS-JSON-GET" USING HTTP-BODY CEPIS-JSON-SCAN
    END-CALL.

*> -------------------------------------------------------------------
GET-GEO-MEMBER.
    MOVE WS-GEO-FROM TO JS-FROM
    MOVE WS-GEO-TO TO JS-TO
    CALL "CEPIS-JSON-GET" USING HTTP-BODY CEPIS-JSON-SCAN
    END-CALL.

*> -------------------------------------------------------------------
GET-FAIXA-MEMBER.
    MOVE WS-FAIXA-FROM TO JS-FROM
    MOVE WS-FAIXA-TO TO JS-TO
    CALL "CEPIS-JSON-GET" USING HTTP-BODY CEPIS-JSON-SCAN
    END-CALL.

*> -------------------------------------------------------------------
PARSE-ADDRESS.
    MOVE "cep" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-TYPE = "E"
        SET CEPIS-INVALID-RESPONSE TO TRUE
        MOVE "response is not a JSON object -- something between you "
          &  "and the API is answering instead of it"
          TO CEPIS-STATUS-MESSAGE
        EXIT PARAGRAPH
    END-IF

    IF JS-VALUE-LEN > 8
        SET ADDR-WAS-TRUNCATED TO TRUE
    END-IF
    MOVE JS-VALUE TO ADDR-CEP
    IF ADDR-CEP = SPACES
        MOVE WS-CEP TO ADDR-CEP
    END-IF
    STRING ADDR-CEP(1:5) "-" ADDR-CEP(6:3)
        DELIMITED BY SIZE INTO ADDR-CEP-MASKED
    END-STRING

    MOVE "logradouro" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-VALUE-LEN > 120
        SET ADDR-WAS-TRUNCATED TO TRUE
    END-IF
    MOVE JS-VALUE TO ADDR-LOGRADOURO

    MOVE "complemento" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-VALUE-LEN > 120
        SET ADDR-WAS-TRUNCATED TO TRUE
    END-IF
    MOVE JS-VALUE TO ADDR-COMPLEMENTO

    MOVE "unidade" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-VALUE-LEN > 80
        SET ADDR-WAS-TRUNCATED TO TRUE
    END-IF
    MOVE JS-VALUE TO ADDR-UNIDADE

    MOVE "bairro" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-VALUE-LEN > 100
        SET ADDR-WAS-TRUNCATED TO TRUE
    END-IF
    MOVE JS-VALUE TO ADDR-BAIRRO

    MOVE "localidade" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-VALUE-LEN > 100
        SET ADDR-WAS-TRUNCATED TO TRUE
    END-IF
    MOVE JS-VALUE TO ADDR-LOCALIDADE

    MOVE "uf" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-VALUE-LEN > 2
        SET ADDR-WAS-TRUNCATED TO TRUE
    END-IF
    MOVE JS-VALUE TO ADDR-UF

    MOVE "estado" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-VALUE-LEN > 60
        SET ADDR-WAS-TRUNCATED TO TRUE
    END-IF
    MOVE JS-VALUE TO ADDR-ESTADO

    MOVE "regiao" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-VALUE-LEN > 30
        SET ADDR-WAS-TRUNCATED TO TRUE
    END-IF
    MOVE JS-VALUE TO ADDR-REGIAO

    MOVE "ibge" TO JS-KEY
    PERFORM GET-MEMBER
    MOVE JS-VALUE TO ADDR-IBGE

    MOVE "gia" TO JS-KEY
    PERFORM GET-MEMBER
    MOVE JS-VALUE TO ADDR-GIA

    MOVE "ddd" TO JS-KEY
    PERFORM GET-MEMBER
    MOVE JS-VALUE TO ADDR-DDD

    MOVE "siafi" TO JS-KEY
    PERFORM GET-MEMBER
    MOVE JS-VALUE TO ADDR-SIAFI

    PERFORM PARSE-GEO
    SET CEPIS-OK TO TRUE.

*> -------------------------------------------------------------------
*> The coordinate is omitted entirely when the database has none, so its
*> absence is normal, not an error. A geo block that is present but
*> malformed is dropped the same way: the rest of the address is still
*> useful and losing it over a bad float would be a poor trade.
PARSE-GEO.
    SET ADDR-GEO-ABSENT TO TRUE
    MOVE "geo" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-TYPE NOT = "O"
        EXIT PARAGRAPH
    END-IF
    MOVE JS-VSTART TO WS-GEO-FROM
    MOVE JS-VEND TO WS-GEO-TO
    MOVE "Y" TO WS-GEO-OK

    MOVE "lat" TO JS-KEY
    PERFORM GET-GEO-MEMBER
    PERFORM CHECK-NUMERIC
    IF WS-NUM-OK = "Y"
        COMPUTE WS-LAT = FUNCTION NUMVAL (JS-VALUE)
    ELSE
        MOVE "N" TO WS-GEO-OK
    END-IF

    MOVE "lon" TO JS-KEY
    PERFORM GET-GEO-MEMBER
    PERFORM CHECK-NUMERIC
    IF WS-NUM-OK = "Y"
        COMPUTE WS-LON = FUNCTION NUMVAL (JS-VALUE)
    ELSE
        MOVE "N" TO WS-GEO-OK
    END-IF

    MOVE "raio_m" TO JS-KEY
    PERFORM GET-GEO-MEMBER
    PERFORM CHECK-NUMERIC
    IF WS-NUM-OK = "Y"
        COMPUTE WS-RAIO ROUNDED = FUNCTION NUMVAL (JS-VALUE)
    ELSE
        MOVE "N" TO WS-GEO-OK
    END-IF

    MOVE "precisao" TO JS-KEY
    PERFORM GET-GEO-MEMBER
    IF JS-TYPE = "S" AND JS-VALUE-LEN > 0
        MOVE JS-VALUE TO WS-PRECISAO
    ELSE
        MOVE "N" TO WS-GEO-OK
    END-IF

    IF WS-GEO-OK = "Y"
        SET ADDR-GEO-KNOWN TO TRUE
        MOVE WS-LAT TO ADDR-GEO-LAT
        MOVE WS-LON TO ADDR-GEO-LON
        MOVE WS-RAIO TO ADDR-GEO-RAIO-M
        MOVE WS-PRECISAO TO ADDR-GEO-PRECISAO
    END-IF.

*> -------------------------------------------------------------------
*> The 404 body may name the municipality the CEP would have belonged
*> to. It is context for the error and nothing else: the status stays
*> CEPIS-NOT-FOUND and CEPIS-ADDRESS stays empty, because a CEP that the
*> DNE does not have is not an address no matter which city it is near.
*>
*> "faixa" is omitted -- never sent as null -- when the CEP falls in a
*> band no municipality owns, so its absence is the normal case, not an
*> error. Everything here is therefore best effort: a faixa that is
*> missing, is not an object, or is short of the members below is
*> dropped, and the caller still gets a clean not-found. Turning a 404
*> the caller already understands into CEPIS-INVALID-RESPONSE over a
*> decoration would be a poor trade.
*>
*> cep_ini and cep_fim must both be eight digits and ceps must be a
*> number, or the whole block is dropped. The span is sparse -- Areal's
*> 118 CEPs are spread over the ~4900 numbers between 25845001 and
*> 25849899 -- so handing a caller the pair without the count invites
*> reading it as a list of valid CEPs, which it is not.
PARSE-FAIXA.
    INITIALIZE CEPIS-FAIXA
    SET CEPIS-FAIXA-ABSENT TO TRUE
    MOVE "faixa" TO JS-KEY
    PERFORM GET-MEMBER
    IF JS-TYPE NOT = "O"
        EXIT PARAGRAPH
    END-IF
    MOVE JS-VSTART TO WS-FAIXA-FROM
    MOVE JS-VEND TO WS-FAIXA-TO
    MOVE "Y" TO WS-FAIXA-OK

    MOVE "localidade" TO JS-KEY
    PERFORM GET-FAIXA-MEMBER
    IF JS-TYPE = "S" AND JS-VALUE-LEN > 0
        MOVE JS-VALUE TO WS-F-LOCALIDADE
    ELSE
        MOVE "N" TO WS-FAIXA-OK
    END-IF

    MOVE "uf" TO JS-KEY
    PERFORM GET-FAIXA-MEMBER
    IF JS-TYPE = "S" AND JS-VALUE-LEN = 2
        MOVE JS-VALUE TO WS-F-UF
    ELSE
        MOVE "N" TO WS-FAIXA-OK
    END-IF

    MOVE "cep_ini" TO JS-KEY
    PERFORM GET-FAIXA-MEMBER
    IF JS-TYPE = "S" AND JS-VALUE-LEN = 8
        MOVE JS-VALUE TO WS-F-CEP-INI
    ELSE
        MOVE "N" TO WS-FAIXA-OK
    END-IF

    MOVE "cep_fim" TO JS-KEY
    PERFORM GET-FAIXA-MEMBER
    IF JS-TYPE = "S" AND JS-VALUE-LEN = 8
        MOVE JS-VALUE TO WS-F-CEP-FIM
    ELSE
        MOVE "N" TO WS-FAIXA-OK
    END-IF

    MOVE "ceps" TO JS-KEY
    PERFORM GET-FAIXA-MEMBER
    PERFORM CHECK-NUMERIC
    IF WS-NUM-OK = "Y"
        COMPUTE WS-F-CEPS ROUNDED = FUNCTION NUMVAL (JS-VALUE)
    ELSE
        MOVE "N" TO WS-FAIXA-OK
    END-IF

    IF WS-FAIXA-OK NOT = "Y"
        EXIT PARAGRAPH
    END-IF

    *> The three below are decoration: a faixa the API sends without
    *> them still names the municipality, so they never fail the block.
    MOVE SPACES TO WS-F-ESTADO
    MOVE "estado" TO JS-KEY
    PERFORM GET-FAIXA-MEMBER
    IF JS-TYPE = "S"
        MOVE JS-VALUE TO WS-F-ESTADO
    END-IF

    MOVE SPACES TO WS-F-REGIAO
    MOVE "regiao" TO JS-KEY
    PERFORM GET-FAIXA-MEMBER
    IF JS-TYPE = "S"
        MOVE JS-VALUE TO WS-F-REGIAO
    END-IF

    MOVE SPACES TO WS-F-IBGE
    MOVE "ibge" TO JS-KEY
    PERFORM GET-FAIXA-MEMBER
    IF JS-TYPE = "S"
        MOVE JS-VALUE TO WS-F-IBGE
    END-IF

    SET CEPIS-FAIXA-KNOWN TO TRUE
    MOVE WS-F-LOCALIDADE TO CEPIS-FAIXA-LOCALIDADE
    MOVE WS-F-UF TO CEPIS-FAIXA-UF
    MOVE WS-F-ESTADO TO CEPIS-FAIXA-ESTADO
    MOVE WS-F-REGIAO TO CEPIS-FAIXA-REGIAO
    MOVE WS-F-IBGE TO CEPIS-FAIXA-IBGE
    MOVE WS-F-CEP-INI TO CEPIS-FAIXA-CEP-INI
    MOVE WS-F-CEP-FIM TO CEPIS-FAIXA-CEP-FIM
    MOVE WS-F-CEPS TO CEPIS-FAIXA-CEPS.

*> -------------------------------------------------------------------
*> FUNCTION NUMVAL answers 0 for text it cannot read, which would turn
*> "lat": "not a number" into a coordinate off the coast of Africa. The
*> text is therefore checked before it is converted.
CHECK-NUMERIC.
    MOVE "N" TO WS-NUM-OK
    IF JS-TYPE NOT = "N" AND JS-TYPE NOT = "S"
        EXIT PARAGRAPH
    END-IF
    IF JS-VALUE-LEN = 0 OR JS-VALUE-LEN > 32
        EXIT PARAGRAPH
    END-IF
    MOVE 0 TO WS-DIGITS
    MOVE 0 TO WS-DOTS
    MOVE 0 TO WS-JUNK
    PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > JS-VALUE-LEN
        MOVE JS-VALUE(WS-I:1) TO WS-C
        EVALUATE TRUE
            WHEN WS-C >= "0" AND WS-C <= "9"
                ADD 1 TO WS-DIGITS
            WHEN WS-C = "."
                ADD 1 TO WS-DOTS
            WHEN (WS-C = "-" OR WS-C = "+") AND WS-I = 1
                CONTINUE
            WHEN OTHER
                ADD 1 TO WS-JUNK
        END-EVALUATE
    END-PERFORM
    IF WS-DIGITS > 0 AND WS-DOTS <= 1 AND WS-JUNK = 0
        MOVE "Y" TO WS-NUM-OK
    END-IF.

END PROGRAM CEPIS-LOOKUP.