Booking systems
Pricing a Booking Across Rate Boundaries
A one-hour booking can occupy two price bands. I made the quote follow the occupied minutes, then reused the same calculation for preview and order creation.
Senior AI Engineer · M.S. Computer Science, Tsinghua University · See the products I built
In this article · 6 sections
The engineering decisions
- Split the occupied interval at every rate boundary.
- Use the same server calculation for preview, order creation and the daily price view.
- Select the billing policy per venue and round once after summing the segments.
The booking start time was the wrong abstraction
The venue had an off-peak rate and a later, higher rate. A calculation that selected one rate from the booking start time could look reasonable for most bookings while getting the boundary case wrong. A session beginning at 13:30 and ending at 14:30 occupied both bands. Charging the whole hour at the first rate did not represent the service the customer had reserved.
The fix started with the business rule, not the price label. This venue wanted occupied time priced in segments. Other venues still used a session-start policy. Replacing a global pricing function with the new behavior would have silently changed their agreements, so I made the policy explicit at the venue boundary.
Split first, accumulate second, round last
The calculation partitions the booking at applicable rate boundaries, prices each overlap for its duration, and adds the contributions. Currency rounding happens once after the accumulation. Rounding each intermediate segment independently can make a subdivided interval disagree with the same interval priced as a whole.
In the recorded example, 30 minutes at NZD 25 per hour contributes NZD 12.50. The next 30 minutes at NZD 50 contributes NZD 25.00. The verified total is NZD 37.50. A booking wholly inside the second band, from 14:00 to 15:00, remains NZD 50.00. These paired cases distinguish the new boundary behavior from a general price change.
segments = splitAtRateBoundaries(booking.start, booking.end)
amount = sum(segment.hours * segment.hourlyRate for segment in segments)
total = roundToCurrency(amount)
audit.rateRuleIds = unique(segments.rateRuleIds)Preserve the reasons behind the amount
The audit metadata records all rate-rule identifiers used by a multi-band booking. Keeping only the first rule would conceal why the final amount differs from the starting rate. This matters when support needs to explain a receipt without reconstructing a past configuration from today’s settings.
The new venue selected the time-segment policy. Existing venues retained their session-start behavior. That separation kept a local business requirement from becoming an accidental platform default. The same principle applies to booking windows, time increments, membership allowances and tax presentation.
Test the boundary and the customer path
The August 15 record reports 46 booking-rule checks and 297 application acceptance checks passing, alongside TypeScript and relevant lint checks. New regressions covered the crossing interval and an interval wholly within the later band. Those totals describe the recorded revision, not a permanent guarantee for every later release.
The test deployment was also checked in desktop and 390-pixel mobile browsers. Selecting the one-hour session at 13:30 showed NZD 37.50, with no horizontal overflow or console errors in that pass. That task did not publish the change to production. Its evidence is a completed implementation and test release, and should be described at that scope.
Treat a quote as a reproducible decision
The durable improvement was a single explanation of the amount: selected venue policy, occupied interval, applicable rules and one final currency result. The UI, creation path and audit trail could all point back to that explanation. A pricing bug became much easier to reason about once the system stopped treating a booking as one start-time label.
Project context and evidence
Written from my August 2026 GolfJoy Play pricing implementation and test-release records. Public links below support the product and attribution context. Internal project records are summarized, with customer records, credentials, and employer source code omitted.
- GolfJoy Play ↗
Public platform context. Historical test and release scopes are described in this article.
- GolfJoy engineering case study ↗
Product interfaces and the company attribution for my five delivered projects.
