SysEngineering

Purchasing Desk

Lenovo and Amazon order emails, matched to their PSA purchasing tickets and kept up to date, with every change approved by a buyer first, an order ledger of what each email said, and Ask, a chat panel that answers purchasing questions from both. Built for Jennifer and Joe (procurement); Robbie and Adam also have access. JSM stays the source of truth.

Operational state, setup and deploy commands: HANDOFF.md.

How an order moves

  1. Confirmation. Lenovo emails procurement@hillspire.com; Amazon emails the buyer who ordered, and a Google Workspace rule copies Jennifer’s and Joe’s Amazon order mail there (HANDOFF). The order appears under Needs you. The buyer links it to the PSA ticket once (open tickets are suggested, unprefixed ones and those already naming the order’s vendor first). On approval the desk:
    • adds Lenovo (K…) or Amazon (114-…) to the title in the buyer’s own style (, , ; or &, or inside an existing group, with or without a colon after it); the title is editable in the preview;
    • adds the order total and item count to Total Order Cost (customfield_10883) and Total Items Ordered (customfield_10884); a ticket often holds several vendors’ orders, so it never overwrites;
    • posts the buyer’s update comment: ORDER CONFIRMATION UPDATE: Lenovo (K…) with Order Date, Entity, Item and Tracking/eta (Lenovo’s “Get it by” or Amazon’s arrival date, editable);
    • if the ticket is unassigned, assigns the buyer who approved (shown in the preview). If Jira refuses, the ticket stays unassigned and the rest stands.
  2. Shipping. The shipping email finds the ticket by the order number in its title. On approval: the same comment with the tracking link under the item, Tracking/eta: Shipped (or Amazon’s date), and the “Ordered” transition, In Progress to Shipped. A second shipment of a split order only comments. Lenovo’s link is its own tracking page. Amazon’s email names no carrier, so the preview takes the carrier’s tracking number from Amazon’s tracking page (optional) and posts it the buyers’ way, USPS_9361… to the carrier’s page (UPS, USPS, FedEx; Amazon Logistics links Amazon’s tracker); left empty, it links Amazon’s tracker as Amazon_114-….
  3. Delivery. Once Shipped, the order waits under Waiting (“Arriving by …”) until the latest “Get it by” date on Lenovo’s confirmation has passed, then returns to Needs you; it can be closed early from the card. Amazon sends a delivery email per package: once every shipped package has one, the order returns at once with that day filled in. The buyer picks or checks the delivery date and approves: the comment with DELIVERED <DAY>, M/D, then “Confirm Delivery”, Shipped to Closed. If the title tracks other vendors’ orders, the preview lists them first. Nothing closes on its own.

Prices. The confirmation email’s total is what the desk adds. Amazon’s shipped and delivered emails print none, so an order whose confirmation never reached procurement@ shows the ticket’s own Total Order Cost (“From the ticket”) or “Needs a price”, and its shipping or delivered preview offers an optional Order total. With one, the same approval adds it to Total Order Cost; it is asked only once and never goes in the comment.

If the buyer types the order number into the title herself, the desk matches it and treats step 1 as done by hand: it adds no totals and posts no confirmation.

Comments are public replies, as the buyers’ own updates are, so the requester gets them. Nothing is screenshotted; the comment carries the link and details.

Today. The top of Orders says what the day holds: orders to review, orders arriving today, orders running late (the vendor’s date passed, shipped or not) and the open request longest without an update (7+ days). Each item opens the order, or jumps to its section. Every card shows Ordered → Shipped → Delivered with each step’s date, the last leg filling toward the vendor’s date, a countdown (“in 3 days”, “Arriving today”) and a red late badge once the date passes. Late orders sort first. Display only: nothing here writes to Jira or changes when an order comes back to Needs you.

Requests lists every open PSA purchasing request (issue type 10824), with search, status and assignee filters and a sort. Clicking a request or an order opens a side panel with its details and a timeline: for a request, Jira’s status and assignee changes, its latest comments (as short excerpts, replies and internal notes told apart) and the desk’s own approvals; for an order, its vendor emails and each approval, with the next step on top. Approvals happen in the order’s panel.

Rules the code keeps

Order ledger

What each vendor email said, for the questions Jira can’t answer: items, part numbers, prices, and each vendor’s own total on a ticket that holds several orders. JSM stays the record of the ticket.

Ask the desk

A chat panel (the Ask tab) for “how many Lenovo laptops did we order this month?”, “what have we spent with Amazon since October?” or “what have we paid per X1 Carbon?”. Claude, the same backbone as the other agents (claude-opus-5-5, one setting), with a fixed set of read-only tools:

Tool Reads Answers
purchase_requests PSA, requests by day Counts, Total Order Cost, items, by status, entity, vendor, month
vendor_orders Ledger orders Orders and the vendors’ own totals, by vendor and month
ordered_items Ledger items Quantities and amounts per item, unit price per order by date

Reports

The Reports tab builds a report from PSA or the ledger, for spend by entity, for-profit against non-profit, vendors, buyers’ open work, days to close, and what was bought at what price.

Apple Business Manager (not wired in yet)

Apple’s emails will drive Apple orders; Apple Business Manager adds each device’s serial number (ROADMAP goal 5). server/abm-client.mjs reads ABM’s device list and nothing else. Nothing calls it yet.

Access

Layout

Path What it does
server/hosted-server Routes, security headers, origin and session checks
server/okta-auth OIDC sign-in, ID-token verification, session cookies
server/desk Reads mail and Jira, previews, applies approved changes
server/orders Pure rules: grouping, matching, titles, totals, comments
server/jira-client The fixed Jira reads and writes
server/gmail-reader Keyless delegation and the fixed mailbox query
server/title Reads the orders a PSA title names; shared with the page
server/vendors The vendors the desk reads; picks each email’s parser
server/ledger The order ledger’s line for each vendor email
server/ask Ask the desk: the Claude loop, its prompt, the record of use
server/ask-tools Ask’s three read-only tools, counting and adding in code
server/ledger-reader The ledger’s fixed BigQuery reads
server/entities The entities’ tax status, from Business Operations’ sheet
server/reports Reports’ reads: one source and one range, rows the page adds up
server/abm-client Apple Business Manager’s device list, GET only (not wired in yet)
ledger_bigquery.sql The ledger’s BigQuery views
server/lenovo-mail LenovoPRO confirmation and shipping email parser
server/amazon-mail Amazon Business ordered, shipped and delivered email parser
server/mail-message Minimal RFC 822/MIME reader and sender verification
src/ React UI: Orders, Requests, side panels, previews
src/ui.jsx Shared pieces: status badges, avatars, stat tiles, panel, timeline
src/TodayStrip.jsx Today strip: to review, arriving today, running late, quietest
src/journey.mjs Countdown, late and progress per order, against the vendor’s date
src/Reports.jsx The Reports tab: builder, headline figures, tables, sharing
src/report-engine Reports’ periods, filters, groups, cross-tab, chart series, CSV
src/ReportChart.jsx The report’s column chart, plain SVG
src/DataTable.jsx A report’s records: sortable, a page at a time
src/Ask.jsx The Ask tab: starting points, the conversation, answers as entries
src/ask.css Ask’s own styles (the ruled hero, entries, figures, ticket chips)
public/ Landing page, app shell, logo (built app.js/app.css here)
scripts/dev-server Local only: real UI with invented data and a stand-in Okta
scripts/verify-ui Headless Chrome walkthrough of the whole flow
scripts/try-ask Local only: one question to Ask against real PSA and the ledger
scripts/try-report Local only: one report’s figures from real PSA or the ledger
scripts/dev-reports Local only: invented history behind Reports on the dev server
tests/ Unit tests; fixtures/ holds invented copies of vendor mail

Develop and test

Node 22+. From this folder (npm.cmd on Windows if PowerShell blocks npm.ps1):

npm ci --ignore-scripts
npm test            # 191 unit tests
npm run build
npm run verify      # 90 checks in headless Chrome, needs Chrome installed
npm run dev         # http://127.0.0.1:4317, an invented queue, no Okta

PROCUREMENT_CHROME_PATH selects another Chrome; PROCUREMENT_SCREENSHOTS=<dir> saves screenshots. The dev server and tests use invented requests (PSA-9001…) and the invented Lenovo fixtures; they never call Okta, Gmail or Jira. npm run dev dates its seven invented orders from today, so one is always due today, one late after shipping, one late without shipping, one in transit and one closed. Real vendor emails stay outside Git (*.eml is ignored at this folder’s root).

Configuration

Non-secret settings: hosted-env.json.example (copy outside Git, pass with --env-vars-file). Secrets, from Secret Manager only: PROCUREMENT_OKTA_CLIENT_SECRET, PROCUREMENT_SESSION_KEY (32+ random bytes, base64url), PROCUREMENT_JIRA_API_TOKEN and, for Ask, PROCUREMENT_ANTHROPIC_API_KEY (unset: Ask is off). PROCUREMENT_ASK_MODEL (default claude-opus-5-5) and PROCUREMENT_ASK_EFFORT (default low) are plain settings. Missing or malformed settings refuse startup without echoing values. PROCUREMENT_MAIL_SERVICE_ACCOUNT unset means the mailbox is off and the desk works from dropped-in emails. PROCUREMENT_JIRA_ASSIGNEES (email=accountId,…, people on the access list only) maps each buyer to their Jira account for the assignee; the desk’s token cannot look users up. Unset means the assignee is never touched.

To count the desk’s tickets in Jira: project = PSA AND issue in updatedBy("712020:641e70b0-d5ba-4215-895a-bc029ea1622f") (the Purchasing Desk account; add dates for a range).