feat(publish): S6 Woo create sequence + UAT draft + dup/stale/partial failure + Publish UI

- backend/publish.py: POST /slabs/{id}/publish. Sequence (PUBLISH-5): validate ready
  set -> SKU lookup (409 on dup, PUBLISH-15) -> refresh taxonomy if >1h (SETTINGS-60)
  -> resolve cats/tags/5 attributes -> upload processed PNGs -> POST /products ->
  finalize (set woo_product_id, purge photos, PUBLISH-25). SLAB-UAT-* always draft
  (PUBLISH-10). Failure: mark failed, retain photos, sync_log, plain recoverable/
  unrecoverable message (PUBLISH-30/36/37/38). Partial (media ok, POST failed):
  orphan media logged + manual cleanup note (PUBLISH-31). Payload: only non-null,
  slug = name+sku, sold_individually true (PUBLISH-55/60).
- frontend: create/publish/Publish.svelte (slab id, publish, result + recoverable/
  orphan tags) + dev toggle.
- tests: 5 publish tests (success+purge, UAT draft, not-ready 422, dup 409,
  partial-media 502). Woo transport mocked.

Verified: pytest 48 passed; gates exit 0; npm build ok.
This commit is contained in:
2026-09-19 17:00:26 -07:00
parent 732310fdbe
commit 5e03f45660
5 changed files with 493 additions and 0 deletions
+6
View File
@@ -13,6 +13,7 @@ import Settings from './create/settings/Settings.svelte';
import SlabList from './create/slabs/SlabList.svelte';
import PhotoAttach from './create/photos/PhotoAttach.svelte';
import Review from './create/review/Review.svelte';
import Publish from './create/publish/Publish.svelte';
const gate = typeof window !== 'undefined' ? supported() : 'window unavailable';
const session: CaptureSession = createCaptureSession();
@@ -24,6 +25,7 @@ import Review from './create/review/Review.svelte';
let showSlabs = false;
let showPhotos = false;
let showReview = false;
let showPublish = false;
const unsubCap = session.capture.subscribe((c) => {
cap = c;
@@ -101,6 +103,7 @@ import Review from './create/review/Review.svelte';
<button on:click={() => (showSlabs = !showSlabs)}>Slabs (dev)</button>
<button on:click={() => (showPhotos = !showPhotos)}>Photos (dev)</button>
<button on:click={() => (showReview = !showReview)}>Review (dev)</button>
<button on:click={() => (showPublish = !showPublish)}>Publish (dev)</button>
</div>
{#if showSettings}
<Settings />
@@ -114,6 +117,9 @@ import Review from './create/review/Review.svelte';
{#if showReview}
<Review />
{/if}
{#if showPublish}
<Publish />
{/if}
</main>
{/if}