Skip to content

Conventions, debt and risks

All counts below were measured against the model at commit 83d87ba8e (Mendix 10.24.21.108016, 17,297 units). Reproduce them with tools/mxinspect.py (see README); model/PROVENANCE.md records the exact revision.

Naming conventions

The team follows a consistent prefix scheme. Learn it and the model becomes navigable:

Prefix Count Meaning
IVK_ 3,934 Invoked from the UI - button, menu item or event handler. The entry points
OCh_ / OCH_ 462 On-change handler for an attribute or association
DS_ / Ds_ 439 Data source for a data view, grid or list
Sub_ / SUB_ 375 Sub-microflow, called from other flows only
TP_ 240 TourPlan integration flow
Sn / SNP 313 Snippet
SE_ 179 Scheduled-event target (or a job step)
ACT_ 120 Action flow (newer Marketplace-style equivalent of IVK_)
Get_, Chk_, VAL_, tmp_ ~200 Retrieval helper, check/validation, validation, temporary/one-off

Folders carry the feature boundaries - Booking/BookingWizard/Step2 - Availability check, Booking/Receivables/CashReceiptMove, BookingMasterData/RARA/RateSheets. See generated/folder-map.md. Treat folders, not modules, as the unit of feature ownership inside the big modules.

Debt: dated clones

457 microflows, pages and document templates carry a date suffix (_270526, _20230413, _2025_10_06). The working practice is duplicate-then-date before changing a flow, keeping the old copy parked in place. Full pairing and similarity scores: reports/dated-clones.md.

  • 448 are already excluded from deployment - safe to delete once confirmed.
  • 7 are still referenced by live documents - these are not backups any more; something calls the old copy. Check these before anything else.
  • 49 have no undated sibling at all - the original was deleted or renamed, so the dated copy is the live version and its name now lies about it. Examples: API.GetLeadEmployee_010726, Booking.AddGuestToPCG_290525, Booking.AgencyInvoice_20240328 (a document template), Administration.Database_Debug_Dashboard_MergeConflict_05_09_2025.

Why it matters: the four API.ReplaceQuote* variants each call ~60 microflows, so a reader cannot tell which path is live without checking callers, and a bug fixed in one copy is not fixed in the others. This made sense when the model was one binary .mpr file with poor diffing; since the v2 split-MPR migration (March 2025) git handles it, so the practice is now pure cost.

Highest concentrations: Booking (185), Pricing (48), DataManagement (35), API (22).

Debt: unreferenced documents

2,003 documents are referenced by nothing - absent from the model's reference graph and mentioned nowhere in the 106 MB text extract or the Java source. Full list: reports/unreferenced-documents.md.

Type Count Type Count
Microflow 1,123 JavaScript action 51
Page 290 Snippet 50
Java action 162 Enumeration 30
Import mapping 138 Nanoflow 26
Export mapping 119 Document template 14

The two-signal method matters: 2,711 documents are missing from the reference graph, but 708 of those are genuinely used in ways the graph cannot see - enumerations named inside expressions (Booking.BookingStatus.C), mappings named by published REST operations, Java actions called from other Java. Reference-graph-only analysis would have condemned all 2,711.

Separately, 1,367 documents are marked "Excluded from deployment" - already known-dead but still in the model.

Verify before deleting. A document can still be reached by name at runtime: - through MxModelReflection or the database-driven process queue; - through DataManagement.jaCallMicroflow, a Java action that executes any microflow named in a string parameter (Core.executeAsync(getContext(), this.MicroflowName)) - every flow it can reach is invisible to static analysis; - as a published REST/SOAP operation, or from an anonymous deep link.

Debt: intent recorded on the canvas and never carried out

1,871 developer sticky notes sit on microflow canvases. They are the only commentary in the model — there is no other prose — and they carry the reasoning that names and structure cannot. Full classified index: reports/annotations.md.

Category Count What it is
Unfinished intent 367 "need to", "must", "temporary", "while we", "not yet" — behaviour described, not necessarily built
Records a removal 394 "Removed X", "no longer" — explains why a flow looks incomplete
Carries a date 145 lets you age a decision and find who made it

Three things follow.

Some are unbuilt requirements, not comments. Booking.CancelBooking carries "if cancel from step 2 - need to send ARCs" with no corresponding activity anywhere in the flow. If step-2 cancellations are meant to notify camps of released availability, they do not. Each of the 367 is a claim to check, not a fact.

Some are temporary fixes that outlived their authors' intent. Booking.CancelBooking also carries "2021-10-20 : Put this in while we sort out the statuses - Al" — five years old. Search the unfinished-intent list before touching any area you think is simple.

Some settle questions the logic cannot. Booking.SE_CancelExpiredBookings retains "5/5/23 changed to 4 years Francis and Joe" and "per Ianka - we need to keep cancelled / old bookings on hand for at least 6 months". Together these explain why the job's queries say four years while its log strings say six months: the queries implement an agreed change and the strings are pre-2023 leftovers. Without the notes, that reads as an unresolvable contradiction.

Where a note and the logic disagree, the logic is what runs. Notes are undated commentary; several are demonstrably stale. Booking.Sub_AssignExpiryDate claims to use the latest inventory expiry date while the retrieve sorts ascending and takes the first, i.e. the earliest. Booking.SE_WaitlistRelease says it clears waitlists inside "8 weeks" while the constant is 42 days. Both are worth resolving; neither is safe to assume.

These notes were invisible until 2026-08-07 — see Extraction fidelity.

Debt: parallel generations kept live

Old New Both still in the model
Nightsbridge NightsBridge_V5 yes
API (WWAPI_Rest, SOAP WWAPI_V2, WWAPI_V2_20240724) API_V3 (wwapi_v3) yes - 4 published generations
DataManagement/MFTourplan (81) MFTourplanNew (276) yes
Itinerary/MF (103) Itinerary/V8 (197) yes
Marketplace ProcessQueue + TaskQueueManager + 5 Mendix task queues home-grown Maintenance process queue three overlapping async mechanisms

Debt: documentation coverage

328 of 17,297 units carry any documentation text (~2%), and most of those are Marketplace modules that shipped documented. Project-authored flows are essentially undocumented, which is why the acronym meanings (ROAR, GEL, RARA, EAH) cannot be recovered from the model at all.

Debt: module size and coupling

Booking holds 2,532 microflows, 485 pages, 147 entities and 72 of the app's 80 document templates. BookingWizard alone is 1,845 documents. There is no sub-module boundary inside it; folder discipline is the only structure. Any change to booking behaviour has a large blast radius, and Pricing.Sub_PriceBookingAll (89 callers) / Administration.GetServerSettings (466 callers) are effectively public API.

Coupling between modules is dense (reports/module-dependencies.md):

From To References
Pricing Booking 1,396
DataManagement Booking 1,078
SRM BookingMasterData 837
Booking BookingMasterData 768
API Booking 749

Most of that weight is entity references, not microflow calls - modules read each other's data directly rather than through an interface.

81 of the 110 modules form a single dependency cycle. They reference each other directly or transitively, so no module in that group can be versioned, deployed or extracted on its own. That is the structural reason a "split the monolith" effort would have to start by breaking specific edges (PricingBooking, BookingDataManagement) rather than by picking a module to move.

Debt: unused attributes

3,813 of 15,795 attributes (24%) appear in no microflow, page, mapping, published service or Java file (reports/attribute-usage.md). Concentrated in Booking (533), BookingMasterData (442), DataManagement (185), TravelStart (181), Itinerary (165).

The detection is deliberately conservative (an attribute counts as used if its short name appears anywhere, even on another entity), so this under-reports rather than over-reports. Still confirm against production data before dropping columns - attributes can be read by external systems through published services, and integration modules mirror partner payloads whose fields may be written but never read by this app.

The Java layer

984 source files, 105,564 lines (reports/java-index.md). 622 files implement action documents, 362 are helper classes. Every action document has a source file - no orphans.

  • 232 of 622 action documents are never called from any microflow or page - but most of that is unused Marketplace library surface (CommunityCommons 68, NanoflowCommons 40, PDFUtils 18, LocalFileOperations 11), which is expected and harmless. The one worth reviewing is DataManagement with 54 uncalled actions of its own - that is project code, not a library.
  • DataManagement is where the custom Java lives: 311 files, 48,012 lines, 256 actions - bigger than every Marketplace module combined.
  • 44 sites build XPath by string concatenation ("//" + entityName + ...), so those reads and writes bind to no entity that static analysis can see. Only 7 literal XPath strings exist.
  • 13 files use raw JDBC/SQL, all inside ExternalDatabaseConnector and its callers.
  • Java hard-codes no outbound HTTP endpoints - every integration URL comes from model constants or runtime config. That is the good pattern here.
  • Microflow callbacks from Java are almost all dynamic (18 sites take the flow name from a variable); only one literal call exists (ProcessQueue.SF_WriteExecutionLog).

Security findings

These are real and worth acting on. The values are in the model and in git history - do not copy them into new files (including these docs).

  1. Admin password in the model, in plaintext. Security$ProjectSecurity stores the MxAdmin password as a literal string, and it has been committed. Anyone with repo read access has it. MxAdmin's role is Debug. Rotate it, and set admin credentials per environment through runtime configuration instead (AdminPassword / environment variables), not in the model.

  2. Developer database credentials in the model, in plaintext. Of the 17 server configurations, several store a database password directly: Local (SQL Server at 172.16.2.165, user mendix), Joeg, Joeg_Test, Ashley D, Ashley T, Carl - Home (all sharing one password against host lion, user jdbc), Altostratus, and Matt_Squirrel - which holds an sa password. All are in git history. Rotate those accounts, and delete personal configurations from the shared model (each developer can keep local settings in project-settings.user.json, which .gitignore already excludes).

Two further credentials hide where a key-name scan would miss them: custom settings named DatabaseJdbcUrl embed the password inside the connection string - jdbc:sqlserver://lion:1433;databaseName=WildernessWindow_V10;user=mendix;password=... (configuration faiz) and jdbc:sqlserver://127.0.0.1:1433;...;user=sa;password=... (configuration Matt_Squirrel). Grep for password= inside setting values, not just for password-named settings, when you sweep for these. By contrast BookingMasterData.Database_DBPassword is correctly configured as a private constant value, so it is not stored in the model - that is the pattern to follow.

  1. Internal network topology is exposed through the same settings: proxy 172.22.0.17:8080, database hosts lion, 172.16.2.165, WINDOWSSERV-001, Altostratus, DESKTOP-SQS5T7N. Low severity on its own, useful to an attacker who gets the credentials above.

source/resources/sso.properties adds the Active Directory side: domain mendixdomain.local, domain controller 10.140.10.18, Kerberos service account mendixacc. Two observations beyond the topology:

  • kerberos_protocol = http - the SPN was generated for plain HTTP, so an SSO handshake that honours this setting is not protected by TLS in transit. Confirm what the deployed environments actually terminate before assuming this is only a dev value.
  • kerberos_keytab_file = mendixacc.keytab is referenced but is not in the repository, which is the correct outcome - a keytab holds the service account's credentials. Keep it that way; do not commit it to fix a deployment error.

debug = false, correctly.

  1. The unauthenticated surface is small and well scoped - measured, not assumed. Guest access is enabled with role Anonymous_UseWithCaution, which maps to three module roles and reaches 4 entities, 3 pages and 6 microflows (Administration.ForgotPassword, Anonymous.PasswordData, Anonymous.URL are creatable/changeable, none of them behind an XPath constraint - which is normal for password-reset and deep-link tokens). Security level is CheckEverything. The residual items to check are that StrictPageUrlCheck and StrictMode are off, and that deep-link hashes are unguessable - they do expire (Anonymous.SE_DeleteExpiredDeepLinkHashes). Full matrix: reports/role-access-matrix.md.

The bigger role risk is the other end: Administrator reaches 1,658 entities, 1,329 pages and 3,852 microflows, and Agent - an external-facing role - reaches 802 entities with create on 467 and delete on 483. Worth a review pass against the row-level constraints listed per role in that report.

4b. Arbitrary microflow dispatch from Java. DataManagement.jaCallMicroflow runs Core.executeAsync(getContext(), this.MicroflowName) - it executes whatever microflow name it is handed, bypassing the model's own allowed-roles checks on the caller side. Confirm no path lets a user-supplied value reach that parameter, and that its callers are all internal.

  1. Integration payload logging. WishAPI.LogWishMessagesToDisk, API.LogXMLRequestResponse and HubSpot.LogRestfulMessage persist full request/response bodies containing guest personal data and possibly payment details. Confirm retention and access controls match your data-protection obligations (EmailLogRetention / EmailMessageRetention constants exist for email; check whether equivalents govern these).

  2. 49 microflows run with security disabled and no user attribution. Every CommunityCommons.executeMicroflowInBackground call runs its target in a system transaction. The action's own documentation states it plainly: "since the microflow is run as system transaction, $currentUser is not available and no security restrictions are applied." So entity access is not enforced and nothing is attributable to a user for any of the 17 distinct targets — including Booking.IVK_ConfirmFromBookingFile, a booking confirmation invoked this way from Tools.IVK_ConfirmBookings.

This is normal, documented Marketplace behaviour and is often the right tool. The finding is the scale and the criticality: 49 sites, reached from published API operations, with no record of who caused the work. Anything relying on entity access or on audit attribution should not be behind this call. Inventory: reports/async-handoffs.md; consequences: 04-integrations.md.

Related throughput risk rather than security: the same action guarantees FIFO order with only one microflow running at a time, so all 49 sites share one serialised lane. A slow target blocks every other background handoff in the application.

  1. A private-key store is committed to Team Server. resources/privkeystore is a Java KeyStore (JKS, 45,809 bytes, magic feedfeed), added 2025-03-29 and never touched since. JKS files hold private keys and certificates, and a keystore in version control is readable by everyone with repository access, forever, including in history.

This compounds finding 1. JKS protects its key entries with a password, so the store's safety rests entirely on that password not being guessable and not being written down somewhere the same people can read - and this model already stores credentials in plaintext. Treat the keys as exposed until proven otherwise: identify what they certify, reissue them, and load the replacement from deployment configuration rather than from the repository.

It is deliberately not copied into this repo's source/ - see SOURCE_EXCLUDE in tools/sync.py. That limits the blast radius to one repository; it does not fix the underlying exposure, which is in Team Server.

Financial control findings

Distinct from the security findings above: these are weaknesses in the controls around money, found while documenting pricing. Node references are to commit 83d87ba8e; the detail is in deep-dives/pricing-manual-adjustments.md.

  1. A missing exchange rate silently defeats the discount limit. Dashboard.SumBookingLine_CheckLimit converts the adjustment's effect to USD via DataManagement.GetReal_ERate('USD', $Currency, false, now) and divides by BidRate. When no rate is returned, [18] falls through and compares the raw local-currency amounts against the USD limit. For a currency weaker than the dollar the limit becomes far stricter than intended; for a stronger one, looser. Nothing is logged, no message is shown, and no flag records that the check ran unconverted. Highest-value fix in this list — it is a financial control that fails silently and invisibly.

  2. The limit is enforced after the fact. IVK_ApplyManualDisc_fromBL_Unit_New applies the adjustment at [15], reprices the entire booking at [20], and only then checks the limit at [24]. Rejection is a rollback, which is why the user-facing message says the adjustment "has been removed" rather than that it was not allowed. Any side effect of that repricing pass that is not part of the rolled-back totals persists. (inferred) — what exactly survives was not traced.

  3. Withdrawing approval destroys data. ToggleDiscountApproval [6] calls IVK_ClearManualDiscount_BL_All, so turning the approval flag off deletes every manual discount on the booking rather than returning them to an unapproved state. There is no confirmation in the flow itself.

  4. The discount audit trail names the wrong person. BookingLine.ManualDiscountAppliedBy is written only when empty (CreateManDisc [11]), so it records the first consultant to adjust the line. Any later adjustment — including the one under review — leaves the original name in place.

  5. Two reconciliation checks exist and neither raises anything. PPPriced compares two independently computed totals within an absolute 0.1 (pricing-calculation.md §2), and CheckAllocation_BL [10] computes NetErrorDBls = sell − (gross − discount − cancellation adjustment). Both store a value and continue. A booking whose numbers do not reconcile looks normal, and CheckAllocation_BL [3] deletes the previous Temp_totals rows first, so there is no history of failures to report on.

  6. Percentage recording is lossy at the edges. An override to zero records a 100% discount; an adjustment to a line whose original price was zero records 0% regardless of magnitude (CreateManDisc [15]). Any reporting that averages ManualDiscountPercSP inherits both.

  7. Self-approval prevention is wider than it looks. OCh_CheckApprover rejects the approver being the current user [3] or the booking's owner [5]. Correct as a control, but it means a manager cannot approve discounts on a booking they own — which will be reported as a permissions fault. Worth stating in training rather than fixing.

  8. No range validation on any discount percentage. Neither ValidatePricingRule nor the manual path bounds a percentage, so 150% or −10% saves without complaint (pricing-rules.md §6).

Cross-checked against an independent document

MaiaPricingRules.docx — pricing documentation produced with Mendix's own Maia assistant — was compared against this repo. It is entity-centric where these documents are behaviour-centric, and the comparison was useful in both directions.

It found things this repo had missed, all since documented: the ManualDiscounts approval chain, ChooseExclusiveDiscount_DBL, GetFreeDays, Pricing.LateBookingDiscount, and the configuration and logging entities. It also independently reported PricingRule as having "54+ attributes", which prompted a recount and corrected a wrong figure here.

Three of its claims do not survive checking against the model:

  • LateBookingDiscount is listed as pricing rule type #8. It is a standalone entity, not a PricingRule specialisation, so it is subject to none of the qualification pipeline.
  • CheckExclusivePricingRules and CheckNonExclusivePricingRules do not exist. The real names are Sub_CheckExclusivePricingRules_List and Sub_CheckNonExclusivePricingRules_List.
  • Its call chain omits Sub_PriceBooking_Wilderness, which sits between Sub_PriceBookingAll and Sub_PriceBookingLine_Wilderness.

It also describes AllowPartialAppllication as "Apply to subset of booking" where the model documents it "NOT USED", and lists 19 rule types against the model's 23.

It carries no provenance. Its revision history reads Version 1.0 / [Current Date] — an unfilled placeholder — with no commit, no date and no Mendix version, so there is no way to tell which revision it describes. For pricing documentation that is the difference between useful and dangerous, and it is the reason model/PROVENANCE.md exists here.

Repo hygiene

  • .gitignore case bug: it lists /wildernesswindow.mpr.lock (lowercase) but the actual file is WildernessWindow.mpr.lock, so the lock file shows as untracked on case-sensitive setups. Fix the case.
  • The lock file also means Studio Pro may have the model open; coordinate before editing the model from outside Studio Pro.
  • Root-level clutter to remove or relocate: Desktop - Shortcut.lnk, doc.html (242 KB one-off model dump), dead_code_recommendations.csv (empty), Find results for changes to attribute 'Booking.Booking.WindowStatus' ....csv.
  • pom.xml declares Java source/target 1.8 while Mendix 10 runs Java 21. It only affects IDE behaviour, but it misleads anyone editing javasource/.
  • Git history begins 2025-03-29; anything older needs repository_log.xml (SVN) or the retired SVN repository.

If you are cleaning up, do it in this order

  1. Rotate the credentials in findings 1 and 2, then strip them from the model, including the passwords embedded in DatabaseJdbcUrl custom settings.
  2. Reissue whatever resources/privkeystore certifies, and stop shipping the keystore in version control (security finding 7).
  3. Make the discount limit fail closed when no exchange rate is available (financial control finding 1). It is a one-branch change — [18] of Dashboard.SumBookingLine_CheckLimit currently falls through to comparing unconverted amounts — and until it is made, the limit is unenforced for any currency without a rate, with nothing recording that.
  4. Delete the 15 personal server configurations from the shared model.
  5. Fix the 49 dated clones with no undated sibling and the 7 still referenced - these are correctness risks, not just clutter, because the naming lies about which version is live.
  6. Delete the 1,367 already-excluded documents (lowest risk, already dead).
  7. Work through the 2,003 unreferenced documents module by module, Booking first, confirming each against mxinspect callers, the process-queue configuration rows, and the callers of jaCallMicroflow.
  8. Review the 3,813 unused attributes per module with someone who knows the integrations, starting with Booking and BookingMasterData.
  9. Stop the duplicate-then-date practice: git now gives you the history that practice was substituting for.
  10. Triage the 367 unfinished-intent notes. Each is either a requirement to build, a stale comment to delete, or a decision to record properly. Leaving them is what makes the model's stated behaviour and its actual behaviour drift apart unnoticed.

Extraction fidelity

Everything in these docs is derived from a text extract of the model produced by tools/mxrender.py. On 2026-08-07 that extract was found to be dropping four classes of content silently. All are now rendered; if you are reading a document or an analysis written before that date, treat these as blind spots:

Content Was rendered Now What it hid
Microflow/entity-valued call parameters 0 of 95 95 The target of every executeMicroflowInBackground — 17 distinct systems and flows, OnBase among them
Canvas annotations 0 of 1,835 1,871 NOTE: lines Every note discussed above
Rule-based split conditions 81 blank 81 named Branch conditions rendered as a bare SPLIT if
Sort attribute and order 1,190 of 1,292 blank 0 blank Whether a first retrieve takes the earliest or the latest row

Two SPLIT if with no condition remain, both in orphaned unreachable nodes with a genuinely empty condition in the model. Regenerate and check at any time:

python3 tools/mxdump.py
grep -rh 'SPLIT if$'  model/*/flows.txt          # expect 2
grep -rhoE 'sort by *(->|$)' model/*/flows.txt   # expect 0
grep -rhc '^ *NOTE: ' model/*/flows.txt          # expect 1,871 total

The general lesson for anyone extending the tooling: a renderer that omits a property produces output that looks complete. Prefer failing loudly over rendering a partial line — the four gaps above all produced plausible, readable, wrong output.