Regression test cases — Quote to Booking conversion¶
Audience: QA. Scope: creating a quote → converting it to a live booking → holding inventory provisionally → confirming to Tourplan, plus the expiry and housekeeping jobs that act on quotes.
Every case below is derived from a specific branch in the Mendix model, and the
Source column names the microflow and node number so you can re-derive the expected result
rather than trusting this table. Mechanism detail lives in
docs/deep-dives/quote-to-booking.md.
Priority: P1 = blocks release, P2 = must pass before sign-off, P3 = worth covering when time allows.
None of these cases have been executed. They are derived from the model's decision logic and state what the system currently does; a passing case says the behaviour is unchanged, not that the behaviour is correct. Cases that assert a known defect are marked as such.
Revised 2026-08-07. Sections M (API path), N (OnBase) and O (HubSpot) are new — they cover behaviour that was invisible in the previous model extract. See
quote-to-booking.md§12.
How to read a booking's state¶
Four attributes change independently. Assert on all four, not just the one on screen.
| Attribute | Quote | After convert | After provisional | After confirm |
|---|---|---|---|---|
Source |
Quote_only ("Quote") |
Wilderness_window ("Window") |
unchanged | unchanged |
WindowStatus |
Quote_only ("Quote") |
Draft |
P ("Prov") |
C ("Conf") |
InventoryStatus |
Quote_only |
Draft |
set by Wish sync | set by Wish sync |
TourplanStatus |
Draft |
Draft — unchanged |
unchanged | set by Tourplan write |
Booking-line status runs Draft → Provisional → Confirmed, with Waitlisted as the
no-availability branch and Requested ("On request") for BookingMethod = RQ lines.
A. Quote creation¶
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| A1 | Agent creates a quote, single lead consultant | Agent user, agency with AgencyAccess ≠ PortalUserOnly, exactly one Agency_EmployeeLead |
New Quote | Booking created; Source = Quote_only, WindowStatus = Quote_only, BookingStep = Step1, reference DRAFT; wizard Step 1 opens |
P1 | Booking.IVK_NewQuote_Only [13-14] |
| A2 | Agent creates a quote, multiple lead consultants | Same but two or more Agency_EmployeeLead |
New Quote | Page SelectConsultantBooking_Quote_Only opens instead of creating directly |
P2 | Booking.IVK_NewQuote_Only [16] |
| A3 | Portal-only agent is blocked | Agent whose agency has AgencyAccess = PortalUserOnly |
New Quote | Warning: "You currently only have access rights to the Information Centre and camp availability…". No booking created | P1 | Booking.IVK_NewQuote_Only [7-8] |
| A4 | Employee creates a quote | Employee user | New Quote | NewBookingByEmployee path runs with BookingSource.Quote_only |
P2 | Booking.IVK_NewQuote_Only [21-22] |
| A5 | Consultant not set up in Tourplan | Owner/agency combination with no TPLoginDetails, or a TPLoginDetails with empty instance |
New Quote | Info: "{name} is not setup in any Tourplan". No booking created — verify no orphan Booking row |
P1 | Booking.CreateAgencyBooking [4],[6],[29] |
| A6 | Default child structure is seeded | A1 passes | Inspect the new quote | One BookingLeg (number 1, active), one PartyCostGroup named Guests with Pax_Adults_Std = 2, Pax_Total = 2, StandardRoomCount = 1, TotalRooms = 1, one Booking_RequiredRooms |
P2 | Booking.CreateAgencyBooking [15-17] |
| A7 | Agency defaults are copied onto the quote | Agencies configured with contrasting values (CurrencyPreferred, UsesDMC, AllowInvoiceSharing, UseGEL, HasAccessSADCPricing, MeetGreet_*) |
Create a quote from each | Each corresponding booking attribute matches its agency source. Regression risk: these are copied once at creation and never re-read | P2 | Booking.CreateAgencyBooking [10] |
| A8 | Audit trail on creation | A1 passes | Check event log | Booking.Event._New with 'New quote by agent #:' + BookingNumber (note the colon — matching #12345 will not hit #:12345); a Logging.SystemLog row 'New booking by agent' |
P3 | Booking.CreateAgencyBooking [21],[24] |
| A9 | API-created quote | Partner API call with IsQuote = true |
POST via API | Source = Quote_only, WindowStatus = Quote_only, and BookingStep set to Step3 by the API path (not Step1) |
P2 | API.CreateWindowBooking [105],[192] |
| A10 | API-created live booking | Same with IsQuote = false |
POST via API | Source = Wilderness_window, WindowStatus = Draft |
P2 | API.CreateWindowBooking [105] |
B. Convert to live booking — guards¶
All of these must reject and leave Source = Quote_only unchanged. Assert the booking is
untouched, not merely that a message appeared.
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| B1 | Non-quote cannot be converted | Booking with Source = Wilderness_window |
Convert | Info: "You can only convert quote bookings with availability to a live booking"; nothing changes | P1 | Booking.ConvertToLiveBooking [2],[22] |
| B2 | Already-converted quote cannot be re-converted | Run C1 first, then convert again | Convert twice | Second attempt hits B1's message. Conversion is not idempotent — confirm no second Event._New is logged |
P1 | Booking.ConvertToLiveBooking [2] |
| B3 | Wrong wizard step | Quote at BookingStep = Step1 |
Convert | Info: "You cannot convert the booking unless you are on step 3 of the booking wizard and have valid pricing" | P1 | Booking.ConvertToLiveBooking [3-4] |
| B4 | Step 2 also rejected | Quote at BookingStep = Step2 |
Convert | Same rejection as B3. Only Step3 and Step4 are accepted |
P2 | Booking.ConvertToLiveBooking [3] |
| B5 | Non-manager, trip starts inside 24 h | Non-manager user; TripStartDate less than 1 day away; quote at Step 3/4 |
Convert | Warning: "Only a manager can book less than 1 day prior to the trip start date. Please ask you manager to create the booking (complete step1)"; nothing changes | P1 | Booking.ConvertToLiveBooking [6],[17-19] |
| B6 | Manager, trip starts inside 24 h | Manager user, same booking | Convert | Info: "Please be aware that this booking is less than a day before trip start date. Because you have manager rights, we will allow this booking", then conversion proceeds | P1 | Booking.ConvertToLiveBooking [21] |
| B7 | Boundary — exactly 1 day | TripStartDate exactly 1 day out |
Convert as non-manager | Condition is daysBetween(now, TripStartDate) < 1, so 1 day should pass without the manager gate. Verify the boundary explicitly |
P2 | Booking.ConvertToLiveBooking [6] |
| B8 | Rooming check fails | Quote with an AvailableRoomConfig flagged BookThis whose RequiredRoom violates its Option_Policy (e.g. child outside the age policy) |
Convert | Conversion is blocked and booking unchanged. Known defect D1: Booking.ConvertToLiveBooking [9] has no message of its own — if RoomingChecks is silent for this failure mode, the button appears to do nothing. Raise a bug if no message appears |
P1 | Booking.ConvertToLiveBooking [8-9],[16] |
| B9 | Locked booking cannot re-enter the wizard | Quote with NoChangesAllowed = true at Step 3/4 with valid rooming |
Convert | Status changes are applied at [10], then Booking.WizardStep2_FromBookingFile [2-3] shows "This booking has been locked. You may not make any changes to this booking in the Window, it must be taken ex-Window." — the conversion still happened. Confirm this is intended behaviour with the product owner |
P2 | Booking.ConvertToLiveBooking [10-12], Booking.WizardStep2_FromBookingFile [2] |
| B10 | Grid button — nothing selected | Quotes overview grid | Click Convert with no rows selected | Info: "Please select quote to convert" | P3 | Booking.IVK_ConvertToBooking_List [10] |
| B11 | Grid button — multiple selected | Same | Select 2 quotes, click Convert | Info: "Please select only 1 booking at a time"; neither booking changes | P2 | Booking.IVK_ConvertToBooking_List [8] |
| B12 | Role check | Users in each of Agent, Employee, Manager, Debug, and one role outside that list (e.g. Booking.SupplierUser) |
Attempt convert | Only the four listed roles can execute ConvertToLiveBooking / IVK_ConvertToBooking_List |
P2 | model roles on ConvertToLiveBooking |
C. Convert to live booking — happy path¶
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| C1 | Convert from Step 3 | Quote at BookingStep = Step3, rooming valid, trip more than 1 day out |
Convert | Source = Wilderness_window, WindowStatus = Draft, InventoryStatus = Draft, NotificationEmail = false. Wizard returns to Step 2 via IVK_GoBackToScreen2 |
P1 | Booking.ConvertToLiveBooking [10-12], Booking.WizardStep2_FromBookingFile [17] |
| C2 | Convert from Step 4 clears pricing | Quote at BookingStep = Step4 with Priced = Yes |
Convert | Same status changes and Priced is cleared to empty. The booking cannot be confirmed until it is repriced |
P1 | Booking.WizardStep2_FromBookingFile [14-15] |
| C3 | Tourplan status untouched | C1 | Convert, inspect | TourplanStatus still Draft. Nothing is written to Tourplan at conversion |
P1 | Booking.ConvertToLiveBooking [10] |
| C4 | Audit trail on conversion | C1 | Convert, check log | Booking.Event._New with description "New real booking created from a quote" |
P2 | Booking.ConvertToLiveBooking [11] |
| C5 | Availability is refreshed | C1 with a camp whose availability changed since the quote was priced | Convert | IVK_RefreshAvailability_X runs; lines whose availability has gone should surface as unavailable on Step 2 rather than silently carrying stale quote availability |
P1 | Booking.ConvertToLiveBooking [13] |
| C6 | Commit fires no event handlers | C1 | Convert | The write is commit YesWithoutEvents. Any integration or handler expected to trigger on the booking commit will not fire. Verify nothing downstream depends on it |
P2 | Booking.ConvertToLiveBooking [10] |
| C7 | API conversion locks the ITRVL booking | Quote with APIBooking = true, API_System = ITRVL, converted with CalledFromApi = true |
Convert via API | AgentBlock = true and API.SendWebHookLockBooking is called. Agent can no longer see the booking |
P2 | Booking.WizardStep2_FromBookingFile [6-7] |
| C8 | API conversion, non-ITRVL | APIBooking = true, different API_System |
Convert via API | AgentBlock unchanged; webhook still called |
P3 | Booking.WizardStep2_FromBookingFile [6-7] |
| C9 | Unknown wizard step | Booking whose BookingStep is empty or otherwise unhandled, forced to pass guards |
Convert | Info: "Unknown booking step - apologies just use wizard" | P3 | Booking.WizardStep2_FromBookingFile [12] |
| C10 | Quote-to-booking on every entry point | Repeat C1 from: booking file (BookingFile_Agent_050526), booking menu snippet (Sn_BookingMenu), agent overview grid (BookingOverview_Agent), quotes overview grid (BookingOverview_Quotes) |
Convert from each | Identical outcome from all four. These are separate buttons wired to two different wrappers | P2 | pages.txt references |
D. Provisional hold¶
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| D1 | Draft booking goes provisional | Converted booking, WindowStatus = Draft, all lines bookable |
Book provisionally (all inventory) | WindowStatus = P; live lines move to Provisional |
P1 | Booking.Sub_BookProvisionally_AllInventory [6],[17] |
| D2 | Pending also goes provisional | WindowStatus = Pending |
Same | WindowStatus = P |
P2 | Booking.Sub_BookProvisionally_AllInventory [6] |
| D3 | Confirmed booking is not downgraded | WindowStatus = C |
Same | WindowStatus stays C |
P1 | Booking.Sub_BookProvisionally_AllInventory [6] |
| D4 | Unavailable accommodation waitlists | Accommodation line where CanBook is false and line is Draft or Waitlisted |
Book provisionally | Line → Waitlisted, RoomWaitlistFlag = true |
P1 | Booking.Sub_BookProvisionally_AllInventory [19] |
| D5 | Unavailable non-accommodation drops to Draft | Non-accommodation line where CanBook is false |
Book provisionally | Line → Draft, RoomWaitlistFlag = false |
P2 | Booking.Sub_BookProvisionally_AllInventory [19] |
| D6 | On-request lines | Line with BookingMethod = RQ |
Book provisionally | BookingLineStatus = Provisional, InventoryBookingSectorStatus = Requested ("On request") |
P2 | Booking.Sub_BookProvisionally_AllInventory [11],[17] |
| D7 | Third-party inventory included on "All" | Booking with LiveRequest lines, selection All |
Book provisionally | ProvisionallyBookLR runs before the Wilderness-only path |
P2 | Booking.Sub_BookProvisionally_AllInventory [38-39] |
| D8 | Wish failure does not report success | Force Wish_Provisional to fail |
Book provisionally | Flow does not take the success path — no Event.Provisional log, no consultant e-mail, no expiry date assigned |
P1 | Booking.Sub_BookProvisionally_AllInventory [22-23] |
| D9 | Provisional notifications | D1 passes | Book provisionally | EmailConsultantsAgentBooking(..., BookingStatus.P) sends; Logging.SystemLog row "Booking updated to provisional"; booking-history entry Booking.Event.Provisional |
P2 | Booking.Sub_BookProvisionally_AllInventory [29],[31],[32] |
E. Expiry¶
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| E1 | Expiry taken from earliest inventory line | Provisional booking with several Wish inventory lines with differing ExpirationDate |
Book provisionally | Booking.ExpirationDate = earliest such line's date, trimmed to day (UTC) |
P1 | Booking.Sub_AssignExpiryDate [2],[4] |
| E2 | 48-hour warning | Earliest line expires within 2 days | Book provisionally | Info: "Please be aware one or more of your Wish bookings will expire within the next 48 hours" plus a matching event-log entry | P2 | Booking.Sub_AssignExpiryDate [6-8] |
| E3 | Already-expired warning | Earliest line's ExpirationDate in the past |
Book provisionally | Info: "…may already be released in Wish" plus event-log entry | P2 | Booking.Sub_AssignExpiryDate [23-24] |
| E4 | No inventory line, trip far out | No qualifying inventory line; trip start more than 7 days away | Book provisionally | ExpirationDate = today + Agency/ExpiryDays. Test with at least two agencies with different ExpiryDays |
P1 | Booking.Sub_AssignExpiryDate [20-21] |
| E5 | No inventory line, trip within 7 days | Trip start within 7 days | Book provisionally | ExpirationDate = TripEndDate + 1 day |
P2 | Booking.Sub_AssignExpiryDate [22] |
| E6 | Expiry cascades to chargeable lines | Booking-level expiry set; chargeable non-Wish lines have no expiry of their own | Book provisionally | Those lines inherit the booking's ExpirationDate |
P2 | Booking.Sub_AssignExpiryDate [15-18] |
| E7 | Only a Manager can extend | Non-manager and Manager users | Attempt Extend Expiry | Non-manager cannot execute IVK_ExtendExpiry; Manager can |
P1 | model roles on IVK_ExtendExpiry |
| E8 | Extension pushes to Wish | Manager extends expiry | Extend | DataManagement.UpdateExpiryDatesWish runs; event log "Expiry date updated with warning of WAITLISTS" |
P2 | Booking.IVK_ExtendExpiry [4-5] |
| E9 | Expired quote prices warn but do not block | Live Draft line with BookingMethod = 'LR' and QuoteExpirationDate in the past |
Continue through the wizard | Info listing each expired line with its start date and expiry date. The user is not blocked | P2 | Booking.CheckQuoteExpiry [2],[7] |
| E10 | Dynamic-pricing expiry sourced correctly | Supplier with UsesDynamicPricing = true |
Price a quote | QuoteExpirationDate comes from the earliest PricingDynamic.DynamicPrice/ExpiryDate for the room config |
P3 | Booking.GetQuoteExpiry [2-4] |
F. Confirmation — guards¶
Each of these must block confirmation and leave WindowStatus unchanged.
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| F1 | Missing tax indicator | Agency with no Agency.TaxIndicator for the booking's TourplanInstance |
Confirm | Info: "The is no TaxIndicator setup for Agency {name} for Tourplan Instance {instance}. Please contact RARA if you urgently need to confirm the booking or else try tommorrow after the nighly updates."; confirmation aborts | P1 | ConfirmBooking [3-4], Booking.CheckAgencyTaxIndicator [6] |
| F2 | Tax indicator exists for a different Tourplan instance | Agency has a TaxIndicator but for another instance |
Confirm | Still blocked — the check is per instance, not per agency | P1 | Booking.CheckAgencyTaxIndicator [3] |
| F3 | Wrong wizard step | BookingStep = Step1 or Step2 |
Confirm | Info: "You must be on step 3 of the booking wizard before you can confirm your booking, please go to the booking wizard and navigate to the quote screen" | P1 | ConfirmBooking [6-7] |
| F4 | Locked booking | NoChangesAllowed = true |
Confirm | Info: "Please have your consultant take this booking ex-window as it is locked for further changes." | P1 | ConfirmBooking [10-11] |
| F5 | Tourplan closed | Tourplan closed in the Window | Confirm | Info: "Tourplan is currently closed in the Window. Please try again later or contact support to reopen Tourplan"; WindowStatus unchanged |
P1 | ConfirmBooking [19-20] |
| F6 | Unpriced booking | Priced is No (or empty) |
Confirm | Info: "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" | P1 | Booking.Sub_ValidateBefore_TPConfirm [16-17] |
| F7 | Convert-then-confirm without repricing | Quote converted from Step 4 (so Priced was cleared by C2), then confirm immediately |
Convert, Confirm | Blocked by F6's message. This is the highest-value cross-stage regression in the suite | P1 | Booking.WizardStep2_FromBookingFile [14] + Booking.Sub_ValidateBefore_TPConfirm [16] |
| F8 | Partial confirm with no confirmed line | $Full = false (Confirm Partial), no live chargeable line in Confirmed |
Confirm partial | Info: "At least one chargeable booking line needs to be in a confirmed status otherwise there are no lines to write to Tourplan" | P1 | Booking.Sub_ValidateBefore_TPConfirm [46] |
| F9 | Full confirm with un-provisionalised inventory | $Full = true, a live chargeable line whose InventoryBookingSectorStatus is outside Confirmed/Provisional/Cancelled/Not_applicable/Waitlisted |
Confirm | Page Booking.UnProvisionalBookingLines opens listing them; confirmation aborts |
P1 | Booking.Sub_ValidateBefore_TPConfirm [49-51] |
| F10 | Draft booking is auto-provisionalised | WindowStatus = Draft at confirm time |
Confirm | Sub_BookProvisionally_AllInventory(All, …) runs first; if it succeeds, validation continues. A user can go Draft → Confirmed in one click |
P1 | Booking.Sub_ValidateBefore_TPConfirm [14],[42] |
| F11 | Auto-provisional failure blocks confirm | Same as F10 but force the provisional step to fail | Confirm | Confirmation aborts, returns false | P1 | Booking.Sub_ValidateBefore_TPConfirm [43-45] |
| F12 | Override line without an edited description | Live chargeable line with OverrideOption, non-Accommodation, linked Option also OverrideOption, DescriptionEdited = false, not a private journey, subtype not Provide_wiggle/DMC_Override |
Confirm | Info: "You cannot confirm this booking as the following override lines require an updated description …" naming line description and supplier | P2 | Booking.Sub_ValidateBefore_TPConfirm [21-34] |
| F13 | Override line exclusions | Same but subtype Provide_wiggle / DMC_Override / accommodation / private journey |
Confirm | Those lines are excluded and do not block | P2 | Booking.Sub_ValidateBefore_TPConfirm [21] |
| F14 | Unconfirmed suppliers — advisory only | Supplier confirmations outstanding, HoldingPattern = false |
Confirm | InventoryStatus set to Pending, page UnconfirmedBookingLines_AP opens — but validation returns true and confirmation continues. Verify this is intended |
P1 | Booking.Sub_ValidateBefore_TPConfirm [35-38] |
| F15 | Holding pattern bypasses supplier check | Same but HoldingPattern = true |
Confirm | No InventoryStatus = Pending, no page; proceeds |
P2 | Booking.Sub_ValidateBefore_TPConfirm [36] |
| F16 | Holding pattern bypasses validation failure | Any validation failure with HoldingPattern = true |
Confirm | ConfirmBooking [16] proceeds regardless of the validation result. Deliberately powerful — verify it is restricted appropriately |
P1 | ConfirmBooking [16] |
| F17 | Wish lines not all confirmed | Wish booking where Confirm_Wish cannot confirm every sector |
Confirm | 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)"; returns false | P1 | Booking.Sub_ConfirmBooking [4-5],[39] |
G. Confirmation — permissions¶
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| G1 | Agent without confirm rights | Agent/AllowConfirmOfBookings = false |
Confirm | Info: "Please contact your Agency Administrator in order to confirm this booking." | P1 | Booking.Sub_ValidateBefore_TPConfirm [72] |
| G2 | Agent whose agency lacks full access | AllowConfirmOfBookings = true but Agency/AgencyAccess ≠ FullWindowAccess |
Confirm | Info: "You do not have rights to confirm a booking, please speak to a WS consultant" | P1 | Booking.Sub_ValidateBefore_TPConfirm [68-69] |
| G3 | Agent fully entitled | AllowConfirmOfBookings = true, AgencyAccess = FullWindowAccess |
Confirm | Proceeds to the content checks | P1 | Booking.Sub_ValidateBefore_TPConfirm [68] |
| G4 | ZZZ agency blocked | Employee, Agency/EmployeeAccess = ProvisionalBookingOnly, agency Code starts ZZZ |
Confirm | Info: "You cannot confirm a 'ZZZ' agency in Tourplan. Please change the agency in order to proceed." | P1 | Booking.Sub_ValidateBefore_TPConfirm [55-56] |
| G5 | Provisional-only agency, non-manager | Employee (not manager), EmployeeAccess = ProvisionalBookingOnly, non-ZZZ agency |
Confirm | Info: "You cannot confirm this agency, unless you are a manager and it is within 14 days of travel date." | P1 | Booking.Sub_ValidateBefore_TPConfirm [65] |
| G6 | Provisional-only agency, manager outside 14 days | Manager, EmployeeAccess = ProvisionalBookingOnly, trip more than 14 days out |
Confirm | Same rejection as G5 — both conditions are required | P1 | Booking.Sub_ValidateBefore_TPConfirm [63-64] |
| G7 | Provisional-only agency, manager inside 14 days | Manager, trip 14 days or less away | Confirm | Proceeds | P1 | Booking.Sub_ValidateBefore_TPConfirm [63] |
| G8 | Boundary — exactly 14 days | Manager, daysBetween(now, TripStartDate) exactly 14 |
Confirm | Condition is <= 14, so this should pass. Test explicitly |
P2 | Booking.Sub_ValidateBefore_TPConfirm [63] |
| G9 | Unrestricted agency employee | Employee, EmployeeAccess ≠ ProvisionalBookingOnly |
Confirm | No manager or 14-day constraint applies | P2 | Booking.Sub_ValidateBefore_TPConfirm [53] |
H. Confirmation — happy path and downstream effects¶
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| H1 | Full confirm | All guards pass, $Full = true, provisional lines present |
Confirm | WindowStatus = C; ConfirmationDate set to now; all live Provisional lines and Draft lines with empty BookingMethod become Confirmed |
P1 | ConfirmBooking [23],[26-29] |
| H2 | ConfirmationDate not overwritten on reconfirm |
Booking already confirmed once, ConfirmationDate populated |
Reconfirm | ConfirmationDate retains its original value |
P1 | ConfirmBooking [23] |
| H3 | Partial confirm leaves other lines alone | $Full = false, mix of Confirmed and Provisional lines |
Confirm partial | Only the already-Confirmed lines go to Tourplan; Provisional lines are untouched |
P1 | ConfirmBooking [25] |
| H4 | No Tourplan confirmation required | TP_IsConfirmationRequired returns false |
Confirm | Info: "No Tourplan Confirmation Required !", returns success, WindowStatus = C. Nothing is written to Tourplan and FinishConfirmation does not run — so no deposit invoice, no HubSpot push. Verify against expectations |
P1 | ConfirmBooking [31-34] |
| H5 | Tourplan write succeeds | Normal path | Confirm | TP_ConfirmLinesInTourplan runs; WishAPI.UpdateTourplanReference populates TourPlanFullReference; GuestDetailsStatus = NeedsUpdate; OriginalConfirmedStartDate set to TripStartDate if it was empty |
P1 | Booking.Sub_ConfirmBooking [18],[20],[25] |
| H6 | Tourplan notes updated for eligible statuses | TourplanStatus in C, D, I, Pending |
Confirm | TP_UpdateTourplanNotes runs; failure is logged Critical but does not abort |
P2 | Booking.Sub_ConfirmBooking [21-23],[31] |
| H7 | Tourplan write fails | Force TP_ConfirmLinesInTourplan to error |
Confirm | Error: "Booking has failed to confirmed. Please see error log for details."; a Critical Logging.SystemLog entry naming Sub_ConfirmBooking. WindowStatus is already C — assert the inconsistent state and that LeaveBookingChecks nags on exit (defect D2) |
P1 | Booking.ConfirmBooking [43], Booking.Sub_ConfirmBooking [34] |
| H8 | Ex-Tourplan booking | TourplanStatus = EX_Tourplan and confirmation fails |
Confirm | Error: "The booking has been disconnected from Tourplan (ExTP). All Tourplan changes required must now be made in Tourplan as the booking has travelled already." | P2 | Booking.ConfirmBooking [46] |
| H9 | Referral marked confirmed | Booking linked to a Booking.Referral not yet Confirmed |
Confirm | Referral/Status = LeadStatus.Confirmed |
P2 | Booking.UpdateReferralAfterConfirm [5] |
| H10 | Referral already confirmed | Referral already Confirmed |
Confirm | No change, no error | P3 | Booking.UpdateReferralAfterConfirm [4],[7] |
| H11 | First confirm raises a deposit invoice | TourPlanFullReference empty; AllowInvoiceSplitting, GroupsBooking, EU_Booking, HoldingPattern all false; CheckInvoicing allows |
Confirm | CheckDepositInvoice runs and a deposit invoice is created |
P1 | Booking.FinishConfirmation [12-14] |
| H12 | Second confirm does not re-invoice | TourPlanFullReference populated |
Reconfirm | $FirstConfirm = false → no deposit invoice |
P1 | ConfirmBooking [14], Booking.FinishConfirmation [13] |
| H13 | Deposit suppressed — invoice splitting | AllowInvoiceSplitting = true |
Confirm | Info: "No deposit invoice has been created due to invoice splitting being allowed on this booking. Please ensure the booking is deposit invoiced."; no invoice | P2 | Booking.FinishConfirmation [30] |
| H14 | Deposit suppressed — groups/series | GroupsBooking = true |
Confirm | Info: "…due to this being a Groups or Series booking…" | P2 | Booking.FinishConfirmation [29] |
| H15 | Deposit suppressed — EU booking | EU_Booking = true |
Confirm | Info: "…due to this being an EU booking…" | P2 | Booking.FinishConfirmation [28] |
| H16 | Deposit suppressed — holding pattern | HoldingPattern = true |
Confirm | Info: "No deposit invoice has been created due to this being in a holding patter. Please ensure the booking is deposit invoiced." (typo is in the model) | P2 | Booking.FinishConfirmation [26] |
| H17 | HubSpot receives confirmed lodges | H1 passes | Confirm | HubSpot.AddConfirmedLodgesToHubSpotBooking runs; confirmed lodges appear against the HubSpot booking |
P2 | Booking.FinishConfirmation [4] |
| H18 | Onbase notified | H1 passes | Confirm | Onbase.BookingStatus_Post runs |
P2 | Booking.FinishConfirmation [21] |
| H19 | Guests written to Wish | WishBooking = true |
Confirm | WriteGuestsToWish($Booking, true) runs. On failure a Critical log "Failed to write guests to Wish on confirmation" is written and confirmation still completes |
P2 | Booking.FinishConfirmation [23-24] |
| H20 | Agency invoice link set | H1 passes | Confirm | Booking_AgencyInvoice points at the booking's agency |
P3 | Booking.FinishConfirmation [19-20] |
| H21 | Confirmation e-mails | H1 passes | Confirm | SendAutomatedEmails_Confirmation for chargeable confirmed lines whose TourPlanStatus ≠ Confirmed; EmailConsultantsAgentBooking(…, BookingStatus.C) |
P2 | Booking.Sub_ConfirmBooking [7],[12] |
| H22 | Audit trail on confirmation | H1 passes | Confirm | Booking.Event.Confirm "Booking updated to confirmed"; booking-history entry from BHL_LogEntry_Create with 'Confirm booking' |
P2 | Booking.ConfirmBooking [38], Booking.Sub_ConfirmBooking [26] |
| H23 | Gorilla permits on cancelled lines | Booking with a GorillaReceiptedVoucher on a Cancelled/CancelledWithCosts line, or a permit-date mismatch |
Confirm | HasCancelLinesWithPermits = true; false when no such line |
P3 | Booking.Sub_ConfirmBooking [14-15] |
I. Housekeeping job — SE_CancelExpiredBookings¶
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| I1 | Stale provisional cancelled in Wish | Booking with TripEndDate more than 13 months ago, WindowStatus P or Pending, TourplanStatus Draft or EX_Tourplan, Source Wilderness_window or EX_Tourplan, TourPlanReference = 0, and Wish reports CANC |
Run the job | WindowStatus = CX, InventoryStatus = CX, CancelBooking runs, ArchiveLog row written |
P1 | Booking.SE_CancelExpiredBookings [8-17] |
| I2 | Not cancelled in Wish is skipped | Same but Wish does not report CANC |
Run the job | Booking untouched | P1 | Booking.SE_CancelExpiredBookings [11-12] |
| I3 | Old quotes deleted | Source Quote_only or Placeholder, BrochureCosting = false, TripEndDate more than 4 years ago |
Run the job | DeleteBooking runs; ArchiveLog written first |
P1 | Booking.SE_CancelExpiredBookings [23-28] |
| I4 | Brochure-costing quotes deleted earlier | Same but BrochureCosting = true, TripEndDate more than 2 years ago |
Run the job | Deleted | P2 | Booking.SE_CancelExpiredBookings [32-37] |
| I5 | Recent quotes survive | Quote with TripEndDate 1 year ago |
Run the job | Untouched. Guards against an off-by-one in the date arithmetic | P1 | Booking.SE_CancelExpiredBookings [23] |
| I6 | Cancelled bookings deleted only when the party has none live | WindowStatus = CX, TripStartDate more than 4 years ago, no Tourplan or Wish reference |
Run twice: once where the party has a live C/P booking, once where it does not |
Deleted only in the second case | P1 | Booking.SE_CancelExpiredBookings [43-49] |
| I7 | Batch cap | More than 150 qualifying records in any sweep | Run the job | At most 150 processed per sweep per run; the rest are picked up on later runs | P2 | SE_CancelExpiredBookings limits |
| I8 | Archive log accuracy | I3 or I6 | Run, inspect ArchiveLog |
Known defect D4/D5: reason strings state 6 months / 1 year while the queries use 13 months / 4 years, and the sweep-4 party name is read off an empty object. Log the mismatch rather than adjusting expectations | P2 | Booking.SE_CancelExpiredBookings [16],[27],[46-47] |
J. Known defects to assert against¶
Turn each of these into a standing test so a fix is detected, and a regression is caught.
| ID | Defect | Test |
|---|---|---|
| J1 | D1 — silent rooming rejection on convert | B8. Assert some user-visible message appears |
| J2 | D2 — WindowStatus = C before the Tourplan write succeeds |
H7. Assert the inconsistent state is reachable and that the warning appears on leaving the booking |
| J3 | D3 — Sub_ConfirmBooking returns success when Tourplan is closed |
If ConfirmBooking's own Tourplan guard can ever be bypassed (e.g. HoldingPattern), confirm whether FinishConfirmation runs with nothing written to Tourplan |
| J4 | D6 — quote-expiry chaser e-mail is dead | Maintenance.SE_SendQuoteExpiryEmails is excluded from deployment and filters on QuoteExpiry_notused. Assert no such e-mail is sent, and correct any test or runbook expecting one |
| J5 | D7 — ConvertToLiveBooking_170626 stale duplicate |
After any fix to ConvertToLiveBooking, assert the clone is deleted or updated too |
| J6 | D9 — messages say "step 3" while Step3 or Step4 is accepted |
B3, F3. Cosmetic; log for copy review |
| J7 | D10 — BookQuote collapses all conversion failures into a bare Success = false |
M10 |
| J8 | D11 — API_System stamped ITRVL for every API caller |
M8 |
| J9 | D12 — Sub_AssignExpiryDate's note says "latest", the sort takes the earliest |
E1. Assert the earliest is used and raise the contradiction with the flow's owner |
| J10 | OnBase failures are silent | N4 |
K. Suggested smoke path¶
Ten minutes, one booking, covers the spine:
- A1 — agent creates a quote. Assert
Source/WindowStatus=Quote_only. - Complete Steps 1–3, price it. Assert
BookingStep = Step4,Priced = Yes. - B3 — try to convert from Step 1 first (use a second quote). Assert rejection.
- C2 — convert from Step 4. Assert
Source = Wilderness_window,WindowStatus = Draft,TourplanStatusstillDraft,Pricedcleared. - F7 — try to confirm without repricing. Assert the "not priced" rejection.
- Reprice. D1 — book provisionally. Assert
WindowStatus = Pand anExpirationDate. - H1 — confirm. Assert
WindowStatus = C,ConfirmationDateset,TourPlanFullReferencepopulated, linesConfirmed. - H11 — assert a deposit invoice was raised.
- H12 — reconfirm. Assert no second deposit invoice and
ConfirmationDateunchanged.
M. The API conversion path — API.BookQuote¶
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| M1 | Missing payload | Request with no BookQuote object |
Call BookQuote |
Error SYS1010, no conversion |
P1 | API.BookQuote [6-7] |
| M2 | Unknown booking number | BookingNumber matching nothing |
Call | Error IN4050 |
P1 | [10-11] |
| M3 | Non-API booking is invisible | Booking created in the UI (APIBooking = false) with a valid number |
Call | Error IN4050 — the lookup is constrained to [APIBooking], so UI bookings cannot be converted via the API even with a correct reference |
P1 | [9] |
| M4 | Cancelled booking | WindowStatus = CX or CC |
Call | Error IN4080 |
P1 | [14-15] |
| M5 | Dropped booking | Source neither Wilderness_window nor Quote_only |
Call | Error IN4090 |
P1 | [17-18] |
| M6 | Already converted | Source = Wilderness_window |
Call | Error IN4100 |
P1 | [20-21] |
| M7 | Full happy path | Valid API quote at Step 3/4, rooming valid | Call | ConvertToLiveBooking → ProceedToScreen3_CreateBLs → IVK_ProvisionalAll → UpdateReferralAfterCreate all run; booking ends Provisional with booking lines built, in one call |
P1 | [23-26] |
| M8 | ITRVL stamped for every caller | Call from a non-iTrvl partner integration | Call | API_System = ITRVL regardless of actual caller — defect D11. Assert and log |
P2 | [27] |
| M9 | Lock released | M7 | Call | Booking_CurrentlyUsedBy cleared, AgentBlock = false |
P2 | [27] |
| M10 | Conversion failure returns a bare false | API quote that passes all five guards but fails CheckRooming |
Call | BookQuoteResponse.Success = false with no error code and no diagnostic, because success is inferred from $Booking/Source. Defect D10 — highest-value API case |
P1 | [28],[32] |
| M11 | Success flag on the happy path | M7 | Call | Success = true; request/response logged via LogXMLRequestResponse |
P2 | [28-30] |
N. OnBase notification¶
Onbase.BookingStatus_Post is invoked fire-and-forget on convert, provisional, confirm and
cancel. UseReturnVariable is false everywhere, so no caller detects failure.
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| N1 | Fired on conversion | Valid conversion | Convert | Onbase.BookingStatus_Post invoked asynchronously with the booking as context |
P2 | Booking.ConvertToLiveBooking [14] |
| N2 | Fired on provisional | D1 | Book provisionally | Same call fires | P2 | Booking.Sub_BookProvisionally_AllInventory [34] |
| N3 | Fired on confirmation | H1 | Confirm | Same call fires | P2 | ConfirmBooking [24] |
| N4 | Failure is invisible | Make Onbase.BookingStatus_Post fail |
Convert | The calling flow completes normally and reports success. Nothing surfaces to the user or the caller. Confirm the business accepts silent loss of document-generation triggers | P1 | UseReturnVariable: false at all sites |
| N5 | Asynchronous, not inline | Any of N1–N3 | Trigger | Runs in its own transaction after the caller commits; a rollback in the caller does not prevent it | P2 | CommunityCommons.executeMicroflowInBackground semantics |
O. HubSpot on provisional¶
| ID | Case | Preconditions | Steps | Expected | Pri | Source |
|---|---|---|---|---|---|---|
| O1 | Standard provisional updates HubSpot | Booking provisioned via IVK_ProvisionalAll |
Book provisionally | HubSpot.AddProvisionalExpiryDateToHubSpot and HubSpot_DMC.UpdateDealAfterProv both run |
P1 | Booking.IVK_ProvisionalAll [12-13] |
| O2 | Agent variant does not | Same booking via IVK_ProvisionalAll_Agents |
Book provisionally | Neither HubSpot call occurs. The deal is not updated | P1 | IVK_ProvisionalAll_Agents |
| O3 | The split is per button, not per user | An Agent-role user |
Reach both buttons on BookingWizard_Step_3 |
IVK_ProvisionalAll allows Agent, so an agent using that button does update HubSpot. Confirm which button each role actually sees |
P1 | role lists on both flows |
| O4 | API path updates HubSpot | API.BookQuote |
Call | Reaches IVK_ProvisionalAll, so HubSpot is updated for partner bookings |
P2 | API.BookQuote [25] |
| O5 | HubSpot outage does not block | Force HubSpot to fail | Book provisionally | Both calls are [queued], so the provisional hold still completes |
P1 | Booking.IVK_ProvisionalAll [12-13] |
| O6 | Locked booking blocked in both variants | NoChangesAllowed = true |
Book provisionally | Info "Please have your consultant take this booking ex-window as it is locked for further changes." in both | P2 | [2],[19] |
| O7 | ProvisionalDate stamped once |
First and second provisional | Book provisionally twice | Set on first only | P2 | [16] |
L. Environment notes¶
- Tourplan and Wish are live integrations. Cases F5, H5, H7, D8 and H19 need either a
Tourplan/Wish test instance or a stubbed
DataManagement.ServerSettings— seedocs/02-architecture.mdfor the 17 configured server configurations. TourplanInstancematters for F1/F2: the tax-indicator lookup is keyed on(Agency, TourplanInstance), so an agency valid in one instance can be invalid in another.- Several checks depend on
daysBetween(now, TripStartDate). Prefer fixtures with dates set relative to run time over fixed dates, or the boundary cases (B7, G8) rot.