Skip to content

Cancellation, amendment and cancellation fees — developer reference

Audience: engineers working on Booking/BookingWizard/General, Booking/Receivables, Booking/Waitlist Management, Booking/Dynamic Pricing/Cancellation, or the Tourplan/Wish cancel paths in DataManagement.

Source of truth: the Mendix model, read via model/. Microflow names, node numbers, XPath and message strings were verbatim from commit 255b346f3, Mendix 10.24.21. Node numbers in [n] match python3 tools/mxrender.py <flow>.

model/ has since moved to commit 83d87ba8e. Of the flows this document cites, the rendering of API.BookQuote, API.CreateWindowBooking, API.GetQuote_B2B, API.ReplaceQuote and API.UpdateDMCOverrideLine changed length, so [n] references into those five need re-checking. The cancellation and fee flows in Booking and DataManagement are byte-identical between the two revisions.

Anything the model does not state is marked (inferred). Quoted NOTE: lines are Studio Pro canvas annotations, reproduced verbatim — they record intent, but where a note and the logic disagree, this document says so and the logic wins.

Revised 2026-08-07 against a re-generated extract that no longer drops microflow-valued call parameters, canvas annotations, Rule-based split conditions or sort orders. See quote-to-booking.md §12. Sections changed as a result are marked (corrected) or (new).

This is the sequel to quote-to-booking.md and assumes its vocabulary — the four status axes, BookingStatus, BookingLineStatus.


1. Cancellation has three different outcomes

"Cancel" is one button and three genuinely different operations. Which one you get is decided inside Booking.CancelBooking, not by the caller.

Outcome When What happens to the record
Hard delete No TravelLocation exists, WindowStatus = Draft, and TourPlanReference is empty or ≤ 0 DELETE $Booking — the row is gone. An ArchiveLog is written first.
Cancel, no fees Draft / Placeholder / Quote-only, or a booking with no Wish reference WindowStatus = CX, InventoryStatus = CX, lines cancelled
Cancel with costs A confirmed booking where the fee engine finds a chargeable fee WindowStatus = CC ("CCost"), fee-bearing lines stay LIVE carrying the fee

The distinction between CX and CC is the commercial one: CX is gone and costs nothing, CC is gone but somebody owes money. CancelBooking [35] preserves an existing CC:

WindowStatus = if $Booking/WindowStatus = BookingStatus.CC then BookingStatus.CC else BookingStatus.CX

so once a booking is CC a later cancel cannot demote it to CX.

The line-level markers

Three attributes together encode a cancelled line. Reading only one of them is the most common mistake in this area.

Attribute Meaning
BookingLine.Cancel Model doc: "Set to pass through Delete/cancel process. May be deleted or cancelled, and may or may not end up LIVE (Depending if fees)" — an in-flight marker, not a final state
BookingLine.LIVE false once cancelled. Almost every retrieve in the app filters on [LIVE]
BookingLine.aSequence Bumped by +1000 on cancellation (CancelBL [4]). This is how cancelled lines are later found — TP_ReviveCancelledBookingLines [5] matches on aSequence >= 1000
BookingLineStatus Cancelled or CancelledWithCosts

A fee-bearing line is deliberately not removed — it stays as the carrier of the charge. That is why Cancel = true does not imply LIVE = false.

Cancellation attributes on Booking

Attribute Notes
Cancelled Boolean, default false — set only when the Wish cancel succeeded
CancelledByUser String, username
CancellationDate Set at [35]; note the delete/no-fee path at [24] sets ChangeDate instead
CancelBookingReason Booking.CancellationReason enum
CancelBookingReasonDesc Free text
NoCancelFees Model doc: "Do not calc any cancel fees for this booking" — the commercial override, see §5
RelaxedPmtTermsApply Model doc: "Covid, if a new booking, relaxed terms will apply (up until 31 Dec 21), then no longer" — feeds Pricing.GetPmtTerms
Insured Gates an extra confirmation page on line cancellation

Booking.CancellationReason values: Client_cancelled, Client_postponed_trip, Trip_postponed_no_fees_charged, Cancelled_but_not_charging_fees_as_a_favour_to_agent, Superceded_by_a_new_booking, Moving_booking_to_correct_Tourplan, Other.

Two of those — "not charging fees as a favour to agent" and "trip postponed no fees charged" — describe a fee waiver but do not themselves set NoCancelFees. The reason is recorded independently of the override. Reporting on waivers must read NoCancelFees, not the reason.


Every live flow that writes the cancellation flags

flow folder step sets_to runnable_by
Booking.CancelBooking BookingWizard/General/MF 24 true Booking.Agent, Booking.Debug, Booking.Employee, Booking.ExternalConsultant
Booking.CancelBooking BookingWizard/General/MF 35 if $CancelSuccessWish then true else false Booking.Agent, Booking.Debug, Booking.Employee, Booking.ExternalConsultant
Booking.IVK_Uncancel BookingOverview/MF 2 false Booking.Debug
Tools.IVK_CancellBookings MF 8 true Tools.Debug

expr is verbatim; a conditional write is one row, not a decision table.

entity moment event runs aborts_transaction
Booking.TourplanBookingServiceLine Before Commit Booking.BeforeCommit_SetPaymentStatus no
Booking.TravelLocation Before Delete Booking.BDe_TL_UpdateSequences yes
Booking.VoucherTotals Before Commit Booking.BeforeCommit_AddTotals yes

2. Entry points and their guards

IVK_CancelBooking_Confirm is the main route. It does not cancel — it picks a confirmation page.

[2] WindowStatus in (Traveled, Travelling)
      -> "You cannot cancel a booking that is already travelling or has travelled
          (why would you do that?) Please get your manager to speak to WW support"
[5] Source = Ex_window
      -> "Booking is ex-Window and cannot be cancelled via the Window"
[6] TourPlanFullReference not empty
      -> "As this booking has been confirmed in Tourplan, please cancel from the booking file"
[7] WindowStatus in (Draft, empty, Placeholder, Quote_only)
      -> page Booking.CancelBooking_confirm          (simple path)
   otherwise
      -> page Booking.CancelBookingConfirmation      (full path, captures reason)

Roles: Administrator, Agent, Employee, Manager, Debug, ExternalConsultant.

The _NoRedirect variant diverges

IVK_CancelBooking_Confirm_NoRedirect has the same role list but two different rules:

Rule IVK_CancelBooking_Confirm ..._NoRedirect
Travelled / travelling Hard block for everyone [12-15] IsManager() — a manager may proceed
TourPlanFullReference populated Blocked, "cancel from the booking file" No check at all

So the same booking is cancellable from one button and not the other, and the Tourplan-confirmed guard is enforced on only one of the two paths. See defect C2.

The other callers

Flow Roles Notes
IVK_CancelBooking_Overview Admin, Agent, Employee, Manager, Debug, ExtConsultant Writes ArchiveLog, then CancelBooking($Booking, true)
IVK_CancelBooking_Overview_NoCancelFees same Sets NoCancelFees = true first, then cancels — see §5
IVK_CancelBookings Debug Bulk loop, no per-booking confirmation
Sub_CancelOrphanDraftBookings Housekeeping, see §8
API.CancelBooking Partner API path
Booking.SE_CancelExpiredBookings Documented in quote-to-booking.md §7

IsCancellationValid

The only shared validity gate, called from CancelBooking [5]:

[2] WindowStatus in (Traveled, Travelling)
      -> "Booking is in an invalid state ( Status = {1}) to be Cancelled in the Window."
      -> return false
[5] otherwise return true

That is the whole of it. Note what it does not check: Tourplan state, ex-Window source, or role. Those live in the IVK wrappers, so any caller reaching CancelBooking directly bypasses them — IVK_CancelBookings (bulk, Debug) and Sub_CancelOrphanDraftBookings both do.


3. Booking.CancelBooking($Booking, $ShowConfirmationAfterCancel)

Folder BookingWizard/General/MF. Roles Agent, Debug, Employee, ExternalConsultant. Returns Boolean.

Four annotations sit on this flow (new):

NOTE: "Sets Wish inventory lines to cancelled (inventory status)"

NOTE: "if cancel from step 2 - need to send ARCs" — an unimplemented requirement left on the canvas. (inferred: ARC = an availability-release notification to the camp.) Worth asking whether step-2 cancellations are silently skipping a supplier notification.

NOTE: "2021-10-20 : Put this in while we sort out the statuses - Al" — a temporary workaround, still in place five years later.

NOTE: "Removed empty retrieve"

[4]  $IsConfirmedBooking = WindowStatus = C          <- computed, never used (defect C5)
[5]  IsCancellationValid -> false: return false
[8]  LogEvent(Event.Cancellation, 'Booking cancelled by user')
[9]  retrieve first TravelLocation under the booking's legs
[10] no TravelLocation AND WindowStatus = Draft:
       [68] TourPlanReference empty or <= 0
              -> [70] ArchiveLog (Reason 'No travellocations so delete')
              -> [71] DELETE $Booking ; return true
            otherwise fall through to the normal path
[12] BHL_LogEntry_Create(..., 'Pricing change on booking', 'SumDetailedBookingLines', Event.Update)
[15] Explorations.ClearExploration($Booking)
[16] branch on whether Wish is involved:
       WishBookingReference empty, or Source in (Placeholder, Quote_only)  -> [17] simple path
       otherwise                                                          -> [59] Wish path

Simple path — no Wish involvement

[17] If WindowStatus is Draft, Placeholder, Quote_only or empty:

[18] BookingGuestManagement.CancelBooking_Guests
[19] Logging.LogMessage 'Manual delete draft booking ...'
[21] if $ShowConfirmationAfterCancel -> "Your booking has been successfully cancelled."
[24] CHANGE: WindowStatus = CX; InventoryStatus = CX;
             TourplanStatus = Draft if it was Draft else Pending;
             CancelledByUser = currentUser; Cancelled = true; ChangeDate = now
[25] EmailConsultantsAgentBooking(..., BookingStatus.CX)

Note [24] sets ChangeDate, not CancellationDate. The full path at [35] sets CancellationDate. Anything reporting on when a booking was cancelled must handle both.

Wish path

[59] DataManagement.CancelWishHeader($Booking)
       failure -> [61] LogMessage Critical 'There is an issue cancelling the booking in Wish...'
                  [62] MESSAGE error "Your booking could not be cancelled die to Wish error"
                  return $Cancelled (false)
       success -> $CancelSuccessWish = true, continue to [29]

The Critical log carries a user-facing resolution string: "The cancellation of this booking failed due to a system issue. The booking has been handed over to a consultant to complete the cancellation." — i.e. a failed Wish cancel becomes a manual support task.

Common tail — [29] onwards

[30] BookingGuestManagement.CancelBooking_Guests
[31] retrieve live lines not already Cancelled/CancelledWithCosts
[33] set Cancel = true on each
[34] Booking.Sub_DeleteBookingline_Mem(true, $Booking, true, $BookingLineList_Live)   <- fee engine runs here
[35] CHANGE: WindowStatus = CC if already CC else CX
             InventoryStatus = CX if the Wish cancel succeeded, else unchanged
             TourplanStatus = Draft if it was Draft else Pending
             CancelledByUser / Cancelled = only if the Wish cancel succeeded
             CancellationDate = now
[36] DataManagement.CancelBooking_LR      (third-party / LiveRequest inventory)
[38] per PartyCostGroup: Itinerary.DeleteItineraryInWetu
[40] EmailConsultantsAgentBooking(..., BookingStatus.CX)
[41] Pricing.SumDetailedBookingLines($Booking, false)
[43] BHL_LogEntry_Create(..., 'Cancel Booking', 'IVK_CancelBooking', Event.Cancel)
     (and, on the confirmation branches, executeMicroflowInBackground(
      microflow=Onbase.BookingStatus_Post, $Booking) — OnBase is told about the cancellation)
[45] DataManagement.RefreshWishHistoryById_FromBooking
[46] retrieve open Logging.SystemLog rows for this booking (Status not Resolved/Audit)
[48] mark them Resolved by 'System' with Resolution 'Booking cancelled'
[50] if $ShowConfirmationAfterCancel:
       TourPlanReference > 0 -> "A consultant has been informed of the cancellation of this
                                 booking and will be in contact with you in the event of any
                                 financial settlement. Please be aware that we can not show
                                 this financial settlement in the Wilderness Window."
       otherwise             -> "Your booking has been successfully cancelled and the reserved
                                 inventory has been released where applicable"

[46-49] auto-resolves the booking's open system logs. Any unresolved error on a booking is silently closed when it is cancelled. That is reasonable operationally but means cancellation destroys the error trail — relevant when investigating "why did this booking fail" after the fact.

EmailConsultantsAgentBooking is called with BookingStatus.CX on both paths, including when the booking ends up CC. (inferred: the e-mail template does not distinguish, so a cancelled-with-costs booking is announced as a plain cancellation.) Worth checking the template before relying on it.


4. Line-level cancellation

Booking.CancelBL($BookingLine, $Booking)

Folder Receivables/MF. The primitive every cancel path funnels into.

[2] Sub_UpdateBookinglineSequenceNoTL_delete(aSequence, $Booking)
[3] $TPUpdateReq = Chargeable and TourPlanStatus != Draft
[4] CHANGE $BookingLine set {
      aSequence            = aSequence + 1000;     <- the cancelled-line marker
      LIVE                 = false;
      BookingLine_TravelDay      = empty;
      BookingLine_TravelLocation = empty;
      TPUpdateRequired     = $TPUpdateReq;
      TourPlanStatus       = Draft if it was Draft else Pending;
      Cancel               = true
    } [commit YesWithoutEvents]
[5] CHANGE $Booking set {
      ReSumPricingTotals = true if the line was Chargeable;
      TourplanStatus     = Pending, unless already CC or CX
    }

Detaching TravelDay and TravelLocation is what removes the line from the itinerary while leaving the row intact for financial history.

Booking.IVK_CancelBookingLineFully($BookingLine, $Booking)

Roles Debug, Employee. Adds one guard before CancelBL:

[2] if $Booking/Insured:
      [8] $WS = Supplier/OwnershipID in (A1, B1, C1, G1, I1, J3, K2, K3, K4, Q1)
      [9] first live Window-created Tourplan service line on the booking, RVLine = false
      [11] if $WS and $TourplanBookingServiceLine/R_Invoiced > 0
             -> [12] OPEN PAGE Booking.BookingLine_CancelCheck   (stop, ask the user)
      otherwise -> proceed
[4] CancelBL($BookingLine, $Booking)
[5] Pricing.SumDetailedBookingLines($Booking, true)

That $WS list is a hardcoded set of supplier ownership codes standing for Wilderness-owned suppliers (inferred from the contrast with the $IsAP list in §5). See defect C1.


5. The cancellation-fee engine

Three microflows: decide whether a fee applies, work out the percentage, apply it.

Booking.CancelFees($Booking, $BookingLine, $FullBookingCancel) : Boolean

Gate order — any of these returns false, meaning no fee:

# Node Condition for no fee
1 [2] $Booking/NoCancelFees is true
2 [3] the line is not Chargeable
3 [4] BookingLineStatus != Confirmed
4 [7] the line has no TourplanBookingServiceLine records
5 [15] the computed fee percentage is exactly 0

Gate 4 is worth dwelling on: a line that was never written to Tourplan attracts no cancellation fee regardless of how close to travel it is. Provisional-only bookings therefore cancel free.

[8]  $SumInvoiced = Sum of TourplanBookingServiceLine.R_Invoiced
[9]  $SumCredited = Sum of TourplanBookingServiceLine.R_Credited
[10] $Invoiced    = $SumInvoiced >= $SumCredited        <- never referenced again (defect C4)
[12] GetCancelFeePerc($Booking, $BookingLine, $Supplier) -> $Fee
[13] $CancelFee    = if $Fee = empty then 1 else $Fee
[14] $HasCancelFee = if $Fee = empty then true else $Fee > 0
[17] if $HasCancelFee -> ApplyCancelFees($BookingLine, $CancelFee, $Booking, $FullBookingCancel)

empty means 100%. GetCancelFeePerc returning empty is not "unknown, skip" — [13] converts it to a full-value charge. Every path that returns empty is a 100% fee.

Booking.GetCancelFeePerc($Booking, $BookingLine, $Supplier) : Decimal

Folder Dynamic Pricing/Cancellation. Returns a fraction (0.25 = 25%), not a percentage.

[2]  Day_First empty                        -> return empty        => 100%
[3]  $IsAP = Supplier/OwnershipID in (M1, J1, J2, K1, CR, DM, N1, TN)
[4]  $DaysBeforeTravel = round(daysBetween(now, BookingLine/Day_First))
[5]  Pricing.GetPmtTerms($DaysBeforeTravel, Day_First, $Supplier,
                         $Booking/RelaxedPmtTermsApply, $BookingLine, $Booking)
[7]  payment terms found -> return CancellationFee / 100  (0 if CancellationFee is empty)
[8]  no payment terms:
       [9]  $IsAP                            -> return empty       => 100%
       [11] Accommodation / Exploration / Private_Activity
                                             -> DaysBeforeTravel > 60 ? 0.25 : 1
       [14] Flight  -> >7 days  : 0
                       >3 days  : 0.25
                       >2 days  : 0.5
                       >1 day   : 0.75
                       otherwise: 1
       [13] anything else                    -> 0

The configured PaymentTerms/CancellationFee is the primary source. Everything below [8] is a fallback for missing master data, and the fallback is punitive: an associated-product supplier with no payment terms charges the client 100%.

The previous ladder is preserved on the canvas (new):

NOTE: "if confimed <31 Dec 20 if $DaysBeforeTravel >= 56 then 0.20 else if $DaysBeforeTravel >= 28 then 0.45 else if $DaysBeforeTravel >= 21 then 0.50 else if $DaysBeforeTravel >= 14 then 0.60 else 1"

That is the pre-2021 fallback, and it is a genuinely graduated scale — 20% / 45% / 50% / 60% / 100% across five bands. The current accommodation fallback has two bands, 25% and 100%, with the cliff at 60 days. Whatever replaced the old ladder made the fallback markedly blunter and, between 60 and 56 days, markedly more expensive (100% where it used to be 20%).

This matters for the argument in §11 that the fallback is a master-data failure mode rather than policy: the graduated version looks like a commercial position, the current one looks like a default nobody revisited. Worth putting to whoever owns cancellation policy.

Fallback ladder as a table:

Line type > 60 days 8–60 days 4–7 days 3 days 2 days ≤ 1 day
Accommodation / Exploration / Private Activity 25% 100% 100% 100% 100% 100%
Flight 0% 0% 25% 25% 50% 75–100%
Associated product ($IsAP) 100% 100% 100% 100% 100% 100%
Anything else 0% 0% 0% 0% 0% 0%

The flight ladder's boundaries are strict >, so day 7 exactly falls into the 25% band, day 3 into 50%, day 1 into 100%.

Booking.ApplyCancelFees($BookingLine, $CancelFee, $Booking, $FullBookingCancel)

Rewrites the money on every DetailedBookingLine under the line.

[6]  $SellFee = SellingPrice * $CancelFee
[7]  $CostFee = CostPrice    * $CancelFee
[8]  CHANGE $IteratorDetailedBookingLine set {
       SellingPriceAfterDiscounts        = SellingPrice if currently 0 else unchanged;
       SellingPrice_CancellationAdjustment = SellingPrice - $SellFee;   <- written off
       SellingPrice                      = $SellFee;                    <- retained as the fee
       CostPrice_CancellationAdjustment  = CostPrice - $CostFee;
       CostPriceAfterDiscounts           = CostPrice if currently 0 else unchanged;
       CostPrice                         = $CostFee;
       GrossProfit                       = $SellFee - $CostFee;
       CancelFee_Cost                    = round($CancelFee * 100, 2);
       CancelFee_Sell                    = round($CancelFee * 100, 2);
       GrossProfitPerc                   = (SellingPrice - CostPrice) / SellingPrice * 100
     }
[9]  recompute tax: if TaxRate_Cost and TaxRate_Mkup are both set, compute inline;
     otherwise Pricing.CalculateDetailedBookingLineTax
[15] COMMIT $DetailedBookingLineList [without events]
[16] Booking.CancelFee_PCG_BL($BookingLine, 1 - $CancelFee)     <- note the inverted fraction
[17] CHANGE $BookingLine set { CancelFeeAdj_Cost = $CancelFee * 100;
                               CancelFeeAdj_Sell = $CancelFee * 100 }

Two things to be careful with:

  • The price fields are destructively overwritten. SellingPrice becomes the fee, and the original is recoverable only by adding SellingPrice_CancellationAdjustment back. Any report reading SellingPrice on a cancelled line is reading the fee, not the original price.
  • GrossProfitPerc at [8] is computed from the pre-change values. All expressions inside a single Mendix CHANGE evaluate against the object's state on entry, so this stores the original margin percentage while GrossProfit immediately above stores the post-fee cash margin. Verify against intent — see defect C6.
  • [16] passes 1 - $CancelFee — the waived fraction, not the charged one. Read CancelFee_PCG_BL with that in mind.

Rounding is inconsistent: DetailedBookingLine.CancelFee_* is round(x, 2), BookingLine.CancelFeeAdj_* is unrounded.


6. The NoCancelFees override

The commercial escape hatch. Booking.NoCancelFees short-circuits CancelFees [2] for every line on the booking.

Two ways to set it:

Booking.IVK_ToggleCancelFees($Booking)        roles: Employee, ExternalConsultant
  [2] NoCancelFees = not NoCancelFees  [commit YesWithoutEvents]
  [3] LogEvent(Event.Cancellation, 'Cancellation fees turned off' / '...turned on')

Booking.IVK_CancelBooking_Overview_NoCancelFees($Booking)
  [3] NoCancelFees = true  [commit YesWithoutEvents]
  [4] LogEvent(Event.Cancellation,
        'No cancellation fees to be calculated for this booking - assume a postponement')
  [7] CancelBooking($Booking, true)

Exposed on Sn_BookingMenu as a pair of buttons, "Override cancellation fees" and "Cancel fees overridden", toggling on NoCancelFees.

There is no approval step and no manager gate. Roles are Employee and ExternalConsultant — an external consultant can waive every cancellation fee on a booking, and the only trace is a LogEvent entry. Given this directly determines whether the client is charged, it is the highest-value control finding in this document. See defect C3.

The NoCancelFees flag is also read by page visibility conditions (Sn_BookingMenu, BookingFile_Agent_050526) to hide financial-settlement panels on cancelled bookings.


7. Undo — revive a cancelled booking

Booking.IVK_Uncancel($Booking) — role Booking.Debug only

Surfaced as the button "Revive cancelled booking" on BookingFile_Agent_050526 and Sn_BookingMenu. Because the role list is Debug alone, ordinary users cannot run it — there is no self-service undo. This is a support tool.

[2]  CHANGE: WindowStatus = Draft; InventoryStatus = Pending; Cancelled = false;
             WishBookingReference = empty; WishPartyGroupId = empty;
             WishUpdateStatus = Update_required; WishPartyGroupStatus = Update_required;
             ReadyForTravel = false
[4]  clear WishGuestId on every guest
[8]  reset WishPartyGroupId to 0 on every PartyCostGroup
[12] reset every Wish AvailableRoomConfig: WishStatus = Draft, WishSectorBookingId = empty,
     Waitlist = No, Booked_* cleared, all BookedRoomCount_* to 0, HoldingSpace = false
[16] Booking.Uncancel_CC_BL($Booking)
[17] LogEvent(Event.Update, 'Booking UNCANCELLED')
[18] WizardStep2_FromBookingFile($Booking, false)
[19] Booking.Step2Checks($Booking)
[22] Sub_AuditBookingCheck
[23] DataManagement.TP_UncancelBooking($Booking)

The Wish linkage is destroyed, not restored. WishBookingReference is cleared, so the revived booking has no memory of its previous Wish holds and must be re-provisioned from scratch. Reviving does not recover the original inventory — it recreates the opportunity to ask for it.

DataManagement.TP_UncancelBooking($Booking) : Boolean

[2] TP_ResetTPHeaderStatus($Booking)
      success -> [4] TourplanStatus = Pending
                 [5] TP_ReviveCancelledBookingLines($Booking)
                 return true
      failure -> return false

DataManagement.TP_ReviveCancelledBookingLines($Booking)

Re-attaches orphaned Tourplan service lines to the new live lines:

[2] for each live chargeable BookingLine:
      [5] find a cancelled sibling: aSequence >= 1000,
                                    InventoryBookingSectorStatus = 'Cancelled',
                                    same Option, same Day_First
      [7] its live Window-created TourplanBookingServiceLine rows
      [10] re-point them at the new line, TPServiceLineStatus = VY
      [11] new line TourPlanStatus = Pending

The match is on (Option, Day_First) only. Two lines for the same option on the same day — different guests, different room configurations — are indistinguishable here, and first takes whichever the database returns. See defect C7.


8. Waitlist release — Booking.SE_WaitlistRelease

Folder Waitlist Management. Roles Administrator, Debug.

[2]  if today is Sat or Sun -> do nothing
[3]  $CutOffTime = start of day - 2 days
[4]  set A: AvailableRoomConfig with a live Waitlisted line, Waitlist = Yes,
            WaitlistAvailableFrom <= $CutOffTime, WaitListNowAvailable,
            booking Source in (Wilderness_window, EX_Tourplan)
[5]  $TravelCutOffTime = today + 42 days
[6]  set B: AvailableRoomConfig with a live Waitlisted line whose booking TripStartDate
            <= $TravelCutOffTime, Waitlist = Yes, same Source filter
[7]  union A and B
[9]  per config:
       [13] branch on the type of the booking owner
       [14] Employee: send "WARNING - we have released your waitlist of :<supplier>
                            for booking :<ref>"  to the branch/department e-mail,
                            or the owner's own address if there is none
            [23] set Cancel = true on the matching live lines
            [24] Sub_DeleteBookingline_Mem(false, $Booking, false, $BookingLineList_All)
       [26] any other owner type: CONTINUE — nothing happens

The flow's own annotation states the intended rule (new):

NOTE: "This microflow clears 1 - waitlisted items that have become available but were not taken up by the agent within 48 hours, and 2 - clears waitlisted items not taken up for trips with less than 8 weeks to go"

Rule 1 matches: addDaysUTC([%BeginOfCurrentDay%], -2) is 48 hours.

Rule 2 does not match. The note says 8 weeks (56 days); the code says addDaysUTC([%CurrentDateTime%], 42)6 weeks. Waitlists are being released two weeks earlier than the documented intent. See defect C14.

Two rules with commercial weight:

  • Set B releases on proximity to travel alone. Any waitlist on a trip departing within 42 days is released whether or not space came free, and whether or not the two-day availability rule in set A applies.
  • Only Employee-owned bookings are processed. [26] skips every other owner type, so an agent-owned booking's waitlist is neither released nor notified. See defect C8.

Sub_DeleteBookingline_Mem is called with $FullBookingCancel = false here, versus true from Booking.CancelBooking [34] — the fee engine sees a different flag on the two paths.


9. Housekeeping — Booking.Sub_CancelOrphanDraftBookings

[2] Source = Wilderness_window AND BookingStep = Step1 AND PartyName empty
    AND BookingReferenceDescriptive empty AND WishBookingReference empty
    AND TripStartDate empty AND TripEndDate empty AND NotificationEmail = false
    AND WindowStatus not in (CX, CC)
[4] ArchiveLog (Reason 'Step 1 no party name or reference or dates')
[5] CancelBooking($I_Booking, false)

Every one of those records satisfies CancelBooking's delete condition (no TravelLocation, Draft, no TourPlanReference), so in practice this job deletes rather than cancels, despite the name. There is no limit on the retrieve at [2], unlike SE_CancelExpiredBookings which caps every sweep at 150. See defect C9.


10. Integration touchpoints

System When Call
Wish booking cancel DataManagement.CancelWishHeader; WishAPI.CancelWishBooking, CancelWishBookingById
Wish uncancel linkage cleared in IVK_Uncancel, re-provisioning required
Tourplan line cancel TPUpdateRequired / TourPlanStatus = Pending set by CancelBL; Booking.CancelBL_TP
Tourplan uncancel TP_ResetTPHeaderStatus, TP_ReviveCancelledBookingLines
LiveRequest / third party booking cancel DataManagement.CancelBooking_LR
Wetu (itineraries) booking cancel Itinerary.DeleteItineraryInWetu per PartyCostGroup
NightsBridge channel cancel NightsBridge_V5.Cancel_NightsBridge_Booking
Partner API inbound cancel API.CancelBooking, CancelBookingLines, CancelDMCLines, CancelFailedBooking
Explorations booking cancel Explorations.ClearExploration

11. Defects and traps

C1 — Supplier ownership codes are hardcoded in two places, with different lists. GetCancelFeePerc [3] hardcodes M1, J1, J2, K1, CR, DM, N1, TN as $IsAP. IVK_CancelBookingLineFully [8] hardcodes A1, B1, C1, G1, I1, J3, K2, K3, K4, Q1 as $WS. Neither is master data; onboarding a supplier under a new ownership code silently changes its cancellation-fee treatment with no configuration change and no error. These belong on BookingMasterData.Supplier as a flag.

C2 — The two cancel-confirmation entry points enforce different rules. IVK_CancelBooking_Confirm blocks travelled bookings outright and blocks anything with a TourPlanFullReference. IVK_CancelBooking_Confirm_NoRedirect lets a manager cancel a travelled booking and omits the Tourplan check entirely. Same roles on both. Whichever is correct, they should not disagree.

C3 — Cancellation fees can be waived with no approval and no manager gate. IVK_ToggleCancelFees is Employee, ExternalConsultant. It flips NoCancelFees, which suppresses every fee on the booking at CancelFees [2]. The only record is a LogEvent. Compare IVK_ExtendExpiry, which is Manager-only for a materially smaller decision.

C4 — CancelFees computes an invoiced/credited check and discards it. [8-10] sum R_Invoiced and R_Credited and derive $Invoiced, which is then never referenced. Either the fee calculation was meant to depend on whether the line had been invoiced, or the three activities are dead. Given the surrounding logic, the former looks likelier.

C5 — CancelBooking computes $IsConfirmedBooking and never uses it. [4]. Same pattern as C4 — an intended branch that was removed or never wired up.

C6 — GrossProfitPerc is stored as the pre-cancellation margin. ApplyCancelFees [8] sets GrossProfit from the post-fee figures but computes GrossProfitPerc from SellingPrice and CostPrice, which inside a single CHANGE still hold their pre-change values. The result is a percentage that does not correspond to the cash margin recorded beside it. Confirm intent before "fixing" — reporting may already depend on it.

C7 — Revive matches cancelled lines on (Option, Day_First) and takes the first. TP_ReviveCancelledBookingLines [5]. Two lines for the same option on the same day cannot be told apart, so Tourplan service lines can be re-attached to the wrong booking line. Most likely to bite on multi-room or multi-party-cost-group bookings.

C8 — Waitlist release ignores agent-owned bookings. SE_WaitlistRelease [26] hits CONTINUE for any owner type other than Employee. Those waitlists are never released and no e-mail is sent. Either agent-owned bookings are handled elsewhere, or they accumulate indefinitely.

C9 — Sub_CancelOrphanDraftBookings has no batch limit. The retrieve at [2] is unbounded, and each iteration performs a delete plus an ArchiveLog insert. Every comparable housekeeping sweep in the app caps at 150.

C10 — CancellationDate is not set on the simple cancel path. [24] sets ChangeDate; [35] sets CancellationDate. Draft, placeholder and quote-only cancellations therefore have a null CancellationDate. Any report filtering on it silently excludes them.

C11 — Cancellation auto-resolves the booking's open system logs. [46-49] marks every non-Resolved, non-Audit Logging.SystemLog row for the booking as Resolved by 'System'. Deliberate housekeeping, but it erases the diagnostic trail for bookings that were cancelled because something went wrong.

C12 — Sn_BookingMenu visibility conditions list every enum value. The NoCancelFees cancel buttons carry visible-if(attr Booking.Booking.TourplanStatus; values C,CC,CX,P,X,D,Draft,Traveled,Travelling,Pending,I,Placeholder,Quote_only,Paid,EX_Tourplan,(empty)) — every member of BookingStatus plus empty, i.e. always visible. This reads as an intended restriction that was never narrowed.

C14 — Waitlist release fires two weeks earlier than its documented intent. (new) SE_WaitlistRelease's own annotation says it clears waitlists "for trips with less than 8 weeks to go". $TravelCutOffTime is addDaysUTC([%CurrentDateTime%], 42) — 6 weeks. Either the note is stale or the constant is wrong; given the note also correctly describes the 48-hour rule, the constant is the likelier error. Commercially this drops waitlists a fortnight sooner than the business appears to expect.

C15 — CancelBooking carries an unimplemented supplier-notification requirement. (new) Annotation: "if cancel from step 2 - need to send ARCs". No corresponding call exists in the flow. If step-2 cancellations are meant to notify camps of released availability, they are not.

C16 — A 2021 "temporary" workaround is still live. (new) Annotation: "2021-10-20 : Put this in while we sort out the statuses - Al". The statuses were evidently not sorted out. Worth identifying what the workaround guards before touching the cancellation status logic.

C13 — Dated clones in this area. CancelBooking_230226, SE_WaitlistRelease_230226, SE_WaitlistRelease_260326, IVK_CheckForWaitlistBehind_120126. Any fix must account for them — see reports/dated-clones.md.


12. Reproducing this analysis

python3 tools/mxrender.py Booking.CancelBooking
python3 tools/mxrender.py Booking.CancelFees
python3 tools/mxrender.py Booking.GetCancelFeePerc
python3 tools/mxrender.py Booking.ApplyCancelFees
python3 tools/mxrender.py Booking.IVK_Uncancel
python3 tools/mxrender.py Booking.SE_WaitlistRelease

## every write of a cancelled status
grep -rn 'BookingStatus.CX\|BookingStatus.CC' model/*/flows.txt

## every reader of the fee override
grep -rn 'NoCancelFees' model/*/flows.txt model/*/pages.txt

## the hardcoded ownership-code lists
grep -rn "OwnershipID = '" model/*/flows.txt

## who can cancel
python3 tools/mxinspect.py callers Booking.CancelBooking

13. What this document does not cover

Generated from journeys/cancellation-and-fees.toml and the model - not recalled, and not maintained by hand. applies_to counts the live documents each decision covers, so an exclusion that has stopped matching anything shows as 0 instead of quietly passing.

excluded live_documents reason source
Booking/Receivables/* 286 Invoicing and credit notes downstream of a cancellation fee. Fee calculation is covered; what finance does with the resulting balance is a separate journey. (inferred - confirm with the team)
API.SwitchAccomodation 1 BookingLine.Cancel is not a cancellation-only flag - the model's own doc string says it marks a line to pass through the delete/cancel process. The partner API's itinerary-editing operations set it while amending, so they belong to the booking-mutation journey, not this one. docs/deep-dives/cancellation-and-fees.md#13
API.ChangeAccomNumberOfNights 1 Amendment surface: removes lines during an edit, reusing cancellation machinery. docs/deep-dives/cancellation-and-fees.md#13
API.DeleteAccomodation 1 Amendment surface, as above. docs/deep-dives/cancellation-and-fees.md#13
API.AddDaysToBooking 1 Amendment surface, as above. docs/deep-dives/cancellation-and-fees.md#13

this is the "does not cover" section, generated from journeys/.toml plus the model rather than from memory of where the author stopped reading*.

The measured gaps behind those decisions:

kind item detail
island Administration.TmpTest_ClearOwnedAgencyBookings in Administration/DebugScreens/MF, unreachable from the declared roots
island Booking.SE_UpdateBookingStatus in Booking/BookingWizard/General/MF, unreachable from the declared roots
island Tools.IVK_CancellBookings in Tools/MF, unreachable from the declared roots
undocumented_trigger Booking.IVK_CancelBookingLineFully button: Booking.Sn_Financials / actionButton24
undocumented_trigger Booking.IVK_CancelBooking_ClosePopup_NoCancelFees button: Booking.CancelBookingConfirmation_NoRedirect / actionButton3
undocumented_trigger Booking.IVK_CancelBooking_Confirm button: Booking.BookingOverview_Bookings / microflowButton77
undocumented_trigger Booking.IVK_CancelBooking_Confirm_FromBookingUserHistory button: Booking.BookingOverview_Agent / microflowButton61
undocumented_trigger Booking.IVK_CancelBookings button: Booking.BookingOverview_Support / actionButton14
undocumented_trigger Booking.IVK_ClearLeg button: Booking.Booking_ClearByLeg / actionButton1
undocumented_trigger Booking.IVK_ConfirmWishOnly button: Booking.BookingFile_Agent_050526 / microflowTrigger59
undocumented_trigger Booking.IVK_OverrideOption_Cancellation button: Booking.Sn_Financials / actionButton70
undocumented_trigger Booking.IVK_RemoveFromWindow button: Booking.ExWindowConfirmation / microflowButton1
undocumented_trigger Booking.IVK_ToggleCancelFees button: Booking.Sn_BookingMenu / actionButton1
undocumented_trigger Booking.IVK_Uncancel button: Booking.BookingFile_Agent_050526 / microflowTrigger15
undocumented_trigger Booking.IVK_UpdateTourplanTo_CC button: Booking.BookingFile_150726 / actionButton52
undocumented_trigger Tools.IVK_CancellBookings button: Tools.CancelTestBookings / actionButton1
undocumented_writer API.CancelFailedBooking writes Booking.Booking.WindowStatus and is never named
undocumented_writer Administration.TmpTest_ClearOwnedAgencyBookings writes Booking.Booking.WindowStatus and is never named
undocumented_writer Booking.IVK_CancelBooking_ClosePopup_NoCancelFees writes Booking.Booking.NoCancelFees and is never named
undocumented_writer Booking.IVK_ClearLeg writes Booking.Booking.WindowStatus and is never named
undocumented_writer Booking.IVK_ConfirmWishOnly writes Booking.Booking.WindowStatus and is never named
undocumented_writer Booking.IVK_OverrideOption_Cancellation writes Booking.Booking.NoCancelFees and is never named
undocumented_writer Booking.IVK_RemoveFromWindow writes Booking.Booking.CancelledByUser and is never named
undocumented_writer Booking.IVK_UpdateTourplanTo_CC writes Booking.Booking.WindowStatus and is never named
undocumented_writer Booking.NewBooking_fromLead writes Booking.Booking.WindowStatus and is never named
undocumented_writer Booking.SE_UpdateBookingStatus writes Booking.Booking.WindowStatus and is never named
undocumented_writer BookingTemplates.CreateTripBooking writes Booking.Booking.WindowStatus and is never named
undocumented_writer Tools.IVK_CancellBookings writes Booking.Booking.Cancelled, Booking.Booking.WindowStatus and is never named

Cancellation itself is complete. Every published or scheduled entry point that writes Cancelled, CancelledByUser, CancellationDate, NoCancelFees or CancelBookingReason is covered here — 1 of 1, API.CancelBooking.

python3 tools/coverage.py Booking.Booking \
    --attr Cancelled,CancelledByUser,CancellationDate,NoCancelFees,CancelBookingReason \
    docs/deep-dives/cancellation-and-fees.md          # -> 0 uncovered

But BookingLine.Cancel is not a cancellation-only flag, and running the same check against it returns 14 uncovered entry points. That is not a gap in this document — it is the attribute doing two jobs. The model's own doc string says so: "Set to pass through Delete/cancel process. May be deleted or cancelled, and may or may not end up LIVE (Depending if fees)". The partner API's itinerary-editing operations set Cancel = true when removing a line during an amendment, reusing the same machinery:

Operation Sets Cancel via
API.ChangeAccomNumberOfNights UnselectAndRemoveBLsClearAccommodationRelatedBookingLines
API.DeleteAccomodation Booking.DeleteTravelLocation
API.AddDaysToBooking AmendTLDates_CheckForTPUpdateTLDays_XAdjustBookingLinesForDayChanges
API.AddAccomodationToBooking, AddAccomodationToStartEnd, BookQuote ProceedToScreen3_CreateBLsSub_CreateBookingLines
API.AddFlightToStartEnd, AddTravelHubAndFlightToStartEnd IVK_NextStepFrom2Step2Checks

Practical consequence: BookingLine.Cancel = true does not mean the guest cancelled anything. Any report or query treating it as a cancellation signal will count itinerary edits as cancellations. Use BookingLineStatus in (Cancelled, CancelledWithCosts) for that, and Booking.Cancelled at booking level.

Those amendment operations belong to a separate journey — the partner API's booking-mutation surface, listed in quote-to-booking.md §10 — which no document in this set yet covers.

Also out of scope here: the pricing engine that computes the pre-cancellation figures (Pricing.Sub_PriceBookingAll), invoicing and credit notes for a CC booking, and the supplier-side cancellation messaging beyond SendAutomatedEmails_Confirmation.


14. See also