Contract Loading

Status: deprecated. The background contract preload and its Redis cache described under Status quo are scheduled for removal. Do not build new features on them. New code loads contract data on demand, as described under Target architecture. This page documents the current state (verified against the code on 2026-10-12) for the team taking over KBP, and the migration plan in Migration roadmap.

Why a preload exists

KBP has always loaded full commodity contracts from EnWiCore through the legacy client interface (VertraegeFuerKundeHolen service consumer). That interface returns the contract list and then fetches tariffs, prices, meters, products and IMS data per contract. For customers with many contracts (for example housing companies, Wohnungswirtschaft), one load takes from several seconds up to minutes.

To hide that latency, KBP starts a full load in the background as soon as an agent opens a customer, stores the result in Redis and lets every feature read from that cache. Features that need contract details either wait for the preload or fall back to overview data.

Status quo (deprecated)

Building blocks

Building block Layer Role
App.vue → contractsStore.preloadContracts() Frontend Fires the preload once LoadCustomerInfo has succeeded and the Vertraege API entity is accessible
contractsStore.contractsPreloadFinished / contractsPreloadSuccessful Frontend (Pinia, features/vertraege/store/pinia.ts) Preload state that gates UI in several features (see Frontend gating)
GET api/vertraege/preload → VertraegeController.ContractsPreload Web Feature Vertraege_Preload; sends GetCustomerCommodityContractsRequest { FetchFromRemoteSystem = true } unless a load is already running
GetCustomerCommodityContractsCommand Web (Commands/, legacy MediatR) Orchestrator: coordinates loading flags, waits for a running preload, reads/writes the Redis contract cache, triggers the full EnWiCore load
CacheManager (ICacheManager) Web (Core/CacheManager.cs) Redis access for contracts, loading flags, contract annotations and account balances
GetCustomerContractsQuery → ContractRepository.GetCustomerContractsAsync Application → Infrastructure.Data The query every feature uses. Default includeDetails: true routes commodity contracts through GetCustomerCommodityContractsRequest, i.e. through the preload cache
GetCustomerCommodityContractsRequest (Infrastructure) → VertraegeFuerKundeHolenCommand Infrastructure (legacy service consumer) Full EnWiCore load with per-contract tariff, meter, product and IMS calls
GetContractAnnotationsCommand Web Loads contract annotations (Vertragshinweise) in parallel to the preload and caches them in Redis
EnwicoreContractRepository, EmobilityContractRepository (derive from Cacheable) Infrastructure.Data Already use the new lightweight endpoints, but also cache their results in Redis for 60 minutes

Diagram

Runtime: session start and preload

Diagram

Runtime: a feature reads contract details

Any GetCustomerContractsQuery with includeDetails: true (the default) ends in the orchestrator. What happens there depends on the Redis flags and the request options:

Diagram

Redis keys

All keys are scoped by {agent e-mail}_{tenant}_{default reference id}.

Key suffix Content TTL
_contracts_{contractId} Full Contract entity (written in chunks of 10) 60 min
_contracts_loading A full load is in progress 60 min
_contracts_loaded A full load has completed 60 min
_contracts_successfully_pre_loaded Marker for the multi-tab workaround (bug 827678); deliberately not reset by InvalidateCache 10 s
_contract_annotations* Contract annotations and their loading flags 60 min
_financial_account_balance* APA account balances (customer and per contract) 5 min
Cacheable entries EnWiCore contract overviews, e-mobility contracts 60 min

Write-back. When an agent changes contract data in KBP, the change is sent to EnWiCore. The cached contracts in Redis are not deleted and reloaded afterwards. Instead, KBP copies the new values into the cached contracts itself, so that other features see the change without a new full load:

Command What it writes into the cached contracts
UpdateAddressCommand Name and address of the deviating correspondence recipient, for contracts where the EnWiCore update succeeded
UpdateBankAccountDataCommand IBAN and account holder, for contracts where the EnWiCore update succeeded
EmailAdresseAendernCommand E-mail address of the deviating correspondence recipient
MobilfunknummerAendernCommand Mobile phone number
VertragAktualisierenCommand Replaces one contract with a fresh copy loaded from EnWiCore
GetCustomerContractCommand Not a write: when it loads single contracts from EnWiCore, it adds them to the cache or replaces cached ones, but only if the new data is complete (bug 645291)

Consequences:

  • The cache only matches EnWiCore if every command copies exactly what EnWiCore changed. Values that EnWiCore changes as a side effect are not updated in the cache.
  • Any new command that changes contract data must also update the cache. Otherwise features show the old values until the cache expires (60 minutes) or the next session start.
  • Changes made outside KBP never reach the cache.

Invalidation. None of the upstream systems KBP consumes (EnWiCore, APA and others) send cache headers or change notifications. KBP therefore never knows whether cached data is still current. It relies on fixed TTLs and on clearing the cache at points where a change is likely:

  • EntryPointController.LoadCustomerInfo clears the customer’s cache on every session start.
  • KBP’s own write commands patch the cache (see Write-back above).
  • For APA account balances, KontodatenController deletes all cached balances of the customer whenever api/kontodaten is called with a contract number. The frontend makes that call after every change to APA data in KBP. As a side effect, the cache is also cleared when an agent only switches to another contract in Kontodaten. If the frontend stops making that call, KBP shows stale balances without any error.

Changes made outside KBP (directly in EnWiCore, APA or another system) become visible only when the TTL expires or the cache is cleared at one of these points.

Where the preloaded data is used

The contract overview endpoint already delivers these fields per contract (ContractOverview): contract number, sector, contract type, status / active flag, start date, delivery address and current tariff name. Everything else needs the full load. The tables below list, per use case, only the fields that are not in the overview — these are the reason the use case depends on the preload.

Claims management (Forderungsmanagement)

Claims management builds lists of selectable contracts for a task. It loads all contracts of the customer with details and filters them, because eligibility depends on detail data.

Use case (agent view) Code Detail data needed (not in overview) Why
Create debt collection task (Inkassoauftrag): contract selection with balances GetInkassoAuftragVertraegeQuery Meters Only contracts with at least one meter and active delivery are offered. Then one APA balance call per eligible contract (VertragssaldenLadenCommand)
Create disconnection task (Sperrauftrag): contract selection GetSperrauftragVertraegeQuery, SperrauftragVertragDto Meters, network operator, market location Filter: not water, meter present, delivery active. The list shows market location, network operator and meter number per contract
Create research task: contract selection GetResearchTaskContractsQuery Meters Filter: meter present, delivery active
Forced-access lawsuits and legal files (Zutrittsklagen, Akte) ZutrittsklagenLadenCommand Contract notes (ForcedAccess, File) Marks every contract that is under a forced-access lawsuit or has a legal file
Disconnection process version (V1/V2) SperrProcessVersionResolver — (sector only) Uses the full query today, but the overview is sufficient
Task lists, triggering events, delimitation tasks, avoidance agreements InkassoController, SperrInkassoController, SperrenEntsperrenController, ContractDelimitationTaskController, AvoidanceAgreementController — (contract number, type, status) Use the full query today, but the overview is sufficient. Two controllers block on it via TasksUtils.RunSync

Account data (Kontodaten)

Kontodaten combines APA account data with EnWiCore contract data. The cross-contract overview takes its contract list from APA (balances per contract); the contract selection is built from EnWiCore contracts. The UI waits for the preload before contracts can be opened.

Use case (agent view) Code Detail data needed (not in overview) Why
Contract selection (Vertragsauswahl) on the account data page GetKontodatenQuery Payment type (direct debit / self-payer), next billing date (RotationEnd), judicial dunning procedure, business-customer flag, products Shown per contract in the selection. Products only serve to detect incomplete cached contracts, which are then reloaded in batches of 50
Warning “customer handled by legal department” GetKontodatenQuery Contract notes (ForcedAccess) of all contracts Customer-level warning derived from any contract
Cross-contract account overview (Kundenkontoübersicht) GetKundenkontoUebersichtQuery Tariffs Flexible-tariff check per contract, only if the customer has at most MaxAnzahlVertraegeFuerStandardApaDatenabruf contracts
Payments and receipts: contract pre-selection ZahlungenUndBelegeVertragsvorauswahl.vue — Needs only contract numbers, but the button stays disabled until the preload succeeded
Account balances VertragssaldenLadenCommand, GetCustomerFinancialAccountOverviewCommand — APA data, cached in Redis for 5 minutes; not part of the contract preload

Frontend gating

Component Flag Effect
kontodaten/views/UebergreifendeUebersicht.vue contractsPreloadFinished, contractsPreloadSuccessful Reloads the account overview when the preload finishes; contract rows cannot be opened until the preload succeeded
kontodaten/views/UebergreifendeZahlungenUndBelege.vue contractsPreloadFinished Shows a loading state and hides the contract selection until the preload finishes
kontodaten/components/kundenkonto/ZahlungenUndBelegeVertragsvorauswahl.vue contractsPreloadSuccessful Contract pre-selection button disabled, with tooltip
auftragserfassung/views/Auftragserfassung.vue contractsPreloadSuccessful Loads existing-customer data (Bestandskundendaten) only after a successful preload
shared/master-data-cleanup/store/useLoadingStates.ts contractsPreloadFinished Master data cleanup dialog waits for the preload
shared/components/SectorIcon.vue contractsPreloadSuccessful Shows “unknown business area” instead of “unknown contract type” while not loaded
kontodaten/views/Kontodaten.vue, vertraege/views/VertragDetails.vue contractsPreloadSuccessful Declared but not used for rendering

Other consumers of contract details

Tariff change offers (GetTariffChangeOffersCommand: meters, tariffs, market location), warnings (GetWarnhinweiseQuery), payment data (GetPaymentDataQuery: bank account), owner management (OwnerController, UpdateOwnerCommand: market location), permissions, PIN phone numbers, contact and address data, paper option cancellation and order capture. Overview-only pages (contract list, contract count, relocations, NBA, tariff change entry, self-service) already use GetCustomerContractOverviewsQuery and do not depend on the preload.

Known problems

  • Latency is only hidden, not removed. Features that need details still wait — through the orchestrator’s polling and wait loops on a request thread — until the full load is done.
  • Silent degradation. With reloadDetailsFromSourceIfMissing: false the orchestrator returns overview data while the preload is still running. Detail fields (tariffs, meters, notes) are then empty without any error.
  • Multi-tab race. Opening the same customer in two tabs clears the cache of the first; the short-lived contracts_successfully_pre_loaded flag forces a reload (bug 827678).
  • Cached failures. Both Cacheable repositories can cache an upstream failure as “no contracts” for 60 minutes (bug audit 2026-09-08, finding H-05).
  • Overfetching and coupling. One Contract entity carries tariffs, prices, meters, notes, products and documents for all features; every feature pays for all of it.
  • Blocking calls. TasksUtils.RunSync around contract loading in two claims-management controllers.

Target architecture

The target removes the preload, the orchestrator and the Redis contract cache. Each feature calls the query that returns exactly what it needs:

  • Contract lists come from the lightweight EnWiCore endpoint getContractOverviewsByCustomerId (optional current tariff name) via GetCustomerContractOverviewsQuery. The endpoint supports paging (limit, offset), but KBP currently requests all contracts in one call (limit = int.MaxValue).
  • Contract details come from domain-based application queries — tariffs, prices, notes, flexible tariff flag and working price type, meter, documents (Endia), products — that are independent of each other and return a ContractDetailsNew entity (final name to be decided).
  • Controllers and frontend contracts stay unchanged during migration: a combined DTO in the Web project replaces the legacy Contract + ContractDetails shape.
  • No preload state in the frontend. Views show their own loading state per request.

Diagram

Diagram

Impact on claims management and Kontodaten

Without a cache, loading details for all contracts on every request is as slow as the preload itself. The features therefore have to change when they need details, not only where they get them from. Three patterns are possible and can be combined:

Pattern Idea Effect on the agent Dependency
A. Contract pre-selection Show the overview list first (fast, all contracts). The agent selects the contract(s) to work on. Details are loaded only for the selection One extra step; detail columns move from the list to a detail or confirmation step. Ineligible contracts can only be rejected after selection None — KBP only
B. Lazy enrichment Render the overview list immediately and fill detail columns per row in the background, with limited parallelism No extra step; columns and eligibility appear progressively. Still N calls, so slow for large customers None — KBP only
C. Richer upstream endpoint EnWiCore adds the needed flags to the overview (e.g. has active meter, forced-access note, payment type) or offers customer-level filters No change for the agent; fastest EnWiCore change request

Pattern A is the default proposal: it removes the dependency on caches and the N calls, and many KBP processes already work on one selected contract. Pattern C is needed where information must be visible before selection, for example legal warnings.

Diagram

Claims management

Use case Proposal without preload What changes for the feature
Debt collection task: contract selection A: overview list pre-filtered on active commodity contracts; meter check and APA balance only for selected contracts Balances are no longer shown for all contracts up front. Contracts without a meter appear in the list and are rejected after selection. Check in the spike whether “active” already implies “has meter” — then the filter needs no detail data at all
Disconnection task: contract selection A: same pre-filter plus “not water”; market location, network operator and meter number loaded for the selection These three columns move from the selection list to the confirmation step
Research task: contract selection A: as debt collection task, without balances Same as debt collection task
Forced-access lawsuits and legal files C: customer-level indicator from EnWiCore, or B: notes query per contract Needed before selection (legal risk). With B the indicators appear progressively; with A they would only be visible after selection, which is likely not acceptable
Disconnection process version Overview Sector None
Task lists, events, delimitation tasks, avoidance agreements Overview query None; also removes TasksUtils.RunSync

Kontodaten

Kontodaten already has the natural pre-selection: APA returns the contracts with balances, and most pages work on one selected contract (/kontodaten/:vertragsnummer).

Use case Proposal without preload What changes for the feature
Contract selection APA list + overview fields for every contract; payment type, next billing date, judicial dunning and business-customer flag loaded only for the selected contract The selection list shows fewer columns; these values move to the header of the selected contract. Check in spike 1423738 whether APA already provides payment type or dunning state
Legal-department warning C: customer-level indicator, or notes query in the background (B) Same open question as forced-access lawsuits in claims management
Flexible-tariff check in the account overview Tariff query only for the selected contract, or B for small customers as today Marker appears after selection or progressively instead of with the first render
Payments and receipts pre-selection Overview list Button enabled immediately
UI gating Remove contractsPreloadFinished / contractsPreloadSuccessful Each view shows its own loading state

Open questions for the spikes

  • Which detail fields does the business really need before selection? Everything else can move behind a pre-selection.
  • Can EnWiCore extend the overview or offer customer-level flags (active meter, forced-access and legal-file notes, payment type)? This decides between pattern B and C for legal warnings.
  • Does APA already deliver payment type and dunning state, so that Kontodaten does not need EnWiCore details for its selection list?
  • UX changes (pre-selection step, moved columns) need agreement with the product owner and the claims-management and account-data users.

Migration roadmap

Parent: 1341022 — KBP: 2026 Technical maintenance. States as of 2026-10-12.

Step Work item Content State
1 1264317 Integrate getContractOverviewsByCustomerId; switch overview pages to it Done
2 1423721 Split the monolithic contract-details loading into domain-based queries; introduce combined DTO (CMDM and Kontodaten out of scope) New
3.1 1423731 Spike: move every feature except CMDM and Kontodaten to the new queries; create one story per feature In Progress
4.1 / 4.2 1423734, 1423735 Spike and refactoring for CMDM New
5.1 / 5.2 1423738, 1423740 Spike and refactoring for Kontodaten New
6 1423742 Verify ContractNew / ContractDetailsNew is the only access path New
7 1423744 Remove ContractsPreload and the Redis contract caching, including both Cacheable contract repositories New
8 1423746 Delete legacy Contract and ContractDetails entities New
9 1423748 Rename ContractNew / ContractDetailsNew to final names New

Diagram

Step 1 states that preloading stops, but the frontend still triggers the preload on every session start and the endpoint Vertraege_Preload is active. Removing it is part of step 7; until then the preload must keep working, because claims management and Kontodaten still depend on it.