Skip to content

Quote to Booking conversion — developer reference

Audience: engineers working on Booking/BookingWizard, the Tourplan/Wish integrations, or anything that reads Booking.Booking.WindowStatus.

Source of truth: the Mendix model itself, read via model/. Every microflow name, activity number, XPath constraint and message string below was verbatim from the model at commit 255b346f3, Mendix 10.24.21. Node numbers in [n] refer to the rendered flow listing — run python3 tools/mxrender.py Booking.ConfirmBooking to see the same numbering.

Node numbers in five flows have moved since this was written. model/ now holds commit 83d87ba8e (108 commits later, same Mendix version), in which the rendering of API.BookQuote, API.CreateWindowBooking, API.GetQuote_B2B, API.ReplaceQuote and API.UpdateDMCOverrideLine changed length. Names, XPath and message strings elsewhere are unaffected, but re-render those five before trusting an [n] that points into them. Everything else here was checked against 255b346f3 and its documents are byte-identical at 83d87ba8e.

Anything not stated by the model is marked (inferred). Quoted NOTE: lines are Studio Pro canvas annotations — developer sticky notes, reproduced verbatim. They record intent, but they are undated commentary, not executable truth: where a note and the logic disagree, this document says so and the logic wins.

Revised 2026-08-07. The first version of this document was written against an extract that silently dropped four classes of model content: microflow-valued call parameters, canvas annotations, Rule-based split conditions, and sort orders. All four are now rendered — see §12 Rendering fidelity. Sections corrected as a result are marked (corrected).


1. What "conversion" actually means here

There is no single "convert quote to booking" transaction. The journey a user calls "turning a quote into a booking" is four distinct model operations, each with its own guard set, its own failure modes, and its own downstream integration calls:

# Operation Microflow Net effect on Booking.Booking
0 Create the quote CreateAgencyBooking / NewBookingByEmployee Source = Quote_only, WindowStatus = Quote_only, BookingStep = Step1
1 Convert to live booking Booking.ConvertToLiveBooking Source → Wilderness_window, WindowStatus → Draft, InventoryStatus → Draft
2 Provisional (hold inventory) Booking.Sub_BookProvisionally_AllInventory WindowStatus Draft\|Pending → P, lines → Provisional, expiry date assigned
3 Confirm Booking.ConfirmBookingSub_ConfirmBookingFinishConfirmation WindowStatus → C, lines → Confirmed, booking written to Tourplan

Step 1 is cheap and reversible-ish. Step 3 is the expensive one: it writes to Tourplan, Wish, HubSpot and Onbase, and may raise a deposit invoice.

Step 2 is not always a separate user action. Sub_ValidateBefore_TPConfirm [42] will call Sub_BookProvisionally_AllInventory(All, $Booking) itself if the booking is still Draft when confirmation is attempted, so a user can appear to go straight from Draft to Confirmed.

The four status axes

A booking carries four independent status-ish attributes. Confusing them is the single most common source of bugs in this area.

Attribute Type Default Meaning
Source Booking.BookingSource Wilderness_window How the booking came into being. Quote_only is what makes a record a quote.
WindowStatus Booking.BookingStatus Draft The status users see and most logic branches on.
InventoryStatus Booking.BookingStatus Draft Model doc: "really only reflects Wish status" — i.e. own-camp inventory.
TourplanStatus Booking.BookingStatus Draft Mirror of the record's state in Tourplan. Not written by conversion.

Booking.BookingSource values: Wilderness_window "Window", Ex_window "EX Window", EX_Tourplan "EX Tourplan", Placeholder "Placeholder", Quote_only "Quote".

Booking.BookingStatus values: Draft, Quote_only "Quote", P "Prov", Pending "Pend", C "Conf", I "Invoiced", Paid, D "DeInv", CX "Canc", CC "CCost", X "Deleted", Placeholder "Place", Travelling "Trav'g", Traveled "Trav'd", EX_Tourplan "EX TP".

Booking.BookingLineStatus values: Draft, Provisional, Waitlisted, Confirmed, Requested "On request", Pending, Cancelled, CancelledWithCosts, Not_applicable, Placeholder, Pseudo.

Plus the wizard position: Booking.BookingStep = Step1..Step4 (default Step1). Note there is no Step5 enum value even though the wizard has a Step 5 folder — Step 5 pages run with BookingStep = Step4.

Guard-relevant flags

Attribute Where it bites
NoChangesAllowed Blocks confirm (ConfirmBooking [10]) and blocks re-entry to the wizard (WizardStep2_FromBookingFile [2])
HoldingPattern Bypasses the validation result in ConfirmBooking [16], bypasses supplier-confirmation check, and suppresses the deposit invoice
AgentBlock Hides the booking from the agent; set on ITRVL API bookings during conversion
Priced (Booking.Priced) Must be Yes or Confirmed to confirm
ExpirationDate / EmployeeCanExtend Provisional-hold expiry; extension is Manager-only
TourPlanFullReference Empty ⇒ this is the first confirmation ⇒ deposit-invoice path runs
ConfirmationDate Model doc: "First conf" — only set if currently empty

2. Stage 0 — how a quote is created

Interactive entry point

Booking.IVK_NewQuote_Only (folder BookingWizard/General/MF; roles Agent, Debug, Employee, ExternalConsultant):

[2] CALL Booking.LeaveBookingChecks()
[3] SPLIT on type of currentUser
  [4] Administration.Agent:
      [5] retrieve the agent's Agency
      [7] if Agency/AgencyAccess = PortalUserOnly -> warning, stop
      [10-12] count Agency_EmployeeLead employees
      [13-14] exactly one -> CreateAgencyBooking($Agent, $LeadEmployee,
                 BookingSource.Quote_only, BookingStatus.Quote_only, $Agency, false, empty)
      [16] otherwise -> open page Booking.SelectConsultantBooking_Quote_Only
  [21] Administration.Employee:
      [22] CALL Booking.NewBookingByEmployee($Employee, BookingSource.Quote_only, empty, empty, false)

LeaveBookingChecks runs on entry, not exit — it finds any booking still flagged Booking_CurrentlyUsedBy = currentUser, warns about un-pushed Wish changes or a confirmed booking that is unpriced / not fully written to Tourplan, and releases the lock via ClearUser_Webhook.

Booking.CreateAgencyBooking

Signature: ($Agent, $Owner, $BookingSource, $BookingStatus, $Agency, $FlightsOnly, $Party). Source and status are parameters, so the same flow creates quotes and live bookings — the caller decides.

Hard precondition: GetTPdetails($Owner, empty, $Agency) must resolve a Tourplan login with a non-empty instance. Otherwise [29] "{1} is not setup in any Tourplan" and the flow returns empty — no booking at all.

On success [10] it creates the Booking with BookingReferenceDescriptive = 'DRAFT', BookingStep = Step1, and ~35 attributes copied off the Agency (currency, DMC, SADC pricing access, invoice splitting, meet-and-greet defaults, GEL flag, Tourplan instance, branch). Then [15-17] it seeds the child structure: BookingLeg #1, a PartyCostGroup named 'Guests' with Pax_Adults_Std = 2 / Pax_Total = 2 / StandardRoomCount = 1, and one Booking_RequiredRooms. [21] logs Booking.Event._New with 'New quote by agent #<n>' when the source is Quote_only. [25] opens Booking.BookingWizard_Step_1.

API entry point

API.CreateBooking and Booking.CreateAgencyBooking take the same parameter pair driven by a single boolean:

if $IsQuote then Booking.BookingSource.Quote_only else Booking.BookingSource.Wilderness_window,
if $IsQuote then Booking.BookingStatus.Quote_only else Booking.BookingStatus.Draft

and later set BookingStep = Step3 directly (API/flows.txt:4202, :4490) — the API path skips the interactive wizard steps.


3. Stage 1 — Booking.ConvertToLiveBooking

  • Folder: BookingWizard/Step4 - Quote/MF
  • File: mprcontents/1f/72/1f72d5dd-12c9-4fb3-8d91-aa272287f61d.mxunit
  • Params: $Booking : Booking.Booking, $CalledFromApi : Boolean
  • Roles: Booking.Agent, Booking.Debug, Booking.Employee, Booking.Manager
  • Returns: nothing

Guards, in evaluation order

# Node Condition Failure behaviour
G1 [2] $Booking/Source = BookingSource.Quote_only info "You can only convert quote bookings with availability to a live booking"
G2 [3] BookingStep = Step4 or Step3 info "You cannot convert the booking unless you are on step 3 of the booking wizard and have valid pricing"
G3 [6], [17-21] daysBetween(now, TripStartDate) < 1 ⇒ requires Booking.IsManager() non-manager: warning "Only a manager can book less than 1 day prior to the trip start date…" and stop. Manager: info "…Because you have manager rights, we will allow this booking" then continue
G4 [8-9] Booking.CheckRooming($Booking, false) returns true silent stop — no message is shown (see §8)

CheckRooming walks every AvailableRoomConfig flagged BookThis under the booking's legs, then every RequiredRoom flagged Required, and calls Booking.RoomingChecks(...) per room with the room's Option and Option_Policy. Any single failure sets the result false. The per-room messages come from RoomingChecks, not from ConvertToLiveBooking.

The mutation

[10] CHANGE $Booking set {
       Source            = BookingSource.Wilderness_window;
       WindowStatus      = BookingStatus.Draft;
       InventoryStatus   = BookingStatus.Draft;
       NotificationEmail = false
     } [commit YesWithoutEvents]
[11] CALL Booking.LogEvent($Booking, Booking.Event._New,
          'New real booking created from a quote', Booking.Object_Type.Booking)
[12] CALL Booking.WizardStep2_FromBookingFile($Booking, $CalledFromApi)
[13] CALL Booking.IVK_RefreshAvailability_X($Booking)
[14] JAVA CommunityCommons.executeMicroflowInBackground(microflow=Onbase.BookingStatus_Post, $Booking)

The flow carries one annotation, on the rooming check:

NOTE: "If not available status after checking availability, we must reselect rooms to ensure they have a sensible room make up - whether they can provisionally book them or waitlist them"

That is the stated purpose of guard G4 — it exists to force room reselection, not merely to validate. It also confirms that a rooming failure is meant to send the user back to Step 2 rather than fail quietly, which sharpens defect D1.

Points that matter when changing this:

  • commit YesWithoutEvents. No entity commit-event handlers fire. Anything that expects to react to a booking becoming live via a before/after-commit handler will not see this transition. Downstream sync happens only through the explicit calls that follow.
  • TourplanStatus is not touched. It stays at whatever it was (Draft by default). Tourplan only learns about the booking at confirmation.
  • Source is overwritten, so conversion is not idempotent and not reversible through this flow — G1 will reject a second attempt. There is no ConvertToQuote counterpart in the model.
  • NotificationEmail = false re-arms a notification, it does not suppress one. The model documents this attribute as *"Whether an email has been sent regarding new booking by an agent
  • to all in mailing list for the overnight job". Resetting it to false at conversion makes the newly-live booking eligible for the overnight new-booking mailing again (inferred: so the booking is announced as a booking, having previously been announced — or not — as a quote)*. Read it as a "sent" flag, not a "send" flag.
  • (corrected) executeMicroflowInBackground runs Onbase.BookingStatus_Post. Not generic follow-up work: this is a fire-and-forget notification to OnBase, the document-management system — a system boundary that is invisible in a synchronous call graph. UseReturnVariable is false, so success or failure is never checked by the caller. The same target is invoked from Booking.ConfirmBooking [24], Booking.Sub_BookProvisionally_AllInventory [34] and Booking.CancelBooking — i.e. OnBase is told on convert, on provisional, on confirm and on cancel. (inferred: it drives document generation — vouchers, itineraries — since nothing in the model consumes a response.) See §12 for why this was missing.

Re-entry into the wizard — WizardStep2_FromBookingFile

[2] if $Booking/NoChangesAllowed -> info "This booking has been locked. You may not make any
       changes to this booking in the Window, it must be taken ex-Window." and stop
[5] if $CalledFromApi:
      [6] AgentBlock = true when APIBooking and API_System = ITRVL
      [7] CALL API.SendWebHookLockBooking($Booking)
[9] branch on BookingStep:
      Step2 -> Sub_BookingStep2($Booking)
      Step3 -> IVK_GoBackToScreen2($Booking)
      Step4 -> Priced = empty, then IVK_GoBackToScreen2($Booking)
      other -> info "Unknown booking step - apologies just use wizard"

Converting from Step 4 clears Priced, which forces a reprice before confirmation can pass Sub_ValidateBefore_TPConfirm [16]. That is the mechanism by which quote pricing is not allowed to become booking pricing without re-running.

Entry points (corrected)

ConvertToLiveBooking has exactly 2 call sites, and they are not both UI:

Caller $CalledFromApi Kind
Booking.IVK_ConvertToLiveBooking false thin UI wrapper
API.BookQuote true published API operation — the partner/iTrvl path (§3a)

An earlier version of this document said the two call sites were "the two IVK wrappers". That was wrong: IVK_ConvertToBooking_List calls IVK_ConvertToLiveBooking, not ConvertToLiveBooking directly, and the second real call site is API.BookQuote.

The UI surface:

Caller Kind Where
Booking.IVK_ConvertToLiveBooking thin wrapper, ConvertToLiveBooking($Booking, false) BookingFile_Agent_050526, snippet Sn_BookingMenu, BookingOverview_Agent
Booking.IVK_ConvertToBooking_List grid button; rejects empty selection and >1 selection; calls the wrapper above BookingOverview_Agent, BookingOverview_Quotes and other overview grids

IVK_ConvertToBooking_List messages: "Please select quote to convert" (empty) and "Please select only 1 booking at a time" (multi-select). Both role lists are Agent, Debug, Employee, Manager.


3a. The API path — API.BookQuote

Published operation, roles API.Administrator, API.ApiUser. Parameter: $Request : API.Request. This is how a partner system (iTrvl and similar) converts a quote without touching the wizard.

Guard chain, each returning a coded error response:

# Node Condition Error code
1 [6] request carries a BookQuote payload SYS1010
2 [9-10] a Booking exists with [APIBooking] and the given BookingNumber IN4050
3 [14] WindowStatus is not CX or CC IN4080
4 [17] Source is Wilderness_window or Quote_only IN4090
5 [20] Source is Quote_only IN4100

Note guard 2: the lookup is constrained to [APIBooking], so a booking created in the UI cannot be converted through this operation even with a valid booking number.

On success:

[23] CALL Booking.ConvertToLiveBooking($Booking, true)
[24] CALL Booking.ProceedToScreen3_CreateBLs($Booking)
[25] CALL Booking.IVK_ProvisionalAll($Booking)
[26] CALL Booking.UpdateReferralAfterCreate($Booking)
[27] CHANGE $Booking set { API_System = API.API_System.ITRVL;
                           Booking_CurrentlyUsedBy = empty;
                           AgentBlock = false } [commit Yes]
[28] SPLIT if $Booking/Source = BookingSource.Wilderness_window
       true  -> BookQuoteResponse.Success = true
       false -> BookQuoteResponse.Success = false

So a single API call performs convert → build booking lines → hold inventory provisionally.

[27] stamps API_System = ITRVL unconditionally — even for a request that arrived from a different partner system. Any per-partner behaviour keyed on API_System is therefore wrong for non-iTrvl callers. (inferred: iTrvl was the only consumer when this was written.)

[28] infers success by re-reading Source. ConvertToLiveBooking sets Source = Wilderness_window only if all four of its guards passed, so BookQuote detects conversion failure by checking whether the attribute changed. That works, but it is an implicit contract between two microflows, and it collapses every distinct failure into one boolean — see defect D10.


4. Stage 2 — provisional hold

The two wrappers, and the HubSpot split (new)

Two near-identical invocation flows sit above the shared logic, and the difference between them is commercially significant:

Booking.IVK_ProvisionalAll Booking.IVK_ProvisionalAll_Agents
Roles Agent, Employee, ExternalConsultant Agent
NoChangesAllowed guard [2] yes [2] yes
Validate → provision → rooming list → Wish refresh [4-8] [4-8]
UpdateStatusChangeLog [11] [11]
HubSpot.AddProvisionalExpiryDateToHubSpot [12] [queued] absent
HubSpot_DMC.UpdateDealAfterProv [13] [queued] absent

Both stamp ProvisionalDate on first provisional [16], defaulting to ConfirmationDate if that is somehow already set.

HubSpot only hears about a provisional booking through one of the two buttons. Note the distinction is the button, not the user: IVK_ProvisionalAll's role list includes Agent, so an agent can reach the HubSpot-updating path too. Which button a user sees is a page visibility question, not a security one. Both buttons live on BookingWizard_Step_3.

API.BookQuote [25] calls IVK_ProvisionalAll — the HubSpot-updating variant — so partner bookings do reach HubSpot.

The two HubSpot calls are [queued], i.e. dispatched through the Mendix task queue rather than run inline, so a HubSpot outage delays but does not fail the provisional hold.

The shared logic

Booking.Sub_BookProvisionally_AllInventory($BookingSelection, $Booking) — 12 call sites, so treat it as a shared API rather than a wizard-local helper.

Four annotations on this flow, all load-bearing:

NOTE: "This is NB - if not allowed to book the BL status set to WAITLIST, otherwise it will always be set to PROV" — confirms the CanBook branch below is the intended design.

NOTE: "Either we update only WS bls, or all bls. - Existing will include WS also. We then set BookingLineStatus to PROV, and then update the Inventory status in the appropriate way, depending on Wish, LR or Window. (RQ must be done one by one)" — names the three inventory systems this flow updates. Tourplan is not one of them.

NOTE: "JG 050716 Removed RefreshWishHistorybyId - added as a job rather" — a Wish refresh that once ran inline now runs as a scheduled job. Worth confirming before assuming the inline sequence is complete.

NOTE: "Joe 13/5/16 removed 'CreateWIshHistory'"

[6]  WindowStatus = Draft|Pending -> P ;  C -> C (unchanged) ; anything else unchanged
[7]  retrieve live lines whose InventoryBookingSectorStatus is not Provisional/Confirmed/Requested
     and whose BookingLineStatus is not Cancelled/CancelledWithCosts
[8]  per line:
       Wish-irrelevant lines -> Provisional (Requested if BookingMethod = RQ)
       otherwise CheckSupplier + CanBook:
         CanBook -> Draft|Pending|Waitlisted (or Provisional-with-LR-Pending) become Provisional
         !CanBook -> Accommodation Draft/Waitlisted become Waitlisted with RoomWaitlistFlag = true;
                     everything else drops to Draft
[21] COMMIT the line list [without events]
[22] CALL Booking.Wish_Provisional($Booking)
     on success:
       [27] DataManagement.Sub_UpdateBookingStatus_Prov
       [28] Booking.Sub_AssignExpiryDate
       [29] BHL_LogEntry_Create(..., Booking.Event.Provisional)
       [31] EmailConsultantsAgentBooking($Booking, BookingStatus.P)
       [32] Logging.SystemLog 'Booking updated to provisional'
       [33] CreateWishHistory_lite
       [34] JAVA executeMicroflowInBackground(microflow=Onbase.BookingStatus_Post, $Booking)

BookingSelection.All additionally runs Booking.ProvisionallyBookLR for third-party (LiveRequest) inventory before the Wilderness-only path.

Expiry dates — Booking.Sub_AssignExpiryDate

[2] live inventory line with ExpirationDate, WishInventory = 'Inventory',
    InventoryBookingSectorStatus in (Provisional, Pending),
    first sort by Booking.BookingLine.ExpirationDate Ascending      <- i.e. the EARLIEST
[4] Booking.ExpirationDate = trimToDaysUTC(that line's ExpirationDate)
[6-8]  expiry within 48h        -> info "…will expire within the next 48 hours" + LogEvent
[23-24] expiry already past     -> info "…may already be released in Wish" + LogEvent
[11] no such line, and trip starts within 7 days -> ExpirationDate = TripEndDate + 1 day
[20-21] no such line otherwise  -> ExpirationDate = today + Agency/ExpiryDays   (default 7)
[15-18] push the booking-level ExpirationDate down onto chargeable non-Wish lines
        that have no ExpirationDate of their own

A 2017 annotation on this flow disagrees with the code:

NOTE: "JG 13.4.17 We now [take] the latest expiry date received on an Inventory line, and apply that to the all booking lines, if any have an earlier expiry date, we update Wish with the new date. If there is a header expiry, it should have been set to the latest inventory line expiry already"

The note says latest; the retrieve is first sort by ExpirationDate Ascending, which is the earliest. The code is conservative (the booking expires when its first component expires); the note describes the opposite. Treat the note as stale — but it is worth asking whoever owns this whether the current behaviour is the intended one, because the difference is the whole length of a hold. Sort order was invisible in the previous extract, so this contradiction could not have been spotted before.

Extension is Manager-only: Booking.IVK_ExtendExpiry (role Booking.Manager) commits the booking, logs Booking.Event.Update with 'Expiry date updated with warning of WAITLISTS', and pushes the new dates to Wish via DataManagement.UpdateExpiryDatesWish.

Quote price expiry — different thing

Booking.CheckQuoteExpiry($Booking) looks at live Draft lines with BookingMethod = 'LR' whose QuoteExpirationDate is in the past, and warns "The following booking line's quotation prices have expired, the supplier may ask you to fetch recent prices …". It returns false in that case but is advisory — it does not block. Booking.GetQuoteExpiry sources that date from PricingDynamic.DynamicPrice/ExpiryDate for suppliers flagged UsesDynamicPricing.


5. Stage 3 — confirmation

Booking.ConfirmBooking($Booking, $Full) : Boolean

Folder BookingWizard/Step4 - Quote/MF. Roles Administrator, Agent, Debug, Employee, ExternalConsultant. Five call sites, all in Booking:

Caller $Full
IVK_ConfirmConfirmation $Booking/Tmp_FullConfirmation
IVK_ConfirmFromBookingFile true
IVK_ConfirmFromBookingFile_Agent true
IVK_ConfirmFromBookingFile_Partial false
ReConf true

$Full = true means "confirm every provisional/draft line"; $Full = false is a partial confirmation of lines already set to Confirmed by hand.

Guard chain:

# Node Condition Failure message
C1 [3-4] CheckAgencyTaxIndicator — an Agency.TaxIndicator exists for this agency and this TourplanInstance "The is no TaxIndicator setup for Agency {1} for Tourplan Instance {2}. Please contact RARA…" (emitted inside CheckAgencyTaxIndicator)
C2 [6-7] BookingStep = Step3 or Step4 "You must be on step 3 of the booking wizard before you can confirm your booking…"
C3 [10-11] not NoChangesAllowed "Please have your consultant take this booking ex-window as it is locked for further changes."
C4 [15-16] Sub_ValidateBefore_TPConfirm($Booking, $Full) or HoldingPattern validation messages, see below
C5 [19-20] (corrected) RULE DataManagement.TourplanActive($Booking) "Tourplan is currently closed in the Window. Please try again later or contact support to reopen Tourplan"

C5 previously rendered as a bare SPLIT if with no condition, and the first version of this document told readers to confirm it in Studio Pro. It is a Rule-based split — the condition is the rule DataManagement.TourplanActive, evaluated against this booking. No Studio Pro trip needed; 81 splits across the app were affected by the same gap.

[14] captures $FirstConfirm = TourPlanFullReference is empty or '' before anything is written — this is what later drives the deposit-invoice decision.

Then:

[23] CHANGE $Booking set {
       WindowStatus     = BookingStatus.C;
       ConfirmationDate = if ConfirmationDate = empty then [%CurrentDateTime%] else ConfirmationDate
     }
[24] JAVA executeMicroflowInBackground(microflow=Onbase.BookingStatus_Post, $Booking)
[25] if $Full:
       [26] live lines where BookingLineStatus = 'Provisional'
                          or (BookingLineStatus = 'Draft' and BookingMethod = empty)
       [28] -> BookingLineStatus = Confirmed
       [29] COMMIT [without events]
[31] CALL DataManagement.TP_IsConfirmationRequired($Booking)
       false -> [33] info "No Tourplan Confirmation Required !"; return true
       true  -> [35] CALL Booking.Sub_ConfirmBooking($Booking, $Full)
[36] success:
       [37] UpdateReferralAfterConfirm   (lead/referral status -> Confirmed)
       [38] LogEvent(Booking.Event.Confirm, 'Booking updated to confirmed')
       [39] FinishConfirmation($Booking, $FirstConfirm)
       return true
     failure:
       TourplanStatus = EX_Tourplan -> error "The booking has been disconnected from Tourplan
                                               (ExTP). All Tourplan changes required must now be
                                               made in Tourplan…"
       otherwise                    -> error "Booking has failed to confirmed. Please see error
                                               log for details."
       return false

WindowStatus is set to C at [23], before Tourplan is written. If Sub_ConfirmBooking subsequently fails, the booking is left WindowStatus = C with TourplanStatus unchanged. LeaveBookingChecks [10]/[26] exists precisely to nag about that state: "Your booking … has been confirmed but is not in a completed state. It is either not priced or not fully written to Tourplan." Treat "Confirmed in Window" as not implying "Confirmed in Tourplan".

An annotation on Sub_ConfirmBooking states the opposite intent: NOTE: "Cannot let the Agent think they have confirmed the booking, if there is a problem confirming in Wish for any reason". That rule is enforced for the Wish leg (the flow returns false before writing anything) but not for the Tourplan leg, where the status is already C by the time the write is attempted. Defect D2 is therefore a deviation from documented intent, not merely an unfortunate ordering.

The whole flow is bracketed by DataManagement.jaLog(BookingNumber, -1, empty, true/false, …) calls — every exit path closes its log entry.

Booking.Sub_ValidateBefore_TPConfirm($Booking, $Full) : Boolean

Folder BookingWizard/Step5 - Confirmation/MF. The real gatekeeper. In branch order:

Permission branch (SPLIT on type of currentUser)

  • Administration.Agent [66-73]: requires Agent/AllowConfirmOfBookings (else "Please contact your Agency Administrator in order to confirm this booking.") and Agency/AgencyAccess = FullWindowAccess (else "You do not have rights to confirm a booking, please speak to a WS consultant").
  • Administration.Employee [53-65]: only constrained when Agency/EmployeeAccess = ProvisionalBookingOnly. Then:
  • agency Code starting 'ZZZ'"You cannot confirm a 'ZZZ' agency in Tourplan. Please change the agency in order to proceed."
  • otherwise requires IsManager() and daysBetween(now, TripStartDate) <= 14, else "You cannot confirm this agency, unless you are a manager and it is within 14 days of travel date."
  • Everything else falls through to the common path.

Content branch

Path Check Outcome on failure
$Full = false [10-11] at least one live chargeable line already Confirmed "At least one chargeable booking line needs to be in a confirmed status otherwise there are no lines to write to Tourplan" → false
$Full = true [49-51] no live chargeable line has InventoryBookingSectorStatus outside Confirmed/Provisional/Cancelled/Not_applicable/Waitlisted opens page Booking.UnProvisionalBookingLines → false
[13-14] WindowStatus = Draft, or LR/Wish lines in an unexpected inventory status, or CheckWish says an update is required [42] runs Sub_BookProvisionally_AllInventory(All, …) first; if that fails → false
[16-17] Priced is Yes or Confirmed "The booking cannot be taken further as the booking is not priced currently - please reprice, otherwise there may be a pricing issue that needs to be resolved first" → false
[21-34] every override line (OverrideOption, non-Accommodation, live, chargeable, not a PrivateJourney, subtype not Provide_wiggle/DMC_Override, whose linked Option is itself OverrideOption) has DescriptionEdited "You cannot confirm this booking as the following override lines require an updated description {1}" → false
[35-38] Sub_CheckBookingLinesConf_AP — supplier confirmations present, unless HoldingPattern sets InventoryStatus = Pending, opens Booking.UnconfirmedBookingLines_AP, and still returns true — advisory, not blocking

Booking.Sub_ConfirmBooking($Booking, $Full) : Boolean

[4]  Confirm_Wish($Booking, $Full)
       false -> info "Not all of your Wish booking lines were confirmed, please resolve this
                      before confirming to Tourplan (or exclude these Wish lines from the
                      confirmation process)" ; return false
[7]  SendAutomatedEmails_Confirmation for chargeable Confirmed lines whose TourPlanStatus != Confirmed
[8-11] non-Wish live Confirmed lines with InventoryBookingSectorStatus = Provisional
         -> InventoryBookingSectorStatus = Confirmed ; COMMIT (with events)
[12] EmailConsultantsAgentBooking($Booking, BookingStatus.C)
[13] Sub_CheckBookingLinesConf
[14-15] HasCancelLinesWithPermits recomputed from gorilla-permit vouchers on cancelled lines
[16] DataManagement.CheckTPOpen
       closed -> return true    (!! see note)
       open   -> [18] DataManagement.TP_ConfirmLinesInTourplan($Booking, true, false)
                    error -> Logging.LogMessage(Critical, 'The booking failed to write to TP…')
                             return false
                    ok    -> [20] WishAPI.UpdateTourplanReference($Booking)
                             [21-22] TourplanStatus in (C, D, I, Pending) -> TP_UpdateTourplanNotes
                             [25] GuestDetailsStatus = NeedsUpdate
                                  OriginalConfirmedStartDate = TripStartDate (if empty)
                             [26] BHL_LogEntry_Create(..., Booking.Event.Confirm)
                             return true

Confirm_Wish [8-13] is where Wish sector bookings are flipped to Confirmed via WishAPI (WishHeader_CreateUpdate, SetWishSBStatus) and DataManagement.Sub_UpdateBookingStatus_CONF is called. It returns true immediately for non-Wish bookings [21].

Two on error handlers wrap the Tourplan write and the booking-history-log write; both log to Logging.SystemLog at Critical and do not rethrow.

Booking.FinishConfirmation($Booking, $FirstConfirm)

Post-confirmation fan-out:

[2-3] GetServerSettings for Tourplan (this booking's instance) and WishAPI
[4]   HubSpot.AddConfirmedLodgesToHubSpotBooking($Booking)
[5]   Booking.UpdateInvoiced_R(...)
[6]   Booking.CheckInvoicing($Booking, false)
[7-14] deposit invoice runs only when ALL of:
         CheckInvoicing allowed
         not AllowInvoiceSplitting
         not GroupsBooking
         not EU_Booking
         not HoldingPattern
         $FirstConfirm
       -> CheckIsFinal, then CheckDepositInvoice($Booking, true, false, InvoiceType.All, …)
[16-23] WishBooking -> WriteGuestsToWish($Booking, true)   [error-handled]
[18]  DataManagement.RefreshWishHistoryById_FromBooking
[20]  Booking_AgencyInvoice = the booking's Agency
[21]  Onbase.BookingStatus_Post($Booking)

Each suppressed-deposit branch emits its own information message, e.g. "No deposit invoice has been created due to this being an EU booking. Please ensure the booking is deposit invoiced." — same for Groups/Series, invoice splitting, and holding pattern.


6. Integration touchpoints

System When Call
Wish (own-camp inventory) provisional Booking.Wish_Provisional, CreateWishHistory_lite
Wish confirm Booking.Confirm_WishWishHeader_CreateUpdate, SetWishSBStatus; WriteGuestsToWish
Wish expiry change DataManagement.UpdateExpiryDatesWish
Tourplan confirm DataManagement.CheckTPOpen, TP_ConfirmLinesInTourplan, TP_UpdateTourplanNotes, WishAPI.UpdateTourplanReference
HubSpot provisional (IVK_ProvisionalAll only) HubSpot.AddProvisionalExpiryDateToHubSpot, HubSpot_DMC.UpdateDealAfterProv — both [queued]
HubSpot confirm HubSpot.AddConfirmedLodgesToHubSpotBooking
OnBase convert, provisional, confirm, cancel Onbase.BookingStatus_Post, always fire-and-forget via executeMicroflowInBackground, return value never checked
ITRVL (partner API) convert, when $CalledFromApi API.SendWebHookLockBooking
LiveRequest (3rd-party) provisional Booking.ProvisionallyBookLR

7. Housekeeping — Booking.SE_CancelExpiredBookings

Four sweeps, each limit 150:

Sweep Selection Action
1 TripEndDate < now − 13 months, WindowStatus in (P, Pending), TourplanStatus in (Draft, EX_Tourplan), Source in (Wilderness_window, EX_Tourplan), TourPlanReference = 0 if GetWishBookingStatus = 'CANC'InventoryStatus = CX, WindowStatus = CX, ArchiveLog, Booking.CancelBooking
2 TripEndDate < now − 4 years, Source in (Placeholder, Quote_only), BrochureCosting = false Booking.DeleteBooking
3 TripEndDate < now − 2 years, Source in (Placeholder, Quote_only), BrochureCosting = true Booking.DeleteBooking — matches the attribute's own doc, "if brochure - keep on file 2 years"
4 TripStartDate < now − 4 years, WindowStatus = CX, no TourPlanFullReference, no WishBookingReference delete when the party has no other C/P booking

Every deletion writes a DataManagement.ArchiveLog first with process, Action, Booking, Reason and party name.


8. Known defects and traps

Each of these is read straight off the model. They are candidates for tickets, and several map directly onto the regression cases in docs/qa/regression-quote-to-booking.md.

D1 — ConvertToLiveBooking fails silently on rooming. ConvertToLiveBooking [9] case false: END with no message. If RoomingChecks happens not to emit a message for a given failure mode, the user clicks Convert and nothing at all happens. Compare G1/G2/G3, which all message.

D2 — Confirmed-in-Window ≠ confirmed-in-Tourplan. ConfirmBooking [23] sets WindowStatus = C before the Tourplan write. Any failure after that point leaves the booking claiming Confirmed. This is a designed-around condition (LeaveBookingChecks nags about it), not an accident, but every report or downstream consumer that reads WindowStatus = C as "in Tourplan" is wrong.

D3 — Sub_ConfirmBooking returns true when Tourplan is closed. [16-17] case false: return true — when CheckTPOpen says closed, the flow reports success without writing anything. ConfirmBooking then runs the full success path including FinishConfirmation (deposit invoice, HubSpot, Onbase). Note the outer flow has its own Tourplan-open guard at [19], so this is normally unreachable — but the two checks are different microflows and can diverge.

D4 — SE_CancelExpiredBookings log/reason strings contradict the queries. (resolved — the queries are authoritative.)

Annotations on the flow settle which side is right:

NOTE: "5/5/23 Joe & Francis, agree to keep history of quotes 4 years after travel PLaceholder and quote"

NOTE: "5/5/23 changed to 4 years Francis and Joe"

NOTE: "per Ianka - we need to keep cancelled / old bookings on hand for at least 6 months, Guests shop around and sometimes want their booking requoted at a later date"

The retention period was deliberately changed to 4 years on 2023-05-05, agreed by Joe and Francis. The ArchiveLog.Reason strings are leftovers from the earlier rule — Ianka's original 6-month retention, which is why one of them still says "6 months". So the queries implement agreed policy and the strings are stale text: fix the strings, not the queries. The mismatches: - Sweep 1 filters on $ThirteenMonthsAgo but the ArchiveLog.Reason says "Booking 6 months past trip end date". $SixMonthAgo [6] is computed and never used. - Sweep 2 filters on $FourYearAgo but logs "passed Trip end date by one year". $OneYearAgo [21] is computed and never used. - Sweep 4's reason says "1 year past trip end date" while filtering TripStartDate at 4 years.

The queries are almost certainly the intent and the strings are stale, but anyone auditing deletions from the logs will be misled.

D5 — Sweep 4 reads a party off an empty object. SE_CancelExpiredBookings [45-46]: the branch taken when $Booking_live = empty then does RETRIEVE over association Booking.Booking_Party from Booking_live. $Party_4 is always empty there, so the ArchiveLog.Party string is built from empty values. Should retrieve from $IteratorBooking.

D6 — The quote-expiry chaser e-mail does not run. Maintenance.SE_SendQuoteExpiryEmails is flagged EXCLUDED FROM DEPLOYMENT, has role Maintenance.Debug only, and filters on an attribute literally named QuoteExpiry_notused. It is dead. docs/03-business-flows.md §8's claim that "jobs chase the agent by email" is not supported by the deployed model for this job — the live expiry warnings come from Sub_AssignExpiryDate (in-session messages) and EmailConsultantsAgentBooking.

D7 — ConvertToLiveBooking_170626 is a stale duplicate. Byte-for-byte identical logic to ConvertToLiveBooking, flagged EXCLUDED FROM DEPLOYMENT. One of 457 dated clones (reports/dated-clones.md). Anyone fixing D1 must fix or delete both, or the next person diffs them and gets confused.

D8 — commit YesWithoutEvents on the conversion write. Deliberate, but it means the conversion is invisible to entity event handlers. If a future requirement is "notify X when a quote becomes a booking", it must be added as an explicit call in ConvertToLiveBooking, not as a commit handler.

D9 — Step-number drift in user-facing copy. G2 accepts Step3 or Step4 but the message says only "step 3". C2 likewise. The BookingStep enum has no Step5 although the wizard has a Step 5. Cosmetic, but it makes support tickets harder to triage.

D10 — API.BookQuote collapses every conversion failure into one boolean. [28] infers success by re-reading $Booking/Source. Combined with D1, a partner system whose quote fails the rooming check receives Success = false with no error code and no diagnostic — unlike guards 1–5, which all return specific codes (SYS1010, IN4050, IN4080, IN4090, IN4100). The five cheap validation failures are diagnosable; the one expensive business failure is not.

D11 — API.BookQuote stamps API_System = ITRVL for every caller. [27], unconditionally. Any logic branching on API_System misattributes non-iTrvl partners.

D12 — Sub_AssignExpiryDate's annotation contradicts its sort order. The note says the latest inventory expiry is used; the retrieve sorts Ascending and takes first, i.e. the earliest. One of the two is wrong and has been since 2017.


9. Reproducing this analysis

## the four core flows, verbatim
python3 tools/mxrender.py Booking.ConvertToLiveBooking
python3 tools/mxrender.py Booking.ConfirmBooking
python3 tools/mxrender.py Booking.Sub_ValidateBefore_TPConfirm
python3 tools/mxrender.py Booking.Sub_ConfirmBooking

## every write to WindowStatus anywhere in the app
grep -rn 'WindowStatus = Booking.BookingStatus' model/*/flows.txt

## who calls the conversion / confirmation
python3 tools/mxinspect.py callers Booking.ConvertToLiveBooking
python3 tools/mxinspect.py callers Booking.ConfirmBooking

## which pages expose the convert buttons
grep -rn 'IVK_ConvertToLiveBooking\|IVK_ConvertToBooking_List' model/*/pages.txt

## the status enums
grep -A20 'ENUMERATION Booking.BookingStatus' model/Booking/enums.txt

## every asynchronous handoff in the app, and its target
grep -rhoE 'executeMicroflowInBackground\(microflow=[A-Za-z_]+\.[A-Za-z_0-9]+' \
     model/*/flows.txt | sed 's/.*microflow=//' | sort | uniq -c | sort -rn

## developer intent left on the canvas, for one flow or for a whole module
python3 tools/mxrender.py Booking.ConfirmBooking | grep 'NOTE:'
grep -rh 'NOTE: ' model/Booking/flows.txt

## the API conversion path
python3 tools/mxrender.py API.BookQuote

10. What this document does not cover

Measured, not recalled — python3 tools/coverage.py Booking.Booking --attr WindowStatus,Source,BookingStep,InventoryStatus,TourplanStatus <this file> reports 18 of 20 published entry points that drive a booking's lifecycle as uncovered by this document and its cancellation sibling. The two covered are API.BookQuote and API.CancelBooking.

The uncovered set is a coherent body of work, not a scattering of edge cases — the partner API's booking-mutation surface:

Entry point Published on Reaches the lifecycle via
API.ReplaceQuote WWAPI_Rest, WWAPI_V2 directly
API.ReplaceBooking WWAPI_V2 directly
API.UpdateDMCOverrideLine WWAPI_V2 directly
API.AddAccomodationToBooking, API.AddAccomodationToStartEnd, API.AddDaysToBooking, API.ChangeAccomNumberOfNights, API.DeleteAccomodation, API.SwitchAccomodation WWAPI_V2 Booking.ProceedToScreen3_CreateBLs
API.AddFlightToStartEnd, API.AddTravelHubAndFlightToStartEnd WWAPI_V2 WizardStep2_FromBookingFile
API.CopyBooking WWAPI_V2 Booking.Sub_CopyBookingToDraft
API.ConfirmRooms WWAPI_V2 API.ConfirmBooking
API.UpdateBookingService WWAPI_V2 API.AddDMCToBL_Service
API.GetQuote_B2B, API.GetQuote_V2 WWAPI_Rest, WWAPI_V2 API.CreateWindowBookingread names, write operations
Transfers.AddGuideToBooking, Transfers.AddRTConsultant Transfers.RTWebhook Booking.Sub_CheckBookingLinesConf

Also deliberately out of scope here: booking-line construction internals (Sub_CreateBookingLines and its two siblings), the pricing engine (Pricing.Sub_PriceBookingAll), and the itinerary/document generation that follows confirmation.

API.CreateWindowBooking deserves its own note: it is not a published operation, but it is the shared booking-creation engine behind ProfitRoom, SiteMinder, NightsBridge and both B2B quote operations — the largest single orchestration in the model at 73 distinct calls.


11. See also


12. Rendering fidelity

The extract this document is built from is produced by tools/mxrender.py. On 2026-08-07 four classes of model content were found to be dropped silently. All are now rendered; the counts below are what was invisible before, across the whole application:

Content Was Now Why it mattered here
Microflow/entity-valued call parameters 0 of 95 rendered 95 Hid Onbase.BookingStatus_Post and 16 other async targets
Canvas annotations (sticky notes) 0 of 1,835 1,871 NOTE: lines Hid developer intent, author initials and dates
Rule-based split conditions 81 blank 81 named Hid guard C5 and 80 others
Sort attribute and order 1,190 of 1,292 blank 0 blank Hid whether a first retrieve takes earliest or latest

Verify the current state at any time:

grep -rhc 'NOTE: ' model/*/flows.txt      # annotations rendered
grep -rh  'SPLIT if$' model/*/flows.txt   # should be 2 (orphaned nodes)
grep -rhoE 'sort by *(->|$)' model/*/flows.txt   # should be 0

Two SPLIT if with no condition remain (Booking/flows.txt lines 10830 and 67246). Both sit in --- unreachable --- sections — orphaned nodes left behind by editing, with a genuinely empty condition in the model. They are not renderer gaps.

Still not rendered, by design: page styling and canvas geometry, custom-widget property schemas, and Studio Pro's visual branch ordering. Six parameter values in DocumentGeneration.SUB_ExampleDocument_Generate (a marketplace sample) carry no value in the model and render empty.