Back to home

RO Processor

Transaction Register · Peru FIU / SBS

Role
Developer
Status
Delivered

The problem

A currency exchange supervised by the Peruvian banking regulator must report every foreign-exchange operation of USD 5,000 or more each month, in an official 26-column format. The information lived in three places that did not talk to each other: a monthly PDF of invoices — 48 to 81 pages, one per page, in irregular format — the tax authority portal, where the legal representative details live because the invoice does not carry them, and an Excel template with rows already typed by hand that could be neither lost nor duplicated. The process was to open page by page, decide whether it clears the threshold, transcribe ten fields, go look up the legal representative, copy five more fields, fill in eleven fixed codes and, at the end, reorder by date and renumber the sequence.

Architecture

pipelinetext
Monthly PDF (48–81 pages)
   └─ pdfplumber ─────► text extraction, page by page
      └─ valid invoice? ──no──► skipped, reason logged
         └─ tolerant parser ──► 41 structural shapes
            └─ ≥ USD 5,000? ──no──► discarded, amount logged
               └─ InvoiceRecord
                  └─ headless lookup ──► legal representative
                     └─ idempotent merge + backfill
                        └─ sort by date + renumber
                           └─ FIU Excel · 26 columns
Nothing is dropped in silence: every omission leaves its reason on the record.

Decisions

01

The invoice format does not exist

The line describing the operation is typed by hand by whoever issues the invoice. Across the 386 pages there were 365 distinct lines that reduce to 41 structural shapes. There is no format — there is a distribution of improvisations. The parser covers all of them without guessing, including a typo by the issuer and an invoice that never says whether it is a purchase or a sale.

95 ×  PURCHASE OF N $ FX N N              dominant shape
85 ×  PURCHASE $N AT FX N N
59 ×  PURCHASE OF N $ AT THE FX N N
27 ×  SALE OF N $ FX N N
 4 ×  PURCHASE $N AT FX N DATED N SEPTEMBER N
 2 ×  PURCHASE OF N $ FX N WITH DATE N/N/N N
 1 ×  PURHCASE OF N $ FX N N               issuer typo
 1 ×  PURCHASE OF N EUROS AT FX N N        another currency
 1 ×  SALE $N ATFX N N                     missing space
 1 ×  $N AT FX N N                         no PURCHASE or SALE
      …and 25 more shapes appearing once each
N = any number. 365 unique lines, 41 shapes. Translated from the Spanish originals.
02

Explicit degradation, never a silent guess

If the line says neither purchase nor sale, the parser assumes purchase and raises a named warning for human review. It prefers a flagged row to an invented one. Same with operations below the threshold: they are discarded, but they stay in the log with their amount. In an auditable report, a value that disappears without a trace is worse than a wrong value that is flagged.

  [202510]   OK E001-4628 | 13/10/2025 | PURCHASE USD 20,000.00 | TRANSPORTES ANDINA EXPRESS S.A.C.
  [202510]   SKIPPED E001-4629 | USD 500.00 < 5,000
  [202510]   OK E001-4639 | 02/10/2025 | SALE     USD 25,000.00 | COMERCIALIZADORA TEXTIL GAMARRA S.A.C.
  [202510] WARN   E001-4642: purchase/sale not stated explicitly
  [202510]        — assumed PURCHASE. Verify.
  [202510] WARN   SKIPPED page 4: receipt number not found (pattern E###-####)
  [202510] Total invoices extracted: 18
  [202510] Looking up legal representatives…
  [202510]   Lookup [1/16]: tax ID 20902887752
  [202510] Total rows in file: 23 (18 invoices + existing entries)
A fragment of a real run. The full log is 590 lines.
03

The key data is not in the document

Five of the 26 columns — the legal representative document type and number, surnames and given names — appear on no invoice: they exist only in the tax authority portal. They have to be fetched company by company. The lookup is cached and deduplicated, with a single browser session for the whole batch and a polite pause between queries: 148 rows resolved with 33 lookups.

04

Idempotence over previous human work

The template arrives with rows typed by hand that can be neither lost nor duplicated. Deduplication is by normalized receipt number, so E001-4801 and 4801-E001 are the same one: a month can be reprocessed as many times as needed without duplicating a single row. And if the template already had rows with a tax ID but no legal representative, the processor completes those too. It does not only add what is new: it repairs what was half done.

October template        5 rows  (one incomplete)
   + 18 new invoices
   + backfill of the incomplete row
   + sort by date and renumber
   ─────────────────────────────────
   output                 23 rows
05

The person who uses it does not open a terminal

The operator who files the report is not a developer. The pipeline lives behind a Tkinter desktop application: pick the folders, press a button, and the log appears in the same window with every page, every warning and every lookup in plain view. Without that layer, the automation would have gone right back to depending on someone from IT every month.

┌────────────────────────────────────────────────────────┐
│  Transaction Register Processor                          │
│  Currency exchange · Peru FIU / SBS format               │
├────────────────────────────────────────────────────────┤
│  PDF folder            [ …/input        ]  [ Browse ]    │
│  Excel template        [ …/template     ]  [ Browse ]    │
│  Output folder         [ …/output       ]  [ Browse ]    │
│  File name             [ RO_202510.xlsx   ]                │
│                                                          │
│                    [       PROCESS       ]                │
├────────────────────────────────────────────────────────┤
│  Processing log                                          │
│  every page, warning and lookup is recorded              │
│                                                          │
│    [202510]   OK E001-4628 | PURCHASE USD 20,000.00      │
│    [202510]   SKIPPED E001-4629 | USD 500.00 < 5,000     │
│    [202510] WARN E001-4642: assumed PURCHASE. Verify.    │
│                                                          │
│  [ Open generated file ]  [ Clear log ]           Ready. │
└────────────────────────────────────────────────────────┘
The operator window, redrawn: there is no screenshot of the application.
06

The result, with its caveat

The volumes in this case are measured on the project files. The manual times are not: they are estimates from the person who used to do it, not stopwatch readings. I say so because the difference between measured and estimated is exactly the kind of distinction this project exists to respect.

                                        manual         automated
  triage of 386 pages                   ≈ 1.5 h        automatic
  transcription of 148 × 26 fields      ≈ 7 h          automatic
  33 legal-representative lookups       ≈ 1 h          ≈ 3–4 min
  dedupe, sort by date, renumber        ≈ 2.5 h        automatic
  ────────────────────────────────────────────────────────────────
  total                                 ≈ 10–12 h      ≈ 5 min
                                        (1.5 work days) + 15 min review
Roughly a 97–99 % cut in human time. The manual figures are estimates.
07

The operation decides which way the money flows

In a purchase the foreign currency comes in and local currency goes out; in a sale it is the other way round. Inverting it produces a mirrored report — perfectly presentable and completely false. The from/to mapping is derived from the operation type, not from where the amounts sit on the invoice.

The product

The input. The whole operation lives in one hand-typed line: «COMPRA DE 6,200 $ AL TC 3.646 22,605.20». Neither the legal representative nor their document number appears anywhere — and those are five of the 26 columns the report needs.

The output

The output, for that same receipt

Reg. no.DateRegionProvinceDistrictDoc. typeDoc. no.Surname 1Surname 2Given namesCountryOccupationOn behalf ofTax IDSurname 1 or company nameSurname 2Given namesOccupationFund typeOp. typeSourceCurrencyAmountCurrencyAmountReceipt no.
101/10/202515012200143••••••BENITESOCHOAGABRIELAPE00400320901996640CONSULTORA FINANCIERA ANDES S.A.C.001003099USD6000.00PEN20388.00B001-1242
201/10/202515012200143••••••BENITESOCHOAGABRIELAPE00400320901996640CONSULTORA FINANCIERA ANDES S.A.C.001003099USD21000.00PEN79569.00B001-1245
301/10/202515012200144••••••HERRERAVALDEZMONICA PATRICIAPE00400320902114428COMERCIALIZADORA TEXTIL GAMARRA S.A.C.001003099USD6200.00PEN22605.20E001-4627
401/10/202515012200107••••••PAREDESCORDOVALUIS ALBERTOPE00400320901224317AGENCIA DE ADUANAS MERIDIANO S.A.001003099USD5000.00PEN17100.00E001-4635
502/10/202515012200144••••••HERRERAVALDEZMONICA PATRICIAPE00400320902114428COMERCIALIZADORA TEXTIL GAMARRA S.A.C.001003099PEN93450.00USD25000.00E001-4639
603/10/202515012200146••••••SALAZARDUENASVERONICAPE00400320903114428REPRESENTACIONES COMERCIALES ORIENTE S.A.C.001003099USD18000.00PEN62478.00E001-4613
704/10/202515012200106••••••FLORESAGUILARCESAR AUGUSTOPE00400320900884751DISTRIBUIDORA COMERCIAL LOS OLIVOS S.A.C.001003099USD8500.00PEN30328.00E001-4602
805/10/202515012200110••••••TAPIABUSTAMANTEOSCAR RAULPE00400320902887752EXPORTACIONES CAFETALERAS CHANCHAMAYO E.I.R.L.001003099PEN33354.00USD9000.00E001-4596

8 of the 23 October rows. The five highlighted columns do not exist in the invoice: they came from a tax-authority lookup. Synthetic data — the tax IDs carry a deliberately invalid check digit and the identity documents are masked, as a compliance demo should.

Figures

386
PDF pages processedBRIEF — measured on the project files
41
structural shapes of the operation lineBRIEF — from 365 unique lines
148
rows generated across 6 monthsBRIEF — 28/34/24/23/22/17
33
lookups needed for 148 rowsBRIEF — deduplicated and cached
87
invoices above the USD 5,000 thresholdBRIEF — measured
~1,500
lines of codeBRIEF — measured

What it demonstrates

SkillEvidence
Real-world dirty data41 structural shapes covered without guessing, including an issuer typo and invoices with no explicit purchase/sale
Judgement in an auditable domainNothing is dropped in silence: every omission and every assumption is logged with its reason
IdempotenceDeduplication by normalized receipt number, plus backfill of incomplete hand-typed rows
Integrating what you cannot requestFive columns that exist only in the tax authority portal, resolved with cached scraping: 148 rows on 33 lookups
Encoded business rulesThreshold, from/to mapping by operation type and eleven fixed codes applied reproducibly

Known technical debt

A case study that only lists wins does not survive a hard question.

  • The scraper depends on the tax authority portal DOM: a redesign breaks it. It fails with a warning, without corrupting the output.
  • The fixed codes are hard-wired for this company — region, occupation, fund type. For another exchange they would need to be parameterised.
  • Invoices with no explicit purchase or sale are assumed to be purchases and flagged for manual validation.

Stack

  • Python 3
  • pdfplumber
  • openpyxl
  • Selenium + headless Chrome
  • Tkinter
  • ThreadPoolExecutor
  • Decimal