Skip to main content

Portal Coverage & Adding Portals

Portal reconciliation data flow

Current Status​

PortalStatusFormatNotes
FM Pilotβœ… ActiveCSVWO/PO report, not invoice report. Vendor Invoice Number links to CieTrade. Unbilled WOs are the primary output.
Aribaβœ… ActiveExcel (.xlsx)Standard invoice report. Invoice # normalized before matching.
Coupaβœ… ActiveExcel (.xlsx, 7 sheets)Line-level data β€” grouped and summed to invoice level automatically. Optional: upload Yani's "Unsubmitted Coupa Customers" Excel to resolve location codes β†’ customer names.
Corrigoβœ… ActiveXLSX/CSVJLL accounts (Wells Fargo, ABB, The Church, Wasteology Branch). 2-row metadata header skip, structured address columns. 6 production sample files validated (2026-06-22).
VendorCafeβœ… ActiveXLSX/CSVPayment history format β€” all records imply is_paid = True. Address-based fallback matching when invoice numbers aren't available.
VAWSβœ… ActiveXLSXUPS invoices (VAWS_Invoices_*.xlsx auto-detected). Separate VAWS_Orders_*.xlsx for CPW backlog tracking (not auto-detected).
MercadoπŸ”² StubTBDAwaiting sample file.
OracleπŸ”² StubTBDAwaiting sample file. Compound invoice numbers (>10 digits) surface as exceptions (D4).
Next priority: Mercado and Oracle

Corrigo, VendorCafe, and VAWS are now active. The remaining gaps are Mercado and Oracle β€” both need a sample export file before the adapter can be finalized. Request exports from the AR team to complete coverage.

Adding a New Portal​

Each portal needs a single adapter file, a registry entry, and a filename detection rule. The whole process takes about 30 minutes once you have a sample file.

Step 1: Get a Sample File​

Download a representative export from the portal. Note:

  • File format (.xlsx or .csv)
  • Whether it has multiple sheets
  • Whether it is invoice-level or line-level data

Step 2: Map Columns to NormalizedInvoice​

Open the sample file and find the columns that map to these fields:

NormalizedInvoice fieldWhat to look for
invoice_numberInvoice # or ID field (may need normalization)
po_numberPO Number, Work Order #, or reference field
customerCustomer or client name (if present)
invoice_dateInvoice date
amountTotal amount (not line-item amount)
statusInvoice status (Approved, Paid, Voided…)
is_paidWhether the invoice has been paid
address, city, state, zip_codeService location (if available)

Step 3: Implement the Adapter​

Create portal_reconciliation/adapters/<portal_name>.py:

from portal_reconciliation.adapters.base import BaseAdapter
from portal_reconciliation.models import NormalizedInvoice
from portal_reconciliation.normalizer import normalize_invoice_number

class MyPortalAdapter(BaseAdapter):

@property
def portal_name(self) -> str:
return "my_portal"

def load(self, file_path) -> list[NormalizedInvoice]:
# Read the file, return list of NormalizedInvoice
...

See portal_reconciliation/adapters/ariba.py or coupa.py for complete examples.

Step 4: Register the Adapter​

In portal_reconciliation/adapters/__init__.py, add two entries:

# 1. Import the class
from portal_reconciliation.adapters.my_portal import MyPortalAdapter

# 2. Add to ADAPTERS dict
ADAPTERS = {
...
"my_portal": MyPortalAdapter,
}

# 3. Add to _DETECTION_RULES
_DETECTION_RULES = [
...
("my_portal", ["my_portal", "myportal", "my-portal"]),
]

Step 5: Add Tests​

Create tests/adapters/test_my_portal.py with a fixture from the sample file under tests/fixtures/.

Step 6: Run and Validate​

uv run python reconcile.py --portal-dir specs/sample-data/ --portals my_portal
uv run pytest tests/adapters/test_my_portal.py -v

Use the Claude Code Command​

There is also a /add-portal command in the project that walks through this process interactively:

/add-portal