> ## Documentation Index
> Fetch the complete documentation index at: https://docs.energy.nlead.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# Lieferantenrahmenvertrag

> Requesting the LRV from each Netzbetreiber, reading the answers, the deadlines

To supply a market location in a grid, the Lieferant needs a
**Lieferantenrahmenvertrag (LRV)** with its Netzbetreiber. The contract is
the BNetzA's standard (BK6-13-042 as amended by BK6-20-160), the same for
every VNB. It needs no written form: it is in force once the Lieferant
accepts it (§ 13 Nr. 1 LRV). Where Lieferant and BKV are the same company,
the Zuordnungsvereinbarung is part of it and needs no signature of its own
(§ 17 Nr. 1). The [Zuordnungsermächtigung](https://docs.energy.nlead.ch/setup/zuordnungsermaechtigung.md)
still goes separately by MaBiS.

The bridge asks each Netzbetreiber from the Lieferant's own mailbox. It
reads the answers and does what an answer allows. Everything else goes to
a person in the [inbox](https://docs.energy.nlead.ch/setup/cases.md). The code lives in `lieferant/lrv/`
and reads partner data through the same directory as the BKV
(`marktpartner/`).

## Two variants

| | `gv` — Generelle Versorgung | `dv` — Direktvermarktung |
| - | - | - |
| Market locations | Entnahme | Einspeisung |
| Basis | The standard LRV | The standard covers Entnahme only (§ 1 Nr. 3 LRV): the VNB has its own Einspeisevertrag, applies the LRV analogously, or needs no contract |
| First mail | Acceptance of the published contract in Textform, with the ZOV under § 17 Nr. 1 and the ZOE announced by MaBiS | The question which of the three applies; no acceptance |
| Attachments | Deckblatt, Kontaktdatenblatt Lieferant, Kontaktdatenblatt BKV | Both Kontaktdatenblätter |

There is one open Vorgang per Netzbetreiber and variant. A second request
for the same pair is skipped, and so is a `gv` request to a partner the
contract records already hold a Lieferantenrahmenvertrag with.

## Whom it goes to

The Netzbetreiber says this itself in the PARTIN it sends to the
Lieferant (PARTIN AHB 1.1, 37001, NAD contact blocks). The block for
*Rahmenverträge* (`Z11`) is sent to a Lieferant only, never to a BKV.
That is why the directory reads the Lieferant's datasheets first, then
the BKV's.

| Variant | Contact functions, first match wins |
| - | - |
| `gv` | `Z11` Rahmenverträge → `Z13` Wechselprozesse → `Z10` Übertragungsweg |
| `dv` | `Z11` → `Z16` Einspeiseprozesse → `Z13` → `Z10` |

The Vorgang records where the address came from: the function, the unit
and the datasheet (sender, receiver, reference, date). If no block yields
an address, nothing is sent. The Vorgang is `NO_CONTACT` and its case
waits for a person, who can either:

* name an address by hand (`email`, one partner per request), or
* allow the BDEW code list's address (`bdew_fallback`).

The bridge never guesses an address. The order of functions is
configurable (`LRV_KONTAKTFUNKTIONEN_GV` / `_DV`).

## Draft, release, send

**1. Request**

`POST /v1/process/lrv/anfragen` with the partners (MP-ID, Bilanzierungsgebiet
EIC or name) and the variant. Each request is drafted from the templates in
`data/lrv/` (German, Sie-Form). Every subject carries the token
`[LRV-<MP-ID>-<kurzid>]` that answers are attributed by.

**2. Release**

The draft waits as a proposal of the tool `lrv-senden` in the Vorgang's
case. Releasing it, in the inbox or by
`POST /v1/process/lrv/vorgaenge/{id}/freigeben`, sends it. With
`autopilot: true` the request goes out at once, and so do the later
answers built from the document pool and the reminder.

**3. Send**

The attachments are made at the moment of sending: the Kontaktdatenblätter
from the company's templates with the send date as *Stand*, and the
Deckblatt as a filled PDF. The mail leaves the Lieferant's mailbox and is
filed in its Sent folder. The exact bytes go to the archive. A `gv`
request sets the contract record to *angefragt*.

## Answers

The routine (or `POST /v1/process/lrv/poll`) reads the Lieferant's inbox.
An answer is attributed by, in this order:

1. the token in subject or text;
2. the Message-ID of one of our mails in `In-Reply-To` / `References`;
3. the sender's domain plus an MP-ID in the text, ours or the
   Netzbetreiber's, if that names exactly one open Vorgang.

A mail that cannot be attributed stays in the mailbox untouched. An
attributed mail is archived exactly as it arrived. It is then read by the
model (`CLASSIFIER_MODEL`, with up to three PDF attachments as documents)
into a fixed schema:

* `status`, `confidence`
* `requested_documents`, `provided_documents`
* `deadline`, `contact_person`, `portal_url`, `conditions`
* `summary_de`
* plus a verbatim `beleg`, and the `abweichungen` from the standard for a
  contract of the Netzbetreiber's own

The model only reads; the bridge decides. A reading leads to a step only
if all of the following hold:

* its confidence reaches `LRV_MIN_KONFIDENZ` (0.8);
* its quote stands in the partner's own words: above the quoted part, and
  not a sentence of our own mail;
* it names no condition that only a person may grant.

| Reading | What follows |
| - | - |
| `ACCEPTED` | Final. `gv`: the Lieferantenrahmenvertrag is recorded *aktiv*. A case **ZOE senden** opens for a person |
| `NO_CONTRACT_NEEDED` | Final. The case **ZOE senden** opens |
| `REJECTED` | Final for the process; its case goes to a person |
| `DOCS_REQUESTED` | If the [document pool](#document-pool) holds everything asked for, a reply with the files is drafted (sent at once on autopilot). Otherwise a person gets the case |
| `COUNTER_FORM` | The Netzbetreiber's PDF form is filled with company data and drafted for release, **always**, autopilot or not. The proposal names the fields left open |
| `EINSPEISE_CONTRACT_PROVIDED` | The contract is filed. The case lists its deviations from the standard and a person decides |
| `AUTO_REPLY` | Recorded, nothing else; the deadline runs on |
| `UNCLEAR`, low confidence, quote not found, no reading | The Vorgang goes to `UNCLEAR` and a person; event `lrv_eskaliert` |

A handled answer moves to the archive folder (`LF_IMAP_FOLDER_ARCHIVE`).
One that went to a person stays in the inbox, where it is read. An answer
to a Vorgang that is already final is archived and handed to a person, not
read again. The BKV's Erstkontakt reconciliation leaves mails
with an LRV token alone.

> [!WARNING]
>
> **Never automatic**, whatever an answer asks: a signature, a registration
> in a portal, a promise of prepayment or security (§ 11 LRV), a change to
> the contract. Each becomes a task for a person. The form filler never
> writes a signature, date, place-of-signing or contact-person field.

## Deadlines

The routine counts in Werktage of the market calendar: no weekends, no
holidays of any Land, not 24 or 31 December. It counts from the day our
last mail went out:

* after `LRV_NACHFASS_WERKTAGE` (5) without an answer: **one reminder**,
  with the attachments made anew, drafted for release or sent at once on
  autopilot;
* after `LRV_ESKALATION_WERKTAGE` (10): **ESKALIERT**. A person takes over,
  an unreleased reminder is dropped, and nothing more is sent. A later
  answer is still processed.

An auto-reply does not stop the clock; an answer does, and a reply of ours
starts it again.

The routine is the built-in agent **Lieferantenrahmenvertrag** (`lrv`),
which is off until it is enabled in the console's
[agents](https://docs.energy.nlead.ch/setup/agents.md). It runs only when the Lieferant's IMAP is
configured, and last in the orchestrator's round, so that a slow mailbox
never delays the day steps. It reads the inbox first and then looks at the
deadlines. A failed read stops the round (`lrv_fehler`), so a reminder is
never sent blind.

## Records

Each Vorgang is a state machine in the ledger `lrv_vorgaenge`:

* **Process states:** `ENTWURF`, `NO_CONTACT`, `GESENDET`, `NACHGEFASST`,
  `ESKALIERT`, `ABGEBROCHEN`.
* **Answer states:** the readings above.

Every transition is kept in the Vorgang's `verlauf` with:

* its moment;
* its trigger: a mail, the reading, a person, a deadline;
* its raw data.

The entries are hash-chained, so a changed entry breaks the chain.

Every mail, sent and received, is stored as raw bytes under
`lrv/<Vorgang>/` on the audit share. A file is written once and never
overwritten, and its SHA-256 is in the verlauf. Contracts and filled
forms lie under `lrv/<Vorgang>/dateien/`.
`GET /v1/process/lrv/vorgaenge/{id}` reports whether the chain and the
archive are intact.

> [!NOTE]
>
> This makes a change **detectable**, not impossible. Storage that cannot be
> changed at all (WORM, an immutability policy on the share) is a matter of
> the storage account, not of this code.

## Document pool

What a Netzbetreiber may ask for beyond the request is uploaded once per
kind:

* `handelsregisterauszug`
* `zertifikat`
* `edi_vereinbarung`
* `vollmacht`

Files are PDF, XLSX, DOCX or an image, up to 5 MB each. That limit keeps
the base64-encoded upload under the service's 8 MB limit on a request
body. A new upload replaces the current file, and the replaced ones are
kept.

In the console, the pool is under *Konfiguration → Lieferant*. Each kind
shows its current file with size, date, uploader and checksum, next to
*Hochladen* / *Ersetzen* and *Entfernen*. An upload takes effect at once,
without *Speichern*; a removal asks first. A script uses the same API:

```bash theme={null}
curl -X PUT "$BASE/v1/process/lrv/dokumente/handelsregisterauszug" \
  -H "X-API-Key: $PROCESS_KEY" -H "Content-Type: application/json" \
  -d "{\"dateiname\": \"HRA.pdf\", \"inhalt_base64\": \"$(base64 -w0 HRA.pdf)\"}"
```

The Kontaktdatenblätter are never in the pool: they are made from the
configuration (`PARTIN_COMPANY`, MP-IDs, EIC) whenever a mail goes out.
The Regelzone and the ÜNB of the Bilanzkreisvertrag come from
`HOME_CONTROL_AREA`; for n:lead that is Amprion.

## API

All endpoints use the process scope under `/v1/process/lrv`.

| Endpoint | What it does |
| - | - |
| `POST /anfragen` | Request: `partner[]`, `auspraegung`, `autopilot`, `dry_run`, `email`, `bdew_fallback` |
| `GET /vorgaenge` | Every Vorgang (`?status=`, `?mp_id=`) |
| `GET /vorgaenge/{id}` | One Vorgang with its verlauf, chain and archive check |
| `POST /vorgaenge/{id}/freigeben` | Send the draft |
| `POST /vorgaenge/{id}/aufloesen` | Close by hand: `ACCEPTED`, `NO_CONTRACT_NEEDED`, `REJECTED`, `ABGEBROCHEN`, with a reason |
| `POST /poll` | Handle the answers now (`dry_run`: attribute only) |
| `GET /dokumente`, `PUT /dokumente/{art}`, `DELETE /dokumente/{art}` | The document pool |

The events are:

* `lrv_gesendet`, `lrv_antworten`, `lrv_fristen`, `lrv_abgeschlossen`;
* `lrv_no_contact`, `lrv_eskaliert` and `lrv_fehler` as warnings in the
  operator app's feed.

`lrv_eskaliert` also raises an [alert](https://docs.energy.nlead.ch/setup/alerting.md). The settings are
listed under [Configuration → Lieferant](https://docs.energy.nlead.ch/setup/configuration.md#lieferant).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.