feat(mcp): create_sales_order accepts shipping fees inline (closes #818)#835
Conversation
There was a problem hiding this comment.
Pull request overview
Adds inline shipping_fees support to the MCP create_sales_order tool, collapsing the common “create SO then modify to add fees” workflow into a single preview/apply card while respecting Katana’s wire-level limitation (fees still require separate POSTs after the SO is created).
Changes:
- Extended
CreateSalesOrderRequest/SalesOrderResponseto accept and report per-feeshipping_fee_outcomes, with best-effort apply semantics. - Implemented chained
POST /sales_order_shipping_feecalls after SO creation and surfaced partial-failure warnings + next-action retry coaching. - Updated Prefab UI rendering and added unit + browser tests covering preview, all-success apply, and partial-failure apply states.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
uv.lock |
Bumps editable package version for katana-mcp-server. |
katana_mcp_server/src/katana_mcp/tools/foundation/sales_orders.py |
Adds request/response models and create-then-add-fees apply pipeline with per-fee outcomes. |
katana_mcp_server/src/katana_mcp/tools/prefab_ui.py |
Renders a “Shipping fees” subsection on the SO create card (preview + applied outcomes). |
katana_mcp_server/src/katana_mcp/resources/help.py |
Documents the new shipping_fees input and shipping_fee_outcomes output behavior. |
katana_mcp_server/tests/tools/test_sales_orders.py |
Adds tool-level tests for preview/apply success, partial failure, and BLOCKing invalid amounts. |
katana_mcp_server/tests/test_prefab_ui.py |
Adds unit tests pinning SO-create shipping-fees section rendering across states. |
katana_mcp_server/tests/browser/render_test_server.py |
Adds canned UI scenarios for browser rendering tests. |
katana_mcp_server/tests/browser/test_other_cards_render.py |
Adds browser tests to ensure the new card states render correctly. |
cdfc38a to
2915c31
Compare
dougborg
left a comment
There was a problem hiding this comment.
Self-review (6D via code-reviewer agent)
CI green. All findings addressed in the fixup just pushed. See thread replies for the inline-comment specifics.
✨ What Looks Good
unwrap_asused correctly for both the SO POST and each fee POST; per-feetry/exceptdoesn't swallow SO errors.to_unset/unset_dictused consistently for the wire request construction; no hand-rolledif value is not None else UNSETpatterns.- Retry coaching in
next_actionsand theAlertincludes the literalso.id— operators can copy-paste the exactmodify_sales_ordercall. - Browser render tests cover all three fee states (preview / all-success / partial-failure) and unit tests assert the exact
shipping_fee_outcomesshape. SOShippingFeeAdd/SOShippingFeeUpdatehoisted to module-top so themodify_sales_orderpath imports them without a forward reference.- Help resource updated in the same commit with wire-level semantics + retry path — no drift surface.
🚫 BLOCKING (addressed in fixup)
Apply path lacked BLOCK-validation on shipping_fees amounts. An agent calling preview=False directly with a malformed amount would leave a created SO + zero applied fees on the books (no rollback on best-effort). Now validates before the SO POST and returns a BLOCK response with id=None if amounts don't parse — pinned by test_create_sales_order_apply_blocks_on_invalid_amount.
⚠️ SUGGESTIONS (addressed in fixup)
total_amountfloat drift → switched toDecimalaccumulation for the fee total (matches the codebase convention for monetary sums); still passesfloat(total)to_format_money.- Missing all-fees-fail test → added
test_create_sales_order_apply_all_shipping_fees_failcovering SO success + every fee failing (0/N applied, SO id preserved, retry hint emitted). - Asymmetric
"PENDING"/"NOT_SHIPPED"fixture status → flipped the ternary to read in impl order + added an inline comment so future readers don't "fix" what isn't broken.
💬 NITPICKS (addressed in fixup)
is_preview or succeeded is Noneconflation → split into two explicit branches (if is_preview:thenif succeeded is None:) with a comment marking the second as defensive (the apply pipeline always resolvessucceededbefore returning).ShippingFeeOutcomemutability → added a one-line note to_outcomes_from_planned_feesdocstring explaining the intentional omission ofConfigDict(frozen=True).
Verdict
Comment / no-block. Both Copilot inline findings + all 6 code-reviewer agent findings are addressed in the fixup. 3565 unit tests pass; agent-check clean.
8219b19 to
1a7cea3
Compare
Round 2 fixup pushed — apply-path BLOCK render + partial-total fixesRebased onto current main + addressed Copilot's second-round findings. Finding 1 (apply-path BLOCK rendered as CREATED): The new apply-path validation returned Finding 2 (misleading partial total): "Total shipping" rendered whenever any fee amount parsed but only summed the parseable subset — so a 2-of-3-parseable plan would show "Total shipping: $X · 3 fee(s)" implying the plan totaled $X. Fixed: gate on 3577 unit tests pass; agent-check clean. Branch rebased onto current main, all 4 round-2 threads resolved. |
1a7cea3 to
69b488c
Compare
Round 3 fixup — Decimal end-to-end through
|
c9006c0 to
b95de01
Compare
Round 4 fixup — non-finite Decimal guard + state-driven apply-outcome morphThree Copilot findings addressed:
Acknowledged limitation: the per-row APPLIED/FAILED badges inside 3579 unit tests pass; agent-check clean. All 3 threads resolved. |
b95de01 to
465ad88
Compare
Round 5 fixup — defensive NaN/Infinity guard in rendererOne Copilot finding addressed: Fix: added a defensive 3579 unit tests pass; agent-check clean. Thread resolved. |
465ad88 to
fe72c99
Compare
fe72c99 to
d93cd99
Compare
Round 7 fixup — retry coaching now survives the morphReal UX bug Copilot caught: the build-time Alert in Fix:
3594 unit tests pass; agent-check clean. Thread resolved. |
Closes #818 Add ``shipping_fees: list[SOShippingFeeAdd] | None`` to ``CreateSalesOrderRequest`` so ecommerce-order workflows no longer need the create-then-modify two-step (``create_sales_order`` followed by ``modify_sales_order(add_shipping_fees=[...])``). Single logical action, one preview/apply card. Wire-level constraint: Katana's ``POST /sales_orders`` does NOT accept fees inline, so the apply path creates the SO first, then fires ``POST /sales_order_shipping_fee`` per fee against the just-created SO id. This issue collapses the two-step into one MCP-wrapper action — it's NOT a Katana API change. Atomicity: best-effort. SO success is durable. Per-fee failures land in ``shipping_fee_outcomes`` with ``succeeded=False`` + ``error``; the response carries a (non-BLOCK) warning telling the operator how to retry the failed fees via ``modify_sales_order(add_shipping_fees=[...])``. Card surface (``build_so_create_ui``): the response now carries ``shipping_fee_outcomes`` (per-fee status rows). The Tier-3 reference section gains a ``Shipping fees`` sub-section with one row per planned fee (description / amount / tax-rate suffix / total summary) on preview and APPLIED/FAILED status pills on apply. Partial failure surfaces a destructive Alert at the bottom of the section with retry coaching. Validation: ``SOShippingFeeAdd`` doesn't carry a per-fee currency field (shipping fees inherit the parent SO's currency at the wire level), so a literal "currency mismatch" check is structurally impossible against the current schema. The impl layer sanity-checks each fee's ``amount`` string parses as a non-negative decimal at preview time and emits a ``BLOCK:``-prefixed warning per offending fee — the UI builder strips the prefix, renders a destructive Badge, and gates the Confirm button. Tests pin: preview-with-fees rendering, apply-all-success outcomes, partial-failure shape (best-effort retain SO + per-fee error + warning + next-action coaching), and amount-shape BLOCK gating. Three browser scenarios pin the iframe-rendered card across the same three states so a ``rows="bare-string"``-shape regression on the new section can't recur silently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3f99bd3 to
30f0908
Compare
Round 8 fixup — misleading-total bug + retry-string sweepTwo real findings + one consistency sweep: Finding 1 (misleading total on partial failure): the bottom-line "Total shipping" summed every parseable fee — including failed ones. On partial failure that meant displaying "$30 total · 3 fee(s)" when only $20 actually attached to the SO. Operators would think they invoiced more than they did. Fix: track two Decimal totals (
New test pins all-failed; partial-failure test extended to assert the "Total shipping applied" label + counter + absence of the misleading planned total. Findings 2 & 3 (missing Rebased onto current main (incorporated #833's prefab_ui.py changes that landed in |
Summary
Closes #818 —
create_sales_ordernow acceptsshipping_feesinline so ecommerce-order workflows no longer need the create-then-modify two-step (currently every fee-bearing SO is two MCP calls + two preview/apply gates).Wire-level reality
Katana's
POST /sales_ordersdoes NOT accept fees inline at the wire. The MCP wrapper collapses the conceptual one-step into a "primary SO POST + chainedPOST /sales_order_shipping_feeper fee" pipeline. This is NOT a Katana API change — it's MCP-wrapper sugar to give the agent a single logical action with one preview/apply card.Atomicity choice
Best-effort (per pre-approved plan). SO success is durable; if a fee POST fails, the response carries:
shipping_fee_outcomes[i].succeeded = False+errorfieldmodify_sales_order(add_shipping_fees=[...])Rollback was considered and rejected — a failed
DELETE /sales_orders/{id}cleanup would leave a worse orphan than a partial create.Test plan
uv run poe agent-check— lint + type-check passuv run poe test— 3563 unit tests passuv run poe check(full incl browser) — verifying in CIcreate_sales_order(shipping_fees=[...])call replaces the previous two-step🤖 Generated with Claude Code