Supply Chain Assessment
The supply chain assessment page (/assess/supply-chain) aggregates footprint data for a set of organisations. The data it shows depends on two things: the scope (which organisations/transactions to start from) and the filters (how to narrow that scope).
Scope vs Filters
| Concept | Params | Purpose |
|---|---|---|
| Scope | organisationIds, segmentIds | The starting set of organisations/transactions |
| Filters | classification, search, category, exposure, spendMin, spendMax, status | Narrow the scope further |
The key rule is: explicit org selection bypasses most filters — category still applies as a transaction-level filter. Segment scope and no scope both respect all filters, including status. Note: filtering by status only considers orgs reachable from the account via transactions, reports, or engagements (the "three-path-OR reachability" — see Flow 2/3 below). It does not surface arbitrary orgs from the global graph.
Always-applied params:
dateFrom,dateTo,transactionType, andmodule/satelliteModeare unconditional — they apply in all three flows regardless of scope or filter state.
Footprint figures on the Identify page
When a segment filter is active on the Identify page, footprint figures shown per company reflect only transactions within that segment — not the company's total footprint across all transactions. This is intentional: if you are evaluating suppliers in the context of a specific segment, you want to see their contribution within that scope, not their global numbers. For example, a company with $10M total spend but only $2M within the segment shows $2M. This is achieved by buildFootprintInput in query-builders.ts setting segmentIds on the OrganisationFootprintInput, which is then passed to OrganisationRepository.findWithFootprint.
Company count alignment with Identify
Identify's companies count and the assessment's "Companies Assessed" metric deliberately use the same definition when a date range is active: orgs whose footprint.source is TRANSACTION for the selected date window and transaction type.
SOURCING orgs are always counted on Identify (they are defined as "never transacted with" and cannot have real footprint data) but are never counted by the assessment (the assessment aggregates transaction-backed footprints only).
When no dateFrom or dateTo is active on Identify, the count reverts to the broader "every org reachable by transactions, reports, or engagements" view — useful for portfolio-level supplier management independent of a specific reporting window.
Flow 1: Explicit Organisation IDs
The user selected specific companies on the Identify page and clicked Assess.
URL example:
/assess/supply-chain?module=modern-slavery&organisations=org-1,org-2,org-3What happens:
- Transactions fetched via
findByOrganisationsForFootprintfor those exact org IDs - All filters (
classification,search,exposure,spendMin,spendMax) are ignored categorystill applies as a transaction-level filter- Org metadata is restricted to only the provided org IDs
Rationale: The org IDs already represent the user's deliberate selection — they may have applied filters on the Identify page before selecting. Re-applying filters here would incorrectly narrow a selection the user has already made.
Flow 2: Segment IDs
The user assessed a saved segment (or a segment filter was active on the Identify page).
URL example:
/assess/supply-chain?module=modern-slavery&segments=seg-1URL example with filters:
/assess/supply-chain?module=modern-slavery&segments=seg-1&classification=Agriculture%2C+Forestry+and+Fishing&exposure=high|moderate-high&spendMin=10000What happens:
- Pre-filter pipeline (status-then-classification ordering). If
statusis active,OrganisationRepository.findIdsByStatusruns first — it walks the three-path-OR (transactions / reports / engagements) so SOURCING orgs (which have no SPENT transactions by definition) are included. Then, ifclassificationorsearchis active,OrganisationRepository.findIdsByFiltersnarrows that candidate set by industry/name. The result becomesscopedOrgIds. If any pre-filter returns zero IDs, an empty assessment is returned immediately. Order matters: running classification first would prune SOURCING orgs before status could include them. - Transactions fetched via
findBySegmentForFootprint(segmentIds, ..., scopedOrgIds)— the segment's transactions, optionally narrowed to the pre-filtered orgs. categoryapplied as a transaction-level filter during the fetch.- Two-pass exposure/spend filter runs after footprint calculation:
- First pass:
CalculateFootprintByKeyaggregates spend and risk per org - Orgs not matching
exposureorspendMin/spendMaxare removed - Transactions are re-filtered in memory for only the matching orgs
- First pass:
- Org metadata is filtered to orgs present in the footprint results (
byOrganisationfrom ClickHouse), then further restricted to the scoped org set if applicable. Engagement data is fetched in parallel (step 2), filtered to the same scoped org set, and returned alongside org metadata. A fetch failure for engagements is swallowed gracefully and returns an empty list.
Rationale: A segment defines a broad scope (e.g. "all tier-1 suppliers"), but the user may still want to filter within it by industry, risk level, or spend. Filters are meaningful here.
Flow 3: No Scope (Assess All)
The user navigated directly to the assessment without selecting any orgs or segments — assessing the entire account.
URL example:
/assess/supply-chain?module=modern-slaveryURL example with filters:
/assess/supply-chain?module=modern-slavery&classification=Mining&exposure=high&spendMin=50000&spendMax=500000What happens:
- Pre-filter pipeline (status-then-classification ordering). If
statusis active,OrganisationRepository.findIdsByStatusruns first — it walks the three-path-OR (transactions / reports / engagements) so SOURCING orgs (which have no SPENT transactions by definition) are included. Then, ifclassificationorsearchis active,OrganisationRepository.findIdsByFiltersnarrows that candidate set by industry/name. The result becomesscopedOrgIds. If any pre-filter returns zero IDs, an empty assessment is returned immediately. Order matters: running classification first would prune SOURCING orgs before status could include them. - Transactions fetched via:
findByOrganisationsForFootprint(scopedOrgIds)if classification/search produced IDsfindByAccountForFootprint(accountId)if no pre-filter was active
categoryapplied as a transaction-level filter during the fetch.- Two-pass exposure/spend filter runs (same as Flow 2).
- Org metadata is filtered to orgs present in the footprint results (
byOrganisationfrom ClickHouse), then further restricted to the scoped org set if applicable. Engagement data is fetched and filtered the same way.
Rationale: No explicit selection means the user wants a broad view, so all filters are respected to let them slice the full account data.
Filter Reference
| Filter | Applied at | Skipped when |
|---|---|---|
classification | Pre-filter (Neo4j org query) | organisationIds provided |
search | Pre-filter (Neo4j org name match) | organisationIds provided |
category | Transaction fetch (all flows) | Never skipped |
exposure | Two-pass (post-footprint calc) | organisationIds provided |
spendMin / spendMax | Two-pass (post-footprint calc) | organisationIds provided |
status | Pre-filter (Neo4j org query + sourcingStatus resolver) | organisationIds provided |
URL Encoding Notes
Some filter values require pipe (|) separators instead of commas, because the values themselves may contain commas (e.g. "Agriculture, Forestry and Fishing"):
| Param | Separator | Example |
|---|---|---|
organisations | , | org-1,org-2 |
segments | , | seg-1,seg-2 |
classification | | | Mining|Agriculture, Forestry and Fishing |
category | | | Food|Tech |
exposure | | | high|moderate-high |
status | , | ACTIVE,INACTIVE |
Invalid exposure and status values are silently dropped. An inverted spend range (spendMin > spendMax) drops both spend params.