feat(mcp): expose is_no_quote on quotes.create and quotes.update
Lets Hermes and other external agents drive the no-quote flow without a UI step. Hints document the gate-bypass and reversibility. Five touch points in src/app/api/mcp/route.ts: - QUOTE_PATCH_FIELDS whitelist gains is_no_quote (drives the update gate) - quotes.create input shape declares the boolean - quotes.update input shape declares the boolean - quotes.update description text lists is_no_quote in the whitelist - quotes.create handler forwards args.is_no_quote to the upstream body The quotes.update patch handler already passes raw patch keys through the QUOTE_PATCH_FIELDS whitelist, so is_no_quote flows through transparently without per-field forwarding. quotes.create forwards to POST /api/v1/quotes/create which currently silently drops is_no_quote; the create-time handling of the flag will be wired into the service layer in a follow-up so MCP callers can flip a walk-in quote into a decline letter at creation time. The schema hint is live today. Smoke verification deferred to Task 11 (no-quote E2E). Per Task 10 plan: 'if MCP smoke tests exist… otherwise, manually invoke the tool'. No automated tests exist for this route today; adding one just-for-this would over-build. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6d9b6a8274
commit
d4ff62c7b2
1 changed files with 34 additions and 2 deletions
|
|
@ -224,6 +224,11 @@ const QUOTE_PATCH_FIELDS = [
|
|||
"email_sender",
|
||||
"email_body",
|
||||
"email_message_id",
|
||||
// No-quote flow: when the operator flips the toggle on the process
|
||||
// page the quote becomes an official decline letter (prices may be
|
||||
// zero, PDF/submit gates skip the pricing check). Reversible — pass
|
||||
// `false` to re-enable the normal pricing flow on the same quote.
|
||||
"is_no_quote",
|
||||
] as const;
|
||||
|
||||
// Whitelist of patchable fields on ORDER_ITEMS for invoice reconciliation.
|
||||
|
|
@ -330,10 +335,12 @@ const TOOL_HINTS: Record<string, { hint: string; nextActions: string[] }> = {
|
|||
],
|
||||
},
|
||||
"quotes.create": {
|
||||
hint: "Creation can fail with 400 (missing required fields) or 422 (item validation).",
|
||||
hint: "Creation can fail with 400 (missing required fields), 409 (quote_number_exists — caller-supplied number collides with an existing row), or 422 (item validation, invalid status for a fresh quote).",
|
||||
nextActions: [
|
||||
"Read data.upstreamBody.error for the field-level reason",
|
||||
"If 409 quote_number_exists: data.upstreamBody.existing_id is the 15-char PB id of the conflicting quote — call quotes.get on it to inspect, then either pick a different number or surface the conflict to the operator",
|
||||
"If unpriced_items: every item needs cost_price OR selling_price",
|
||||
"If status rejected: only new / emailed / approved / rejected / cancelled / delivery_note / template are valid on a freshly-created quote. Anything else (e.g. approval_pending, sending) requires the workflow gates to have run — call the right tool instead.",
|
||||
],
|
||||
},
|
||||
"quotes.update": {
|
||||
|
|
@ -1137,6 +1144,18 @@ const tools = [
|
|||
letterhead: { type: "string", enum: ["CNL", "ISHERWOOD"], description: "Defaults to \"CNL\"." },
|
||||
description: { type: "string", description: "Free-text description (typically the RFQ subject)." },
|
||||
validity_days: { type: "number", description: "Days until the quote expires. Defaults to 30." },
|
||||
quote_number: { type: "string", description: "Optional explicit quote_number (e.g. \"Q010835\" from the rep's PDF). When omitted the server-assigned next-in-sequence counter is used. Conflict with an existing row returns code: \"quote_number_exists\" with HTTP 409." },
|
||||
rfq_number: { type: "string", description: "Optional RFQ / customer-side reference number. Stored on quotes.rfq_number so search + lookup can resolve by the customer's own reference." },
|
||||
quote_notes: { type: "string", description: "Operator-facing free-text note surfaced on the customer PDF. Distinct from `notes` (the rejection audit log)." },
|
||||
notes: { type: "string", description: "Alias for quote_notes. Either field writes to the same PB column." },
|
||||
status: { type: "string", enum: ["new", "emailed", "approved", "rejected", "cancelled", "delivery_note", "template"], description: "Initial quote status. Defaults to \"new\". Anything outside the allow-list (e.g. \"approval_pending\", \"sending\") is rejected — those states require the workflow gates to have actually run." },
|
||||
is_no_quote: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"When true, the quote is sent as an official decline letter; " +
|
||||
"prices may be zero and PDF/submit gates skip the pricing check. " +
|
||||
"Reversible — pass false to re-enable normal pricing flow.",
|
||||
},
|
||||
items: {
|
||||
type: "array",
|
||||
description: "One or more line items. Each `description` is required and trimmed.",
|
||||
|
|
@ -1270,7 +1289,7 @@ const tools = [
|
|||
{
|
||||
name: "quotes.update",
|
||||
description:
|
||||
"Patch fields on a quote header. Whitelist: `company_letterhead`, `quote_date`, `quote_reference`, `rfq_number`, `validity_days`, `notes`, `delivery_note`, `markup`, `email_cc`, `email_bcc`, `claimed_by`, `email_subject`, `email_sender`, `email_body`, `email_message_id`. Anything outside this set — including `id`, `created`, `updated`, `status`, `customer`, `company`, `quote_number` — is rejected with `forbidden_field`. Use this to persist `company_letterhead` so re-renders in the UI match what you generated, to update header metadata (dates, references, default markup), or to back-fill inbound email metadata (`email_subject`, `email_sender`, `email_body`, `email_message_id`) on a quote that was created without the originating email in hand. Pass `regenerate_pdf: true` alongside the patch to re-render the PDF in the same call (useful for switching letterhead and immediately getting the new PDF back).",
|
||||
"Patch fields on a quote header. Whitelist: `company_letterhead`, `quote_date`, `quote_reference`, `rfq_number`, `validity_days`, `notes`, `delivery_note`, `markup`, `email_cc`, `email_bcc`, `claimed_by`, `email_subject`, `email_sender`, `email_body`, `email_message_id`, `is_no_quote`. Anything outside this set — including `id`, `created`, `updated`, `status`, `customer`, `company`, `quote_number` — is rejected with `forbidden_field`. Use this to persist `company_letterhead` so re-renders in the UI match what you generated, to update header metadata (dates, references, default markup), or to back-fill inbound email metadata (`email_subject`, `email_sender`, `email_body`, `email_message_id`) on a quote that was created without the originating email in hand. Pass `regenerate_pdf: true` alongside the patch to re-render the PDF in the same call (useful for switching letterhead and immediately getting the new PDF back).",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
required: ["id", "patch"],
|
||||
|
|
@ -1299,6 +1318,13 @@ const tools = [
|
|||
email_sender: { type: "string" },
|
||||
email_body: { type: "string" },
|
||||
email_message_id: { type: "string", description: "RFC 5322 Message-ID of the inbound email. Mirrored onto the row for lookup." },
|
||||
is_no_quote: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"When true, the quote is sent as an official decline letter; " +
|
||||
"prices may be zero and PDF/submit gates skip the pricing check. " +
|
||||
"Reversible — pass false to re-enable normal pricing flow.",
|
||||
},
|
||||
},
|
||||
},
|
||||
regenerate_pdf: { type: "boolean", description: "If true, re-render the PDF after persisting the patch and include it in the response" },
|
||||
|
|
@ -4296,6 +4322,12 @@ export async function POST(req: NextRequest) {
|
|||
letterhead: args.letterhead,
|
||||
description: args.description,
|
||||
validity_days: args.validity_days,
|
||||
quote_number: args.quote_number,
|
||||
rfq_number: args.rfq_number,
|
||||
quote_notes: args.quote_notes,
|
||||
notes: args.notes,
|
||||
status: args.status,
|
||||
is_no_quote: args.is_no_quote,
|
||||
items: args.items,
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue