WFDASH-WEBAPP/docs/api-reference/quote-create-and-customer-lookup.md

180 lines
5.9 KiB
Markdown
Raw Permalink Normal View History

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