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>
4.9 KiB
WFDASH API — Create Order (POST /api/v1/orders/create)
One payload, one call. The full order header + items array goes in a single request body. There is no
/api/v1/ordersPOST —/ordersis GET-only.
Endpoint
POST /api/v1/orders/create
Bearer-token auth via AGENT_API_KEY header. Hermes drops
AGENT_API_KEY on direct REST calls — use the MCP wrapper
mcp__wfdash-mcp__orders__create with the same body shape.
runtime = "nodejs" (PocketBase admin auth requires Node).
Request body
{
"customer_id": "<15-char PB id>", // required
"company_id": "<15-char PB id>", // required
"notes": "string?", // order-level notes
"contact_name":"string?", // operator-readable contact
"description": "string?", // short description of the order
"email_message_id": "string?", // dedup key
"rep": "string?", // sales rep free-text
"items": [
{
"description": "string", // required, trimmed
"quantity": 1, // default 1; must be > 0 when supplied
"cost_price": 123.45,
"markup": 25,
"selling_price": 154.31, // optional
"sku": "string?",
"unit": "EA",
"supplier_id": "<15-char PB id?>",
"notes": "string?"
}
]
}
Field rules
customer_id,company_id— must resolve in PB. Free-text names are not accepted on this endpoint (use the MCPorders.createwrapper if you need name-resolution; the REST handler requires ids).items[]— non-empty, every item needsdescription.selling_priceauto-derive — whencost_priceandmarkupare both supplied andselling_priceis omitted, it auto-derives ascost_price * (1 + markup / 100)rounded to 2 dp. Passselling_priceexplicitly to override.email_message_id— optional idempotency key. If an order with the sameemail_message_idalready exists, the response returns the existing record withduplicate: trueand HTTP 200 instead of 201. Safe to retry.- Per-row independence — each item creates one
order_itemrow withitem_status = "pending"anddrv_status = "unassigned". One item's secondary-field patch failure does not abort the rest of the call; failures land initems_failed[]with the partial-success shape.
Response
201 — created
{
"order": { /* full PB order record */ },
"order_items_collection": "<relation-id>",
"items_created": [ { "id": "...", "description": "..." } ],
"items_failed": []
}
200 — duplicate (email_message_id matched)
Same shape as 201, plus "duplicate": true. Nothing new was written.
400 — validation failure
{ "error": "customer_id is required" }
{ "error": "items must be a non-empty array" }
404 — not found
{ "error": "Requested resource not found..." }
customer_id or company_id doesn't resolve in PB.
500 — server / PB write failure
{ "error": "<PocketBase error message>" }
Common workflows
Walk-in / counter sale
No upstream quote. Customer walks up, picks items, you write the order straight from the till:
curl -X POST https://wfdash.example/api/v1/orders/create \
-H "Authorization: Bearer $AGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "<customer-id>",
"company_id": "<company-id>",
"notes": "Counter sale — 2026-08-27",
"items": [
{ "description": "SKF 6205 bearing", "quantity": 4,
"cost_price": 85, "markup": 30 }
]
}'
Re-create from a quote
Don't use this endpoint — use
POST /api/v1/orders/from-quote (MCP: orders.create_from_quote).
It clones the matched quote into a fresh ORD-YYYY-MM-NNN order,
inheriting customer / company / rep / letterhead / line items.
Append to an existing order
There is a separate endpoint:
POST /api/v1/orders/{id}/items (MCP: order_items.create). But it
refuses with 409 order_locked the moment any line on the order
is past pending. Use it only on fresh orders — for any order that
has a PO in flight, you must orders.split first.
Pitfalls
- Passing the order header without
items[]→ 400. The handler does not create empty orders. - Passing
quantity: 0or negative → rejected per item by the service layer; the row lands initems_failed[]withvalidation_error. - Forgetting to send
AGENT_API_KEY→ 401 fromassertAgentAuth. From Hermes, usemcp__wfdash-mcp__orders__createinstead — the MCP layer injects the key. - Reusing
email_message_idfor genuinely different orders → the second call returns the first order withduplicate: true. Don't reuse the key; generate a new one per logical order.