[object Object]

← back to Estimate Instant

Add fail-closed room coverage API

8e0c9d3243110e6bc2dee7c777da9dac0aca69a1 · 2026-08-28 22:12:50 -0700 · Steve Abrams

Files touched

Diff

commit 8e0c9d3243110e6bc2dee7c777da9dac0aca69a1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 28 22:12:50 2026 -0700

    Add fail-closed room coverage API
---
 README.md                       |   6 ++-
 server.js                       | 102 ++++++++++++++++++++++++++++++++---
 test/calculate-coverage.test.js | 116 ++++++++++++++++++++++++++++++++++++++++
 verification/e2e-proof.json     |  11 ++--
 4 files changed, 224 insertions(+), 11 deletions(-)

diff --git a/README.md b/README.md
index 5a5f5bf..3233f07 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,11 @@ It returns `rolls_needed`, `waste_pct`, `total_cost`, and calculation notes. The
 endpoint is read-only and uses the checked-in `data/rolls.json` snapshot; its
 response labels that provenance and warns callers to confirm current product
 specifications and price before quoting. It does not query or mutate Shopify or
-`dw_unified`.
+`dw_unified`. Inputs must be JSON number primitives: width is capped at 1,000 ft,
+height at 100 ft, and `num_walls` must be an integer from 1 through 100. These
+conservative bounds prevent coercion and overflow. A Shopify SKU alias is accepted
+only for an exact `shopify_match:true` record. Prototype/stand-in rows still return
+coverage metrics, but return null prices with `quote_authoritative:false`.
 
 ## Fill it in
 - `data/products.json` — array of `{ title, sku, price, hex, image }`.
diff --git a/server.js b/server.js
index d9a8aee..b4c9e61 100644
--- a/server.js
+++ b/server.js
@@ -143,6 +143,81 @@ function estimate({ wallWidthIn, wallHeightIn, roll, checkout_domain }) {
   };
 }
 
+// Stable API contract for room-based consumers. This deliberately uses the
+// checked-in roll-spec snapshot: the dw_unified mirror is not authoritative for
+// sell price, and this local endpoint must never imply a live database read.
+function calculateCoverage(input, rolls = loadRolls(), checkout_domain) {
+  const sku = typeof input?.sku === 'string' ? input.sku.trim() : '';
+  const roomWidthFt = input?.room_width_ft;
+  const roomHeightFt = input?.room_height_ft;
+  const numWalls = input?.num_walls;
+  const errors = [];
+  if (!sku) errors.push('sku is required.');
+  if (typeof roomWidthFt !== 'number' || !Number.isFinite(roomWidthFt) || roomWidthFt <= 0 || roomWidthFt > 1000) {
+    errors.push('room_width_ft must be a JSON number greater than 0 and at most 1000.');
+  }
+  if (typeof roomHeightFt !== 'number' || !Number.isFinite(roomHeightFt) || roomHeightFt <= 0 || roomHeightFt > 100) {
+    errors.push('room_height_ft must be a JSON number greater than 0 and at most 100.');
+  }
+  if (typeof numWalls !== 'number' || !Number.isInteger(numWalls) || numWalls < 1 || numWalls > 100) {
+    errors.push('num_walls must be a JSON integer from 1 through 100.');
+  }
+  if (errors.length) return { ok: false, errors };
+
+  const key = sku.toUpperCase();
+  const roll = rolls.find((item) =>
+    String(item.sku || '').toUpperCase() === key ||
+    (item.shopify_match === true && String(item.shopify_sku || '').toUpperCase() === key)
+  );
+  if (!roll) return { ok: false, errors: [`No local roll specification found for SKU ${sku}.`] };
+
+  const totalWidthIn = roomWidthFt * 12 * numWalls;
+  const wallHeightIn = roomHeightFt * 12;
+  if (!Number.isFinite(totalWidthIn) || Math.abs(totalWidthIn) > Number.MAX_SAFE_INTEGER || !Number.isFinite(wallHeightIn)) {
+    return { ok: false, errors: ['Room dimensions exceed the safe calculation range.'] };
+  }
+  const result = estimate({
+    wallWidthIn: totalWidthIn,
+    wallHeightIn,
+    roll,
+    checkout_domain,
+  });
+  if (!result.ok) return result;
+  const quoteAuthoritative = roll.shopify_match === true &&
+    typeof roll.shopify_price === 'number' && Number.isFinite(roll.shopify_price) && roll.shopify_price > 0 &&
+    typeof roll.roll_width_in === 'number' && Number.isFinite(roll.roll_width_in) && roll.roll_width_in > 0 &&
+    typeof roll.roll_length_ft === 'number' && Number.isFinite(roll.roll_length_ft) && roll.roll_length_ft > 0 &&
+    typeof roll.pattern_repeat_in === 'number' && Number.isFinite(roll.pattern_repeat_in) && roll.pattern_repeat_in >= 0;
+  const numericOutputs = [result.rollsNeeded, result.wastePct,
+    result.totalStrips, result.stripsPerRoll, result.cutLengthIn];
+  if (quoteAuthoritative) numericOutputs.push(result.price, result.pricePerRoll);
+  if (numericOutputs.some((value) => typeof value !== 'number' || !Number.isFinite(value))) {
+    return { ok: false, errors: ['Roll specifications produced a non-finite calculation; verify the product data.'] };
+  }
+
+  const notes = [
+    `Assumes ${numWalls} wall${numWalls === 1 ? '' : 's'} at ${roomWidthFt} ft wide by ${roomHeightFt} ft high.`,
+    'Calculated from the checked-in local roll-spec snapshot; confirm current specifications and price before quoting.',
+  ];
+  if (!quoteAuthoritative) notes.push('This local record is not quote-authoritative; pricing is withheld until an exact matched product has valid price and roll specifications.');
+
+  return {
+    ok: true,
+    sku: result.sku,
+    requested_sku: sku,
+    rolls_needed: result.rollsNeeded,
+    waste_pct: result.wastePct,
+    total_cost: quoteAuthoritative ? result.price : null,
+    price_per_roll: quoteAuthoritative ? result.pricePerRoll : null,
+    quote_authoritative: quoteAuthoritative,
+    strips_needed: result.totalStrips,
+    strips_per_roll: result.stripsPerRoll,
+    cut_length_in: result.cutLengthIn,
+    notes,
+    data_source: 'local-rollspec-snapshot',
+  };
+}
+
 function body(req) {
   return new Promise(r => {
     let d = '';
@@ -168,7 +243,7 @@ function mime(f) {
   return 'text/plain';
 }
 
-http.createServer(async (req, res) => {
+async function handleRequest(req, res) {
   const u = new URL(req.url, 'http://x');
   const p = u.pathname;
 
@@ -199,6 +274,13 @@ http.createServer(async (req, res) => {
     return json(res, 200, estimate({ wallWidthIn: b.wallWidthIn, wallHeightIn: b.wallHeightIn, roll, checkout_domain }));
   }
 
+  if (p === '/api/calculate-coverage' && req.method === 'POST') {
+    const input = await body(req);
+    const { checkout_domain } = loadCatalog();
+    const result = calculateCoverage(input, loadRolls(), checkout_domain);
+    return json(res, result.ok ? 200 : 400, result);
+  }
+
   if (p === '/api/lead' && req.method === 'POST') {
     const b = await body(req);
     if (!b.email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(b.email)) {
@@ -241,9 +323,15 @@ http.createServer(async (req, res) => {
   }
 
   res.writeHead(404); res.end('not found');
-}).listen(PORT, '127.0.0.1', function () {
-  console.log('[estimate-instant] http://localhost:' + this.address().port);
-  console.log('  Calculator: http://localhost:' + this.address().port + '/');
-  console.log('  Admin:      http://localhost:' + this.address().port + '/admin  (auth required — ADMIN_CRED)');
-  console.log('  Embed demo: http://localhost:' + this.address().port + '/embed-demo.html');
-});
+}
+
+if (require.main === module) {
+  http.createServer(handleRequest).listen(PORT, '127.0.0.1', function () {
+    console.log('[estimate-instant] http://localhost:' + this.address().port);
+    console.log('  Calculator: http://localhost:' + this.address().port + '/');
+    console.log('  Admin:      http://localhost:' + this.address().port + '/admin  (auth required — ADMIN_CRED)');
+    console.log('  Embed demo: http://localhost:' + this.address().port + '/embed-demo.html');
+  });
+}
+
+module.exports = { calculateCoverage, estimate, handleRequest };
diff --git a/test/calculate-coverage.test.js b/test/calculate-coverage.test.js
new file mode 100644
index 0000000..25937ba
--- /dev/null
+++ b/test/calculate-coverage.test.js
@@ -0,0 +1,116 @@
+'use strict';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const http = require('node:http');
+const { calculateCoverage, handleRequest } = require('../server.js');
+
+const roll = {
+  sku: 'DW-TEST-1',
+  shopify_sku: 'DW-SHOPIFY-1',
+  pattern: 'Test Pattern',
+  roll_width_in: 24,
+  roll_length_ft: 30,
+  pattern_repeat_in: 0,
+  match: 'straight',
+  price_per_roll: 100,
+  shopify_price: 100,
+  shopify_match: true,
+};
+
+test('calculates the documented room contract without external data', () => {
+  const result = calculateCoverage({
+    sku: 'dw-test-1', room_width_ft: 10, room_height_ft: 8, num_walls: 4,
+  }, [roll], 'example.test');
+  assert.equal(result.ok, true);
+  assert.equal(result.rolls_needed, 7);
+  assert.equal(result.total_cost, 700);
+  assert.equal(result.quote_authoritative, true);
+  assert.equal(result.data_source, 'local-rollspec-snapshot');
+  assert.match(result.notes.join(' '), /confirm current specifications and price/i);
+});
+
+test('rejects false-match Shopify aliases and withholds prototype pricing', () => {
+  const prototype = { ...roll, sku: 'DW-PROTOTYPE', shopify_sku: 'DW-STANDIN', shopify_match: false };
+  const alias = calculateCoverage({
+    sku: 'DW-STANDIN', room_width_ft: 10, room_height_ft: 8, num_walls: 1,
+  }, [prototype]);
+  assert.equal(alias.ok, false);
+
+  const local = calculateCoverage({
+    sku: 'DW-PROTOTYPE', room_width_ft: 10, room_height_ft: 8, num_walls: 1,
+  }, [prototype]);
+  assert.equal(local.ok, true);
+  assert.equal(local.total_cost, null);
+  assert.equal(local.price_per_roll, null);
+  assert.equal(local.quote_authoritative, false);
+  assert.match(local.notes.join(' '), /pricing is withheld/i);
+});
+
+test('accepts the Shopify SKU alias', () => {
+  const result = calculateCoverage({
+    sku: 'DW-SHOPIFY-1', room_width_ft: 10, room_height_ft: 8, num_walls: 1,
+  }, [roll], 'example.test');
+  assert.equal(result.ok, true);
+  assert.equal(result.sku, 'DW-TEST-1');
+});
+
+test('rejects coercible and oversized dimension inputs', () => {
+  for (const value of [true, '10', [10]]) {
+    const result = calculateCoverage({ sku: roll.sku, room_width_ft: value, room_height_ft: 8, num_walls: 1 }, [roll]);
+    assert.equal(result.ok, false, `${JSON.stringify(value)} must not be coerced`);
+  }
+  for (const input of [
+    { room_width_ft: 10, room_height_ft: '8', num_walls: 1 },
+    { room_width_ft: 10, room_height_ft: 8, num_walls: true },
+    { room_width_ft: 1001, room_height_ft: 8, num_walls: 1 },
+    { room_width_ft: 1e308, room_height_ft: 8, num_walls: 100 },
+    { room_width_ft: 10, room_height_ft: 101, num_walls: 1 },
+    { room_width_ft: 10, room_height_ft: 8, num_walls: 101 },
+  ]) {
+    const result = calculateCoverage({ sku: roll.sku, ...input }, [roll]);
+    assert.equal(result.ok, false);
+    assert.doesNotMatch(JSON.stringify(result), /"ok":true/);
+  }
+});
+
+test('fails closed when malformed roll data produces non-finite output', () => {
+  const broken = { ...roll, roll_width_in: 0 };
+  const result = calculateCoverage({ sku: roll.sku, room_width_ft: 10, room_height_ft: 8, num_walls: 1 }, [broken]);
+  assert.equal(result.ok, false);
+  assert.match(result.errors.join(' '), /non-finite calculation/i);
+});
+
+test('rejects missing SKU, invalid dimensions, fractional walls, and unknown SKU', () => {
+  const invalid = calculateCoverage({ sku: '', room_width_ft: 0, room_height_ft: Infinity, num_walls: 1.5 }, [roll]);
+  assert.equal(invalid.ok, false);
+  assert.equal(invalid.errors.length, 4);
+  const unknown = calculateCoverage({ sku: 'NOPE', room_width_ft: 10, room_height_ft: 8, num_walls: 1 }, [roll]);
+  assert.deepEqual(unknown, { ok: false, errors: ['No local roll specification found for SKU NOPE.'] });
+});
+
+test('serves the real local HTTP journey and returns 400 on bad input', async (t) => {
+  const server = http.createServer(handleRequest);
+  await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
+  t.after(() => new Promise((resolve) => server.close(resolve)));
+  const base = `http://127.0.0.1:${server.address().port}`;
+
+  const good = await fetch(`${base}/api/calculate-coverage`, {
+    method: 'POST', headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ sku: 'DW-GRAS-01', room_width_ft: 10, room_height_ft: 8, num_walls: 4 }),
+  });
+  assert.equal(good.status, 200);
+  const payload = await good.json();
+  assert.equal(payload.ok, true);
+  assert.equal(payload.data_source, 'local-rollspec-snapshot');
+  assert.equal(payload.quote_authoritative, false);
+  assert.equal(payload.total_cost, null);
+  assert.equal(payload.price_per_roll, null);
+  assert.match(payload.notes.join(' '), /pricing is withheld/i);
+
+  const bad = await fetch(`${base}/api/calculate-coverage`, {
+    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}',
+  });
+  assert.equal(bad.status, 400);
+  assert.equal((await bad.json()).ok, false);
+});
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index 72a898e..909c364 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -2,7 +2,8 @@
   "intent": "Provide a read-only room-dimensions-to-roll-count API contract using the existing local calculator.",
   "risk_tier": "R1 isolated backend API; no production data or external integration",
   "environment": "local Node HTTP server on an ephemeral loopback port",
-  "timestamp": "2026-08-29T04:52:00Z",
+  "build_identity": "git parent 7dabb56 plus the owned Cycle 12 server/test/documentation diff",
+  "timestamp": "2026-08-29T05:11:00Z",
   "ticket": "TK-10948-add-local-dw-room-coverage-api-contract",
   "precondition": "estimate-instant exposed /api/estimate for inch dimensions but had no stable room-level snake_case API contract.",
   "checks": [
@@ -10,13 +11,13 @@
       "verdict": "PASS",
       "boundary": "calculation module",
       "command": "node --test test/calculate-coverage.test.js",
-      "assertions": "room dimensions and wall count map to strips, rolls, waste, and total cost; Shopify SKU alias works"
+      "assertions": "7/7 pass: room dimensions map to rolls; exact matched alias works; stand-in aliases fail; prototype pricing is withheld; coercible/oversized inputs and non-finite outputs fail closed"
     },
     {
       "verdict": "PASS",
       "boundary": "HTTP API",
       "command": "node --test test/calculate-coverage.test.js",
-      "assertions": "ephemeral local POST returns HTTP 200 with labeled local-snapshot provenance; empty input returns HTTP 400"
+      "assertions": "ephemeral local POST returns HTTP 200 with labeled non-authoritative prototype coverage and null prices; empty input returns HTTP 400"
     },
     {
       "verdict": "PASS",
@@ -31,6 +32,10 @@
     "zero width",
     "non-finite height",
     "fractional wall count",
+    "boolean, string, and array dimensions",
+    "dimensions beyond documented bounds",
+    "false-match Shopify alias",
+    "non-finite estimator output from malformed roll data",
     "empty HTTP payload"
   ],
   "side_effects": "none; test server is ephemeral and closed by the test; no Shopify, dw_unified, lead, send, deploy, restart, or scheduled-job action",

← 7dabb56 auto-data-snapshot: 2026-08-28T21:58:17 (2 data files) — REA  ·  back to Estimate Instant  ·  Harden roll specs and SKU resolution 062bfad →