Skip to main content

How FNP loads and becomes a jobsheet price

FNP has three separate stages: retrieving provider data, saving descriptions against an enquiry, and selecting/calculating the jobsheet price. An upstream lookup can succeed while a later band filter or UI check leaves the supplier with no selectable menu. Start with Configuration for enablement and Providers and data for imports.

1. Enquiry creation and vehicle context

The enquiry records its vehicle, customer, supplier, rate card and pricing terms. Creation paths copy the relevant supplier fnp object and customer fleetLevel into the enquiry. DI network rate cards use the supplier service unit's diNetworkTerms; other rate cards use the customer's supplier service-unit terms.

Creation pathWhere the FNP lookup runs
Supplier walk-inInline in the callable walk-in creation service.
API walk-inInline in the callable API walk-in creation service.
Other enquiriesrepair-serve-functions' onEnquiryCreatedHandler, triggered by creation of accounts/{accountId}/serviceUnits/{serviceUnitId}/enquiries/{enquiryId}.

The background handler explicitly returns for enquiry.enquirySource === WALK_IN, because the normal walk-in creation services do the loading themselves. For other enquiries, it performs/resolves the VRM lookup, updates the enquiry's lookup information, extracts vehicle context and calls saveFnpProviderJobDescriptions.

Vehicle make prefers vrmLookup.make.label, falling back to the stored make. General VIN extraction prefers the lookup VIN, falling back to the stored VIN. VWG specifically uses the VRM lookup VIN; a VIN only on stock does not satisfy its request. At the provider boundary the manufacturer is trimmed and uppercased; a vehicle type of CAR stays CAR, while other values are passed as LCV. Check missing or unexpected vehicle type when resolving VWG credentials.

Sources: creation trigger, inline loader, and VIN extraction.

2. Select an eligible provider

The saver checks MODULUS, OEC and OEM, and uses the first matching provider. For a provider to match:

  1. The applicable supplier terms must exist.
  2. The integration check must pass. Current supported manufacturer/provider pairs explicitly bypass the customer integration enabled check; see the configuration exception.
  3. The supplier terms must have fnp.isEnabled === true.
  4. The manufacturer list must map this make to this provider.
  5. A normalized supplier franchise must match the vehicle manufacturer.

An OEC lookup requires VIN. The inline saver also skips VWG when its selected lookup VIN is missing. In the background saver, the early guard checks the general vinToUse even though the VWG request uses vinFromVrmLookup; a stock-only VIN can therefore pass that guard but still produce an empty VWG response. Inspect the actual provider request, not just the skip log.

Source: supplier/provider checks and background saver.

3. Retrieve and shape the provider data

The background saver calls POST /api/v1/fnp/getFnpDescriptions in the callable service. The inline walk-in saver invokes the same provider service directly. This is the upstream retrieval operation, distinct from the enquiry's saved-description endpoint.

Provider pathLookupShaped result
Modulusmodulus_vehicle by VRM when supplied, otherwise VIN; first match's vehicle_idmodulus_menu_priceCombine short descriptions, group rows by description, parse labour time and part prices/quantities; type: REPAIR.
Vauxhall/OECoec_menu_price by VINMenu description, labour time, specific/generic parts and arrays of prices for each fleet group; type: REPAIR.
Ford/OECPostgreSQL get_vehicle_descriptions_and_parts(vin)Group descriptions, attach parts and complete_code, derive price groups from region codes; type: REPAIR.
Kia/OEMJoin vehicle to menu_price on vehicle_id, filter by VINThe same grouping/parts transformation used by Modulus; type: REPAIR.
VWG/OEMLive packages by VIN, with imported part/fluid lookupsOne description per package ID, package category, labour time and parts/fluids; type: MAINTENANCE.

Modulus does not retry with VIN after an unsuccessful VRM lookup when VRM was supplied. Kia's runtime lookup needs VIN even though its saver does not have OEC's explicit early VIN guard.

For OEC, specific parts are included when number, name and quantity are present; generic parts use - for the part number. Price columns are grouped into service4Fleet, service4FleetPlatinumArval and lexAutoBand. This stage retains multiple bands; the enquiry read later chooses the relevant band.

Sources: provider dispatch, SQL lookups, and description shaping.

VWG request and enrichment

VWG needs accounts/{accountId}/serviceUnits/{serviceUnitId}.ppsoPartnerKey, a matching manufacturer/vehicle-type credential entry from VWG_CLIENT_CREDENTIALS, and a PPSO brand code from the manufacturer list. The outgoing partner key is the supplier's ppsoPartnerKey followed by the brand code. A partner key present in the credentials object does not replace the supplier field used by this path.

Before the API call, ViSN checks vwg_blacklisted_vin. A blacklisted VIN returns no packages. The API client obtains a token, calls getPackagesByVIN and logs request/response or error information. The reviewed callable configuration points to a VWG QA host; verify the deployed configuration for the environment under investigation instead of assuming a ViSN production deployment implies a VWG production endpoint.

The response must contain carPackageSet.packageSet.package. ViSN drops packages without brandPackageId, looks up unique part/fluid codes in Cloud SQL, and builds descriptions:

  • Labour is the sum of LABOUR_CODE amounts divided by 100, rounded to two decimal places. These provider units must not be treated as ordinary clock minutes divided by 60.
  • PART_CODE rows get their price from vwg_parts_price; no match or invalid price produces null.
  • FLUID_CODE rows get their description from vwg_fluid_details, falling back to the code. Their price is deliberately null for supplier entry.
  • hasIncompletePartInfo is true when any non-fluid part has no price. A fluid with no price does not by itself set this flag.
  • Enrichment errors are logged and empty lookup maps are used. Packages can therefore load while being incomplete and unselectable.

Sources: VWG retrieval, credential/partner resolution, package mapping, and API configuration.

4. Save the enquiry's descriptions

In the default Firestore database, under its ancillary collection, the saver writes:

ancillary/enquiries/descriptions/{enquiryId}/{provider}/descriptions

provider = modulus | oec | oem
document field = descriptions

The lowercase provider is a collection name; the final descriptions is a document. This data is not the jobsheet itself and is not stored in Cloud SQL per enquiry. Do not confuse this collection with the separate named ancillary database used for VWG API logs.

The write uses merge semantics for the document but assigns the complete descriptions field. A rerun can replace that array, including with an empty array. Missing eligibility may return without a write; successful lookup with zero matching rows can write [].

The background saver catches errors from the provider call, logs error while getting descriptions, substitutes [], then can log succeeded after saving it. That success log confirms the write path finished, not that prices were found. Outer saver errors are rethrown. The inline callable saver does not have the same inner catch-and-empty wrapper. Do not assume every failure has the same retry behaviour across these paths.

Sources: ancillary reference and background save/error handling.

5. Open the enquiry and filter saved menus

The supplier UI calls POST /api/v1/supplier/checkIfSupplierHasFnpEnabled. It checks the live relevant network terms' FNP flag and franchise. If eligible, the jobsheet requests POST /api/v1/enquiry/getFnpJobDescriptions with:

{
"enquiryId": "<enquiry ID>",
"manufacturer": "VAUXHALL",
"fleetLevel": { "type": "LEX_AUTO", "label": "Lex Auto" },
"fnp": { "isEnabled": true, "fnpBand": "Band 2 LA3" }
}

The fleetLevel and fnp values come from the enquiry. The endpoint maps the make to a provider, reads the saved descriptions and attaches descriptionSource. It does not fetch new SQL menus or call VWG. Missing/empty data returns { descriptions: [] } inside the API's data response.

OEC and Ford filtering

fleetLevel.typeSaved cost groupMatching rule
SERVICE_4_FLEETservice4FleetNumeric band only.
PLATINUM_SERVICE_4_FLEETservice4FleetPlatinumArvalNumeric band only.
LEX_AUTOlexAutoBandBand and LA suffix; regions 22 and 23 match band only.

Band 2 LA3 means band 2 and LA 3. Parsing Band 5 (M25) LA2 ignores the 25 in the label, giving band 5 and LA 2. NONE, missing/unrecognized fleet level, missing band, old numeric bands or no matching cost entry leave no descriptions. Modulus and OEM bypass this band filter.

For Vauxhall the matching cost becomes a scalar price. For an exact manufacturer === FORD request, the backend retains multiple matching costs, removes duplicates with the same band and price, and returns an array of { completeCode, price } options. Normalizing the provider selection does not normalize this later exact Ford comparison; check the actual manufacturer payload when diagnosing Ford behaviour.

Current Ford/Lex Auto caveat: the Ford structurer asks for constant group lexAutoBand, while the band constants use lexAuto. This can leave costs.lexAutoBand empty before filtering. The filter's support for regions 22/23 does not prove that Ford Lex Auto prices were populated. Treat this as an implementation issue when source prices exist but that group is empty.

Sources: enquiry service, band/fleet filter, Ford shaping, and band constants. See FNP Bands for the full price-column mapping.

6. Build the jobsheet price

Provider configuration determines how selection becomes a jobline:

ProviderJobline treatmentPrice basis
OEC, including FordCatalogue; aggregate price availableSelected band/region menu price. Labour time is shown but the FNP labour rate is initialized to zero to avoid adding a second labour charge.
ModulusNon-catalogue; parts selectionImported labour time at the applicable labour rate, plus selected imported parts/fluids.
OEM, including Kia and VWGNon-catalogue; parts selectionProvider labour time at the applicable rate, plus parts/fluids and permitted discounts. Kia has a dedicated FNP labour-rate selection.

The provider metadata declares OEC parts as READ_ONLY, but current parts-dialog rendering also depends on the job's edit/draft state and manufacturer. The metadata alone does not guarantee a read-only dialog. OEC's parts tooltip omits individual prices. Ford has a special parts/fixed-price selection flow: if it supplies one fixed price, the UI can select it automatically; multiple prices require an explicit choice, with the related completeCode retained. An enquiry snapshot with fnp.isEnabled false also prevents the UI helper from extracting an OEC price even if live supplier eligibility now passes.

For non-aggregate menus, the starting calculation is:

labour charge = provider labour hours × applicable hourly rate
part line = quantity × unit price, with applicable discount/goodwill handling
job subtotal = labour charge + selected parts/fluids

The actual jobsheet helpers handle money rounding, goodwill and subsequent edits. For aggregate FNP catalogue lines, the subtotal helper adds the selected menu price to the parts/labour amounts; ordinary FNP labour initialization uses a zero rate. Do not manually add the quoted labour charge again to a fixed-price package.

Kia's rate is read from config/settings.kiaFnpLabourRate and returned in the enquiry's aggregated pricing configuration. See Kia FNP labour-rate setup when menus are available but the labour charge is wrong.

All provider configs set skipPartsDiscountRule, but that does not mean discounts are impossible. The Kia/VWG parts dialog has explicit percentage-discount controls, defaults agreed parts discount for non-fluids, recalculates net amounts and can require a reason for a discount below the agreement. VWG fluid unit prices are entered by the supplier; fluids do not use the non-fluid discount field. Missing non-fluid VWG prices instead mark the package incomplete and withhold selection.

Provider prices are parsed as major-unit numbers; jobsheet monetary objects also carry integer minor-unit value fields. For example, a £100 menu price is converted to 10,000 minor units in the subtotal helper. These examples describe the job subtotal; tax, invoice/export and other commercial rules remain separate downstream calculations.

Sources: provider UI config, parts-dialog editability, labour initialization, subtotal calculation, and parts/discount UI.

Refresh, reassignment and existing enquiries

Reopening the enquiry reruns UI checks and reads its saved descriptions. It does not regenerate them, rewrite enquiry.fnp or enquiry.fleetLevel, or reprice saved joblines. View More reveals other description sources; it is not an FNP retry button.

Supplier reassignment creates a new enquiry ID and snapshots the target supplier's terms and fleet level. Investigate the new ID's cache. A new non-walk-in supplier enquiry follows the creation trigger. The reviewed reassignment helper preserves the original enquiry source, however, and contains neither an inline FNP save nor a copy of the old FNP cache. Consequently, a reassigned WALK_IN can hit the trigger's walk-in skip without regenerating descriptions. Escalate that branch for engineering review rather than using repeated reassignment as a refresh operation.

Use the debugging runbook before deciding whether an incident requires corrected terms, an import, provider remediation or targeted regeneration of the enquiry's saved menu.

Source: reassignment helper.