# Contacts, customers and suppliers

> Xero says one. QuickBooks says two, with no link between them. NetSuite says two, related through a shared entity abstraction. This is the first modelling decision an integration has to make, and it is irreversible in practice.

Source: https://lopifi.com/apis/contacts
Part of: Lopifi — US GAAP books on autopilot, reviewed by a CPA (https://lopifi.com)

---

**Is a customer who is also a supplier one record or two?** Never auto-merge a customer and a supplier on name. Merge on an explicit user action or a strong identifier, and keep both source ids on the merged party.

## Divergences between Xero, QuickBooks Online and NetSuite

### The same real party is two records in QuickBooks

A client who is also a subcontractor is one Contact in Xero and two unlinked records in QBO, each with its own id and balance. DisplayName is unique across customers, vendors and employees, so the two cannot even share a name — the vendor is typically 'Acme Ltd (V)'. Nothing in the API says they are the same party, so any total-exposure figure has to reunite them.

**Handling:** Model a Party with role flags and a list of source refs. Merge is an explicit action showing both balances; auto-merge on a matching tax identifier, never on a name.

### Xero's role flags are read-only and historical

IsCustomer and IsSupplier are not settable. They become true when the contact first appears on a sales or purchase document. A brand-new contact you intend to bill is neither, until it is.

**Handling:** Treat roles as a hint for filtering, never as a constraint on what a user may do. A contact picker that hides suppliers from an invoice form will hide the new contact the user just created.

### Sub-customers are jobs, projects and departments all at once

QBO users routinely encode project structure in the customer hierarchy, because there is nowhere else to put it. The same tree is simultaneously a list of parties and a list of engagements, and BillWithParent decides which level carries the receivable.

**Handling:** Import the hierarchy, and expose it as both a party tree and a dimension. Do not flatten it — the parent-level balance and the child-level balance are both real numbers people reconcile.

## Canonical shape

```ts
type Party = {
  id: string;
  name: string;
  roles: ("customer" | "supplier")[];   // Xero: one record, both roles. QBO: merged, or not.
  parentId?: string;                     // QBO jobs, NetSuite hierarchy; undefined on Xero
  taxNumber?: string;
  defaultCurrency?: string;
  defaultAccountIds?: { sales?: string; purchases?: string };
  addresses: Address[];                  // normalised from three different shapes
  active: boolean;
  /** Plural. A merged party carries the Customer id and the Vendor id. */
  sources: SourceRef[];
};
```
