Skip to content

Regression test cases — pricing rules

Audience: QA. Scope: pricing end to end — which rules attach to a booking and why (groups A–L), and how the resulting number is assembled (groups P–T).

Every case traces to a node in the model at commit 83d87ba8e. [n] refers to the rendered flow listing: python3 tools/mxrender.py <flow> or model/Pricing/flows.txt. Where a case depends on configuration this repo cannot see (rule rows, agency lists, constants overridden per environment), it says so.

Scope. Groups A–M assert attachment and exclusion — which rules apply, and why. Groups N–R assert the arithmetic: reconciliation, tax, rack factor and totals, per pricing-calculation.md. Still not covered anywhere: where a rate comes from. Sub_GetOptionsForBookingline, CheckDynamicPricing, Price_Siteminder and Sub_CreatePricingNon_Accom are untraced, so no case asserts that a looked-up rate is correct — only that what the system does with it is.


What to assert on

Observable Where it lives
Rule attached to a cost group PartyCostGroup_BL_PricingRule_Qualifying
Rule attached to a line BookingLine_PricingRuleQualifying
Rule excluded for this booking Booking_PricingRule_Exclude
Selectable rule available but not applied Booking.HasPricingThatCanInclude = true
Line forced to manual pricing BookingLine.OverridePrice = true
Booking priced state Booking.Priced = Yes / Confirmed / No
Failure visible to support Logging.SystemLog, operation System_Issue / User_issue

A. The age guard — Pricing.Sub_PriceBookingAll [3]

# Status Trip end date Expected
A1 Draft 5 years ago Reprices. The 3-month limit does not apply to this status
A2 Quote_only 5 years ago Reprices
A3 C (confirmed) 5 years ago Reprices
A4 Traveled 2 months ago Reprices — inside the window
A5 Traveled 4 months ago Blocked. Message 'You cannot change pricing on a booking that is more than 3 months after trip end date', returns false
A6 CX (cancelled) 4 months ago Blocked, same message
A7 Traveled exactly 3 months ago Boundary. Condition is addMonthsUTC(TripEndDate,3) > [%CurrentDateTime%] — strictly greater, so exactly 3 months blocks

A5–A7 log severity Critical, operation User_issue.


B. Configuration kill switches — [33]

# Holiday type Wish rate type Expected
B1 not set not set Rules apply (default is true)
B2 applyPricingRules = false anything No rules applied to the whole booking
B3 anything AllowPricingRules = false No rules applied
B4 applyPricingRules = true AllowPricingRules = true Rules apply

B2/B3 are silent — no log, no user-facing message. Assert on absence of attachments, not on a message.


C. Channel exclusion — Pricing.ApplyPricingRules_List [3]

# API_System Channel Expected
C1 Siteminder BC No rules considered. Silent — no log, no flag
C2 Siteminder any other Normal qualification
C3 any other BC Normal qualification

C1 is a regression risk precisely because nothing records it. Assert zero attachments.


D. The malformed-line trap — [7], [43]

# Setup Expected
D1 All chargeable lines have DaysNights Normal qualification
D2 One line has empty DaysNights No rules on any line of the booking. Log Critical / System_Issue: 'Missing days/nights on bookingline'' Pricing rules not checked - please reprice or refer to Support if problem persists'
D3 The offending line also has OverrideOption = true Rules do apply — [6] filters override lines out before the check

D3 is the subtle one: the same data is fatal or harmless depending on an unrelated flag.


E. Candidate query — [17]

Rule must satisfy all of: Active, SADC_only = false, LeadBookingsOnly = false, and FirstTravelDay <= ToDate and LastTravelDay >= FromDate.

# Rule window vs itinerary Expected
E1 Rule covers the whole trip Candidate
E2 Rule covers the first night only Candidate — overlap is enough
E3 Rule covers the last night only Candidate
E4 Rule ends the day before the trip starts Not a candidate
E5 Rule starts the day after the trip ends Not a candidate
E6 Rule Active = false, otherwise perfect Not a candidate
E7 Rule SADC_only = true Not a candidate on the standard path
E8 Rule in Booking_PricingRule_Exclude Removed at [18]

Boundary check for E2/E3: comparison is <= / >=, so a rule whose ToDate equals the first travel day does qualify.


F. Last-minute filter — FilterLastMinute

Only rules with LastMinuteBooking = true are examined. LastMinuteDate = TripStart − BookingLeadTime days; the rule survives when PartyCreatedDate >= LastMinuteDate.

# BookingLeadTime Party created Trip start Expected
F1 30 10 days before travel Kept
F2 30 40 days before travel Removed
F3 30 exactly 30 days before Kept — comparison is >=
F4 empty any any Removed. No lead time is not "unlimited"
F5 30 10 days before travel, booking repriced a year later Kept — uses party created date, not today
F6 30, but rule has LastMinuteBooking = false any any Untouched by this filter

F4 and F5 are the two that get misremembered. Test both explicitly.


G. Agency filter — FilterAgency

# AllAgencies Ring-fence list Exclude list Agency Expected
G1 true any Kept
G2 false contains agency empty matching Kept
G3 false does not contain agency empty other Removed
G4 false empty contains agency matching Removed
G5 false empty does not contain agency other Kept
G6 false contains agency also contains agency matching Kept — the exclusion is ignored

G6 is the important defect case. The exclude list is only consulted when the ring-fence list is empty ([11]). A rule configured with both will apply to an agency the business believes is excluded.


H. Booking-created-date filter — filterBookingCreatedDate

# BookingsCreatedFromDate BookingsCreatedToDate Party created Expected
H1 1 Jan 31 Mar 15 Feb Kept
H2 1 Jan 31 Mar 15 Apr Removed
H3 1 Jan empty 15 Apr Kept — open-ended
H4 empty 31 Mar 15 Feb Kept

Note BookingsCreatedFromDate defaults to the current date/time on a new rule, so a rule created today and saved without touching that field excludes every existing booking. Worth an explicit case.


I. Exclusivity and weighting — Pricing.Sub_CheckExclusivePricingRules_List [3], [8]

# Setup Expected
I1 Two exclusive rules, weightings 10 and 20, both qualify Weighting 10 is applied first — lowest first
I2 Two exclusive rules, both weighting 0 Order is the list's; assert only that one applies, not which (inferred)
I3 One exclusive + one non-exclusive Exclusive evaluated first ([26] before [27])
I4 Exclusive rule with CanCombineOtherExclusiveRules = true May coexist with another exclusive rule
I5 Rule with Universal = true Applies in addition to an exclusive rule
I6 Rule of a subtype with no case in the type switch Qualifies, then does nothing — silent no-op at [17] / (other type)

I6 is the regression to run after any new rule type is added.


J. Selectable rules — CheckPricingRuleBL

# Selectable In IncludeSelectable? Expected
J1 false Applies automatically
J2 true no Not applied. Added to Booking_PricingRule_Exclude; HasPricingThatCanInclude = true
J3 true yes Applies, exactly as a non-selectable rule
J4 true, OnlyManagerCanInclude = true attempted by non-manager Inclusion refused (inferred — the check was not traced to a page)
J5 Booking has BookingLite = true Different branch ([28] onward); rule attaches to the line, not the cost group

K. Side effects of attaching a rule — [10], [13]

When a rule attaches to a cost group, assert all of:

# Assertion
K1 PartyCostGroup_BL.Override = true — unless the rule is AlternateOptionOnly, then false
K2 DiscountedFlightsBlocked copied from the rule's BlockFreeFlying
K3 BookingLine.HasAlternateOptionPricing copied from AlternateOptionOnly
K4 BookingLine.OverridePrice = true ([12], when the rule was not already on the cost group)
K5 Eleven cost fields zeroed: Pax_Adults_Std_Cost, Pax_Adults_Std_Cost_ManDisc, Pax_Adults_SingleSupp_Cost, _ManDisc, Pax_Adults_SingleSuppOnly_Cost, Pax_Adults_SingleSupp_StdCost, Pax_Children_Cost, _ManDisc, Pax_Staff_Cost, _ManDisc, Pax_Total_Cost_ManDisc, Pax_Total_Cost_Net
K6 Reprice = true, then Reprice_DeleteDBLs removes the detailed lines

K5/K6 mean the costing is destroyed and rebuilt. A test that asserts costs survive rule attachment is asserting the wrong thing.


L. Rule maintenance validation — ValidatePricingRule

# Input Expected message
L1 FromDate empty 'You must have both a from date and a to date'
L2 ToDate empty same
L3 FromDate = ToDate 'From date must be less than the to date' — equal is rejected
L4 FromDate > ToDate same
L5 No Wish rate type 'You must assign a wish rate type'
L6 Description empty 'Description is required'
L7 ExclusiveDiscountInd = true, Weighting 0 or empty 'You must give a weighting, the lowest weighting rules will be applied first'
L8 ExclusiveDiscountInd = false, Weighting 0 Accepted — weighting only required for exclusive rules
L9 AllAgencies = false, ring-fence empty 'You must select at least one agency if you are going to limit the rule to certain agencies'
L10 DiscountPercentage = 150 Accepted. No range validation exists
L11 DiscountPercentage = -10 Accepted. Same gap

L10/L11 document a real absence. If validation is added later, these cases invert.


M. Outcome and self-healing — [45], [50][53], [61]

# Setup Expected
M1 Pricing succeeds, status C Booking.Priced = Confirmed
M2 Pricing succeeds, status not C Booking.Priced = Yes
M3 Pricing fails Booking.Priced = No
M4 Exception during pricing Priced = No, log Critical / System_Issue 'This booking failed to price at Sub_PriceBookingAll, please ensure it successfully completes pricing', returns false
M5 Booking had an open "failed to price" log, then prices successfully Prior logs auto-resolved: Status = Resolved, ResolvedBy = 'System', Resolution = 'Ran successully' (sic)

M5 matters for anyone reporting on SystemLog — failures self-clear, so a point-in-time count understates how often pricing failed.


N. The price/per-person reconciliation — Pricing.Sub_PriceBookingLine_Wilderness [14]

PPPriced = abs(TotalSell_PP − TotalSell) <= 0.1, where the two totals are computed independently — per guest, and per detailed line.

# Setup Expected
N1 Totals agree exactly PPPriced = true
N2 Totals differ by 0.05 PPPriced = true
N3 Totals differ by exactly 0.1 true — the test is > 0.1, so 0.1 passes
N4 Totals differ by 0.11 PPPriced = false; line reprices on the next pass
N5 Booking worth 500,000 with a genuine 0.09 discrepancy true — the tolerance is absolute, not proportional. Known weakness
N6 OverrideOption = true, IndicativeCostInd = false Priced = false even when pricing succeeded
N7 OverrideOption = true, IndicativeCostInd = true Priced = true

N3 and N5 are the boundary cases worth automating.


O. Guest allocation guard — [25], [55]

# Setup Expected
O1 All guests allocated to rooms Line prices
O2 One guest unallocated Line unpriced (not zero-priced); Warning / User_issue log 'Missing rooming' naming the line
O3 O2, then allocate and reprice Line prices; assert the earlier log's fate — see Q1

P. Tax — CalculateDetailedBookingLineTax

Tax is inclusive: amount × rate / (100 + rate).

# Cost Sell Cost rate Markup rate Expected
P1 115 230 15 15 TaxOnCost = 115 × 15/115 = 15; TaxOnMarkup = (230−115) × 15/115 = 15; TaxOnSell = 30
P2 100 100 15 15 Markup tax 0 — sell equals cost
P3 200 150 15 15 Cost-tax base is the selling price (150), not 200 — [32]
P4 200 150 15 15, CostsAllocatedToOtherBLs = false Negative markup tax clamped to 0[33]
P5 as P4 but CostsAllocatedToOtherBLs = true Negative markup tax kept
P6 Cost rate 0, markup rate non-zero TaxOnMUonly = true set on the booking line
P7 Cost rate non-zero, markup rate 0 TaxOnCostOnly = true set
P8 No tax rate found for the option TaxedSuccessfully = false; Critical log 'No Tax Rate found for Option (…)'
P9 Detailed line with no booking line / no booking / no agency TaxedSuccessfully = false, Critical log naming which

P4/P5 carry a known contradiction: a canvas note says negative VAT should be allowed "because TP calcs it anyway", but the clamp is still in place. If Tourplan reconciliation disagrees on below-cost lines, this is why.

R-rate selection

# BCQ_ItemNumber Expected TaxCodePrice
P10 3 OPT
P11 6 OPT — anything < 7
P12 7 EX1
P13 8 EX2
P14 9 EX3
P15 Summed rates come to a negative Clamped to 0.0[16]
P16 A TaxTable row marked TaxApply = 'A' Contributes to both the markup rate and the cost rate — asymmetry documented in the deep dive §4
P17 A row marked 'S' (Sell Only) Contributes to the markup rate, not the cost rate

Agency tax indicator — CalculateTaxOnMarkup

# Indicator contains Expected markup rate
P18 0, 1 or 8 from GetMarkupTaxRate(TourplanInstance)
P19 4, 6 or 7 the passed-in input rate
P20 2, 3, 5 or 9 only 0 — no markup tax
P21 Indicator empty 0, returned immediately

Q. Log lifecycle

# Setup Expected
Q1 Line had 'There is no pricing for…', 'No Tourplan pricing found for…' or 'No pricing found for supplier…', then prices successfully Those logs are DELETED[19]/[20]. Not resolved, not archived
Q2 Booking had 'This booking failed to price at Sub_PriceBookingAll', then prices Marked Resolved, ResolvedBy = 'System', Resolution = 'Ran successully'

Q1 vs Q2 is a deliberate assertion of inconsistency: two failure paths, two different outcomes, one of which destroys the record.


R. Rack factor and totals

CalcRackFactor:

# Room factor Line factor PriceSell vs PriceCost MarkupToRack Expected
R1 1.4 different 1.4
R2 1.4 equal 1
R3 none/0 1.2 different 1.2
R4 none/0 none/0 different 0.3 (PriceCost × 1.3 − PriceSell) / PriceSell + 1
R5 none/0 none/0 different 0/empty 1
R6 none/0 none/0 PriceSell = 0 any 1

SumDetailedBookingLines:

# Setup Expected
R7 Sell 900, before-discount 1000 Discount = 100, DiscountPercentage = 10
R8 Before-discount total 0 DiscountPercentage = 0, no division error
R9 Cost 800, sell 1000 MUpPerc = round(200 × 100 / 800, 2) = 25.00
R10 Cost 0, sell 500 MUpPerc = 0 — not infinity, not an error. Complimentary items depress reported markup
R11 Lines in two currencies Totals produced per currency; assert no implicit conversion
R12 Line with a cancellation fee adjustment _CanFeeAdj sums populated per guest category

R7/R10 matter commercially: Discount is a residual of before/after selling price, so any mechanism that lowers sell reads as a discount regardless of cause.


U. Manual adjustment limits — Dashboard.SumBookingLine_CheckLimit

# Sub-category limit Currency Adjustment moves totals by Expected
U1 HasAdjLimit = false any any Accepted$AdjLimit is 0, so [2] returns true
U2 500 USD USD 400 Accepted
U3 500 USD USD 600 cost Rejected, cost-price message, adjustment removed
U4 500 USD USD 600 sell Rejected, sell-price message
U5 500 USD USD exactly 500 Accepted — test is > LimitUSD
U6 500 USD ZAR, rate available 600 USD-equivalent Rejected, after conversion by BidRate
U7 500 USD ZAR, no exchange rate 6,000 ZAR (≈330 USD) Rejected — wrongly. [18] compares unconverted, so the limit behaves as 500 ZAR. Known defect
U8 500 USD ZAR, no rate 400 ZAR Accepted — also for the wrong reason

U7/U8 are the pair that demonstrate the defect in both directions. Assert no log is produced.


V. Approval

# Setup Expected
V1 No approver on the booking CheckManDiscApproval returns false, opens Pricing.AddDiscountAuthorisation
V2 Approver set, someone else returns true
V3 Approver is the current user nomination cleared; "Unfortunately you are not allowed to approve your own discounts…"
V4 Approver is the booking owner same clearing and message — [5]
V5 Manager approving a booking they own Blocked by V4. Expected behaviour, not a permissions fault
V6 ManualDiscApproval false → true committed with events
V7 ManualDiscApproval true → false committed without events, then IVK_ClearManualDiscount_BL_All deletes every manual discount

V7 is the destructive one. Assert the discounts are gone, not merely unapproved.


W. Applying an adjustment — CreateManDisc

# Setup Expected
W1 Adjust one detailed line, cost group present Every sibling line matching date, price type, age category, option and cost group gets the same figures; RepriceSet = true
W2 Same, no cost group The match runs without the cost-group constraint — wider blast radius ([19])
W3 First adjustment on a line ManualDiscountAppliedBy = currentUser, ManualOverrideAppliedDate set
W4 Second adjustment by another consultant ManualDiscountAppliedBy unchanged — first-writer-wins. Known audit weakness
W5 Override a 100 price to 0 ManualDiscountPercSP = 100
W6 Override a 0 price to anything ManualDiscountPercSP = 0 regardless of magnitude
W7 Any adjustment OverridePrice = true, Override_Manual = true, OverrideOption = false, ManualAdjustment.Applied = false

X. Allocation reconciliation — CheckAllocation_BL

# Setup Expected
X1 Consistent booking line Temp_totals.NetErrorDBls = 0
X2 Discount not reflected in SellingPrice NetErrorDBls != 0and no error is raised. Assert on the value, not on a message
X3 Run the check twice Previous Temp_totals rows are deleted first ([3]), so no history accumulates

Y. Late booking discount — IVK_ResidentsLateBookingDisc_Save

# Input Expected
Y1 DaysBeforeTravel = 0 'You must set a valid number of days (eg 30)'
Y2 DaysBeforeTravel = 720 Rejected — bound is < 720
Y3 DaysBeforeTravel = 719, discount 20, membership set Accepted
Y4 Discount = 0 'You must set a valid discount (eg 20 for 20%)'
Y5 No Membership 'You must set which membership type this discount is v…'

Z. Stay-pay tiers — GetFreeDays

# Stay2 Stay3 Days Expected
Z1 1 any any Free1 — tier disabled at <= 1
Z2 7 1 8 Free2 — third tier disabled
Z3 7 14 8 Free2
Z4 7 14 14 Free3 — boundary is >=
Z5 7 14 6 Free1

S. Suggested smoke path

  1. Draft booking, one chargeable camp line, agency with AllAgencies rule in window → rule attaches.
  2. Set the rule Selectable → rule no longer applies; HasPricingThatCanInclude = true.
  3. Include it manually → applies; assert K1–K6.
  4. Blank DaysNights on the line → all rules disappear; assert the D2 log.
  5. Restore, then set the agency onto both ring-fence and exclude lists → rule still applies (G6).
  6. Age the booking to Traveled 4 months back → repricing blocked with the A5 message.

T. Environment notes

  • Rule rows, agency ring-fences and discount percentages are runtime data. Seed them explicitly per environment; do not assume a test environment mirrors production.
  • BookingMasterData.R1Markup (1.1629), _01Markup (1.111111) and Supplier_Portal.DMCMarkup (8) are model defaults and may be overridden per environment. Confirm the deployed values before asserting on any figure derived from them.
  • Pricing carries no documentation text on any of its 637 microflows, so the model offers no intent to compare behaviour against. Canvas notes are the only commentary, and at least one is already stale (see the deep dive, §8.5).