← back to NationalPaperHangers Yfr2 C3

tests/measure-tier.test.js

202 lines

'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');
const { POLICY_VERSION, classifyMeasureTier } = require('../lib/domain/measure-tier');

function base(overrides = {}) {
  return {
    environment: 'production',
    is_test: false,
    captured_at: '2026-08-01T12:00:00Z',
    consent_at: '2026-08-01T12:01:00Z',
    consent_version: 'measure-routing-v1',
    consent_purposes: ['installer_routing'],
    zip: '33138',
    routed_to: 42,
    routed_at: '2026-08-01T12:02:00Z',
    lead_state: 'routed',
    contact_preference: 'email',
    customer_email: 'buyer@example.test',
    customer_phone: null,
    sqft: 240,
    rolls: null,
    measurement_method: 'calculator',
    ...overrides
  };
}

test('returns the stable policy version and Standard for minimum explicit eligibility', () => {
  const result = classifyMeasureTier(base());
  assert.equal(result.policy_version, POLICY_VERSION);
  assert.equal(result.tier, 'standard');
  assert.deepEqual(result.reason_codes, [
    'has_sqft',
    'spec_missing_product_or_material',
    'spec_missing_rolls',
    'spec_missing_surface_state',
    'spec_missing_timeline',
    'spec_missing_valid_room_capture',
    'spec_missing_wall_count'
  ]);
});

test('classifies Specification-ready only when every structured predicate passes', () => {
  const result = classifyMeasureTier(base({
    rolls: 8,
    wall_count: 4,
    product_sku: 'DW-100',
    timeline_band: 'within_90_days',
    surface_state: 'painted_drywall',
    room_captures: [{
      wall_width_ft: 12,
      wall_height_ft: 9,
      photo_url: '/uploads/bookings/0123456789abcdef01234567.jpg'
    }]
  }));
  assert.equal(result.tier, 'spec_ready');
  assert.deepEqual(result.reason_codes, [
    'has_product_or_material', 'has_rolls', 'has_sqft', 'has_surface_state',
    'has_timeline', 'has_valid_room_capture', 'has_wall_count'
  ]);
});

test('product_name and notes never satisfy structured Specification-ready fields', () => {
  const result = classifyMeasureTier(base({
    rolls: 8,
    wall_count: 4,
    product_name: 'Marketing name only',
    notes: 'painted wall, install next week, photos attached',
    room_captures: [{
      wall_width_ft: 12,
      wall_height_ft: 9,
      photo_url: '/uploads/bookings/0123456789abcdef01234567.jpg'
    }]
  }));
  assert.equal(result.tier, 'standard');
  assert.ok(result.reason_codes.includes('spec_missing_product_or_material'));
  assert.ok(result.reason_codes.includes('spec_missing_surface_state'));
  assert.ok(result.reason_codes.includes('spec_missing_timeline'));
});

test('explicit test and non-production records are excluded', () => {
  assert.deepEqual(classifyMeasureTier(base({ is_test: true })).tier, 'ineligible');
  assert.ok(classifyMeasureTier(base({ is_test: true })).reason_codes.includes('test_record'));
  assert.ok(classifyMeasureTier(base({ environment: 'staging' })).reason_codes.includes('not_production'));
  assert.ok(classifyMeasureTier(base({ is_test: undefined })).reason_codes.includes('missing_test_provenance'));
});

test('missing consent, routing, contact, and scope are reported without inference', () => {
  const result = classifyMeasureTier(base({
    consent_at: null,
    consent_version: null,
    consent_purposes: [],
    routed_to: null,
    routed_at: null,
    lead_state: 'captured',
    contact_preference: 'email',
    customer_email: null,
    sqft: null,
    rolls: 0,
    measurement_method: null
  }));
  assert.equal(result.tier, 'ineligible');
  assert.deepEqual(result.reason_codes, [
    'ineligible_lead_state',
    'missing_consent_at',
    'missing_consent_version',
    'missing_installer_routing_consent',
    'missing_or_invalid_measurement_method',
    'missing_positive_scope',
    'missing_routed_at',
    'missing_routed_to',
    'missing_valid_preferred_contact'
  ]);
});

test('contact preference is enforced exactly', () => {
  assert.equal(classifyMeasureTier(base({ contact_preference: 'phone', customer_email: 'buyer@example.test', customer_phone: null })).tier, 'ineligible');
  assert.equal(classifyMeasureTier(base({ contact_preference: 'either', customer_email: null, customer_phone: '+13055550123' })).tier, 'standard');
  assert.ok(classifyMeasureTier(base({ contact_preference: null })).reason_codes.includes('missing_or_invalid_contact_preference'));
});

test('all eligible lead states classify and terminal/no-route states do not', () => {
  for (const lead_state of ['routed', 'viewed', 'accepted', 'declined', 'expired']) {
    assert.equal(classifyMeasureTier(base({ lead_state })).tier, 'standard', lead_state);
  }
  for (const lead_state of ['captured', 'no_installer', 'superseded', null]) {
    assert.equal(classifyMeasureTier(base({ lead_state })).tier, 'ineligible', String(lead_state));
  }
});

test('malformed or partial room captures cannot qualify as Specification-ready', () => {
  const common = {
    rolls: 8, wall_count: 4, material: 'grasscloth', timeline_band: 'within_90_days',
    surface_state: 'painted_drywall'
  };
  for (const room_captures of [
    null,
    [],
    [{}],
    [{ wall_width_ft: 12 }],
    [{ wall_width_ft: 12, wall_height_ft: 9 }],
    [{ wall_width_ft: 12, wall_height_ft: 9, photo_url: 'https://attacker.test/photo.jpg' }],
    [{ wall_width_ft: 12, wall_height_ft: 0, photo_url: '/uploads/bookings/0123456789abcdef01234567.jpg' }]
  ]) {
    const result = classifyMeasureTier(base({ ...common, room_captures }));
    assert.equal(result.tier, 'standard');
    assert.ok(result.reason_codes.includes('spec_missing_valid_room_capture'));
  }
});

test('invalid ZIP, date, method, and preferred-contact formats are ineligible', () => {
  const cases = [
    ['zip', '33138-1234', 'invalid_or_missing_zip'],
    ['captured_at', 'not-a-date', 'missing_captured_at'],
    ['measurement_method', 'guessed_from_notes', 'missing_or_invalid_measurement_method'],
    ['customer_email', 'not-an-email', 'missing_valid_preferred_contact']
  ];
  for (const [field, value, reason] of cases) {
    const result = classifyMeasureTier(base({ [field]: value }));
    assert.equal(result.tier, 'ineligible', field);
    assert.ok(result.reason_codes.includes(reason), field);
  }
});

test('accepts database numeric strings but rejects JavaScript coercion traps', () => {
  assert.equal(classifyMeasureTier(base({ sqft: '240', routed_to: '42' })).tier, 'standard');
  for (const value of [true, false, [], [240], {}, ' 240', '240x', '-1']) {
    assert.equal(classifyMeasureTier(base({ sqft: value })).tier, 'ineligible', `sqft=${String(value)}`);
  }
  for (const value of [true, [], [42], {}, '042', '42.0', '-1']) {
    assert.equal(classifyMeasureTier(base({ routed_to: value })).tier, 'ineligible', `routed_to=${String(value)}`);
  }
});

test('accepts Date/ISO timestamps and rejects coercible non-timestamps', () => {
  assert.equal(classifyMeasureTier(base({ captured_at: new Date('2026-08-01T12:00:00Z') })).tier, 'standard');
  assert.equal(classifyMeasureTier(base({ captured_at: '2024-02-29T12:00:00+05:30' })).tier, 'standard');
  for (const value of [
    true, 1, [], {}, '2026-08-01', '08/01/2026',
    '0000-01-01T12:00:00Z', '2026-02-29T12:00:00Z', '2026-02-31T12:00:00Z', '2026-13-01T12:00:00Z',
    '2026-01-01T24:00:00Z', '2026-01-01T12:60:00Z', '2026-01-01T12:00:60Z',
    '2026-01-01T12:00:00+24:00'
  ]) {
    assert.equal(classifyMeasureTier(base({ captured_at: value })).tier, 'ineligible', String(value));
  }
});

test('room capture dimensions reject coercible arrays and booleans', () => {
  const spec = {
    rolls: 8, wall_count: 4, material: 'grasscloth', timeline_band: 'within_90_days',
    surface_state: 'painted_drywall'
  };
  for (const wall_width_ft of [true, [12], {}, ' 12']) {
    const result = classifyMeasureTier(base({ ...spec, room_captures: [{
      wall_width_ft, wall_height_ft: 9,
      photo_url: '/uploads/bookings/0123456789abcdef01234567.jpg'
    }] }));
    assert.equal(result.tier, 'standard', String(wall_width_ft));
  }
});