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>
5.9 KiB
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), anditems[]. 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_numberis 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 requiressubject+ parsed body and silently rewritesemail_message_idto asynth-…@wfdash.localsentinel. - 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. Thesearchparam doesn't search across thecompanyrelation. Two workarounds:- Pass
expand: "company"on the list call, then filter the returneditems[]client-side by company name. - First
companies.list({search: "<name>"})to get the company id, then iterate the matching customers client-side. (Thecustomers.listsearchfilter does not accept a company relation id today.)
- Pass
-
No dedicated
customers.lookupendpoint —customers.listwith a smallperPage(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:
// 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/phonefrom 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.