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 (Pricing↔Booking, Booking↔DataManagement)
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 (
CommunityCommons68,NanoflowCommons40,PDFUtils18,LocalFileOperations11), which is expected and harmless. The one worth reviewing isDataManagementwith 54 uncalled actions of its own - that is project code, not a library. DataManagementis 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
ExternalDatabaseConnectorand 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).
-
Admin password in the model, in plaintext.
Security$ProjectSecuritystores theMxAdminpassword as a literal string, and it has been committed. Anyone with repo read access has it.MxAdmin's role isDebug. Rotate it, and set admin credentials per environment through runtime configuration instead (AdminPassword/ environment variables), not in the model. -
Developer database credentials in the model, in plaintext. Of the 17 server configurations, several store a database password directly:
Local(SQL Server at172.16.2.165, usermendix),Joeg,Joeg_Test,Ashley D,Ashley T,Carl - Home(all sharing one password against hostlion, userjdbc),Altostratus, andMatt_Squirrel- which holds ansapassword. All are in git history. Rotate those accounts, and delete personal configurations from the shared model (each developer can keep local settings inproject-settings.user.json, which.gitignorealready 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.
- Internal network topology is exposed through the same settings: proxy
172.22.0.17:8080, database hostslion,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.keytabis 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.
- 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.URLare creatable/changeable, none of them behind an XPath constraint - which is normal for password-reset and deep-link tokens). Security level isCheckEverything. The residual items to check are thatStrictPageUrlCheckandStrictModeare 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.
-
Integration payload logging.
WishAPI.LogWishMessagesToDisk,API.LogXMLRequestResponseandHubSpot.LogRestfulMessagepersist full request/response bodies containing guest personal data and possibly payment details. Confirm retention and access controls match your data-protection obligations (EmailLogRetention/EmailMessageRetentionconstants exist for email; check whether equivalents govern these). -
49 microflows run with security disabled and no user attribution. Every
CommunityCommons.executeMicroflowInBackgroundcall runs its target in a system transaction. The action's own documentation states it plainly: "since the microflow is run as system transaction,$currentUseris 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 — includingBooking.IVK_ConfirmFromBookingFile, a booking confirmation invoked this way fromTools.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.
- A private-key store is committed to Team Server.
resources/privkeystoreis a Java KeyStore (JKS, 45,809 bytes, magicfeedfeed), 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.
-
A missing exchange rate silently defeats the discount limit.
Dashboard.SumBookingLine_CheckLimitconverts the adjustment's effect to USD viaDataManagement.GetReal_ERate('USD', $Currency, false, now)and divides byBidRate. 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. -
The limit is enforced after the fact.
IVK_ApplyManualDisc_fromBL_Unit_Newapplies 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. -
Withdrawing approval destroys data.
ToggleDiscountApproval[6] callsIVK_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. -
The discount audit trail names the wrong person.
BookingLine.ManualDiscountAppliedByis 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. -
Two reconciliation checks exist and neither raises anything.
PPPricedcompares two independently computed totals within an absolute 0.1 (pricing-calculation.md §2), andCheckAllocation_BL[10] computesNetErrorDBls = sell − (gross − discount − cancellation adjustment). Both store a value and continue. A booking whose numbers do not reconcile looks normal, andCheckAllocation_BL[3] deletes the previousTemp_totalsrows first, so there is no history of failures to report on. -
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 averagesManualDiscountPercSPinherits both. -
Self-approval prevention is wider than it looks.
OCh_CheckApproverrejects 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. -
No range validation on any discount percentage. Neither
ValidatePricingRulenor 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:
LateBookingDiscountis listed as pricing rule type #8. It is a standalone entity, not aPricingRulespecialisation, so it is subject to none of the qualification pipeline.CheckExclusivePricingRulesandCheckNonExclusivePricingRulesdo not exist. The real names areSub_CheckExclusivePricingRules_ListandSub_CheckNonExclusivePricingRules_List.- Its call chain omits
Sub_PriceBooking_Wilderness, which sits betweenSub_PriceBookingAllandSub_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¶
.gitignorecase bug: it lists/wildernesswindow.mpr.lock(lowercase) but the actual file isWildernessWindow.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.xmldeclares Java source/target 1.8 while Mendix 10 runs Java 21. It only affects IDE behaviour, but it misleads anyone editingjavasource/.- 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¶
- Rotate the credentials in findings 1 and 2, then strip them from the model, including
the passwords embedded in
DatabaseJdbcUrlcustom settings. - Reissue whatever
resources/privkeystorecertifies, and stop shipping the keystore in version control (security finding 7). - 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_CheckLimitcurrently 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. - Delete the 15 personal server configurations from the shared model.
- 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.
- Delete the 1,367 already-excluded documents (lowest risk, already dead).
- Work through the 2,003 unreferenced documents module by module,
Bookingfirst, confirming each againstmxinspect callers, the process-queue configuration rows, and the callers ofjaCallMicroflow. - Review the 3,813 unused attributes per module with someone who knows the integrations,
starting with
BookingandBookingMasterData. - Stop the duplicate-then-date practice: git now gives you the history that practice was substituting for.
- 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.