Introduces the `person_hint` field to the `EmailIntakePayload`, allowing the system to resolve the actual customer when a rep forwards an email. The `findOrCreateCustomer` function now prioritizes `person_hint` over the sender's email, enabling a more accurate attribution of quotes. This change includes a case-insensitive substring match with a token-drop fallback for multi-token names, ensuring better matching against existing customer records. Additionally, new unit tests have been added to validate this functionality, covering various scenarios including token-drop behavior and ensuring the correct customer is linked when a rep forwards an RFQ. Co-Authored-By: Claude <noreply@anthropic.com>
179 lines
5.9 KiB
Markdown
179 lines
5.9 KiB
Markdown
# WFDASH API — Quote Create + Customer Lookup Reference
|
|
|
|
> What the WFDASH REST/MCP API actually exposes today for the two
|
|
> tasks you keep hitting during intake: creating a quote, and
|
|
> finding an existing customer.
|
|
|
|
---
|
|
|
|
## Quote create — three flavours
|
|
|
|
There are three different quote-creation endpoints. Pick by **where
|
|
the data came from**, not by what feels closest:
|
|
|
|
### 1. `quotes.create` (walk-in) — **the one for your intake**
|
|
|
|
```
|
|
POST /api/v1/quotes/create
|
|
```
|
|
|
|
- Use when: there's no inbound RFQ email. Operator types it in, you
|
|
extracted items from a PDF / phone-call / WhatsApp, or you're
|
|
driving the quote from the in-store counter PWA.
|
|
- Body has `customer` (id OR name), `company` (id OR name), and
|
|
`items[]`. **Names are resolved case-insensitive exact** — if
|
|
there's no match, a new customer / company row is created with
|
|
empty defaults. So passing a name is safe; passing the wrong name
|
|
silently creates a duplicate.
|
|
- Returns `{ quote, customer, company, items, quote_number }` on 201.
|
|
- Status default is `"new"`. `quote_number` is auto-generated unless
|
|
you supply one (operator-supplied conflicts return 409).
|
|
- MCP wrapper: `mcp__wfdash-mcp__quotes__create`. Same body shape.
|
|
|
|
### 2. `quotes.ingest_from_email` (inbound RFQ email)
|
|
|
|
```
|
|
POST /api/v1/quotes/ingest-from-email
|
|
```
|
|
|
|
- Use when: the customer actually emailed in. Returns immediately
|
|
with `ai_status="pending"`; the background processor runs Claude
|
|
against the body + attachments to extract items. Don't extract
|
|
items yourself — let the pipeline do it.
|
|
- MCP wrapper: `mcp__wfdash-mcp__quotes__ingest_from_email`.
|
|
|
|
### 3. `quotes.create_from_email` (legacy, **avoid**)
|
|
|
|
```
|
|
POST /api/v1/quotes/create-from-email
|
|
```
|
|
|
|
- Caller passes a pre-extracted `extracted_items[]`. Only use this if
|
|
you're calling on behalf of a parser that already did the
|
|
extraction. **Wrong tool if you don't have an inbound email** —
|
|
it requires `subject` + parsed body and silently rewrites
|
|
`email_message_id` to a `synth-…@wfdash.local` sentinel.
|
|
- MCP wrapper: `mcp__wfdash-mcp__quotes__create_from_email`.
|
|
|
|
---
|
|
|
|
## Customer lookup — fuzzy search by name / company
|
|
|
|
### `customers.list` — substring search
|
|
|
|
```
|
|
GET /api/v1/customers
|
|
```
|
|
|
|
Query params:
|
|
|
|
| Param | Type | What it does |
|
|
|-----------|------|--------------|
|
|
| `search` | string | Substring match on **customer name / email / phone**, OR'd together; also matches **rep name / rep email** (the user that owns this customer). One query covers all four fields. Case-sensitive PB `~` operator. |
|
|
| `rep` | string | Exact filter on the rep relation. Accepts a 15-char PB user id OR a user name / email (case-insensitive). |
|
|
| `page` | number | 1-based, default 1 |
|
|
| `perPage` | number | default 50, hard cap 100 |
|
|
| `sort` | string | PB sort expression, default `-created` |
|
|
| `expand` | string | Comma-separated, e.g. `"company,rep"` to inline relations |
|
|
|
|
Returns `{ items, totalItems, totalPages, page, perPage }`.
|
|
|
|
MCP wrapper: `mcp__wfdash-mcp__customers__list`. Same params.
|
|
|
|
### `customers.get` — fetch by id
|
|
|
|
```
|
|
GET /api/v1/customers/{id}
|
|
```
|
|
|
|
For when you already have the PB id and just need the full row +
|
|
relations.
|
|
|
|
MCP wrapper: `mcp__wfdash-mcp__customers__get`.
|
|
|
|
### What we DO NOT have today
|
|
|
|
- **No direct "filter customers by company name"** on `customers.list`.
|
|
The `search` param doesn't search across the `company` relation.
|
|
Two workarounds:
|
|
1. Pass `expand: "company"` on the list call, then filter the
|
|
returned `items[]` client-side by company name.
|
|
2. First `companies.list({search: "<name>"})` to get the company
|
|
id, then iterate the matching customers client-side. (The
|
|
`customers.list` `search` filter does not accept a company
|
|
relation id today.)
|
|
|
|
- **No dedicated `customers.lookup` endpoint** — `customers.list`
|
|
with a small `perPage` (e.g. 5) is the closest equivalent.
|
|
|
|
- **No fuzzy / Levenshtein matching.** It's PB's literal substring
|
|
`~` operator. "AB" won't match "ABC Trading" if you typed it
|
|
wrong-case, and partial-token matches like "Bear" against "Bearings
|
|
Co" will work — but "Bearings" against "Bear" works too (substring
|
|
is bidirectional). Typos are not forgiven.
|
|
|
|
---
|
|
|
|
## The intake pattern that works
|
|
|
|
Given what we have, the cleanest customer-resolution flow at intake
|
|
is:
|
|
|
|
```ts
|
|
// 1. Try the most specific lookup first.
|
|
const hit = await customers.list({
|
|
search: customerName,
|
|
expand: "company",
|
|
perPage: 5,
|
|
});
|
|
|
|
// 2. If the caller ALSO gave a company name, narrow client-side.
|
|
const filtered = companyName
|
|
? hit.items.filter(c => c.expand?.company?.company === companyName)
|
|
: hit.items;
|
|
|
|
// 3. If 0 hits → create via customers.create (auto-creates company
|
|
// if company name doesn't resolve either). Use quotes.create with
|
|
// the names directly — it has the same auto-create behaviour, so
|
|
// you don't need a separate customers.create call unless you want
|
|
// to backfill email/phone/etc.
|
|
```
|
|
|
|
For quote creation specifically, **just pass `customer` and `company`
|
|
as names to `quotes.create`** — the service layer resolves them by
|
|
case-insensitive exact match and creates missing rows. You only need
|
|
the lookup step when:
|
|
|
|
- You're filling in `email` / `phone` from a contact form.
|
|
- You want to verify the customer exists before showing the operator
|
|
a "is this the right John Smith?" confirmation.
|
|
- You're merging two customer records.
|
|
|
|
---
|
|
|
|
## Companies (related, since you mentioned name + company)
|
|
|
|
### `companies.list` — substring search
|
|
|
|
```
|
|
GET /api/v1/companies
|
|
```
|
|
|
|
Query params: `page`, `perPage`, `search` (substring on
|
|
`company.company` / address / vendor), `sort`. Same shape as
|
|
`customers.list`.
|
|
|
|
MCP wrapper: `mcp__wfdash-mcp__companies__list`.
|
|
|
|
### `companies.get` — fetch by id
|
|
|
|
```
|
|
GET /api/v1/companies/{id}
|
|
```
|
|
|
|
MCP wrapper: `mcp__wfdash-mcp__companies__get`.
|
|
|
|
### `companies.create` / `companies.update`
|
|
|
|
Free-text company name, optional address / vendor / coordinates.
|
|
Same auto-resolve-and-create-on-no-match behaviour as customers.
|