# Content Generation & WooCommerce Integration Status: SPEC · 2026-08-24 · SlabUploader Covers: FR16 (deterministic content templates + optional LLM refinement), FR17 (brand voice / GEO), FR18 (editable, never auto-published), FR25–28 (taxonomy sync, product create, sync log), and the exact WooCommerce REST v3 payload. ## 1. Content generation (FR16–18) ### 1.1 Deterministic templates (always on) Templates are configured in Settings (`content_templates`, editable per FR32). Defaults below. Placeholders use `{field}`. **title** (default): ``` {species} {length_ft}ft {length_in_remainder}in × {width_avg_in}in Wood Slab — {character} ``` Example: `Black Walnut 16ft 0in × 18.2in Wood Slab — Cathedral` - `length_ft` = floor(length_in / 12), `length_in_remainder` = length_in mod 12 (1 dp). **short_title** (default): ``` {species} Slab — {character} ``` Example: `Black Walnut Slab — Cathedral` **description** (default, 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} ``` `geo_sentence` = GEO context sentence from Settings (FR17), e.g. `"Locally milled and shipped from the Pacific Northwest."` (default if unset: none). All template math fields come from the deterministic pipeline (never from the LLM). ### 1.2 Optional LLM refinement (FR17, toggleable) - Triggered only if `content_llm_enabled` is true AND the user taps "Refine with AI" on the review screen (never automatic — FR18). - Prompt (server-side, assembled): system = brand voice + GEO context; user = the template-generated title/description + slab facts; task = "improve the title and description for a premium one-of-a-one wood slab listing. Keep all measurements exact. Do not invent features. Return JSON: {title, short_title, description}." - **Guardrail (deterministic)**: after the LLM returns, the server re-checks that every numeric dimension in the LLM text still matches the pipeline values (regex on numbers). Mismatch → reject the LLM output, keep the template version, log the rejection. This keeps measurement accuracy deterministic even when the LLM is on. - Output is always editable (FR18); `content_source` = `llm` when used. - If the inference endpoint is text-only or unreachable → the button is disabled, templates still work. No failure mode blocks publish. ### 1.3 Audit Every content generation (template or LLM) is logged with inputs (template version, slab facts), the LLM request/response payload when used (PRD "inference calls logged"), and the final stored content. ## 2. WooCommerce integration (FR24–28) ### 2.1 Auth & client - REST v3: `GET/POST/PATCH/DELETE {woo_base_url}/wp-json/wc/v3/…` - Basic auth: `woo_consumer_key:woo_consumer_secret` (Decision 10; the "username" is the consumer key, the "app password" is the consumer secret). - Client: `httpx` with timeouts (connect 10s, read 60s for image-heavy create). - `woo_base_url` default: `https://www.whidbeywoodstore.com`. ### 2.2 Taxonomy sync (FR25) `POST /api/admin/taxonomy/sync` pulls, in order: 1. `GET /products/categories?per_page=100` (paginate) → `woo_taxonomy(kind=category)` 2. `GET /products/tags?per_page=100` → `kind=tag` 3. `GET /products/attributes` → `kind=attribute` Rebuilds the cache table (delete + insert, single transaction). Returns counts. Failure → `502 woo_error`; previous cache retained (never delete-then-fail). ### 2.3 Auto-assign taxonomy (FR26) - **Category**: species value is matched to a Woo category by name (case-insensitive, trimmed). Match → that category id. No match → the slab's `category_override` (user picks in review) or empty (Woo required-field validation will surface it). - **Tags**: `[character]` plus optional `[species]` if present as a tag. - **Attributes**: species/character/length/thickness as product attributes if the corresponding Woo attributes exist in the cache (by name). Missing attributes are skipped, not created (v1: no attribute creation). - Every assignment is overridable in the review screen (FR26). ### 2.4 Product create payload (FR27) `POST /products` — body (only non-null fields sent): ```jsonc { "name": "{title}", "type": "simple", "status": "pending", // test-target decision: NEVER "publish" automatically "sku": "{sku}", "regular_price": "{price}", // 2 dp string, e.g. "2730.00" "description": "{description}", "short_description": "{short_title}", "categories": [ { "id": } ], "tags": [ { "id": }, … ], "images": [ { "src": "", "alt": "{species} {character} slab" } ], "attributes": [ … if present … ], "virtual": false, "downloadable": false, "catalog_visibility": "visible" } ``` **Image upload strategy** (Woo v3): `images[].src` accepts a URL or a data URL. For reliability the server: 1. For each customer-facing (inventory, normalized) photo in `seq` order: `POST /media` with the JPEG bytes → get attachment `source_url`. 2. Reference those `source_url` values in `images` (≤5, per PRD 1–5 inventory photos). 3. Calibration photos are excluded (FR2). **Status = `pending`** by default (test-target decision: test products must never go live without human action). The Settings key `woo_publish_status` (default `pending`) controls this; Ty flips it to `publish` only when intentionally going live. ### 2.5 Dedupe & idempotency (FR24a) Before create: `GET /products?sku={sku}`. - Found → `409 duplicate_sku` (v1 has no update path; human resolves). - Not found → proceed. (Single-owner, slab-by-slab, so no TOCTOU concern in practice; the check is still done for correctness.) ### 2.6 Sync result & logging (FR28) - On success: set `slabs.woo_product_id`, `status=published`, `published_at`, write `sync_log(status=success, woo_product_id, payload_hash)`. - On Woo HTTP error: `status=failed`, `sync_log(status=failed, http_status, error, detail=)`. Surface in UI + poll response. - **Partial-failure note** (risk, see IMPL-PLAN): if media uploads succeed but the final product POST fails, orphaned Woo attachments may exist. v1: log it; a manual cleanup note is surfaced. Not auto-deleted (could delete a real image). ## 3. End-to-end publish sequence (server side) ``` 1. validate slab publishable (length,width,thickness,price,species,char,≥1 photo) 2. dedupe by SKU (GET /products?sku=) → 409 duplicate_sku if exists 3. (re)sync taxonomy if cache older than 1h → best-effort; use existing cache on fail 4. resolve category/tags/attributes (auto-assign + overrides) 5. upload normalized inventory images → media → source_urls 6. POST /products (status=pending) → woo_product_id 7. write sync_log(success), set slab published, purge images on any failure at 2–6: write sync_log(failed), slab=failed, (images retained for retry) ``` ## 4. What is deterministic vs scoped-inference here - Deterministic: templates, all dimension text, category/tag/attribute resolution, payload assembly, dedupe, logging. - Scoped inference: optional LLM title/description refinement only, with the numeric-accuracy guardrail (§1.2).