WFDASH-WEBAPP/docs/api-reference/quote-create-and-customer-lookup.md
Dave cfd3e3e6be feat(quotes): add person_hint for customer resolution in forwarded emails
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>
2026-08-27 11:11:44 +02:00

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), 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

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:

// 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.

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.