Portal Coverage & Adding Portals

Current Statusβ
| Portal | Status | Format | Notes |
|---|---|---|---|
| FM Pilot | β Active | CSV | WO/PO report, not invoice report. Vendor Invoice Number links to CieTrade. Unbilled WOs are the primary output. |
| Ariba | β Active | Excel (.xlsx) | Standard invoice report. Invoice # normalized before matching. |
| Coupa | β Active | Excel (.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 | β Active | XLSX/CSV | JLL 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 | β Active | XLSX/CSV | Payment history format β all records imply is_paid = True. Address-based fallback matching when invoice numbers aren't available. |
| VAWS | β Active | XLSX | UPS invoices (VAWS_Invoices_*.xlsx auto-detected). Separate VAWS_Orders_*.xlsx for CPW backlog tracking (not auto-detected). |
| Mercado | π² Stub | TBD | Awaiting sample file. |
| Oracle | π² Stub | TBD | Awaiting sample file. Compound invoice numbers (>10 digits) surface as exceptions (D4). |
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 (
.xlsxor.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 field | What to look for |
|---|---|
invoice_number | Invoice # or ID field (may need normalization) |
po_number | PO Number, Work Order #, or reference field |
customer | Customer or client name (if present) |
invoice_date | Invoice date |
amount | Total amount (not line-item amount) |
status | Invoice status (Approved, Paid, Voidedβ¦) |
is_paid | Whether the invoice has been paid |
address, city, state, zip_code | Service 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