Parity markup and tolerance configurable as % or flat £

- config: parity_markup_value/_unit + parity_tolerance_value/_unit
  (pct|gbp), legacy *_pct keys still read as fallback
- expected rate = newbook + £X or newbook × (1 + X%); breach test uses
  the tolerance in its own unit
- Settings parity tab: unit selects + live worked example line
- Parity Alerts tab: unit-aware description, badge now shows £ deviation
  alongside %

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-06 07:41:55 +00:00
parent 493e87b2dc
commit 9545fd651d
4 changed files with 131 additions and 53 deletions

View file

@ -115,14 +115,18 @@ function ParityTab({ config, isLoading, onSave, saving }: {
saving: boolean
}) {
const [markup, setMarkup] = useState('')
const [markupUnit, setMarkupUnit] = useState('pct')
const [tolerance, setTolerance] = useState('')
const [toleranceUnit, setToleranceUnit] = useState('pct')
const [loaded, setLoaded] = useState(false)
const [checkResult, setCheckResult] = useState<ParityCheckResult | null>(null)
useEffect(() => {
if (config && !loaded) {
setMarkup(config['parity_expected_markup_pct'] ?? '0')
setTolerance(config['parity_tolerance_pct'] ?? '2')
setMarkup(config['parity_markup_value'] ?? config['parity_expected_markup_pct'] ?? '0')
setMarkupUnit(config['parity_markup_unit'] ?? 'pct')
setTolerance(config['parity_tolerance_value'] ?? config['parity_tolerance_pct'] ?? '2')
setToleranceUnit(config['parity_tolerance_unit'] ?? 'pct')
setLoaded(true)
}
}, [config, loaded])
@ -166,39 +170,67 @@ function ParityTab({ config, isLoading, onSave, saving }: {
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-mid)', display: 'block', marginBottom: 4 }}>
Expected Booking.com markup (%)
Expected Booking.com markup
</label>
<input
type="number" step="0.5" style={{ width: 140 }}
value={markup} onChange={e => setMarkup(e.target.value)}
placeholder="e.g. 15"
/>
<div style={{ display: 'flex', gap: 6 }}>
<input
type="number" step="0.5" style={{ width: 110 }}
value={markup} onChange={e => setMarkup(e.target.value)}
placeholder={markupUnit === 'gbp' ? 'e.g. 20' : 'e.g. 15'}
/>
<select style={{ width: 64 }} value={markupUnit} onChange={e => setMarkupUnit(e.target.value)}>
<option value="pct">%</option>
<option value="gbp">£</option>
</select>
</div>
<div style={{ fontSize: 11, color: 'var(--text-mid)', marginTop: 4 }}>
How much higher Booking.com should be than Newbook.
How much higher Booking.com should be than Newbook
{markupUnit === 'gbp' ? ' (flat £ per night)' : ' (percentage)'}.
</div>
</div>
<div>
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-mid)', display: 'block', marginBottom: 4 }}>
Tolerance (± %)
Tolerance (±)
</label>
<input
type="number" step="0.5" style={{ width: 140 }}
value={tolerance} onChange={e => setTolerance(e.target.value)}
placeholder="e.g. 2"
/>
<div style={{ display: 'flex', gap: 6 }}>
<input
type="number" step="0.5" style={{ width: 110 }}
value={tolerance} onChange={e => setTolerance(e.target.value)}
placeholder={toleranceUnit === 'gbp' ? 'e.g. 5' : 'e.g. 2'}
/>
<select style={{ width: 64 }} value={toleranceUnit} onChange={e => setToleranceUnit(e.target.value)}>
<option value="pct">%</option>
<option value="gbp">£</option>
</select>
</div>
<div style={{ fontSize: 11, color: 'var(--text-mid)', marginTop: 4 }}>
Allowed deviation from expected before alerting.
</div>
</div>
</div>
<div style={{ padding: '8px 12px', background: '#f8fafc', borderRadius: 6, fontSize: 12, color: 'var(--text-mid)' }}>
Rule: alert when Booking.com Newbook {markupUnit === 'gbp' ? `+ £${markup || '0'}` : `+ ${markup || '0'}%`}
{' '}beyond ±{toleranceUnit === 'gbp' ? `£${tolerance || '0'}` : `${tolerance || '0'}%`}.
{' '}Example: Newbook £100 expect £{(markupUnit === 'gbp'
? 100 + (parseFloat(markup) || 0)
: 100 * (1 + (parseFloat(markup) || 0) / 100)).toFixed(0)},
{' '}alert outside £{(() => {
const exp = markupUnit === 'gbp' ? 100 + (parseFloat(markup) || 0) : 100 * (1 + (parseFloat(markup) || 0) / 100)
const tol = toleranceUnit === 'gbp' ? (parseFloat(tolerance) || 0) : exp * (parseFloat(tolerance) || 0) / 100
return `${(exp - tol).toFixed(0)}–£${(exp + tol).toFixed(0)}`
})()}.
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button
className="btn btn-primary"
disabled={saving}
onClick={() => {
onSave('parity_expected_markup_pct', markup || '0')
onSave('parity_tolerance_pct', tolerance || '2')
onSave('parity_markup_value', markup || '0')
onSave('parity_markup_unit', markupUnit)
onSave('parity_tolerance_value', tolerance || '2')
onSave('parity_tolerance_unit', toleranceUnit)
}}
>
<Save size={13} strokeWidth={1.75} />