528 lines
22 KiB
Markdown
528 lines
22 KiB
Markdown
|
|
# Quote Operator — SOP
|
|||
|
|
|
|||
|
|
**Owns:** the full quote pipeline — inbound RFQ intake, item-level
|
|||
|
|
pricing (sprint or manual), approval submission, sending, and
|
|||
|
|
follow-up. The sprint queue is one part of the pricing piece; the
|
|||
|
|
operator owns the whole pipeline from "new quote" to "customer
|
|||
|
|
replied".
|
|||
|
|
|
|||
|
|
**Tools:** `/quotes`, `/quotes/[id]`, `/sprint`, `/quotes/approvals`,
|
|||
|
|
`/quotes/email-monitor`, dashboard widgets.
|
|||
|
|
|
|||
|
|
**Entry points** (where a quote can come from):
|
|||
|
|
- **Inbound email** (n8n ingests RFQ → quote lands in `new`)
|
|||
|
|
- **Walk-in** (operator types "make me a quote for John" — no email)
|
|||
|
|
- **Template** (re-using a previous quote as a scaffold)
|
|||
|
|
- **Sprint** (item-level pricing queue — items from the above that
|
|||
|
|
need a price from you)
|
|||
|
|
|
|||
|
|
**Handoffs:**
|
|||
|
|
- **From** Email Intake (n8n) → quote lands in `new` status.
|
|||
|
|
- **To Approver** → when status flips to `approval_pending`.
|
|||
|
|
- **To Order Operator** → when customer accepts (quote becomes an order).
|
|||
|
|
- **To Customer** → outbound email + inbound reply tracking.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Daily / Weekly Checklist
|
|||
|
|
|
|||
|
|
### Every working day (start of shift, 08:30 SAST)
|
|||
|
|
|
|||
|
|
- [ ] **Open `/quotes?tab=new`** — every quote that came in via
|
|||
|
|
email overnight must have an owner. Either claim it (you'll
|
|||
|
|
work it), or pass to a colleague. Quotes older than 1 working
|
|||
|
|
day in `new` are a process bug.
|
|||
|
|
- [ ] **Open `/sprint`** — clear the "UNCLAIMED" filter. Look for
|
|||
|
|
items you can claim in your strongest categories first. (If
|
|||
|
|
the inbound-email intake has already pre-priced most items,
|
|||
|
|
the sprint queue may be small today — that's the goal.)
|
|||
|
|
- [ ] **Open `/quotes/email-monitor`** — scan for any failed
|
|||
|
|
sends from the last 24h. If any, **retry or escalate** (see
|
|||
|
|
Decision Tree §3.5).
|
|||
|
|
- [ ] **Open `/quotes?tab=claimed`** — pick up any of your items
|
|||
|
|
whose 30-min claim is about to expire.
|
|||
|
|
- [ ] **Open dashboard → "Follow-ups Due"** — every quote you own
|
|||
|
|
with a `follow_up_due` date of today or earlier must get a
|
|||
|
|
follow-up action today (call, email, or set a new
|
|||
|
|
`follow_up_due`).
|
|||
|
|
|
|||
|
|
### Every working day (end of shift, 17:00 SAST)
|
|||
|
|
|
|||
|
|
- [ ] **Release any sprint items you can't finish** — the 30-min claim
|
|||
|
|
expires automatically, but it's polite to leave a `pricing_notes`
|
|||
|
|
breadcrumb on what's been tried.
|
|||
|
|
- [ ] **Confirm every quote you sent today** has moved out of `sending`
|
|||
|
|
— if any are stuck in `sending` for >5 min, see §3.5.
|
|||
|
|
- [ ] **Set `follow_up_due`** for every quote you sent without a reply
|
|||
|
|
expected inside 5 working days. Default: today + 5 working days.
|
|||
|
|
|
|||
|
|
### Every Monday morning
|
|||
|
|
|
|||
|
|
- [ ] **Review your KPIs** (see §4). Look for any that are trending
|
|||
|
|
down vs last week. Note them in `#quote-ops` with a one-line
|
|||
|
|
reason.
|
|||
|
|
- [ ] **Clear the `/quotes/approvals` queue** if you have items there
|
|||
|
|
from Friday. Approvals older than 1 working day are a process bug —
|
|||
|
|
surface to the approver.
|
|||
|
|
|
|||
|
|
### Every Friday afternoon
|
|||
|
|
|
|||
|
|
- [ ] **Close out the week** — every quote in `claim` by you with no
|
|||
|
|
activity for 3+ days must either be priced, sent, or `problem`-ed.
|
|||
|
|
Don't leave zombies for Monday.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Step-by-Step Procedures
|
|||
|
|
|
|||
|
|
The procedures are ordered by the quote lifecycle:
|
|||
|
|
|
|||
|
|
| # | Procedure | Entry point |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 2.1 | Process an inbound email quote | RFQ email arrives via n8n |
|
|||
|
|
| 2.2 | Create a walk-in quote | Phone / face-to-face / WhatsApp |
|
|||
|
|
| 2.3 | Create a quote from template | Repeat customer, similar items |
|
|||
|
|
| 2.4 | Claim a sprint item | Unpriced item from §2.1, §2.2, or §2.3 |
|
|||
|
|
| 2.5 | Price a line item | Inside the line editor |
|
|||
|
|
| 2.6 | Submit a quote for approval | Every line priced |
|
|||
|
|
| 2.7 | Send the quote to the customer | Approver has approved |
|
|||
|
|
| 2.8 | Follow up on a sent quote | `follow_up_due` reached |
|
|||
|
|
| 2.9 | Partial approval / split a quote | Customer accepts some, rejects others |
|
|||
|
|
|
|||
|
|
### 2.1 Process an inbound email quote
|
|||
|
|
|
|||
|
|
**When:** a customer sends an RFQ email. n8n ingests it, the AI
|
|||
|
|
extracts items, and a new quote appears in `/quotes?tab=new` with
|
|||
|
|
status `new`. You'll see a `📨` icon on the dashboard's "New
|
|||
|
|
Quotes" widget.
|
|||
|
|
|
|||
|
|
1. **Open `/quotes?tab=new`.** Find the new quote.
|
|||
|
|
2. **Sanity-check the AI extraction** (open the quote detail):
|
|||
|
|
- **Customer** — did the AI match it to a known customer? If
|
|||
|
|
it landed on `Unknown Sender`, see Decision Tree §3.7.
|
|||
|
|
- **Company** — same check. Wrong company = wrong letterhead
|
|||
|
|
on the PDF.
|
|||
|
|
- **Items** — are the descriptions readable? Are the
|
|||
|
|
quantities right? If a line is missing or wrong, edit it.
|
|||
|
|
- **Attachments** — is the customer's original RFQ attached?
|
|||
|
|
The buyer needs to be able to compare our quote against
|
|||
|
|
their ask.
|
|||
|
|
3. **Check `pricing_source` on each line:**
|
|||
|
|
- `ai` or `catalogue` → already priced. Verify the price
|
|||
|
|
against the AI's reasoning (if shown). If sane, move on.
|
|||
|
|
- `manual` or empty → unpriced. These lines will land in the
|
|||
|
|
sprint queue (see §2.4).
|
|||
|
|
4. **Claim the quote** by clicking **Claim** on the header (or
|
|||
|
|
it'll auto-claim on first edit). Status flips to `claimed`.
|
|||
|
|
5. **Triage unpriced lines:** if the sprint queue has unpriced
|
|||
|
|
lines from this quote, claim them in §2.4.
|
|||
|
|
6. **If everything is priced**, jump to §2.6 (submit for
|
|||
|
|
approval).
|
|||
|
|
|
|||
|
|
**Don't:**
|
|||
|
|
- Don't submit a quote with `Unknown Sender` — the customer
|
|||
|
|
needs to exist first (see §3.7).
|
|||
|
|
- Don't edit the AI's prices without a `pricing_notes`
|
|||
|
|
breadcrumb explaining why.
|
|||
|
|
|
|||
|
|
### 2.2 Create a walk-in quote
|
|||
|
|
|
|||
|
|
**When:** a customer calls / walks in / WhatsApps with an RFQ
|
|||
|
|
that didn't come through email. No inbound message to extract
|
|||
|
|
from — you build the quote from scratch.
|
|||
|
|
|
|||
|
|
1. **Open `/quotes/new` (or click "+ New Quote" on the quotes
|
|||
|
|
list).**
|
|||
|
|
2. **Pick or create the customer** in the customer picker.
|
|||
|
|
- If they exist, pick them. The letterhead, rep, and email
|
|||
|
|
template default from the customer record.
|
|||
|
|
- If they don't exist, type the name. The system creates a
|
|||
|
|
draft customer row with that name — the supervisor
|
|||
|
|
completes the rest of the fields later.
|
|||
|
|
3. **Pick the company** (which letterhead). CNL vs ISHERWOOD
|
|||
|
|
etc — match what the customer is used to.
|
|||
|
|
4. **Set the validity** (default 30 days).
|
|||
|
|
5. **Add the description** — the free-text subject of the RFQ
|
|||
|
|
(e.g. "M16 hex bolts, grade 8.8, zinc, 2000 units"). Goes
|
|||
|
|
on the PDF header.
|
|||
|
|
6. **Add items** in the line editor:
|
|||
|
|
- One row per SKU / spec combo the customer asked for.
|
|||
|
|
- Each row needs at minimum `description`. Add
|
|||
|
|
`quantity` and `unit`.
|
|||
|
|
- If you know the price (e.g. the customer named a SKU that
|
|||
|
|
is in the catalogue), set `cost_price` and `markup` —
|
|||
|
|
`selling_price` is auto-derived.
|
|||
|
|
- If you don't know the price, leave it blank and the lines
|
|||
|
|
will land in the sprint queue (§2.4).
|
|||
|
|
7. **Save.** The quote lands in `new` status.
|
|||
|
|
8. **If items are unpriced**, go to §2.4. If everything is
|
|||
|
|
priced, go to §2.6.
|
|||
|
|
|
|||
|
|
**Don't:**
|
|||
|
|
- Don't pick a company / letterhead you don't recognise —
|
|||
|
|
ask the supervisor.
|
|||
|
|
- Don't save with `quantity = 0` — that's a placeholder, not
|
|||
|
|
an order. If the customer is uncertain about qty, write
|
|||
|
|
"TBD" in the description and price the line anyway, or
|
|||
|
|
mark the line as "Re-quote later" (split it out — §2.9).
|
|||
|
|
|
|||
|
|
### 2.3 Create a quote from a template
|
|||
|
|
|
|||
|
|
**When:** a customer asks for something we've quoted before with
|
|||
|
|
the same line items / similar scope. Re-using a template saves
|
|||
|
|
the line-item re-entry.
|
|||
|
|
|
|||
|
|
1. **Open `/quotes?tab=templates`.** Pick the template that
|
|||
|
|
matches.
|
|||
|
|
2. Click **Create from Template**. A new quote is spawned with:
|
|||
|
|
- Same customer, company, letterhead.
|
|||
|
|
- Same line items (descriptions + quantities).
|
|||
|
|
- Status: `new`.
|
|||
|
|
- Prices reset (templates don't carry live pricing — you
|
|||
|
|
re-price from the current catalogue).
|
|||
|
|
3. **Update `description`** to reflect the new RFQ (date,
|
|||
|
|
customer reference, anything that's changed since the
|
|||
|
|
template was made).
|
|||
|
|
4. **Update quantities** if the customer wants different
|
|||
|
|
amounts.
|
|||
|
|
5. **Re-price** the lines from the current catalogue (or leave
|
|||
|
|
them for sprint if the SKU / supplier has changed).
|
|||
|
|
6. **Continue from §2.6** (or §2.4 if items are unpriced).
|
|||
|
|
|
|||
|
|
**Don't:**
|
|||
|
|
- Don't send a template quote without verifying the prices
|
|||
|
|
against today's catalogue. Supplier cost changes, and
|
|||
|
|
template-stored prices go stale fast.
|
|||
|
|
- Don't re-use a template for a new customer without
|
|||
|
|
confirming the markup policy applies. Strategic accounts
|
|||
|
|
may have different markups.
|
|||
|
|
|
|||
|
|
### 2.4 Claim a sprint item
|
|||
|
|
|
|||
|
|
**When:** an item lands in the sprint queue and you have capacity.
|
|||
|
|
|
|||
|
|
1. Open `/sprint`. The "UNCLAIMED" tab shows available items.
|
|||
|
|
2. **Prefer items you can resolve in <30 min** — known SKUs, items
|
|||
|
|
with attachments, or items in your strongest category.
|
|||
|
|
3. Click **Claim**. The 30-min TTL starts. The item moves to your
|
|||
|
|
`Claimed` filter.
|
|||
|
|
4. **Within 30 min:** price the item (see §2.5), or **release** by
|
|||
|
|
simply not saving (the claim auto-expires).
|
|||
|
|
|
|||
|
|
**Don't:**
|
|||
|
|
- Don't claim more than 3 items at once. The 30-min TTL is per item;
|
|||
|
|
the bottleneck is your attention.
|
|||
|
|
- Don't claim items you'll need a supplier call for. Sprint is for
|
|||
|
|
pricing from known data, not for sourcing.
|
|||
|
|
|
|||
|
|
### 2.5 Price a line item
|
|||
|
|
|
|||
|
|
**When:** you've claimed an item and you're at the line editor.
|
|||
|
|
|
|||
|
|
1. **Read the description.** If the AI provided a match (look for
|
|||
|
|
`matched_price_list_id` filled in), use it.
|
|||
|
|
2. **Decide pricing source:**
|
|||
|
|
- **Catalogue** (`pricing_source: "catalogue"`) — the matched
|
|||
|
|
catalogue row has a valid `cost_price` and `markup`. Use it.
|
|||
|
|
- **AI** (`pricing_source: "ai"`) — the AI agent pre-filled a
|
|||
|
|
suggested price. **Verify against the catalogue or supplier
|
|||
|
|
list** before saving. Never save an AI price blind.
|
|||
|
|
- **Manual** (`pricing_source: "manual"`) — no match. You need
|
|||
|
|
to find a price yourself (see §3.1).
|
|||
|
|
3. **Set `cost_price` and `selling_price`.** Selling price is
|
|||
|
|
auto-derived from `cost_price × (1 + markup/100)` if you set the
|
|||
|
|
markup — pass `selling_price` explicitly only when the catalogue
|
|||
|
|
has a stored selling price you want to preserve.
|
|||
|
|
4. **Set `pricing_source`** to the source you actually used.
|
|||
|
|
5. **Add a `pricing_notes` breadcrumb** (e.g. "matched JEI catalogue
|
|||
|
|
row, ZAR excl VAT, 30% markup per August 2026 policy").
|
|||
|
|
6. **Click Save.** The item drops off the sprint queue. The linked
|
|||
|
|
`quote_item` flips to `user_edited: true`.
|
|||
|
|
|
|||
|
|
**Don't:**
|
|||
|
|
- Don't save with `cost_price = 0` and `selling_price = 0`. That's
|
|||
|
|
a sign you didn't find a price — see §3.1.
|
|||
|
|
- Don't override a catalogue price without a `pricing_notes`
|
|||
|
|
reason.
|
|||
|
|
|
|||
|
|
### 2.6 Send a quote for approval
|
|||
|
|
|
|||
|
|
**When:** every line on a quote has `selling_price > 0`.
|
|||
|
|
|
|||
|
|
1. Open the quote detail (`/quotes/[id]`).
|
|||
|
|
2. Click **Submit for Approval**. The status flips to
|
|||
|
|
`approval_pending`. Idempotent — if it's already in
|
|||
|
|
`approval_pending`, nothing changes.
|
|||
|
|
3. If the submit returns `unpriced_items` with `offendingItemIds`:
|
|||
|
|
- Open each offending line.
|
|||
|
|
- Either price it (see §2.5) or move it to a "Re-quote" group
|
|||
|
|
(see §2.9).
|
|||
|
|
4. **Wait for the approver.** The quote is now in their queue at
|
|||
|
|
`/quotes/approvals`.
|
|||
|
|
|
|||
|
|
**Don't:**
|
|||
|
|
- Don't bypass submit by calling **Send Email** directly. The
|
|||
|
|
pre-send validator will reject with `invalid_status` for `new`
|
|||
|
|
quotes.
|
|||
|
|
|
|||
|
|
### 2.7 Send the quote to the customer
|
|||
|
|
|
|||
|
|
**When:** the approver has flipped the quote to `approved`.
|
|||
|
|
|
|||
|
|
1. Open the quote detail.
|
|||
|
|
2. Click **Send Email**. The email modal opens.
|
|||
|
|
3. **Verify the recipient(s):**
|
|||
|
|
- `To` — the customer's primary email.
|
|||
|
|
- `CC` — usually the customer rep (`rep` on the customer row).
|
|||
|
|
- `BCC` — internal archive address (already pre-filled).
|
|||
|
|
4. **Edit subject/body if needed.** Default subject is `Quote
|
|||
|
|
{number} — {description}`. Don't remove the quote number — the
|
|||
|
|
inbound reply tracker uses it.
|
|||
|
|
5. **Attachment picker:** the quote PDF is auto-included. Add any
|
|||
|
|
supporting docs (datasheet, authorisation letter) the customer
|
|||
|
|
asked for.
|
|||
|
|
6. Click **Send**. Status flips to `sending` → n8n takes over the
|
|||
|
|
SMTP send → callback records `emailed` or `mail_failed`.
|
|||
|
|
7. **Set `follow_up_due`** to today + 5 working days.
|
|||
|
|
|
|||
|
|
**Don't:**
|
|||
|
|
- Don't send without verifying the recipient. Wrong customer = lost
|
|||
|
|
quote.
|
|||
|
|
- Don't include internal-only files. The customer sees everything
|
|||
|
|
in the attachment picker.
|
|||
|
|
|
|||
|
|
### 2.8 Follow up on a sent quote
|
|||
|
|
|
|||
|
|
**When:** a sent quote has a `follow_up_due` date of today or earlier.
|
|||
|
|
|
|||
|
|
1. Open the quote detail. Check the **Reply** chip — green if a
|
|||
|
|
reply is logged, red if not.
|
|||
|
|
2. **If no reply:**
|
|||
|
|
- **Phone first** (faster than email). Log the call in
|
|||
|
|
`quote.notes`.
|
|||
|
|
- If no answer, send a follow-up email: keep it short, reference
|
|||
|
|
the original quote number, ask one direct question
|
|||
|
|
(e.g. "Are the JEI prices workable?").
|
|||
|
|
- Update `follow_up_due` to today + 5 working days.
|
|||
|
|
3. **If replied:** open the reply in `/quotes/email-monitor`, read
|
|||
|
|
it, then **either:**
|
|||
|
|
- **Customer accepted** → hand off to the Order Operator
|
|||
|
|
(create an order from quote). Mark the quote `approved` if not
|
|||
|
|
already.
|
|||
|
|
- **Customer wants changes** → see §3.3.
|
|||
|
|
- **Customer rejected** → flip to `rejected`. Move on.
|
|||
|
|
|
|||
|
|
### 2.9 Partial approval / split a quote
|
|||
|
|
|
|||
|
|
**When:** a customer says "yes to lines 1, 2, 5; no to 3, 4".
|
|||
|
|
|
|||
|
|
1. Open the quote.
|
|||
|
|
2. Click **Partial Approve**. For each line, mark `approved` or
|
|||
|
|
`rejected`. Add a `note` for each rejection.
|
|||
|
|
3. Click **Split Quote** (in the action bar). A new quote is
|
|||
|
|
created with the rejected lines.
|
|||
|
|
4. **Re-price the new quote** if needed (the prices inherit but
|
|||
|
|
sometimes a re-quote justifies a different markup).
|
|||
|
|
5. **Send the new quote** following §2.7.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Decision Tree
|
|||
|
|
|
|||
|
|
### 3.1 I can't find a price for an item
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Is the item in the catalogue (search /pricelist)?
|
|||
|
|
├── Yes → use the catalogue row, set pricing_source="catalogue"
|
|||
|
|
└── No
|
|||
|
|
├── Is there a recent price_list upload from the supplier?
|
|||
|
|
│ ├── Yes → open /pricelist, find the upload, check if the
|
|||
|
|
│ │ item is in the extracted list (sometimes AI misses
|
|||
|
|
│ │ lines)
|
|||
|
|
│ └── No → call the supplier
|
|||
|
|
└── Supplier not reachable / no price available
|
|||
|
|
→ Set the line to "Re-quote" group (separate it from the
|
|||
|
|
rest of the quote). Don't hold up the rest of the quote.
|
|||
|
|
→ Or, if the customer is waiting urgently, price the item
|
|||
|
|
at your best guess with a high margin (40%+) and add a
|
|||
|
|
pricing_notes disclaimer.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 The AI price looks wrong (too low / too high)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Is the catalogue price within 10% of the AI price?
|
|||
|
|
├── Yes → accept the AI price, set pricing_source="ai"
|
|||
|
|
└── No
|
|||
|
|
├── Low (< catalogue - 10%) → reject, use catalogue price,
|
|||
|
|
│ add pricing_notes "AI underpriced
|
|||
|
|
│ by X%, used catalogue per policy"
|
|||
|
|
└── High (> catalogue + 10%) → reject, use catalogue price,
|
|||
|
|
escalate to approver if the
|
|||
|
|
customer has a standing
|
|||
|
|
discount you don't know about
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 Customer wants changes to a sent quote
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Are the changes only pricing/markup?
|
|||
|
|
├── Yes → re-price the affected lines, save, regenerate the PDF,
|
|||
|
|
│ send a "Revised quote" email referencing the original
|
|||
|
|
│ number.
|
|||
|
|
└── No (changes to scope, qty, items)
|
|||
|
|
├── Customer wants fewer lines → Partial Approve (§2.9) + Split
|
|||
|
|
├── Customer wants more lines → Add the lines, re-price, send
|
|||
|
|
│ as "Revised quote"
|
|||
|
|
└── Customer wants different items entirely → Clone the quote
|
|||
|
|
as a new RFQ,
|
|||
|
|
mark old as
|
|||
|
|
`rejected`
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.4 The 30-min sprint claim is about to expire
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Have you made progress (found a price, just not saved)?
|
|||
|
|
├── Yes → save your work, even if partial. The claim can be re-claimed.
|
|||
|
|
└── No
|
|||
|
|
├── Item is too hard → release (just don't save). It returns
|
|||
|
|
│ to the pool.
|
|||
|
|
└── Item needs supplier input → add to your "needs supplier"
|
|||
|
|
personal list, release the claim.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.5 Email send failed
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
What's the failure reason (check n8n log)?
|
|||
|
|
├── SMTP / network error
|
|||
|
|
│ └── Click Retry on the quote. If it fails again, escalate to
|
|||
|
|
│ IT. The quote stays in `mail_failed` until retried.
|
|||
|
|
├── "Recipient rejected"
|
|||
|
|
│ └── Check the email address. If typo, fix and resend. If
|
|||
|
|
│ genuine reject (mailbox full, domain dead), phone the
|
|||
|
|
│ customer for a new address.
|
|||
|
|
├── "PDF render failed"
|
|||
|
|
│ └── Escalate to the Order Operator / dev. Don't retry — the
|
|||
|
|
│ PDF will fail again until the bug is fixed.
|
|||
|
|
└── Unknown
|
|||
|
|
└── Capture the full n8n log. Escalate with the log attached.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.6 Customer escalates (calls you directly to push for a discount)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Is the customer a strategic account (rep = senior staff)?
|
|||
|
|
├── Yes → call the approver before quoting anything. Don't
|
|||
|
|
│ promise a discount over the phone.
|
|||
|
|
└── No
|
|||
|
|
├── Discount < 5% → apply it. Log in pricing_notes with the
|
|||
|
|
│ reason.
|
|||
|
|
├── Discount 5-15% → apply it, but flag the approver in the
|
|||
|
|
│ quote notes.
|
|||
|
|
└── Discount > 15% → don't apply. Tell the customer it's
|
|||
|
|
above your authority, escalate to the
|
|||
|
|
approver. Don't bluff.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.7 An inbound email looks like a new RFQ but lands in `Unknown Sender`
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Is the sender a known customer (search /customers)?
|
|||
|
|
├── Yes → reassign the quote to the right customer via the
|
|||
|
|
│ customer picker on the quote detail.
|
|||
|
|
└── No
|
|||
|
|
├── Is the sender domain one we recognise (e.g. a known
|
|||
|
|
│ customer at a new branch)?
|
|||
|
|
│ └── Yes → ask the supervisor to create the customer, then
|
|||
|
|
│ reassign.
|
|||
|
|
└── No → it's a real new customer. Hand to the supervisor to
|
|||
|
|
create the customer record. Don't price the quote
|
|||
|
|
until the customer exists — the foreign key chain
|
|||
|
|
breaks otherwise.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.8 The AI extracted the quote wrong
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
What's wrong?
|
|||
|
|
├── Wrong company / letterhead
|
|||
|
|
│ └── Re-pick the company on the quote header. The PDF will
|
|||
|
|
│ re-render with the right letterhead on the next send.
|
|||
|
|
├── Wrong items (missing lines, extra lines, wrong description)
|
|||
|
|
│ ├── Missing line → add it manually, price it.
|
|||
|
|
│ ├── Extra line (AI hallucinated) → delete it. Log in
|
|||
|
|
│ │ quote.notes: "removed
|
|||
|
|
│ │ hallucinated line X".
|
|||
|
|
│ └── Wrong description → edit the description. The SKU
|
|||
|
|
│ and matched_price_list_id may
|
|||
|
|
│ need re-linking.
|
|||
|
|
├── Wrong quantities
|
|||
|
|
│ └── Cross-check the original RFQ email attachment
|
|||
|
|
│ (quote.attachments). If the customer typed qty=200
|
|||
|
|
│ and the AI read qty=20, fix it. If the AI hallucinated
|
|||
|
|
│ the qty, delete and re-add.
|
|||
|
|
└── Wrong unit (each / pack / metre)
|
|||
|
|
└── Edit per line. Note: until PB adds the `unit` column
|
|||
|
|
(see `quote-items` AGENTS.md), this is stored in the
|
|||
|
|
description, not a structured field.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.9 A walk-in customer has no account yet
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Do you have a contact name and a way to follow up?
|
|||
|
|
├── Yes (phone, email, name) → create the quote. The system
|
|||
|
|
│ creates a draft customer with the name you typed; the
|
|||
|
|
│ supervisor completes the address / VAT / contact fields
|
|||
|
|
│ later. Don't block the quote on missing customer data.
|
|||
|
|
└── No (anonymous, cash-on-delivery) → ask the supervisor
|
|||
|
|
before creating. We may need a different flow for
|
|||
|
|
cash customers (e.g. a "Cash" pseudo-customer).
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. KPIs
|
|||
|
|
|
|||
|
|
All KPIs are measured in **working days** (Mon–Fri, SAST). The
|
|||
|
|
dashboard's "Quote Pipeline" widget is the source of truth for most
|
|||
|
|
of these.
|
|||
|
|
|
|||
|
|
| KPI | Target | How it's measured |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **Sprint claim → priced turnaround** | < 30 min median | `now - claim_expires_at` for resolved items, where resolution = `pricing_source` set + `user_edited: true` |
|
|||
|
|
| **Quote first-touch turnaround** | < 2 working days | `now - created` for quotes whose status has left `new` |
|
|||
|
|
| **Quote send turnaround (priced → emailed)** | < 4 working hours after approval | `emailed_at - approved_at` |
|
|||
|
|
| **Follow-up rate** | 100% of overdue quotes actioned same day | count of quotes with `follow_up_due <= today AND last_action < today` (should be zero) |
|
|||
|
|
| **Approval rate** | > 70% of `approval_pending` quotes approved within 1 working day | `approved_at - submitted_at` |
|
|||
|
|
| **Sprint abandonment rate** | < 10% of claims released without save | `released_claims / total_claims` |
|
|||
|
|
| **Mail-failure rate** | < 2% of sent emails | `mail_failed / (mail_failed + emailed)` over rolling 30 days |
|
|||
|
|
| **AI pricing override rate** | < 15% of `pricing_source="ai"` lines changed before save | operator save of a different price on an AI-pre-priced line |
|
|||
|
|
| **Win rate** | > 35% of `emailed` quotes become orders | `orders.quote -> quotes.id` join, over rolling 90 days |
|
|||
|
|
| **Quote value captured** | tracked per rep, monthly | sum of `selling_price` on quotes in `emailed`/`approved`/`complete` status, attributed to `claimed_by` |
|
|||
|
|
|
|||
|
|
### How KPIs feed into your review
|
|||
|
|
|
|||
|
|
- **Weekly** (Monday standup): the supervisor pulls a one-pager
|
|||
|
|
showing each quote operator's KPIs. Red flags = items below
|
|||
|
|
target for 2+ weeks.
|
|||
|
|
- **Monthly** (1st of month): the supervisor writes a 1-page review
|
|||
|
|
per operator covering the four KPIs they're weakest on and
|
|||
|
|
agreeing on a 30-day improvement action.
|
|||
|
|
- **Quarterly**: above-target on all KPIs for 2 consecutive months
|
|||
|
|
= eligible for the markup-authority bump (you can approve your
|
|||
|
|
own quotes up to 10% discount without supervisor sign-off).
|
|||
|
|
|
|||
|
|
### What the system does NOT measure (yet)
|
|||
|
|
|
|||
|
|
- Customer satisfaction (no NPS in WFDASH yet)
|
|||
|
|
- Cross-sell effectiveness (no line-item upsell tracking)
|
|||
|
|
- Catalogue coverage of your claims (you might be claiming a lot
|
|||
|
|
of items that the catalogue doesn't cover — that's a catalogue
|
|||
|
|
problem, not a you problem, but it's worth flagging)
|
|||
|
|
|
|||
|
|
If you think a KPI is unfair or misleading, raise it in `#quote-ops`.
|
|||
|
|
The supervisor can adjust targets, but the metric stays.
|