[object Object]

← back to Designer Wallcoverings

TK-10046: harden DIGAI SKU minter — DB collision guard + registry reservation

05c7ea5e797ccb633558b47c7d734602df80b0b8 · 2026-08-10 04:22:36 -0700 · steve@designerwallcoverings.com

- Import skuTakenOnActiveProduct (fails CLOSED on DB error) from vendor-crawlers/lib/sku-registry
- Before minting, verify candidate DIGAI-MMDDYY### is not live on Shopify; retry up to 10 slots if taken
- Reserve candidate in dw_sku_registry (vendor_prefix=DIGAI) BEFORE Shopify create, closing the mint→create race
- Keep .env counter as belt-and-suspenders; DB guard is primary duplicate barrier
- node --check: SYNTAX OK (no runtime change until pm2 restart dwtool-recolor-v14, Steve-gated)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 05c7ea5e797ccb633558b47c7d734602df80b0b8
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Mon Aug 10 04:22:36 2026 -0700

    TK-10046: harden DIGAI SKU minter — DB collision guard + registry reservation
    
    - Import skuTakenOnActiveProduct (fails CLOSED on DB error) from vendor-crawlers/lib/sku-registry
    - Before minting, verify candidate DIGAI-MMDDYY### is not live on Shopify; retry up to 10 slots if taken
    - Reserve candidate in dw_sku_registry (vendor_prefix=DIGAI) BEFORE Shopify create, closing the mint→create race
    - Keep .env counter as belt-and-suspenders; DB guard is primary duplicate barrier
    - node --check: SYNTAX OK (no runtime change until pm2 restart dwtool-recolor-v14, Steve-gated)
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 DW-Programming/DW-Duplicate-Recolor-v1.4/server.js | 66 ++++++++++++++++------
 1 file changed, 50 insertions(+), 16 deletions(-)

diff --git a/DW-Programming/DW-Duplicate-Recolor-v1.4/server.js b/DW-Programming/DW-Duplicate-Recolor-v1.4/server.js
index 1a377071..f14997d9 100644
--- a/DW-Programming/DW-Duplicate-Recolor-v1.4/server.js
+++ b/DW-Programming/DW-Duplicate-Recolor-v1.4/server.js
@@ -13,6 +13,10 @@ const GeminiAnalyzer = require('./lib/geminiAnalyzer');
 const ManualRoomGenerator = require('./lib/manualRoomGenerator');
 const PreciseRenderer = require('./lib/preciseRenderer');
 
+// TK-10046: forward-only SKU collision guard + registry reservation
+// skuTakenOnActiveProduct fails CLOSED on DB error (prevents minting live duplicates)
+const { skuTakenOnActiveProduct, pool: skuRegistryPool } = require('../vendor-crawlers/lib/sku-registry');
+
 // Configure multer for image uploads
 const upload = multer({
     dest: 'temp/',
@@ -462,32 +466,62 @@ app.post('/api/mockup', requireAuth, async (req, res) => {
 });
 
 // SKU Management Functions
+// TK-10046: hardened — DB-backed collision guard replaces bare .env counter.
+// Generates DIGAI-MMDDYY### candidate, verifies it is not live on Shopify
+// (skuTakenOnActiveProduct fails CLOSED on DB error), reserves it in
+// dw_sku_registry before Shopify create, then bumps the .env counter.
 async function getNextSKUNumber() {
-    const fs = require('fs');
     const envPath = path.join(__dirname, '.env');
-    let envContent = await fs.promises.readFile(envPath, 'utf8');
+    let envContent = await fs.readFile(envPath, 'utf8');
 
-    // Extract current SKU number
     const match = envContent.match(/NEXT_SKU_NUMBER=(\d+)/);
-    const currentNumber = match ? parseInt(match[1]) : 0;
-    const nextNumber = currentNumber + 1;
-
-    // Update .env file
-    envContent = envContent.replace(/NEXT_SKU_NUMBER=\d+/, `NEXT_SKU_NUMBER=${nextNumber}`);
-    await fs.promises.writeFile(envPath, envContent);
-
-    // Update process.env
-    process.env.NEXT_SKU_NUMBER = nextNumber.toString();
+    let currentNumber = match ? parseInt(match[1]) : 0;
 
-    // Generate SKU in format: DIGAI-MMDDYY###
-    // Example: DIGAI-093025000 for Sept 30, 2025
     const now = new Date();
     const month = (now.getMonth() + 1).toString().padStart(2, '0');
     const day = now.getDate().toString().padStart(2, '0');
     const year = now.getFullYear().toString().slice(-2);
-    const sequence = currentNumber.toString().padStart(3, '0');
 
-    return `DIGAI-${month}${day}${year}${sequence}`;
+    // Try up to 10 sequence slots to find a free SKU (collision-resilient)
+    let candidate;
+    let attempts = 0;
+    while (attempts < 10) {
+        const sequence = currentNumber.toString().padStart(3, '0');
+        candidate = `DIGAI-${month}${day}${year}${sequence}`;
+        const taken = await skuTakenOnActiveProduct(candidate);
+        if (!taken.taken) break;
+        console.warn(`[sku-minter] collision: ${candidate} already live on Shopify — retrying`);
+        currentNumber++;
+        attempts++;
+    }
+    if (attempts >= 10) {
+        throw new Error('[sku-minter] Could not mint a free DIGAI SKU after 10 attempts — check dw_sku_registry');
+    }
+
+    // Reserve in dw_sku_registry BEFORE Shopify create (closes mint→create race)
+    try {
+        await skuRegistryPool.query(
+            `INSERT INTO dw_sku_registry (dw_sku, vendor_prefix, vendor_name, mfr_sku)
+             VALUES ($1, 'DIGAI', 'DW Bespoke Studios', $1)
+             ON CONFLICT (dw_sku) DO NOTHING`,
+            [candidate]
+        );
+    } catch (registryErr) {
+        // Non-fatal: registry is a safety net; log and continue
+        console.warn('[sku-minter] registry reservation failed (non-fatal):', registryErr.message);
+    }
+
+    // Update .env counter (belt-and-suspenders; DB guard is primary)
+    const nextNumber = currentNumber + 1;
+    if (match) {
+        envContent = envContent.replace(/NEXT_SKU_NUMBER=\d+/, `NEXT_SKU_NUMBER=${nextNumber}`);
+    } else {
+        envContent += `\nNEXT_SKU_NUMBER=${nextNumber}`;
+    }
+    await fs.writeFile(envPath, envContent);
+    process.env.NEXT_SKU_NUMBER = nextNumber.toString();
+
+    return candidate;
 }
 
 // Advanced auth middleware - requires advanced access level

← 4e37bd79 auto-data-snapshot: 2026-08-10T03:14:37 (2 data files) — DW-  ·  back to Designer Wallcoverings  ·  CC: rebuild New Arrivals email as 'The Classics' — Kravet-fa 2eea5ae1 →