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:
parent
493e87b2dc
commit
9545fd651d
4 changed files with 131 additions and 53 deletions
|
|
@ -777,8 +777,12 @@ const ParityAlertsTab: React.FC = () => {
|
|||
},
|
||||
})
|
||||
|
||||
const markup = parityConfig?.['parity_expected_markup_pct'] ?? '0'
|
||||
const tolerance = parityConfig?.['parity_tolerance_pct'] ?? '2'
|
||||
const markupUnit = parityConfig?.['parity_markup_unit'] ?? 'pct'
|
||||
const toleranceUnit = parityConfig?.['parity_tolerance_unit'] ?? 'pct'
|
||||
const markupVal = parityConfig?.['parity_markup_value'] ?? parityConfig?.['parity_expected_markup_pct'] ?? '0'
|
||||
const toleranceVal = parityConfig?.['parity_tolerance_value'] ?? parityConfig?.['parity_tolerance_pct'] ?? '2'
|
||||
const markupLabel = markupUnit === 'gbp' ? `£${markupVal}` : `${markupVal}%`
|
||||
const toleranceLabel = toleranceUnit === 'gbp' ? `£${toleranceVal}` : `${toleranceVal}%`
|
||||
|
||||
const thStyle: React.CSSProperties = {
|
||||
textAlign: 'left', padding: '8px 12px', fontSize: 11, fontWeight: 600,
|
||||
|
|
@ -793,7 +797,7 @@ const ParityAlertsTab: React.FC = () => {
|
|||
<div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-mid)', marginBottom: 14, maxWidth: 720 }}>
|
||||
Dates where our Booking.com rate deviates from the expected level
|
||||
(Newbook rate + {markup}% markup, ±{tolerance}% tolerance). Checked daily at 06:45 —
|
||||
(Newbook rate + {markupLabel} markup, ±{toleranceLabel} tolerance). Checked daily at 06:45 —
|
||||
adjust the markup and tolerance in Settings → Rate Parity. Acknowledge a date once
|
||||
dealt with; alerts auto-resolve when the rates come back in line.
|
||||
</div>
|
||||
|
|
@ -850,11 +854,21 @@ const ParityAlertsTab: React.FC = () => {
|
|||
<td style={tdStyle}>{a.newbook_rate != null ? `£${a.newbook_rate.toFixed(2)}` : '—'}</td>
|
||||
<td style={tdStyle}>{a.booking_com_rate != null ? `£${a.booking_com_rate.toFixed(2)}` : '—'}</td>
|
||||
<td style={tdStyle}>
|
||||
{a.difference_pct != null && (
|
||||
<span style={badgeStyle(a.alert_type === 'higher' ? 'warning' : 'error')}>
|
||||
{a.difference_pct > 0 ? '+' : ''}{a.difference_pct.toFixed(1)}% vs expected
|
||||
</span>
|
||||
)}
|
||||
{a.difference_pct != null && (() => {
|
||||
// expected = booking / (1 + dev%); £ deviation derived from that
|
||||
const expected = a.booking_com_rate != null
|
||||
? a.booking_com_rate / (1 + a.difference_pct / 100)
|
||||
: null
|
||||
const devGbp = expected != null && a.booking_com_rate != null
|
||||
? a.booking_com_rate - expected
|
||||
: null
|
||||
return (
|
||||
<span style={badgeStyle(a.alert_type === 'higher' ? 'warning' : 'error')}>
|
||||
{a.difference_pct > 0 ? '+' : ''}{a.difference_pct.toFixed(1)}%
|
||||
{devGbp != null ? ` (${devGbp > 0 ? '+' : '−'}£${Math.abs(devGbp).toFixed(2)})` : ''} vs expected
|
||||
</span>
|
||||
)
|
||||
})()}
|
||||
</td>
|
||||
<td style={mergeStyles(tdStyle, { color: 'var(--text-mid)', fontSize: 12 })}>{a.room_category || '—'}</td>
|
||||
<td style={tdStyle}>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue