# Plugin lab: build the printedparts plugin yourself A guided, milestone-based exercise: build the 3D-printed-parts storefront + kiosk plugin specified in `docs/proposals/printedparts-plugin.md`. Each milestone lists what to build, which existing code to imitate, and a checkpoint that proves you are done. Read the spec first, keep it open. Prerequisites: a working dev environment (README quick start), the three plugin docs skimmed once - `PLUGIN-QUICKSTART.md` (mechanics), `PLUGIN-GUIDE.md` (the measuringtools walkthrough - your narrative reference), `PLUGIN-HOOKS.md` (hook reference). Naming rules: `CONTRIBUTING.md` - the pre-commit hook enforces them, read it before naming anything. Ground rules - Import core ONLY via `shopdb.api` (+ `shopdb.plugins.base`). The contract test fails your build otherwise. - DB columns: lowercase concatenated (`quantityonhand`, not quantity_on_hand). - Run `bash scripts/check-naming-and-style.sh` and the test suite at every checkpoint. --- ## Milestone 1 - skeleton, models, migration (backend exists) Build 1. `flask plugin new printedparts` - scaffolds `plugins/printedparts/`. 2. The scaffold assumes an Asset-extension plugin; ours is standalone. In `plugin.py` strip the AssetType seeding from `on_install` (imitate `plugins/knowledgebase/plugin.py` instead of the template). 3. Replace the scaffold model with the two spec tables: `PrintedItem`, `PrintedItemTransaction` (`models/printeditem.py`). Use `BaseModel` + `AuditMixin` from `shopdb.api`. Itemcode: leave generation to the API layer (M2), column just `unique=True, index=True`. 4. Register both tables in `PLUGIN_TABLE_OWNERS` (`shopdb/plugins/alembic_template.py`). 5. Create `plugins/printedparts/migrations/` with the 3-line `env.py` + `script.py.mako` (copy from measuringtools) and a REAL baseline `versions/0001_printedparts_baseline.py` - hand-written `op.create_table(...)` for both tables (see `plugins/measuringtools/migrations/versions/0001_measuringtools_baseline.py`). 6. Manifest: api_prefix `/api/printedparts`, `dependencies: ["employees"]`, `default_enabled: false`. Checkpoint ``` flask plugin install printedparts && flask plugin enable printedparts flask plugin upgrade-all # applies your 0001 mysql> SHOW TABLES LIKE 'printed%'; -- both tables mysql> SELECT * FROM alembic_version_printedparts; -- your revision id pytest tests/ -q # nothing broken, contract tests green ``` ## Milestone 2 - CRUD API + permissions + itemcode Build 1. `api/routes.py`: list/detail/create/update/soft-delete per the spec table. Imitate a clean plugin blueprint (`plugins/measuringtools/api/routes.py`) for pagination (`perpage`, `dir`), search, and the shared `success_response`/`error_response` helpers from `shopdb.api`. 2. Itemcode on create: `-`; prefix from Setting `printedparts_code_prefix` (read via `self.get_setting` or Setting model through `shopdb.api`). Two-step: insert, flush to get the id, set code. 3. `get_permissions()` on the plugin class: view/create/edit/delete/restock (tuples, category `printedparts` - copy shape from `plugins/usb/plugin.py`). Gate mutations with `@jwt_required()` + `@require_permission(...)`; reads are `@jwt_required(optional=True)`. 4. Restock + adjust endpoints: both write a LEDGER row and move `quantityonhand` in the same commit. Adjust requires `reason`, rejects a result below zero. Both record the operator: accept `badge` in the body and resolve it (M4 extracts the resolver - for now digits=SSO is enough). 5. Seed the 3 settings in `on_install` (Setting.set pattern - see how other plugins seed in `on_install`; category `printedparts`). Checkpoint ``` pytest plugins/printedparts/tests/ -q # write tests as you go: # - create mints 3DP-0001 style codes # - restock/adjust move both ledger and cache atomically # - adjust below zero -> 400/422 # - permission gates: anonymous create -> 401, wrong-perm user -> 403 curl -s localhost:5001/api/printedparts/items | jq # anonymous list OK ``` ## Milestone 3 - management frontend + images Build 1. The scaffold already dropped `PrintedpartsList/Detail/Form.vue` starters and a router file; rename/build them into `PrintedItemsList/Detail/Form` per the spec. Master templates: `PrintersList.vue` (list), `PrinterDetail.vue` (detail). Global CSS only; CSS variables for colors (frontend/CLAUDE.md rules). 2. Register the API client in `frontend/src/api/index.js` (paste the generated `frontend-api-snippet.js`, extend with restock/adjust/image calls). 3. Low-stock highlighting on the list (`quantityonhand <= lowstockthreshold` -> danger badge). Filters: search + low-stock-only checkbox. 4. Image upload: replicate the models-image trio - upload/serve/delete - from `shopdb/core/api/models.py` INTO the plugin blueprint (`instance/printedpartsimages/`, public GET serve, imageurl column, prefix-guarded delete). Wire the Form upload + Detail hero image. 5. Nav: `get_navigation_items()` -> "3D Parts" (usb plugin shape). Router meta: list/detail plugin-gated, new/edit `requiresAuth` (see `frontend/src/router/routes/usb.js`). Checkpoint: create an item with a photo in the UI; thumbnail on list, hero on detail; restock from detail updates qty + shows in history; frontend build + `npx vitest run` green; naming hook green. ## Milestone 4 - badge resolution + kiosk Build 1. Badge resolver in the plugin (`services/badges.py`): copy the USB contract - all-digits -> SSO; `^0(\d+)BZ$` case-insensitive -> PayNo; resolve display name via the employees plugin directory the way `plugins/usb/api/selfhosted.py::_resolve_name` does (lazy import inside the function, degrade gracefully when the plugin is absent). Policy setting `printedparts_unknown_badge` (deny -> 422). 2. Kiosk endpoints (UNauthenticated - the notifications/employees open-read precedent): `GET /kiosk/item/` and `POST /kiosk/take` {itemcode, badge, quantity}. Take: validate active item, 1 <= qty <= onhand, resolve badge, single-commit ledger row + decrement. Clear error strings - the kiosk displays them verbatim. 3. Kiosk view `/parts-kiosk`: top-level route, NO requiresAuth, outside AppLayout (register beside `/shopfloor` in `frontend/src/router/index.js`). Three-step flow per the spec. The scanner is a keyboard wedge: hidden always-focused input, submit on Enter, route the scan to whichever step is active. Build `TouchKeypad.vue` (3x4 grid of big buttons, emits digits/ clear/backspace) - net-new, nothing to copy, keep it dumb. 4. Manual fallback path (typed item search + badge entry) behind a small "no scanner?" link. Checkpoint: full kiosk walkthrough on a touchscreen (or browser): scan/type an itemcode -> item card; badge `0123456BZ` and plain SSO both resolve; take 3 -> success screen, qty down 3, ledger row has your name; taking more than onhand -> friendly error; unknown badge -> denied message. Backend tests for the resolver shapes + take validation. ## Milestone 5 - labels (1in x 0.5in) Build 1. Public print route `/print/printedparts-labels` + view (imitate `USBLabelBatch.vue` - USB is the precedent for a plugin OWNING its label page instead of joining TYPE_CONFIG). 2. New stock size: `@page { size: 1in 0.5in; margin: 0 }`, one label per page (roll-fed label printers treat each page as one label). Layout: CODE128 via JsBarcode (~0.9in wide, displayValue false), itemcode text ~7pt under it, optional truncated name. Offer QR as a variant but default barcode. 3. Batch: multi-select items -> sequence of labels; plus a ULINE mini-grid sheet fallback (mini72 pattern in `AssetLabelBatch.vue`). 4. Print buttons on Detail (single) and List (batch selected). Checkpoint: print preview shows one 1x0.5 label per page; a printed (or PDF-zoomed) barcode scans back into the kiosk and pulls up the right item. That round trip - label printed from the catalog, scanned at the kiosk, stock decremented with your name on it - is the demo moment; make it work end to end before polishing. ## Milestone 6 - metrics, reports, widget Build 1. `get_reports()` -> stock, consumption (date range), by-person; endpoints in the plugin blueprint, `@jwt_required(optional=True)`, `?format=csv` via the `generate_csv` helper pattern (`shopdb/core/api/reports.py` shows the shape; a plugin report lives in the plugin and is merged into `GET /api/reports` automatically when enabled). 2. Stock report includes the reconcile check: flag rows where cached `quantityonhand` != SUM(ledger). Should always be empty; if not, you have a non-atomic write path - find it. 3. `get_dashboard_widgets()` -> low-stock count. 4. Nice-to-have if time: burn rate (avg weekly takes over trailing 4 weeks + weeks-to-empty). Plain SQL over the ledger. Checkpoint: reports appear on /reports grouped under the plugin, CSV downloads; widget renders on the dashboard; reconcile column all-clear after a kiosk session. ## Milestone 7 - lifecycle + closeout Build/verify 1. Disable/enable cycle: `flask plugin disable printedparts` - nav entry, routes, reports, and grantable permissions all disappear; enable restores. 2. Fresh-database proof: point DATABASE_URL at a scratch DB, `flask db upgrade` + `flask plugin install/enable/upgrade-all` - everything works with zero manual SQL. 3. Full suite: backend pytest, vitest, frontend build, naming hook. 4. End checklist from `PLUGIN-GUIDE.md` section 12. Done means: a colleague can clone the repo, enable the plugin, print a bin label, and take a part at the kiosk with their badge - without asking you anything. --- ## Where each pattern lives (cheat sheet) | Need | Copy from | |---|---| | Standalone (non-asset) plugin shape | `plugins/knowledgebase/` | | Checkout/ledger + badge contract | `plugins/usb/` (`api/routes.py` badge regex, `api/selfhosted.py` name resolve) | | Real-baseline plugin migration | `plugins/measuringtools/migrations/` | | Blueprint style, pagination, authz | `plugins/measuringtools/api/routes.py` | | Image upload/serve/delete | `shopdb/core/api/models.py` | | Open kiosk endpoints precedent | `plugins/employees/api/routes.py`, `plugins/notifications/api/routes.py` | | Plugin-owned label print view | `frontend/src/views/print/USBLabelBatch.vue` | | Barcode/QR rendering | JsBarcode usage in `AssetLabel.vue`, `qrLogo.js` | | Kiosk route posture | `/shopfloor` in `frontend/src/router/index.js` | | List/Detail master templates | `PrintersList.vue`, `PrinterDetail.vue` | | Reports hook + CSV | `plugins/warranty/` report + `shopdb/core/api/reports.py` | | Permissions declaration | `plugins/usb/plugin.py::get_permissions` |