DEV Community

BrowserAct
BrowserAct

Posted on Originally published at browseract.com Fully Autonomous

Stop a billing CSV at the schema boundary

A browser job can save a billing export successfully while the next system reads it incorrectly. The file exists, the download completed, and the spreadsheet opens. None of those observations establishes that an older importer still understands the columns.

UPS provides a concrete example. Its UK Billing Centre page announces a September 2026 expansion of the Global Flat File CSV from 250 to 270 columns and asks users of automated imports to review their integrations. That notice is a schema change to investigate, rather than evidence that a browser download failed.

The following guard is an application-side example with synthetic fixtures. It is not a UPS parser, a current UPS field specification, or a tested BrowserAct billing integration.

Keep the downloaded bytes as the starting artifact

Retain the source file before transforming it. Record its digest, retrieval time, selected account or plan, and requested reporting period in the collection manifest. Keep any invoice PDFs under a separate document contract: a billing-data CSV does not prove that the requested invoice originals were collected.

The digest identifies a particular byte sequence. It does not authenticate the supplier, prove that the account selection was correct, or establish completeness. Those checks need their own evidence.

Avoid trimming the export back to an old column count just to make an importer accept it. That can hide the change that requires review. A transformed copy should have a mapping version and a reference to the source digest.

Approve an ordered header contract

Column count is a useful diagnostic, but equal counts can still conceal renamed or reordered fields. For an existing import, keep an approved ordered header list for each supported export version. Obtain it from reviewed output and the relevant supplier documentation, rather than inventing names from a screenshot.

This small validator deliberately rejects every header change. Its caller must supply the approved headers; the example makes no assumptions about actual UPS names. It preserves values as strings, so identifiers with leading zeros are not silently converted to numbers.

import csv
import hashlib
import io
from pathlib import Path


def validate_export(path: Path, approved_headers):
    expected = tuple(approved_headers)
    if (not expected or any(not name for name in expected)
            or len(set(expected)) != len(expected)):
        raise ValueError("invalid approved header contract")

    raw = path.read_bytes()
    text = raw.decode("utf-8-sig")
    reader = csv.reader(io.StringIO(text, newline=""), strict=True)
    header = next(reader, None)
    if header is None:
        raise ValueError("missing header")
    if tuple(header) != expected:
        raise ValueError("header contract changed")

    records = []
    for record_number, fields in enumerate(reader, start=2):
        if len(fields) != len(expected):
            raise ValueError(f"record {record_number}: width changed")
        records.append(dict(zip(expected, fields)))

    return {
        "source_sha256": hashlib.sha256(raw).hexdigest(),
        "record_count": len(records),
        "records": records,
    }
Enter fullscreen mode Exit fullscreen mode

Here, UTF-8 with an optional byte-order mark is the fixture's encoding contract. Establish the real export's encoding separately; this code does not detect arbitrary encodings. An undecodable file stops processing instead of being repaired through replacement characters.

The CSV reader handles quoted commas and embedded newlines. The reported record number is a logical CSV record, not a physical line number. Blank records are rejected by the width check. A header-only file returns zero records, which still needs a separate business decision about whether an empty export was expected.

Hold a changed schema for review

On a mismatch, retain the raw file and produce a visible exception with the expected contract version, observed header, and responsible owner. Do not send partially accepted records to the destination. This function only returns after the complete small file has passed its structural checks.

For large exports, use a staged artifact and bounded resources rather than accumulating every record in memory. Commit the staged output only after validation completes. That prevents a later malformed record from leaving an apparently successful partial import.

Schema acceptance should precede business validation. The guard does not validate currencies, totals, invoice identity, or the accounting meaning of each field. A structurally correct row may still require investigation.

Review the mapping before releasing a new version

When accepting a revised export, compare actual headers with the relevant data dictionary. Identify added, removed, renamed, and reordered fields. Review the destination mapping with whoever owns reconciliation, then assign the new contract a version instead of changing the old one silently.

Before release, exercise the previous supported fixture, a newly approved fixture, and deliberate failures. Include reordered headers, duplicate names, extra and missing fields, malformed quoting, and empty input. Keep a known amount and an identifier with leading zeros in the fixture so downstream conversions have something concrete to check.

Our UPS invoice download guide covers the documented portal route and the distinction between invoice documents and billing data. This tutorial adds an importer boundary after collection. The browser job and the importer should each report what they actually verified; neither should inherit the other's success badge.

Top comments (0)