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:
@@ -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}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
let slabId = '';
|
||||
let result: {
|
||||
ok?: boolean;
|
||||
status?: string;
|
||||
woo_product_id?: number;
|
||||
draft?: boolean;
|
||||
error?: { code: string; message: string; detail?: Record<string, unknown> };
|
||||
} | null = null;
|
||||
let busy = false;
|
||||
|
||||
async function publish() {
|
||||
if (!slabId) {
|
||||
result = { error: { code: 'input', message: 'Enter a slab id.' } };
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
result = null;
|
||||
try {
|
||||
const res = await fetch(`/api/v1/slabs/${encodeURIComponent(slabId)}/publish`, { method: 'POST' });
|
||||
const body = await res.json();
|
||||
if (res.ok) result = body;
|
||||
else result = { ok: false, error: body.error };
|
||||
} catch {
|
||||
result = { ok: false, error: { code: 'network', message: 'network error' } };
|
||||
}
|
||||
busy = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="publish">
|
||||
<h2>Publish to WooCommerce</h2>
|
||||
<div>
|
||||
<label>Slab ID</label>
|
||||
<input bind:value={slabId} placeholder="uuid" />
|
||||
</div>
|
||||
<button on:click={publish} disabled={busy}>{busy ? 'Publishing…' : 'Publish'}</button>
|
||||
|
||||
{#if result}
|
||||
{#if result.ok}
|
||||
<p class="ok">
|
||||
Published{result.draft ? ' (UAT draft)' : ''} → Woo product #{result.woo_product_id}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="err">
|
||||
{result.error?.message}
|
||||
{#if result.error?.detail?.recoverable === false}
|
||||
<span class="tag">unrecoverable</span>
|
||||
{:else if result.error?.detail?.recoverable === true}
|
||||
<span class="tag">recoverable — retry</span>
|
||||
{/if}
|
||||
{#if result.error?.detail?.partial_media}
|
||||
<span class="tag warn">orphan media — clean up manually</span>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.publish { max-width: 520px; color: #eee; }
|
||||
label { display: block; margin: 8px 0 4px; }
|
||||
input { width: 100%; padding: 6px; }
|
||||
button { margin-top: 12px; }
|
||||
.ok { margin-top: 12px; color: #7fd37f; font-weight: 600; }
|
||||
.err { margin-top: 12px; color: #e88; }
|
||||
.tag { margin-left: 8px; font-size: 12px; padding: 1px 6px; border-radius: 4px; background: #444; }
|
||||
.tag.warn { background: #6a4; color: #000; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user