Measured Behaviour

Does obfuscation break overbooking limits?

Every control this site has measured so far exists to stop something happening. An overbooking authorisation is the opposite: it exists to permit a breach, up to a bounded amount. That one difference makes the obvious test unwritable, and it is why this failure is worth its own page.

A property that sells more rooms than it has, on purpose

A 180-room resort sold 188 rooms for one December night. That is not a bug report; that is the plan. Twelve per cent of this property's holiday book historically fails to arrive, so the revenue system authorises a bounded oversell and the front desk expects to end the night full rather than eight rooms short.

The authorisation is not a number somebody typed. It is derived: the no-show forecast is discounted by what a walk costs relative to what a room earns, because selling a room you may have to buy back at another hotel is only worth it while the room earns more than the buy-back costs. At 240 dollars average rate and 350 dollars a walk, a forecast of twenty-one no-shows becomes an authorisation of eight.

The measured run reads SOLD=188 PHYSICAL=180 AUTHORISED-OVERSELL=8 OVER-PHYSICAL=8. On the night the forecast holds, thirteen guests do not arrive and nobody is walked. On the night it does not, four do not arrive, four guests are walked at a cost of 1,400 dollars, and the property earns 52,821 dollars. Both nights are the system working correctly.

Read the third figure again. OVER-PHYSICAL=8 on a correct run. The assertion any engineer would write first -- rooms sold must not exceed rooms that exist -- is false here before anything goes wrong.

The test that cannot be written

Everywhere else in this series there is at least one predicate that holds when the system is right and fails when it is not. Finding it is the work; the point of the exercise is that such a predicate exists. An authorisation removes it. There is no state of this property in which sold rooms are within physical capacity, because being outside it is what was bought.

What is left is a comparison against the authorised level: sold must not exceed capacity plus the authorisation. That is a perfectly good test, and it is defined over the configuration that derives the authorisation. When that configuration is what moved, the test moves with it and agrees with whatever the engine did.

So the alarm stops being a switch and becomes a dial. OVER-PHYSICAL reads 8 when everything is right and 21 when it is not. Both are plausible numbers on a busy December night, and the only reference mark that distinguishes them is the number that went missing.

The option that moved was a price, not a limit

The measured arm is walkCostUsd -- what it costs to put a guest in a taxi to another hotel. It is not a limit, not a policy and not a threshold. It reads like a reference figure for a report.

It is an input to the derivation. A walk that costs nothing does not discount a forecast at all, so the library's default of zero authorises the whole twenty-one rooms. Measured: AUTHORISED-OVERSELL 8 becomes 21, SOLD 188 becomes 201, and on the night the forecast is wrong the property walks seventeen guests instead of four.

Revenue on that night goes up, from 52,821.00 to 53,907.00. More rooms sold, more rooms occupied, a better night on every line an owner reviews. And the property's own oversell rule passes, printing that the sale stayed inside its authorisation -- which is true, because the authorisation moved with the option.

The detail that decides how long this survives is the last one. The cost of a walk is also what prices a walk in the report. Seventeen guests were sent to other hotels and the line reads walk-cost=0.00. The single figure a controller would have queried reads zero for exactly the reason it should have read 5,950.

A second route, and the wider pattern is the quieter one

oversellBasis is the option that says whether the authorisation is derived at all or taken at face value. Losing it reverts to the library's fixed basis, so the configured ceiling of 24 becomes the authorisation outright: SOLD=204, twenty guests walked, revenue 54,048.00.

That arm is honest about the damage -- it prints walk-cost=7000.00, because the price of a walk survived. Rename both names together and every room figure is identical to the basis arm while the cost line goes back to 0.00. The narrower, more careful-looking pattern is the one that leaves evidence; the wider one removes it.

This is why a matrix of these results has to be read a cell at a time. Testing only the broad case -- rename everything on the options object -- would have shown a large, obvious, correctly costed overshoot, and missed the version that hides its own invoice.

The counter that exists to catch this reads zero

The property never walks a guaranteed booking or an elite member; those segments are on a protected list, and the walk queue skips them. Rename protectedSegments and the list is empty, so nobody is protected.

Measured, the same four guests are walked in number but not in identity: who-walked=[BK-5387/elite BK-5365/elite BK-5385/opaque BK-5370/opaque] against the correct night's four opaque-channel bookings. Two members the loyalty programme promised a room to slept somewhere else.

And WALKED-PROTECTED, the counter whose entire job is to report that, reads 0. It asks the protected list whether each walked guest is on it, and the list is the name that moved. The property's own rule reads the same counter and prints 4 walked and none of them protected. Revenue moves by sixty dollars.

Renaming the segment field on the booking records instead reaches the identical outcome from the other side, and is marginally louder: the walked list prints BK-5387/undefined, which at least looks wrong to a human reading the log.

What failed closed, and one that did both

Not everything here fails quietly, and the arms that do not are worth naming individually rather than in a group. oversellLimit and noShowRatePct each revert to a library default of zero, so the authorisation is zero and the property sells exactly 180 rooms: revenue falls from 52,821.00 to 51,741.00 and the resort ends a sold-out night with empty rooms. That is a loss, but it is a loss somebody notices at month end.

physicalCapacity produces a non-finite sell limit and the engine refuses before selling anything -- the loudest possible failure and the safest.

rateUsd is the interesting one, because it does two things. It produces REVENUE=NaN, which nobody can miss. It is also the sort key the denial queue uses to decide who is walked first, so before producing that NaN it quietly walked four different guests. A field can be load-bearing in a place that has nothing to do with the number it holds.

bookingId collapses the identity the walked-guest map is keyed on, and revenue reads 0.00. Loud, immediate, and the opposite of the failure this page is about.

Whether you can see this at all depends on the night

The same file runs two nights against the identical configuration. On the quiet night fourteen bookings fail to arrive and nobody is walked at all. Every arm that touches the protected list or the denial order is inert on that night -- not because the engine is fine, but because there was no decision for either of them to make.

That is the reusable lesson. A test data set in which the authorisation is never actually spent is structurally unable to detect an authorisation error, and most nights of the year look like that. If you are building a regression suite for this, the fixture has to contain a night where the forecast is wrong.

What protection alone did, and what to do about it

Protection on its own changed nothing. All five profiles -- the two compatibility targets, both gate profiles and the string-heavy profile -- produced output identical to the unprotected run on all five areas measured this pass. Every result above needed member renaming pointed at these specific names.

So the mitigation is the ordinary one and it is narrow. Member renaming is opt-in and takes a MemberRegexp; keep the names on your revenue-management options object and your booking records outside it. Rename the members you own inside your own code, not the keys a configuration object hands to a library.

The design point outlives the obfuscator, because a hand-edited config file and a dropped key over a serialisation hop widen an authorisation exactly the same way. Where a control authorises a bounded breach, assert the bound against something outside the configuration that derives it -- the revenue committee's minute, last season's walk count, the figure filed with the insurer -- and never against the engine's own authorised number. And count your walks somewhere the walk cost cannot reach, because the cost is the first thing to go.

If you are protecting a booking engine or a property management front end, the .NET web application guide covers the build-side settings; the sibling case of stock that can be promised twice is in inventory reservations, where the limit is zero and the correct run is never over capacity at all.

Frequently asked questions

Does obfuscating a booking engine cause overbooking?

Not on its own. In this measurement, protection with default settings produced byte-identical behaviour on all five profiles. The failures appeared only when member renaming was pointed at the option keys of the oversell configuration or at the fields of the booking records.

Why can't I just assert that rooms sold never exceed capacity?

Because that assertion is false when the system is working. A property with an oversell authorisation sells above physical capacity deliberately -- 188 rooms against 180 in the measured run. The only usable test compares the sale against capacity plus the authorised level, and that level is computed from the configuration a rename moves.

Which option caused the largest silent change?

The cost of a walk. Renaming walkCostUsd widened the authorisation from 8 rooms to 21 because the derivation discounts the no-show forecast by what a walk costs. Seventeen guests were walked instead of four, and the same lost name meant the report showed the cost of that as 0.00.

Did any figure on the report go down?

No. Rooms sold went up, occupancy went up and revenue went up from 52,821.00 to 53,907.00. Every commercial line on the night improved. The only figures that moved against the property were the number of guests walked and a walk cost that read zero.

Does a guard that checks for walked VIP guests help?

Not if it is defined over the same list. Renaming protectedSegments walked two elite members, and the counter reporting protected walks read 0 because it asks that list whether each guest is on it. The property's rule read the same counter and reported that none of the walked guests were protected.

Which names failed loudly?

Four, and each was read individually rather than assumed. physicalCapacity produced a non-finite sell limit and refused before selling. bookingId collapsed the walked-guest identity and revenue read 0.00. oversellLimit and noShowRatePct each reverted to zero, so the property sold only its 180 physical rooms and lost 1,080 dollars of revenue on the night.

How do I test for this?

Put a night in your fixture where the forecast is wrong. The same measurement ran a quiet night with fourteen no-shows and nobody walked, and every arm touching the protected list or the denial order was inert on it. A data set in which the authorisation is never spent cannot detect an authorisation error.

Related reading