d754cac9ef
Copy listing-copy/Woo payload spec from main. Lock {character} to the
primary Figure term name (Cathedral example). Lock categories[] to all
ready-set species and wood-category ids.
432 lines
20 KiB
Markdown
432 lines
20 KiB
Markdown
# Milestone 3 Create
|
||
|
||
## Purpose
|
||
|
||
Milestone 3 takes a Photos-ready device draft and makes the slab **submission-ready for WooCommerce**.
|
||
|
||
Submission-ready means the mill owner has filled the full store listing package against **live synced Woo attributes**, with price set, and can create the product in the store (practice SKUs as Woo drafts). Photos and measures from Milestones 1 and 2 are inputs. They are not enough by themselves.
|
||
|
||
**Exit criteria.** On the M3 UAT deploy, a Photos-ready slab reaches submission-ready and a `SLAB-UAT-*` product exists in WooCommerce as a **draft** whose categories, attributes, tags, price, and inventory images match the reviewed ready set. Create still works with inference off.
|
||
|
||
## How to read this file
|
||
|
||
This file is the Milestone 3 source of truth on branch `m3-ds`.
|
||
|
||
- **What is in M3** comes from Ty conversations on this branch (not from older milestone splits on `main`).
|
||
- **How Woo sync, metadata, pricing, and inference behave** is taken from the technical requirements on `main` (`CONTENT-WOO`, `DATA-MODEL`, `UX`, `ARCHITECTURE`, `OPENAPI`, `AGENTS`, `UAT-PRACTICE`, `DEPLOYMENT`) and written here so coding does not need to open `main`.
|
||
|
||
Related shipped specs: `docs/CAPTURE.md`, `docs/PHOTOS.md`. Listing copy and Woo payload detail: `docs/CONTENT-WOO.md` (from `main`). Product overview: `docs/PRODUCT.md`. Folder map: `docs/README.md`.
|
||
|
||
## Preconditions (already shipped)
|
||
|
||
On this branch before M3 work:
|
||
|
||
- Milestone 1 Capture works in the browser (`docs/CAPTURE.md`).
|
||
- Milestone 2 Photos works in the browser (`docs/PHOTOS.md`). UAT evidence on `:10881`.
|
||
- Device draft holds Photos-ready inventory PNGs, calibration measures, and related state.
|
||
- `frontend/` holds that client path. `backend/` does not yet implement the Woo client or inference proxy.
|
||
|
||
M3 adds the server path, Settings, taxonomy sync, review/ready fields, pricing, inference dependency, and Woo create.
|
||
|
||
## In scope
|
||
|
||
1. WooCommerce auth with WordPress application password (server-side).
|
||
2. Sync of **Woo attributes** from the store and correct use of those lists for the listing.
|
||
3. Review UI and rules to reach submission-ready (full ready set).
|
||
4. Pricing proposal from species `$/bdft` rules and user override.
|
||
5. OpenAI-compatible inference endpoint as a Settings dependency, with Call 1 / Call 2 behavior when enabled.
|
||
6. Create product in WooCommerce (force draft for `SLAB-UAT-*`), then clear the device draft on success.
|
||
|
||
## Out of scope unless Ty expands M3 in chat
|
||
|
||
- Creating new Woo categories, attributes, terms, or tags from the app.
|
||
- Updating an existing store SKU in place.
|
||
- Bulk create.
|
||
- Touching deploy port `:10880`.
|
||
- Dumping work onto `m2-ds` or doc-simplification branches.
|
||
|
||
---
|
||
|
||
## Woo attributes
|
||
|
||
Locked name for the synced taxonomy package. WooCommerce is the source of truth. The app caches ids. It does not invent the lists.
|
||
|
||
### Categories (product categories, leaf only)
|
||
|
||
| Field | Cardinality | Rule |
|
||
|---|---|---|
|
||
| Species | exactly 1 | Synced species leaves only. No hardcoded or fallback species set. A new store species appears only after the next successful sync. |
|
||
| Wood category | 1 or more | Leaf ids. Parents are grouping only. |
|
||
|
||
### Attributes (exactly five locked Woo attributes)
|
||
|
||
| Attribute | Cardinality | Rule |
|
||
|---|---|---|
|
||
| Edge Type | 1 | Term id from sync. |
|
||
| Figure | 1 or more | Term ids from sync. |
|
||
| Grade | 1 or more | Term ids from sync. |
|
||
| Thickness | 1 | Measured thickness rounded **up** into the store band (example: 1" lands in the store band labeled like `1"-1 1/2"` when that is the store label). |
|
||
| Moisture | 1 | Defaults to kiln-dried. User sets or keeps it. Never inferred. |
|
||
|
||
v1 does not create missing attributes or terms. If a term is missing in Woo, skip it. Do not invent it.
|
||
|
||
Species, character, length, and free-text thickness are **not** Woo product attributes. Only the five locked attributes go on the create payload.
|
||
|
||
### Tags
|
||
|
||
| Prefix | Cardinality | Rule |
|
||
|---|---|---|
|
||
| `fig-*` | 1 or more | Required. Other Woo tags are ignored at sync. |
|
||
| `feat-*` | 0 or more | Optional. Character, inclusions, voids, and checks map onto existing `feat-*` tags only. |
|
||
|
||
---
|
||
|
||
## Auth and store URL
|
||
|
||
From `main` deploy and agents rules, adopted here:
|
||
|
||
- FastAPI is the only Woo caller. The browser never holds store credentials.
|
||
- Auth is a WordPress **application password** for a dedicated low-privilege WP user (shop manager or custom role with product create/edit and media upload). Not WooCommerce consumer keys (`ck_` / `cs_`). Not the store admin login password.
|
||
- Basic auth shape: `woo_wp_username` + `woo_app_password`.
|
||
- Store URL is **`WOO_BASE_URL`** from `deploy/.env` / Docker Compose (`https://` only). Injected into the FastAPI container. Settings shows it read-only. Not a SQLite settings row. Change store means edit `.env` and recreate the FastAPI container.
|
||
- Username and application password are entered in Settings UI, stored AES-GCM encrypted in SQLite (`SLAB_AES_KEY` env).
|
||
- REST surface: `{WOO_BASE_URL}/wp-json/wc/v3/…`
|
||
- Client: `httpx` with connect timeout 10s and read timeout 60s for image-heavy create.
|
||
- Rotate: revoke the application password in WP Users → Profile, enter a new one in Settings, then run `test-woo`.
|
||
|
||
---
|
||
|
||
## Taxonomy sync
|
||
|
||
Endpoint and behavior from `main` OPENAPI / CONTENT-WOO / ARCHITECTURE:
|
||
|
||
`POST /api/v1/admin/taxonomy/sync` pulls, in order:
|
||
|
||
1. `GET /products/categories?per_page=100` (paginate) → species and wood-category leaves after app filter.
|
||
2. `GET /products/tags?per_page=100` → keep only `fig-*` and `feat-*`.
|
||
3. `GET /products/attributes` plus terms → the five locked attributes.
|
||
|
||
Rebuild cache tables in one transaction. On failure return `502 woo_error` and **keep** the previous cache. Never delete-then-fail.
|
||
|
||
**Stamps.** Woo REST has no usable last-modified for categories, attributes, or tags. FastAPI stores:
|
||
|
||
- `taxonomy_last_sync_at` on every successful pull
|
||
- `taxonomy_anchor` bumped only when the stored-subset hash changes (including deletions)
|
||
|
||
Client re-pulls the full taxonomy only when the anchor advances.
|
||
|
||
**Triggers.**
|
||
|
||
1. Successful `POST /api/v1/settings/test-woo` always resyncs.
|
||
2. On publish, if `now - taxonomy_last_sync_at > 1 hour`, reconcile (bump anchor only on real change).
|
||
3. Optional app launch or Settings refresh.
|
||
|
||
If a picked id disappeared after refresh, show that field as no longer valid. Do not silently swap another value.
|
||
|
||
`GET /api/v1/admin/taxonomy` returns the cached groups plus `taxonomy_anchor` and `taxonomy_last_sync_at`.
|
||
|
||
---
|
||
|
||
## Submission-ready (ready set)
|
||
|
||
A slab is submission-ready only when **all** of the following are present. Species plus one figure is not enough.
|
||
|
||
Required:
|
||
|
||
- At least one inventory PNG (from Milestone 2)
|
||
- SKU matching `[A-Z0-9-]{3,24}` (practice runs use `SLAB-UAT-*`)
|
||
- Length and thickness (from capture, still editable on review)
|
||
- Client-computed widths, sqft, and bdft (user typed number wins if overridden)
|
||
- Exactly one species leaf
|
||
- At least one wood-category leaf
|
||
- One edge type term
|
||
- At least one figure term
|
||
- At least one grade term
|
||
- Thickness band term (round-up from measured thickness)
|
||
- One moisture term (default kiln-dried)
|
||
- At least one `fig-*` tag
|
||
- Price (see Pricing)
|
||
|
||
Optional:
|
||
|
||
- `feat-*` tags (zero or more)
|
||
- Title, short title, description (may be typed, filled by templates at create, or written by Generate text when inference is on). Not required to reach ready.
|
||
|
||
Inline per-field nudges show empty required fields as the owner works. The slab cannot reach ready until the full set is filled.
|
||
|
||
Server status name for this state: `ready` (see data model below). Publish/create is blocked with `422 unpublishable` until then.
|
||
|
||
---
|
||
|
||
## Pricing
|
||
|
||
From `main` DATA-MODEL / UX / AGENTS:
|
||
|
||
- Settings (or admin API) holds `pricing_rules`: one flat `$/bdft` per species name that matches a synced species leaf. No tiers in v1.
|
||
- Seed rule rows from synced species. The mill owner sets the rate per species.
|
||
- Proposal: `price = bdft × price_per_bdft` for the selected species.
|
||
- User may replace the proposal. Override sticks (`price_source` = `override` vs `recommendation`).
|
||
- If no rule exists for the species, price starts empty and the owner types it.
|
||
- Publish/create is allowed with a manual price.
|
||
- `bdft = sqft × thickness_in` (deterministic).
|
||
|
||
Admin API surface from `main` OPENAPI:
|
||
|
||
- `GET /api/v1/admin/pricing`
|
||
- `POST /api/v1/admin/pricing`
|
||
- `PUT /api/v1/admin/pricing/{id}`
|
||
- `DELETE /api/v1/admin/pricing/{id}`
|
||
|
||
---
|
||
|
||
## Inference
|
||
|
||
M3 includes the OpenAI-compatible endpoint as a real server dependency and the Call 1 / Call 2 rules below. Create must pass with inference **off**.
|
||
|
||
### Settings (not compose)
|
||
|
||
Stored encrypted in SQLite Settings (not `deploy/.env`):
|
||
|
||
- `inference_base_url`
|
||
- `inference_api_key`
|
||
- `inference_model`
|
||
- `inference_enabled`
|
||
- `content_llm_enabled`
|
||
|
||
FastAPI proxies all inference calls. The browser never holds the key.
|
||
|
||
`POST /api/v1/settings/test-inference` returns `{ ok, mode: stateful|stateless|fail, vision_capable, detail }`. Mode `fail` blocks Call 2 enablement only. Inference-off create still works.
|
||
|
||
### Call 1 (vision / taxonomy assist)
|
||
|
||
- Runs only when `inference_enabled` is true (auto after calibrated draft upload when on, or via `POST /api/v1/slabs/{id}/infer-taxon`).
|
||
- Input: inventory photos downscaled to max 1024px, user metadata, synced Woo taxonomy lists.
|
||
- Output: species, wood categories, edge, figure, grade, and feature-tag suggestions constrained to synced lists, each with confidence.
|
||
- Default confidence threshold 0.7 (configurable). At or above threshold: pre-fill on review, still editable. Below: leave empty. Do not show low-confidence guesses as defaults.
|
||
- Moisture is never inferred.
|
||
- Typed values always win over model values.
|
||
- Vision never blocks create and never auto-publishes.
|
||
|
||
### Call 2 (Generate text)
|
||
|
||
- User-triggered only (`POST /api/v1/slabs/{id}/infer-content`). Never auto.
|
||
- Requires successful `test-inference` (`ok: true`).
|
||
- Model writes prose only (character, color, grain, uses). It must not invent dimensions.
|
||
- Server assembles title, short title, and description from templates with prose injected.
|
||
- If the endpoint is down, disable the button. Templates still work. Create is never blocked by Generate text failure.
|
||
|
||
### Templates (always available)
|
||
|
||
Full defaults and Woo payload text live in `docs/CONTENT-WOO.md`. Summary here:
|
||
|
||
**title**
|
||
|
||
```
|
||
{species} {length_ft}ft {length_in_remainder}in × {width_avg_in}in Wood Slab - {character}
|
||
```
|
||
|
||
Example from CONTENT-WOO: `Black Walnut 16ft 0in × 18.2in Wood Slab - Cathedral`
|
||
|
||
**short_title**
|
||
|
||
```
|
||
{species} Slab - {character}
|
||
```
|
||
|
||
**description** (multi-paragraph)
|
||
|
||
```
|
||
A one-of-a-one {character} {species} slab, {length_ft}ft {length_in_remainder}in long with an
|
||
average width of {width_avg_in}in and {thickness_in}in thickness ({bdft} bdft).
|
||
|
||
Each slab is naturally shaped. Width varies along the length at {width_min_in}in to
|
||
{width_max_in}in. Sold by the slab; dimensions as measured. {geo_sentence}
|
||
```
|
||
|
||
**What `{character}` means (locked from CONTENT-WOO example + PROMPTS on `main`).**
|
||
The word after the dash in the example is `Cathedral`, which is a **Figure** attribute term.
|
||
Fill `{character}` with the primary (first) Figure term **name** from the ready set.
|
||
If somehow no figure is present, leave that slot empty (ready set normally requires ≥1 figure).
|
||
Do not confuse this with `feat-*` tags. Call 1 free-text "character / inclusions / voids / checks" map to `feat-*` tags, not to this title slot.
|
||
|
||
All template math fields come from the deterministic pipeline, never from the LLM.
|
||
|
||
---
|
||
|
||
## Review and create flow
|
||
|
||
Mill-owner path after Photos-ready:
|
||
|
||
1. Upload calibrated / Photos-ready draft to the server (`POST /api/v1/slabs` multipart) when the M3 client reaches that step. Status becomes `calibrated`. If inference is on, Call 1 may run.
|
||
2. **Review.** Owner fills or overrides every ready-set field. Pickers show only synced Woo attributes.
|
||
3. When the ready set is complete, status becomes `ready` (submission-ready).
|
||
4. Owner taps create/publish → `POST /api/v1/slabs/{id}/publish`.
|
||
5. Server runs the create sequence below. Client polls `GET /api/v1/slabs/{id}`.
|
||
|
||
### Create sequence (server)
|
||
|
||
1. Check the listing against the ready-set rules.
|
||
2. Look up SKU in the store (`GET /products?sku={sku}`, any status). Stop if found.
|
||
3. Refresh taxonomy if last successful pull is older than 1 hour.
|
||
4. Resolve categories, tags, and attributes from the synced lists.
|
||
5. Upload inventory PNGs via `POST /media`, keep alpha. Reference `source_url` values on the product. Cap at 5 images. **Woo image order (locked):** (1) clean calibration inventory PNG, (2) dimension-overlay copy of that calibration PNG, (3+) remaining batch inventory PNGs in the order they were added in the web app. The overlay is part of the submission. Do not upload non-inventory artifacts outside that lineup.
|
||
6. `POST /products` with status from Settings `woo_create_status` (`draft` or `publish`, default `draft`). **`SLAB-UAT-*` always forces draft.**
|
||
7. On success: log, set `woo_product_id`, mark published locally, delete original and processed photos, clear device draft.
|
||
8. On failure: log, mark failed, keep photos, put a field-level error on the phone. No raw status codes for the mill owner.
|
||
|
||
### Create payload (shape)
|
||
|
||
`POST /products` sends only non-null fields. Conceptual body:
|
||
|
||
```jsonc
|
||
{
|
||
"name": "{title}",
|
||
"type": "simple",
|
||
"status": "{woo_create_status}",
|
||
"sku": "{sku}",
|
||
"regular_price": "{price}",
|
||
"description": "{description}",
|
||
"short_description": "{short_title}",
|
||
"categories": [ { "id": <species leaf id> }, { "id": <wood-category leaf id> }, ... ],
|
||
"tags": [ { "id": <fig-* and feat-* ids> } ],
|
||
"images": [ { "src": "<media source_url>", "alt": "{species} {character} slab" } ],
|
||
"attributes": [ /* Edge, Figure, Grade, Thickness, Moisture term ids when present */ ],
|
||
"virtual": false,
|
||
"downloadable": false,
|
||
"catalog_visibility": "visible"
|
||
}
|
||
```
|
||
|
||
App "published" means the store create succeeded. That is not the same as the Woo product being live when status is draft.
|
||
|
||
### Duplicate SKU
|
||
|
||
Before create, if the SKU exists in any store status: block. Show Edit SKU and Open existing listing. This version does not update the other product. API: `409 duplicate_sku`.
|
||
|
||
### Stale taxonomy on submit
|
||
|
||
If a category, attribute, or price is no longer valid after refresh: under that field, tell the owner to pick from the current list and supply current options (`stale_field`).
|
||
|
||
---
|
||
|
||
## Settings keys (M3)
|
||
|
||
| Key | Where | Notes |
|
||
|---|---|---|
|
||
| `WOO_BASE_URL` | compose / `.env` | HTTPS store URL. Read-only in UI. |
|
||
| `SLAB_AES_KEY` | compose / `.env` | AES master key. Never in DB or repo. |
|
||
| `woo_wp_username` | Settings (encrypted) | Dedicated WP user. |
|
||
| `woo_app_password` | Settings (encrypted) | Application password. |
|
||
| `woo_create_status` | Settings | `draft` or `publish`. Default `draft`. |
|
||
| `inference_base_url` | Settings (encrypted) | OpenAI-compatible base URL. |
|
||
| `inference_api_key` | Settings (encrypted) | |
|
||
| `inference_model` | Settings | |
|
||
| `inference_enabled` | Settings | |
|
||
| `content_llm_enabled` | Settings | Gates Generate text. |
|
||
| `taxonomy_last_sync_at` | Settings / derived | |
|
||
| `taxonomy_anchor` | Settings / derived | |
|
||
| `brand_voice`, `geo_context` | Settings | Copy helpers. |
|
||
|
||
Relevant Settings API: `GET/PUT /api/v1/settings`, `POST /api/v1/settings/test-woo`, `POST /api/v1/settings/test-inference`.
|
||
|
||
---
|
||
|
||
## Data shape (submission-ready fields)
|
||
|
||
Core slab fields M3 must persist (from `main` DATA-MODEL, adopted here):
|
||
|
||
| Field | Rule |
|
||
|---|---|
|
||
| `sku` | Required. Unique. |
|
||
| `status` | `draft` → `calibrated` → `ready` → `publishing` → `published`, or `failed` |
|
||
| `length_in`, `thickness_in` | Required. 1/8" step. |
|
||
| `sqft`, `bdft`, widths | From client math. |
|
||
| `species_id` | 1 species leaf. |
|
||
| `wood_category_ids` | 1 or more. |
|
||
| `edge_type_term_id` | 1. |
|
||
| `figure_term_ids` | 1 or more. |
|
||
| `grade_term_ids` | 1 or more. |
|
||
| `thickness_term_id` | Round-up band. |
|
||
| `moisture_term_id` | 1. Default kiln-dried. |
|
||
| `fig_tag_ids` | 1 or more. |
|
||
| `feat_tag_ids` | 0 or more. |
|
||
| `price`, `price_per_bdft`, `price_source` | See Pricing. |
|
||
| `title`, `short_title`, `description` | Optional for ready. |
|
||
|
||
Cache tables: `woo_categories`, `woo_attributes`, `woo_attribute_terms`, `woo_tags`, `pricing_rules`.
|
||
|
||
Device draft (IndexedDB on the phone) is not a WooCommerce draft. Clear the device draft after successful create.
|
||
|
||
---
|
||
|
||
## Practice / UAT rules
|
||
|
||
- Every practice listing uses SKU prefix `SLAB-UAT-*`.
|
||
- `SLAB-UAT-*` always creates a WooCommerce draft, even if Settings say publish.
|
||
- Customers must not see practice listings on the storefront.
|
||
- Inference is **off** for the claimable exit walk. Optional ON path only after inference Settings pass `test-inference`.
|
||
- UAT deploy for this branch: ubudual5060 `http://100.64.0.9:10881`. Do not use `:10880`.
|
||
|
||
---
|
||
|
||
## API surface M3 must implement (summary)
|
||
|
||
Base path `/api/v1` (health may also be `/api/health`).
|
||
|
||
| Area | Methods |
|
||
|---|---|
|
||
| Health | `GET /api/health` |
|
||
| Slabs | `POST/GET/PUT/DELETE /api/v1/slabs`, `GET /api/v1/slabs/{id}` |
|
||
| Photos | original/processed GET while retained |
|
||
| Inference | `POST .../infer-taxon`, `POST .../infer-content` |
|
||
| Publish | `POST .../publish` → 202, poll slab |
|
||
| Taxonomy | `GET/POST /api/v1/admin/taxonomy`, `.../sync` |
|
||
| Pricing | CRUD `/api/v1/admin/pricing` |
|
||
| Settings | `GET/PUT /api/v1/settings`, `test-woo`, `test-inference` |
|
||
|
||
Full request and response field lists on historical `main` `docs/OPENAPI.md` are the detail reference if a field is missing here. Prefer extending this file over coding from `main` directly.
|
||
|
||
---
|
||
|
||
## Done when (exit criteria)
|
||
|
||
**Product state induced.** Overnight UAT app on ubudual5060 `http://100.64.0.9:10881` (or the current M3 image of this branch), with `WOO_BASE_URL` and Woo application password pointed at the intended store.
|
||
|
||
**Evidence from that state.**
|
||
|
||
1. `test-woo` succeeds and taxonomy sync fills Woo attributes into the app cache. Pickers offer only synced ids.
|
||
2. A Photos-ready slab can be filled to the **full ready set** (submission-ready) using those lists and pricing rules, with **inference off**.
|
||
3. Create with `SLAB-UAT-*` yields a WooCommerce **draft** whose categories, attributes, tags, price, and inventory images match the reviewed ready set.
|
||
4. Duplicate SKU is blocked. Device draft clears after success.
|
||
5. Inference Settings accept an OpenAI-compatible base URL, key, and model. `test-inference` runs. Create still succeeds with inference disabled.
|
||
|
||
## Dishonest proxies (do not count as pass)
|
||
|
||
- Hardcoded or fixture taxonomy lists treated as synced.
|
||
- Mock Woo client that never hits the real store for the exit walk.
|
||
- Unit tests alone without a real `SLAB-UAT-*` draft in the store.
|
||
- Consumer-key auth instead of application password.
|
||
- Inference "wired" as dead config with no server proxy, or create that requires inference on.
|
||
- Claiming submission-ready when required ready-set fields are still empty.
|
||
- Touching `:10880` or parking work on `m2-ds` / doc-simplification.
|
||
|
||
## Open decisions (must lock before task breakdown)
|
||
|
||
Locked from Ty conversation or from `docs/CONTENT-WOO.md`:
|
||
|
||
1. **`categories` on `POST /products`.** **Locked from CONTENT-WOO field rules:** send every ready-set category id (species leaf + each wood-category leaf) as `[{"id": n}, ...]`. The single-id example in CONTENT-WOO §2.4 is incomplete relative to those field rules.
|
||
|
||
2. **Template `{character}`.** **Locked from CONTENT-WOO example (`… Wood Slab - Cathedral`):** use the primary Figure term **name**. `feat-*` tags are separate (Call 1 free-text character/inclusions/voids/checks map there).
|
||
|
||
3. **Dimension-overlay PNG.** **Locked (Ty 2026-09-16):** Woo image order is (1) clean calibration PNG, (2) dimension-overlay copy, (3+) other inventory PNGs in add order. Cap ≤5.
|
||
|
||
Still open (CONTENT-WOO does not fully settle these):
|
||
|
||
4. **`attributes` element shape on `POST /products`.** CONTENT-WOO still has `"attributes": [ … if present … ]` with no fields. Need a live Whidbey product JSON sample, or Ty pointing at the official Woo REST fields to use. Do not invent.
|
||
|
||
5. **Server persistence.** CONTENT-WOO describes create sequence and payload. It does not choose full OpenAPI slab CRUD vs a thinner FastAPI path on this branch. Still need a lock before coding the server store.
|
||
|
||
Non-blocking notes (shipped M1/M2 win on this branch): fill target 90% and 800px floor (not main's 80%/1600); Vite + Svelte SPA (not SvelteKit docs); overlay draw spacing may differ from measure spacing by design.
|
||
|
||
If store-specific term labels differ from examples in this file, the synced store lists win.
|