Skip to content

The order-to-printer pipeline - #12

Merged
tech5-opti merged 3 commits into
mainfrom
production-pipeline
Sep 5, 2026
Merged

The order-to-printer pipeline#12
tech5-opti merged 3 commits into
mainfrom
production-pipeline

Conversation

@tech5-opti

Copy link
Copy Markdown
Collaborator

A Shopify order now reaches a printer without anyone driving it.

Sync pulls the order, a job is created for each plank, the model is rendered
from the customer's own names, matching colours are batched onto a bed, and a
full bed is sliced and sent to BambuBuddy. Each step enqueues the next, so the
floor sees work appear rather than waiting for somebody to press a button.

Workers start with the backend

internal/workerset registers the job-creation, render, batch-plan and dispatch
workers in one place, and both cmd/api and cmd/productionworker start it. A
plain go run ./cmd/api now runs the whole pipeline.

Every stage is event-driven, not periodic. River's periodic jobs are
leader-gated and this database's leader is a remote instance, so a tick-based
design would simply never fire here. Each worker enqueues its successor instead.

Beds

  • A bed holds four planks. It stays a Draft until it is full, so the next
    matching order completes it rather than opening a bed of its own; when it
    fills, it locks.
  • A partial bed is never locked. The old escape after BATCH_MAX_WAIT_HOURS
    is gone: it committed a wasted plate and denied the fourth order its place.
    A person can still approve a partial bed by hand.
  • Planning is oldest-order-first, so the oldest work fills the full beds. It
    used to be possible for orders 114654 and 114680 to sit on a locked bed of two
    while newer ones filled a bed of four beside them.
  • Locked and Draft beds are both editable. Adding or removing a job
    withdraws the plate from BambuBuddy's queue, re-renders the merged plate to
    match (remove one plank and you get a plate of three), and re-reserves
    filament.
  • Mark done is a selection, not a switch: a plate comes off with three good
    and one warped, so the bed stays open until nothing on it is outstanding.

The packer was under-fitting - it charged the inter-part gap after the last part,
so a 256mm bed held three planks instead of four. internal/bedpack now charges
the gap when it splits a free rectangle, and a geometry test asserts no overlap
or overhang across four beds x five shapes x five part counts.

Orders that are already out the door

A fulfilled order makes no jobs, and one fulfilled mid-flight is pulled off its
bed and out of the merged plate, then routed to Done. The priority-dispatch
exclusion is removed - it was holding nineteen unfulfilled orders out of
production on an instruction that no longer describes the floor.

Rendering

The templates are the 4 September OpenSCAD files. The old no-heart template
dropped a padding slot outright rather than filling it, and a padding slot is
what holds the extra letters of the longer name - so a plank for KAUSTUBH and
LIPIKA printed "AUSTUB", losing one letter from each end. Orders with a heart
were never affected, because the heart filled the slot.

Property lookup also gained whole-word containment, because nine orders label
the field "First Name on Plank" rather than "First Name". The heart count is
read from the first candidate whose value parses, so a combo product's "PAVI"
is no longer read as a quantity.

BambuBuddy

  • Push events land at POST /integrations/bambubuddy/events, authenticated with
    a constant-time token compare and debounced for ten seconds. BambuBuddy's own
    /webhook/* endpoints are pulls despite the name; real push comes from a
    notification provider.
  • FAILED maps to idle, not error. A printer whose last print failed is
    available - it just wants its plate cleared - and treating it as an error was
    excluding five of thirteen machines from scheduling.
  • Print time and filament come back from the queue item into the plate's slice
    result.

Sync

The Shopify pull moved into a River worker. It ran inline before, so the
frontend's five-second abort cancelled the request context mid-import and the
remaining orders were logged and skipped - which is why 114774 was followed by
114810. The endpoints return 202 and the import finishes on its own.

Commands

markprinted, relinkbeds, rerender, droppedletters and reformbeds are
one-off tools for repairing history - reopening beds frozen under the old
locking rule, re-rendering models built from the broken template, and naming the
planks that printed with letters missing so somebody can decide what to reprint.
reformbeds checks BambuBuddy's live queue before reopening anything, because a
plate pulled from under a running printer cannot be put back.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W4ND3pFVaX7yGzWxZiHPYu

tech5-opti and others added 3 commits August 27, 2026 14:51
Order T3DPS-114567 carries the personalisation the whole business runs
on - STEP 4-First Name: RAHUL, STEP 5-Second Name: RANU, a WhatsApp
number. Tensor fetched every one of those from Shopify and threw them
away.

The import already requested customAttributes. But mapShopifyLineItems
only recognised a hardcoded list - personalisation_name, custom_name,
engraving_text - and a real storefront personaliser names its questions
for humans, not for parsers. Nothing matched, so the fields were read and
silently dropped: the operator sees a blank where a customer typed their
child's name, and nothing anywhere records that it was discarded.

Two changes, and the difference between them matters.

LineItem.Properties now keeps EVERY attribute verbatim and in order. The
typed fields stay the machine-readable path, but they are a mapping, and
a mapping only knows the keys it was told about. Keeping the raw list
means an unrecognised question still reaches the person doing the work
instead of vanishing. It is a slice, not a map: "STEP 3 / 4 / 5" is the
order the customer answered in, and duplicate keys are legal.

On top of that, matching is done on a NORMALISED key, so a store's
punctuation does not have to be reproduced exactly - "STEP 4-First
Name-:" and "STEP 4 First Name" are the same question, and a label edited
in Shopify no longer silently stops matching. Two names on one plank join
into one engraving rather than one of them being dropped.

No migration: line_items is jsonb, so the shape simply widened.

The test uses the real order's property names verbatim, and it earned its
keep immediately - normalisePropKey kept only a-z and DELETED uppercase,
turning "STEP 6 - WhatsApp Number:" into "6 hats pp umber", which still
looked like a plausible key and would have quietly failed to match.

Still missing, and not fixable here: customer name, email, phone and the
shipping address. Those need the read_customers scope, and Shopify fails
the ENTIRE GraphQL call for an inaccessible field rather than omitting it
- see the note above listOrdersQuery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything between a Shopify order and a plate on a machine: job creation,
personalised model rendering, colour batching, dispatch to BambuBuddy, and the
fulfilment path that takes finished work back out again.

Batching
- One colour per bed, at most four products, oldest order first. A bed locks
  only when it is full; an under-full bed stays a Draft so the next order in
  that colour completes it rather than opening a bed of its own.
- The merged plate carries each plank's colour (3MF materials plus Bambu's
  per-part extruder config), and is named after the orders on it.
- A bed can be edited by hand after locking: the plate is withdrawn from
  BambuBuddy's queue, the membership changed, and the plate rebuilt from what
  is left. Editing is all-or-nothing.

Rendering
- OpenSCAD templates for the Dual Name Plank, rendered per job from the order's
  own customisation properties.
- The property matcher accepts the storefront's longer labels ("First Name on
  Plank"), and reads a heart count from the first candidate whose VALUE parses -
  a combo product carries a field called "Name On Heart Keychain" holding a name.

Dispatch
- A plate reaches only a slicer pipeline targeting the printer class it was
  planned for. Pipelines were previously tried in whatever order BambuBuddy
  returned, so every plate was sliced for the first class in that list.
- A colour gate holds a bed no machine has the filament for, naming the spool.
- BambuBuddy pushes events (print start/complete/failed, queue changes) to
  /integrations/bambubuddy/events, which refreshes the fleet within seconds
  instead of on the next 60-second tick.

Orders
- The Shopify pull runs on a worker, not on the request. It used to run inside
  the HTTP handler, whose caller gives up after five seconds, so the import was
  cancelled part way and reported the handful that made it as a success.
  Thirty-five orders were missing from the Orders page because of it.
- An order already fulfilled creates no jobs, and one that becomes fulfilled
  leaves its bed AND the merged plate.
- Priority-dispatch orders are no longer excluded from production.

Fleet
- A printer whose LAST PRINT failed is idle and schedulable, with the advice to
  clear its plate shown as a note. It used to be reported as a machine error,
  which took five of thirteen printers out of scheduling entirely.

Packing
- bedpack takes a bed rather than reading compile-time constants, and charges
  the clearance gap BETWEEN parts instead of after each one - a 256mm bed holds
  four 200x50 planks, and the old rule said three. A geometry test asserts no
  overlap and full separation across beds, shapes and counts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4ND3pFVaX7yGzWxZiHPYu
@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown

Too many files changed for review (131 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 20c4a09e-b32a-426a-84f8-a14cef7bf4f3


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

sonarqubecloud Bot commented Sep 5, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
4.0% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@tech5-opti
tech5-opti merged commit 906ef59 into main Sep 5, 2026
3 of 4 checks passed
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