← back to Majilite Quote Cta
scripts/importer-guard-proposal.md
85 lines
# Importer fix — `~/Projects/majilite-onboard/scripts/publish_shopify.js`
## What shipped the broken half-state
- **L34**: `const STATUS = (opt('status','active')==='draft') ? 'DRAFT' : 'ACTIVE';`
→ default `ACTIVE`, no validate-before-activate gate.
- **L118–119**: creates ONLY a `Type: Sample` option + a single `$4.25` Sample
variant. No sellable variant.
- **L113**: `tags = [...p.tags, dw_sku, 'display_variant', 'Sample']` — no
quote-only trigger tag.
Result: 160 products went straight to `ACTIVE` with a $4.25 sample as the only
purchasable variant and no quote mechanism → the $4.25 shows as the headline
price and the material can't be bought. This is precisely the
`has_product_variant = false` failure the **dw-five-field-canary** guards
(rule #2), and it advertises to GMC at $4.25.
## Proposed guard (quote-only lines ship WITH the trigger tag, and never
## active-without-a-sellable-variant)
Two changes to `publish_shopify.js`:
### 1. Tag quote-only products with the trigger tag at create time
Add a `QUOTE_ONLY` flag (config or per-product), and inject the tags:
```js
// near the top config
const QUOTE_ONLY = true; // Majilite Metallic Specialties I is quote-only (no yardage price loaded)
const QUOTE_TAGS = QUOTE_ONLY ? ['quotes', 'Quote Only'] : [];
// L113 — add QUOTE_TAGS to the tag set
const tags = Array.from(new Set([...p.tags, p.dw_sku, 'display_variant', 'Sample', ...QUOTE_TAGS])).join(', ');
```
`quotes` is the tag the dw-five-field-canary already exempts (auditor.mjs L51),
so a correctly-tagged quote-only line no longer trips the canary.
### 2. Refuse ACTIVE without a sellable variant UNLESS quote-only
The canary's rule is: an ACTIVE product must have a sellable variant OR carry the
`quotes` tag. Enforce the same invariant at the importer chokepoint so a future
line can't ship sample-only-and-untagged:
```js
function assertActivatable(input, quoteOnly) {
const hasSample = input.variants.some(v => (v.sku || '').endsWith('-Sample'));
const hasSellable = input.variants.some(v => !(v.sku || '').endsWith('-Sample'));
const hasQuoteTag = input.tags.includes('quotes');
if (input.status === 'ACTIVE') {
if (!hasSample) throw new Error(`GATE: ${input.handle} ACTIVE without a Sample variant`);
if (!hasSellable && !hasQuoteTag) {
throw new Error(`GATE: ${input.handle} ACTIVE sample-only without the 'quotes' tag — ship as DRAFT or tag quote-only`);
}
}
}
// before the productSet mutation (after building `input`):
assertActivatable(input, QUOTE_ONLY);
```
With QUOTE_ONLY=true the guard passes because `quotes` is in the tags; a future
run that forgets the tag on a sample-only line HARD-FAILS instead of shipping
the broken state.
## Canonical enforcement (better than per-script)
The DW canon already has ONE enforced validator:
`~/Projects/Designer-Wallcoverings/shopify/scripts/lib/validate-before-activate.js`
→ `validateBeforeActivate(product)`, wired into `cadence-import.js` and
`activate-gated.js`. The Majilite onboarder BYPASSED it (it's a standalone
project, not a cadence import). The durable fix is to route one-off vendor
onboarders through that same validator — OR extend the validator to encode the
"ACTIVE sample-only ⇒ must carry `quotes`" rule so it's enforced everywhere, not
just in the two cadence chokepoints.
## Did dw-five-field-canary fire on this?
- The 06:15 run today returned **PASS, recent_fail=0** — but ONLY because the
Majilite line synced to the dw_unified mirror at 13:22 PT, AFTER the 06:15
run. Simulating the canary's `recent_fail` query against the CURRENT mirror
returns **168/168 Majilite failing** (has_product_variant=false, no `quotes`
tag, created <7d).
- So the canary is CORRECT and WILL fire FAIL at the next 06:15 run — unless the
`quotes` tag lands first (which auto-resolves it) or sellable variants are
added.
- Recommendation: apply the `quotes` tag (Deliverable 2) BEFORE the next 06:15
canary run to avoid a self-inflicted FAIL alert, since the quote-only design
is the intended end state.