Skip to main content

Infor CPQ — configuring a part that does not exist yet

Audience: developers & AI agents · Scope: the CPQ branch of BOM resolution — the wire contract, the product/question mapping, and its failure modes · Last reviewed: 2026-07-25

TL;DR — CPQ (Configure-Price-Quote) is Infor's rule engine: hand it a ruleset and a set of option-list values and it configures a part and returns its manufacturing data, even for a combination that has never been built. It is the fallback when the slash method finds no matching item. Note the surprise: CPQ is a SOAP/WSDL service with its own endpoint and settings — it is neither the IDO REST API nor the MSSQL connection described in the Infor hub. It is a third access path.

What it is & why it exists

Beta-Calco's configurator lets a customer combine options in ways that no Infor item covers. The slash method can only find BOMs for builds Infor already knows. CPQ closes that gap: it evaluates the same rules the plant uses, produces the component list for the requested configuration, and hands it back.

The trade-off is cost and fragility. A CPQ call is a two-step SOAP round trip against a live rule engine, and it fails whenever the website's answer codes and the CPQ ruleset's option lists have drifted apart — which is exactly what the failure register exists to surface.

How it fits together

configuration code segments (from the quote line's selections)


getRapidOptions() ← per question: is it a CPQ question?
│ apply value overrides, collect them

RapidOption[] = [{VariableName: "Luminaire", ValueExpression: "PD4"}, …]


┌──────────────────────────────────────────────┐
│ nusoap_client(cpq_endpoint_url, 'wsdl') │
│ │
│ 1. Configure(…) → Result must be │
│ "Success" │
│ 2. LoadMfgData(…) → BomComponentDto[] │
└──────────────────┬───────────────────────────┘
│ Part Number + Quantity per component

BcCsiItemMaterialsRequest[] ("build me these, this many")


getMaterialsForItems() → the SAME 5-level MSSQL explosion
│ used by the cache

flat BOM for the position
FileRole
CUST/models/bcquotes.phpgetItemsFromCpq()the SOAP call — builds the payload, both calls, parses BomComponentDto
CUST/models/bcquotes.phpgetRapidOptions()maps questions to CPQ option-list values and applies overrides
CUST/models/bcquotes.phpgetMaterialsViaCpq()glue: CPQ → item requests → explosion
CUST/models/bcquotes.phpgetCpqMaterialsFromCode()the same thing starting from a bare configuration code (used by the REST API)
CUST/models/csimaterials.phpthe explosion of whatever CPQ returns
system_overrides/BcCpqConfigurationException.phpcarries the ruleset, the options sent, the overrides applied and the CPQ detail id
system_overrides/ObserverBcCpqFails.phpCloudWatch counters when an operator marks a fail report fixed/unfixable
CUST/controllers/adminbccpqbom.php + views/adminbccpqbom/the CPQ Flat BOM Calculator screen
libs/vendor (nusoap)the SOAP client, loaded via the customization autoloader

Integrations & contracts

Two calls, in order, against cpq_endpoint_url (a WSDL):

1. Configure — asks CPQ to configure the part.

'inputParameters' => [
'Application' => ['Instance' =>, 'Name' =>], // cpq_instance, cpq_app_name
'Part' => ['Namespace' =>, 'Name' =>], // the product's ruleset
'Mode' => 'InteractiveRuleset',
'Profile' =>, // cpq_profile
'HeaderDetail' => ['HeaderId' => 'BOM Fetching', 'DetailId' => 'Detail-<random>'],
'IntegrationParameters' => [ /* SiteID, SourceID, SourceLineSuffix, Quantity=1, UOM=EA, CreateUser=false */ ],
'RapidOptions' => ['RapidOption' => $rapidOptions],
]

Anything other than ConfigureResult.Result === 'Success' raises BcCpqConfigurationException carrying CPQ's own message.

2. LoadMfgData — retrieves the manufacturing data for the configuration just created, correlated by the same HeaderDetail. The response's BomComponentDto list is walked; each component's Attributes are scanned for three names — Part Number, Quantity and jobmatl.u_m — and components with a null ParentId (the configured part itself) are skipped. A component only survives if it has a part number and a quantity greater than zero.

Session correlation matters. DetailId is 'Detail-'.rand(0, 1000000) and ties the two calls together; it is also stored on failure reports as the CPQ Detail ID, which is how someone debugging in Infor finds the same session. Mode is fixed to InteractiveRuleset (the code notes PostConfigurationRuleset and Constraints as the other options), Quantity is always 1 and UOM always EA — quantities are applied later, during explosion.

CPQ does not return a BOM. It returns the top-level component list. Those part numbers are then exploded by the same five-level MSSQL query the cache uses — so a CPQ answer still depends on the Infor database connection being up.

How a BOM is exploded

Shared by CPQ and the cache, ConfigboxModelCsimaterials::fetchMaterialsDataViaDb() runs a single query joining itemjobjobmatlitem five times over, producing one wide "matrix" row per leaf path. Two conditions carry the business rules:

  • job.type = 'S' AND (job.suffix = '0' OR job.suffix > 1) — standard jobs only,
  • itemrev1..5.job IS NULL — rows with an item revision at any level are excluded.

The wide rows are then flattened: for each row the code walks from the deepest non-null level upward, emitting one BcCsiItemMaterial per level, multiplying quantities down the path, and de-duplicating on a signature (the md5 of the path). Results are sorted by path.

On a query failure the method reconnects and retries itself exactly once, detected by inspecting the call stack — a neat trick, and worth knowing about before you refactor the method name.

Product & question configuration

CPQ needs the site's answers expressed in its vocabulary. That mapping is admin data, not code.

On the product (product edit screen → Infor CPQ Integration; stored in #__configbox_external_product_appends, migration 0.3.4.php):

FieldLabelNotes
cpq_has_configurationProduct has a CPQ configurationthe gate — off means the product never reaches the CPQ rung
cpq_ruleset_namespaceCPQ Ruleset Namespacedefaults to Default
cpq_ruleset_nameCPQ Ruleset Nametogether these form the Part sent to CPQ

On the question (question edit screen; stored in #__configbox_external_element_appends):

FieldLabelNotes
cpq_makes_selectionQuestion makes a CPQ selectiononly these questions contribute a RapidOption
cpq_variable_nameCPQ Option List Namewithout the namespace — Luminaire, not BLOCK-PD.Luminaire
cpq_value_overrideCPQ Value Overridesend this instead of the real answer code, for answers the ruleset cannot handle
cpq_override_conditionOverride only if one of these codes are selectedcomma-separated codes that trigger the override; blank = always override
cpq_override_notesOverride notesdocumentation for whoever inherits the override

Every override applied is recorded on the position in bom_overrides (variable, original, override) and is what makes bom_is_exact_match '0' for a CPQ answer. An override is a known inaccuracy — the BOM returned is for a slightly different configuration than the customer chose.

The override condition is a substring test. The check is strstr($question->cpq_override_condition, $code) — it asks whether the selected code appears anywhere in the condition string, not whether it is one of the comma-separated entries. A short code can therefore match a longer unrelated entry in the list. Keep condition lists to codes that are not substrings of one another.

Admin settings

Custom Settings → Infor CPQ Integration (all added by migrations 0.3.6, 0.3.9, 0.3.10):

SettingLabelRequiredEffect if blank
cpq_endpoint_urlCPQ Integration API Endpoint URLyesthe SOAP client cannot be constructed — every CPQ attempt becomes an exception failure
cpq_instanceCPQ Instanceyessent as Application.Instance; CPQ rejects the call
cpq_app_nameCPQ Application Nameyessent as Application.Name; CPQ rejects the call
cpq_profileCPQ Profileyessent as Profile; CPQ rejects the call
cpq_site_idCPQ Site IDyessent as the SiteID integration parameter
cpq_fail_reporting_emailEmail address for reporting fails because of selectionsnono mail for configuration-class failures (reports are still filed)
cpq_fail_reporting_email_errorsEmail address for reporting fails because of system errorsnono mail for exception-class failures

These are entirely separate from the syteline_* (IDO REST) and infor_db_* (MSSQL) groups — see the Infor hub. CPQ can be down while both of those are healthy.

The two failure keywords

bom_failure_keyword on the position, and failure_keyword on the register row:

KeywordRaised whenWho should look
configurationCPQ answered, but negatively — Result != 'Success', or LoadMfgData returned no BomComponentDto. The rules could not build this combination.product/ruleset owners — usually an option list or an answer code has drifted
exceptionanything else — SOAP transport failure, WSDL unreachable, malformed responsedevelopers/IT — an infrastructure or contract problem

They are routed to different email addresses on purpose. Both file a report into #__configbox_external_cpq_bom_fails including a rendered snapshot of the BOM-status screen, so the evidence survives even after the position is recalculated.

The interactive tools

CPQ Flat BOM Calculator (adminbccpqbom) takes configuration codes rather than item names and runs getCpqMaterialsFromCode(): it resolves the first code segment to a product via the linear price list (ConfigboxModelAdminbcpricelistlinear::getProductIdFromCatRef()), matches the remaining segments against the product's questions and answer SKUs, then calls CPQ. It shows the configured endpoint so an operator can see which environment they are hitting.

Compare with the two item-name tools in bom-cache.md — cached versus fresh.

Deployment runbook (manual steps)

  1. Run migrations (automatic on next page load): 0.3.4 (product/question fields), 0.3.6, 0.3.9, 0.3.10 (settings columns).
  2. Custom Settings → Infor CPQ Integration: set endpoint URL, instance, application name, profile and site id for this environment. Point non-production at a non-production CPQ.
  3. Set the two fail-reporting addresses, or leave blank while testing so you are not mailing people.
  4. Confirm outbound HTTPS to the CPQ host, and that libs/vendor (nusoap) is installed.
  5. Per product: tick Product has a CPQ configuration and fill in the ruleset namespace and name.
  6. Per question that drives a CPQ option list: tick Question makes a CPQ selection and set the option list name; add overrides only where the ruleset genuinely cannot take the real code.
  7. Add the cron entry for cb_pos_bom_send_report_notifications.php — see scheduled jobs.
  8. Smoke test: open the CPQ Flat BOM Calculator, enter a configuration code for a CPQ-enabled product, and confirm components come back. Then add a quote line for that product and confirm the position lands on done / cpq.

Turning it off: untick Product has a CPQ configuration on the product — that product then falls through to partial matching instead of erroring. Clearing cpq_endpoint_url disables CPQ globally but turns every attempt into an error position rather than skipping the rung.

Testing

No automated coverage — it needs a live CPQ instance. The CPQ Flat BOM Calculator is the manual test harness; the failure register is the regression signal. See testing/guide.md.

Gotchas & caveats

  • CPQ is a third Infor access path. Do not assume the syteline_* settings or the IDO REST client have anything to do with it. "Infor is up" says nothing about CPQ.
  • A CPQ product never falls back. See position-boms.md — the branch ends in finally { return; }.
  • CPQ still needs the MSSQL connection, because its part numbers are exploded locally. A VPN outage breaks CPQ-sourced BOMs too.
  • Overrides silently change the answer. They are recorded, not flagged; nothing warns the agent that the BOM is for a different configuration.
  • Quantity is hard-coded to 1 and UOM to EA in the integration parameters. Line quantity is applied during explosion, not at CPQ.
  • nusoap runs with setDebugLevel(9), which makes it accumulate debug output in memory on every call.

Possible follow-ups

  • Make the override condition an exact list-membership test rather than strstr().
  • Surface "this BOM used overrides" in the UI, not just in bom_overrides.
  • Reduce the nusoap debug level, or make it conditional on a debug setting.