A seat roster is a poor primary key for an invoice archive. In a design scenario, a team adds seats after an annual renewal, then finance asks for the period's documents. Building the expected invoice list from today's users can lose the renewal, the adjustment, or the distinction between billing groups.
I work on BrowserAct. Our Figma billing guide made a narrower implementation question useful: when two observations appear to describe one invoice, what should an index merge, and what should it preserve for review? The example below is application-side Python with local synthetic checks. It is not a Figma API integration, account download, PDF parser, or tested Figma Bot.
Start from issued documents and billing scope
The Figma source guide, based on documentation checked November 9, separates team or organization billing from Enterprise billing groups. It describes selecting an invoice and downloading its PDF. The invoice-number link can open the corresponding Stripe view; it does not establish that all plans share one identical external interface.
Renewals, additional seats and AI credits can produce different billing records. That is a reason to discover the actual invoice history, not a formula for generating invoice IDs or predicting one document per month. Keep the reviewed history scope and the collection interval beside the register, without claiming that the current member list proves historical completeness.
Use an internal scope reference that resolves to the relevant team, organization and, where applicable, billing group. The code assumes an upstream adapter has already resolved that reference. It does not discover the hierarchy or prove the operator has billing permission.
Separate identity, observation and disagreement
An invoice candidate has a composite identity: scope_ref plus invoice_number. Neither a filename nor a display name belongs in that key. We do not assume invoice numbers are globally unique across unrelated billing scopes. Preserve them exactly; this example does not lowercase, trim or otherwise rewrite identifiers.
An observation has its own reference and evidence reference. Seeing the same candidate twice should retain two observations, even when the amount and currency agree. However, replaying an observation reference is a different condition: the batch needs review, so a conflicting replay cannot silently disappear into the first record.
Different amounts or currencies under one composite key are conflicts. Do not implement last-write-wins or assume the later page is a corrected invoice. Preserve both upstream evidence records and have an authorized reviewer decide whether the scope, extraction, document version or association is wrong.
from decimal import Decimal, InvalidOperation def build_invoice_index(rows): groups, rejected, observations = {}, [], set() required = ("scope_ref", "invoice_number", "currency", "amount", "observation_ref", "evidence_ref") for row in rows: if any(not isinstance(row.get(k), str) or not row[k].strip() for k in required): rejected.append("missing_or_untyped_field") continue observation = row["observation_ref"] if observation in observations: rejected.append("duplicate_observation_ref") continue observations.add(observation) currency = row["currency"] if len(currency) != 3 or any(c not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" for c in currency): rejected.append("invalid_currency_code") continue try: amount = Decimal(row["amount"]) if not amount.is_finite(): raise InvalidOperation except InvalidOperation: rejected.append("invalid_amount") continue key = (row["scope_ref"], row["invoice_number"]) entry = groups.setdefault(key, {"variants": set(), "observations": []}) entry["variants"].add((currency, amount)) entry["observations"].append((observation, row["evidence_ref"])) result = {} for key, entry in groups.items(): result[key] = { "status": "conflict_review" if len(entry["variants"]) > 1 else "candidate_for_document_check", "observations": entry["observations"], "variant_count": len(entry["variants"]), } return {"invoices": result, "rejected": rejected, "batch_needs_review": bool(rejected) or any( v["status"] == "conflict_review" for v in result.values())} The return value intentionally contains no grand total. Currency syntax is checked, but three uppercase letters do not prove a valid or applicable currency. Decimal values avoid binary-float comparison, and equivalent decimal spellings compare equally. This code does not impose two decimal places, round values, convert currencies or infer tax treatment.
A negative amount remains a candidate value. Its sign alone does not identify a refund, credit note, payment or replacement invoice. Those classifications need separate reviewed document metadata. Amount and currency agreement is also only a consistency check: two observations can agree and still describe the wrong document.
Route each ambiguity to a named action
| Input situation | Index behavior | Required follow-up |
|---|---|---|
| Same scope and invoice, matching values | One candidate, both observations retained | Verify the selected original document |
| Same invoice number, different scopes | Separate candidates | Review each billing scope independently |
| Same key, different amount | conflict_review | Inspect both evidence records |
| Same key, different currency | conflict_review | Resolve association or extraction |
| Replayed observation reference | Batch requires review | Resolve replay before accepting the batch |
| Missing identifiers, invalid amount or code syntax | Rejected row, batch requires review | Repair from source evidence |
A candidate_for_document_check result does not mean the PDF exists, is authentic, or has been saved. Rejected rows are not proof of missing invoices. A production caller must respect batch_needs_review and retain the original input plus row-level failure provenance; this compact function returns reason codes only.
Keep document delivery outside this index
The local synthetic checks exercise identity collisions across scopes, repeated observations, conflicting values, equivalent decimal spellings, negative values, non-finite numbers, missing evidence, malformed currency syntax and a duplicate observation reference. They use fictional records and perform no network or file operations.
After this stage, a separate worker must match the retained candidate to the authorized invoice view, download and inspect the original document, and record its artifact receipt. If a linked Stripe view is used, establish its relationship to the selected Figma invoice; do not substitute an unrelated processor identifier without an explicit, reviewed mapping.
Persist observations append-only, and commit an accepted candidate version with concurrency control. This pure in-memory reducer cannot serialize two workers or enforce database uniqueness. It also cannot establish that every page of billing history was inspected. Capture that discovery evidence separately before calling a collection complete.
For BrowserAct or another browser-assisted collector, this is a boundary to test around the native administrative route. It requires a separately validated Figma adapter and authorized access. The register should explain which documents were observed and which disagreements remain, while membership changes stay contextual evidence rather than becoming an invented invoice schedule.
