Skip to content

Add read-only pcb order command group - #926

Open
lucia3e8 wants to merge 5 commits into
mainfrom
pcb-order
Open

lucia3e8 wants to merge 5 commits into
mainfrom
pcb-order

Conversation

@lucia3e8

@lucia3e8 lucia3e8 commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a read-only pcb order command group with three subcommands, all backed by existing authenticated API endpoints (reusing the pcb-diode-api client and pcb auth bearer-token infra used by search/scan/preview). No new backend routes.

  • pcb order list → GET /api/boards/:workspace/:name/orders — renders id, name, status, quantity, release version, provider, created date.
  • pcb order show <order-id> → GET /api/boards/:workspace/:name/orders/:orderId — renders the full order (release id/version, bom id, quote summary if present, timeline, shipping location id).
  • pcb order bom <order-id> → fetches the order, then GET /api/boms/:bomId (fails clearly with "order has no BOM" if bomId is null) and GET /api/boards/:workspace/:name/orders/:orderId/selections, joined client-side.

Board identity

Resolves from workspace.repository (e.g. code.diode.computer/demo/b/DM0002 → workspace demo, board DM0002, as surfaced by pcb info), overridable with --workspace <slug> / --board <name>. Errors clearly when run outside a workspace with no flags.

order bom join

One row per BOM line: bomLineId, design entry (mpn, manufacturer, package, value, designator/path, declared alternatives), match status, candidate offers (id, mpn, manufacturer, distributor, stock, price), and the effective selection:

  • order's selections map ({bomLineId → offerId}) if present, else
  • the line's default selectedOfferId, else
  • none

surfaced as selectionSource (order_override | default | none) plus convenience selectedMpn/selectedManufacturer.

--mismatches-only filters to lines where the selected MPN differs from the design MPN under the backend's normalizeBomLookupMpn normalization (trim, uppercase, strip everything except A–Z, 0–9, .). Lines with no selection or no design MPN are excluded. Table mode stays compact (designator, design MPN, selected MPN, match, selection source); full offer detail is reserved for -f json.

Output & errors

  • All subcommands support -f table|json (table default), following pcb bom conventions.
  • 401/403 → prompt to run pcb auth login; 404 → distinguishes unknown board from unknown order id; network failures → single-line error, non-zero exit.

Tests

  • Board-identity resolution (parsing, flag overrides, out-of-workspace error, partial flags).
  • Selection-join precedence (override beats default; none when neither).
  • --mismatches-only normalization edge cases (case, punctuation, missing MPNs).
  • JSON snapshot output for list/show/bom (+ mismatches-only) against mocked API responses.

Out of scope

Order-mutating subcommands (create, select) intentionally land in a separate PR.


Note

Low Risk
Read-only GETs against existing APIs; no mutations or auth changes beyond reusing pcb auth tokens.

Overview
Adds a read-only pcb order CLI group (list, show, bom) wired through pcb-diode-api and the existing authenticated Diode API client—no new backend routes.

Board targeting resolves from pcb.toml (workspace.name, workspace.repository with /b/ parsing) or --workspace / --board. Subcommands support -f table|json; table output uses comfy-table.

pcb order bom loads the order BOM and order selections, joins them client-side with precedence order override → line default → none, and exposes selectionSource plus selected MPN fields. --mismatches-only filters lines where design vs selected MPN differ after backend-aligned MPN normalization.

Docs and changelog are updated; tests cover identity resolution, join logic, mismatch rules, and JSON snapshots.

Reviewed by Cursor Bugbot for commit 03c31d9. Bugbot is set up for automated code reviews on this repo. Configure here.

cursor[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

lucia3e8 added a commit that referenced this pull request Jul 7, 2026
In map_error_response, the BOM-not-found branch was gated on the JSON
error text containing "bom". Since RequestTarget.bom_id is only set for
GET /api/boms/:bomId — a request that is neither board- nor order-scoped —
a 404 with an empty, non-JSON, or differently worded body fell through to
the order branch and claimed the order id was not found, even though
`pcb order bom` had already fetched that order successfully.

Report BOM-not-found unconditionally whenever the failed request targeted
a BOM id.

Flagged by Cursor Bugbot on #926.
lucia3e8 and others added 3 commits August 5, 2026 21:53
Introduce `pcb order list|show|bom`, backed by existing authenticated API
endpoints (reusing the pcb-diode-api client + bearer-token auth). No new
backend routes.

- Board identity resolves from `workspace.repository`
  (code.diode.computer/demo/b/DM0002 -> workspace demo, board DM0002),
  overridable with --workspace/--board; clear error when run outside a
  workspace with no flags.
- `order list`  -> GET /api/boards/:workspace/:name/orders
- `order show`  -> GET /api/boards/:workspace/:name/orders/:orderId
- `order bom`   -> fetch order, then GET /api/boms/:bomId joined client-side
  with GET .../orders/:orderId/selections. Emits one row per BOM line with
  design entry, candidate offers, and the effective selection
  (order_override > default > none) plus selectedMpn/selectedManufacturer.
  --mismatches-only filters lines whose selected MPN differs from the design
  MPN under the backend's normalizeBomLookupMpn normalization.
- All subcommands support -f table|json (table default); table stays compact,
  JSON carries full offer detail.
- Uniform error handling: 401/403 -> prompt `pcb auth login`, 404 ->
  distinguish unknown board vs unknown order, network failures -> single-line
  error with non-zero exit.
- Tests for board-identity resolution, selection-join precedence,
  --mismatches-only normalization edge cases, and JSON snapshots against
  mocked API responses. Docs + CLI help updated.

Order-mutating subcommands (create, select) intentionally out of scope.
In map_error_response, the BOM-not-found branch was gated on the JSON
error text containing "bom". Since RequestTarget.bom_id is only set for
GET /api/boms/:bomId — a request that is neither board- nor order-scoped —
a 404 with an empty, non-JSON, or differently worded body fell through to
the order branch and claimed the order id was not found, even though
`pcb order bom` had already fetched that order successfully.

Report BOM-not-found unconditionally whenever the failed request targeted
a BOM id.

Flagged by Cursor Bugbot on #926.
cursor[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +384 to +387
// Resolve the selected offer (if any) to convenience MPN/manufacturer.
let selected_offer = selected_offer_id
.as_deref()
.and_then(|id| line.offers.iter().find(|o| o.id == id));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Order lines whose chosen part cannot be looked up vanish from the mismatch report

A line whose chosen part cannot be found in its own list of quoted parts is treated as having no chosen part name (line.offers.iter().find(...) at crates/pcb-diode-api/src/order.rs:385-387), so pcb order bom --mismatches-only silently drops it instead of showing that the order picked something unresolvable.
Impact: Users auditing an order for wrong parts can miss lines where the order points at a part that is no longer offered.

Why the unresolved selection is silently swallowed

build_order_bom_rows resolves selected_offer_id (either the order override from the selections map or the line default) against line.offers. When the referenced offer id is absent from offers (stale selection, offers list trimmed/paginated by the backend, or a default id pointing at a removed offer), selected_mpn/selected_manufacturer stay None even though selection_source is OrderOverride/Default.

OrderBomRow::is_mpn_mismatch (crates/pcb-diode-api/src/order.rs:336-343) returns false whenever selected_mpn is None, so rows.retain(OrderBomRow::is_mpn_mismatch) in crates/pcb-diode-api/src/order.rs:1022 removes such rows entirely. In table mode the row would also show — under "Selected MPN" while claiming order_override, which is contradictory.

A safer behavior is to treat an unresolvable selection as a reportable condition (e.g. keep it under --mismatches-only, or mark it explicitly) rather than as "no selection".

Prompt for agents
In crates/pcb-diode-api/src/order.rs, build_order_bom_rows resolves the effective selected offer id against line.offers; when the id is not present in that list (stale or trimmed offers), selected_mpn/selected_manufacturer end up None while selection_source still says order_override/default. OrderBomRow::is_mpn_mismatch then returns false for such rows, so `pcb order bom --mismatches-only` silently hides lines whose selection cannot be resolved, and table output shows an em dash for the selected MPN next to an 'order_override' label. Consider distinguishing 'no selection' from 'selection could not be resolved' — e.g. an extra flag on OrderBomRow or an additional SelectionSource variant — and make the mismatch filter/table surface unresolved selections instead of dropping them.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +647 to +655
fn fmt_price(currency: Option<&str>, price: Option<f64>) -> String {
match price {
Some(p) => match currency {
Some(c) if !c.is_empty() => format!("{p:.2} {c}"),
_ => format!("${p:.2}"),
},
None => NONE.into(),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Prices with an unknown currency are displayed as US dollars

A quoted amount whose currency the server did not report is printed with a dollar sign (format!("${p:.2}") at crates/pcb-diode-api/src/order.rs:651), so a non-USD quote can be shown to the user as if it were in US dollars.
Impact: Someone reviewing an order quote can misread the price as USD when it is in another currency.

Currency fallback in fmt_price

fmt_price (crates/pcb-diode-api/src/order.rs:647-655) formats 12.34 USD when a currency string is present, but falls back to $12.34 when currency is None/empty. Since QuoteSummary.currency is optional (crates/pcb-diode-api/src/order.rs:174-175), a quote in EUR/CNY with a missing currency field renders with a $ prefix. A neutral fallback (bare 12.34) avoids asserting a currency the API never returned.

Suggested change
fn fmt_price(currency: Option<&str>, price: Option<f64>) -> String {
match price {
Some(p) => match currency {
Some(c) if !c.is_empty() => format!("{p:.2} {c}"),
_ => format!("${p:.2}"),
},
None => NONE.into(),
}
}
fn fmt_price(currency: Option<&str>, price: Option<f64>) -> String {
match price {
Some(p) => match currency {
Some(c) if !c.is_empty() => format!("{p:.2} {c}"),
_ => format!("{p:.2}"),
},
None => NONE.into(),
}
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant