Measured Behaviour
Published
A tier threshold looks like a number and is three separate decisions: which stays count, over what window, and whether the two criteria are joined by AND or by OR. The third one changes no number anywhere.
Sixty members and a programme year
The measured ledger is three hundred stays across sixty members of a hotel programme, closed at the end of a calendar year. Three tiers, published as nights and spend: Silver at ten nights and 2,000 dollars, Gold at twenty-five and 5,000, Platinum at fifty and 12,000.
Correct, seven members reach Silver and fifty-three stay at base. The tier cost to the programme is 3,360.00 dollars, the outstanding point liability is 1,510,000, and the programme's own audit rule reports that every tier held was held on both criteria.
Redemption nights and promotional rates are excluded, because a night the programme paid for does not qualify anybody for anything. Last year's stays fall outside the published calendar window. Ten dormant members have not stayed since 2023 and their balances have expired.
The word between two numbers
requireBothCriteria is a boolean. It does not hold a threshold, a rate or an amount. It says whether a member must clear the nights and the spend, or either one. The library's default is either, because a library cannot know which a programme meant and refusing somebody who has clearly earned a tier is the worse mistake for it to make.
Renaming it alone is caught: the programme's own rule refuses the close, reporting that members hold a tier on one criterion alone. That is the system working.
Rename it together with the flag that turns that rule on -- one adjacent line in the same object -- and the close completes. Measured: forty members promoted on one criterion, seven of them all the way to Gold, and the tier cost goes from 3,360.00 to 19,200.00.
Not one published threshold changed. Ten nights, 2,000 dollars, twenty-five, 5,000, fifty, 12,000 -- all six numbers print correctly on the configuration screen in every arm above. What changed is the word between two of them, and a programme built for members who spend money is now staffed by members who took cheap nights.
Which stays count is a separate failure
qualifyingKinds nominates the stay types that earn qualification. An empty nomination cannot sensibly mean nothing counts, so the library treats it as everything counts.
Renaming it lets redemption and promotional nights qualify: Silver members go from seven to seventeen and the tier cost from 3,360.00 to 8,160.00. Members are promoted by nights the programme itself gave away.
Renaming the kind field on the stay records instead points the opposite way: nothing matches the nomination, nothing counts, every member falls to base and the tier cost is 0.00. Same concept, two sides of the boundary, opposite directions -- the option inflates the programme and the record empties it.
The arm that moved nothing, and why it is worth printing
qualificationWindow was expected to matter. The programme publishes a calendar year; the library's default is a rolling twelve months. On this ledger the two select the same stays, so every figure is identical to the cent and only the configuration display line changes.
That is an honest negative and it is reported here for the same reason the positives are. It is a property of the data, not of the engine: this ledger's out-of-window stays sit far enough back that both windows exclude them. A book with stays in the fortnight before the year opened would separate the two immediately.
The practical form is that an arm reading clean in your test data is not evidence the name is safe. It is evidence your data cannot tell.
A liability that grows while every tier stays right
pointsExpiryMonths decides when a dormant balance comes off the books. The library's default is that points never expire, which is the only choice it can make without inventing a programme rule.
Renaming it restores the ten dormant members' balances: point liability goes from 1,510,000 to 1,630,000. Every tier award is unchanged, every member's nights and spend are unchanged, the audit rule passes, and 120,000 points of expired liability quietly return to the balance sheet.
It is the only figure in the file that moves, and it is not on the operational report anybody reviews at a year close. It is on the accounting one, months later.
The loud arms
tiers empties the tier list, so every member is base and the tier cost is 0.00 -- obvious the moment anybody looks. day puts every stay outside the window, with the same result plus a zero liability. spendUsd makes a stay non-finite and the engine refuses before qualifying anybody.
memberId is the identity collapse: all three hundred stays merge into one member, who reaches Platinum on 664 nights and 109,880 dollars. One Platinum member and fifty-nine that vanished is not a subtle result.
Each of these was executed and read rather than predicted. That distinction matters: in the previous pass of this series three arms confidently described as failing closed turned out not to, one of them failing open through a NaN comparison.
What protection alone did, and what to do about it
Protection alone changed nothing on any of the five profiles, for this area and the four others measured alongside it. Everything above needed member renaming aimed at the programme's option keys or the stay records' fields.
Keep them out of your MemberRegexp. If your loyalty front end is shipped to partner brands, the white-label protection guide covers the case where the same bundle runs under several names.
The design point is independent of obfuscation. Assert the combinator, not the thresholds: publish a test that a member clearing nights but not spend is refused, because every threshold on the screen stays correct in the arm that cost 15,840 dollars. And reconcile the point liability against the prior close as well as the tier counts -- the expiry arm moved nothing else at all. The same idea applies wherever a promise is counted against stock, as in inventory reservations.
Frequently asked questions
Does obfuscation promote members to the wrong tier?
Not on its own. Protection with default settings produced identical output on all five profiles. The tier changes measured here required member renaming aimed at the programme's option keys.
What was the largest measured change?
Renaming the AND/OR combinator together with the flag that enables the programme's own audit rule. Forty members were promoted on one criterion, seven of them to Gold, and the annual tier cost went from 3,360.00 to 19,200.00.
Did any published threshold change?
No. All six numbers -- ten nights and 2,000 dollars for Silver, twenty-five and 5,000 for Gold, fifty and 12,000 for Platinum -- printed correctly on the configuration screen in every arm. Only the word joining each pair changed.
What happens to points balances?
Renaming pointsExpiryMonths reverts to the library's default of never expiring, and the point liability went from 1,510,000 to 1,630,000 while every tier award stayed correct. It is the only figure that moves in that arm, and it appears on an accounting report rather than an operational one.
Was anything measured as harmless?
Yes, and it is reported as such. Renaming the qualification window changed no money figure at all, because on this ledger a calendar year and a rolling twelve months select the same stays. That is a property of the data rather than a property of the engine, and a book with stays just before the year opened would separate them.
Which arms failed loudly?
Four, each executed and read. tiers and day put every member at base with a zero tier cost. spendUsd made a stay non-finite and the engine refused. memberId merged all three hundred stays into a single Platinum member.
What single test would have caught the worst case?
One member who clears the nights but not the spend, asserted to stay at base. Counting members per tier does not catch it -- the counts stay plausible -- and the thresholds themselves are never wrong.
Related reading