# B — `maptara_san_provider_egeko` anatomy

Target: `/home/liviuxyz/workspace/OdooProjects/maptara-demo/maptara_san_provider_egeko`
Produced: 2026-09-18. Read-only survey. Every claim carries file:line.

## 0. Baseline confirmation

Manifest facts all confirmed verbatim:
- `__manifest__.py:4` — `"version": "19.0.0.15"`
- `__manifest__.py:22-24` — `"depends": ["maptara_san_provider_base"]` — single dependency, confirmed
- `__manifest__.py:25` — `"author": "baotnp"`
- `__manifest__.py:30` — `"license": 'OPL-1'`
- 20 `.py` files / 3930 lines — **confirmed exactly**. Breakdown that sums to 3930: addon runtime 1448, tests 1382, `tools/` dev scripts 1070, manifest 30.

The depends list is a half-truth. `models/maptara_provider.py:16` does `from ...maptara_base.utils import convert_german_date` and `:426` uses `self.env['maptara.document.service']`, `:427` `self.env['maptara.document.attachment']` — neither `maptara_base` nor `maptara_document_manager_api` is declared here. They arrive transitively via `maptara_san_provider_base/__manifest__.py:28-31`. Works, but the module has undeclared hard deps on two addons and reaches across the addons path with a `...` relative import.

---

## 1. File inventory

### Addon runtime — 1448 lines, this is the whole rewrite target

| File | Lines | Purpose |
|---|---|---|
| `models/maptara_provider.py` | 687 | Everything. Payload build, all 6 provider operations, wire number formatting, status→`insurance_state` write-back |
| `models/egeko_request.py` | 575 | The transport. zeep client, login/session, 6 SOAP calls, status code tables, endpoint resolution, credential probe |
| `models/res_config_settings.py` | 130 | Endpoint setting + 7 computed advisory fields for the settings panel |
| `models/sale_order.py` | 48 | Override of `_get_ekv_position_lines` adding two egeko filters |
| `__manifest__.py` | 30 | Manifest |
| `models/__init__.py` | 4 | imports sale_order, maptara_provider, res_config_settings |
| `__init__.py` | 3 | imports models, wizard |
| `wizard/__init__.py` | 1 | **Empty.** One comment line. The `wizard/` package exists and contains nothing. `__init__.py:3` imports it anyway. |

### Tests — 1382 lines

`tests/common.py` 48, `test_connection_probe.py` 258, `test_wire_format.py` 256, `test_endpoint_config.py` 221, `test_document_state.py` 189, `test_position_selection.py` 135, `test_media_type_code.py` 102, `test_provider_routing_egeko.py` 101, `test_request_transport.py` 62, `tests/__init__.py` 10.

### Dev tools — 1070 lines, not imported by the addon

- `tools/ekv_body_diff.py` (630) — offline format-signature diff of a captured SOAP body vs optadata reference XML. Stdlib only, no Odoo import (`tools/README.md:13`).
- `tools/ekv_doctor.py` (440) — read-only live census, run by pasting its source into `odoo-shell` (`tools/ekv_doctor.py:19-27`).
- `tools/fixtures/` — 4 XML + `Egeko.dtd`. See §7.

### Docs — 746 lines of Markdown + 8.0 MB of vendored optadata PDFs. See §7.

---

## 2. THE PROTOCOL SURFACE

### Transport: SOAP 1.x via zeep, WSDL-driven, document/literal-ish with positional `argN` params

Import lines, verbatim, `models/egeko_request.py:2-7`:

```python
from odoo.tools.zeep import Client, Transport, Plugin
from odoo.exceptions import UserError
from odoo.tools.translate import _
from lxml import etree
from requests import Session
from zeep.cache import InMemoryCache
```

Split personality: `Client`/`Transport`/`Plugin` come from Odoo's wrapper `odoo.tools.zeep`, but `InMemoryCache` is imported from upstream `zeep.cache` directly (`:7`). If Odoo's wrapper exists to sandbox zeep (XXE, network policy), that import bypasses it for the cache object. The Odoo source tree was not locatable in this checkout to confirm what `odoo.tools.zeep` does — **UNKNOWN**, worth 5 minutes before the rewrite copies the pattern.

### Endpoints

`egeko_request.py:35-37`:
```python
EGEKO_URL_PROD = 'https://ws.optadata.com/EgekoService/Egeko-Service-V2'
EGEKO_URL_TEST = 'https://ws.t-egeko-services.de/EgekoService/Egeko-Service-V2'
EGEKO_URL_DEAD_TEST = 'https://ws-t.optadata.com/EgekoService/Egeko-Service-V2'
```

WSDL is fetched from `f'{self.url}?wsdl'` (`:272`). One base URL, one WSDL, no per-operation paths.

`EGEKO_URL_DEAD_TEST` is a *documented-by-the-vendor-but-broken* host kept as a named constant purely so the UI can shout at you when you pick it (`res_config_settings.py:90`, `views/res_config_settings_views.xml:36-42`). Unusual and correct — keep it.

**There is deliberately no default endpoint.** `resolve_egeko_url` (`egeko_request.py:64-83`) returns `''` and `EgekoRequest.__init__:250-269` raises a 12-line `UserError` rather than falling back. Resolution order: `os.environ['MAPTARA_EGEKO_URL']` (`:39`, `:76`) → `ir.config_parameter` key `maptara_san_provider_egeko.egeko_url` (`:40`, `:80`) → refuse. Env var wins so a staging DB restored from prod can't file real eKVs. This is the single best design decision in the module; the new driver layer must preserve "env var overrides DB param" as a first-class feature, not an egeko quirk.

### Credentials

Per-provider-row, plain columns on `maptara.service.provider` from base (`maptara_san_provider_base/models/maptara_provider.py:163-165`): `username`, `password`, `client_key` — all `fields.Char`. **No encryption, no vault, no `groups=` restriction on the password field.** Passed positionally into `EgekoRequest(username, password, client_id, ...)` at five call sites: `maptara_provider.py:449-455, 470-476, 525-530, 594-600, 628-634` — the same six-line block copy-pasted five times.

Login is `self.client.service.login(arg0=self.username, arg1=self.password, arg2=self.client_id)` (`:295`). Session state is the `requests.Session` cookie jar (`:248`, `:273`). Line `:299` logs `self.session.cookies.get_dict()` at INFO — **the session cookie goes into the Odoo log on every successful login.**

### Session / auth lifecycle

`require_auth` decorator (`:180-189`) wraps all six operations; on `False` raises `UserError(_("Authentication failed with Egeko service"))` — the single most useless error string in the module, which is precisely why `probe_egeko_credentials` was written to work around it (`:451-476`).

`_web_authenticated` (`:282-303`): if `last_login_time` is inside the session window (`:18`, value `10` minutes), it **calls `getClosedMessages()` as a liveness ping**, and if that throws, re-logs in. So every business call inside the window costs an extra round trip. Every failure path is `except Exception` → log → `return False`.

### Request body construction — NOT string templating, NOT lxml

zeep type factory. `:275` `self.factory = self.client.type_factory('ns0')`, then `:312-330` and `:411-429`:

```python
document = self.factory.document()
position = self.factory.positionSan()
for pos_field, pos_value in pos_data.items():
    setattr(position, pos_field, pos_value)
```

Attributes are set by **name from a dict, unvalidated**. A typo'd key raises inside zeep at send time, never at build time. `document.state = 0` is hardcoded on both send and resend (`:321`, `:420`).

The XML is therefore fully schema-driven. Good news for a driver split: the "SOAP driver" can own `Client`/`factory`/`Transport` and the egeko plugin only owns the dicts.

`LogPlugin.marshalled` (`:234-235`) calls `context.envelope.prune()` — drops empty elements before the wire. Quiet but load-bearing.

### Operations (the complete Egeko verb list)

| Python method | SOAP operation | Args | Source |
|---|---|---|---|
| `send_document_template_san` | `sendDocumentTemplateSan` | `arg0=template` | `:331` |
| `resend_document_template_san` | `resendDocumentTemplateSan` | `arg0=egeko_id, arg1=template` | `:430` |
| `get_document_state_by_egeko_id` | `getDocumentStateByEgekoNumber` | `arg0=egeko_id` | `:344` |
| `get_document_template_san_by_egeko_id` | `getDocumentTemplateSanByEgekoNumber` | `arg0=egeko_id` | `:357` |
| `get_response_documents_by_egeko_id` | `getResponseDocumentsByEgekoNumber` | `arg0=egeko_id` | `:370` |
| `send_message_by_egeko_id` | `sendMessageByEgekoNumber` | `arg0, arg1=subject, arg2=message` | `:443` |
| (auth only) | `login` | `arg0, arg1, arg2` | `:295`, `:524` |
| (liveness ping only) | `getClosedMessages` | — | `:290` |

Everything is `arg0/arg1/arg2`. The WSDL has no parameter names. Any generic SOAP driver must not assume named params.

### Response parsing

Three completely different styles, which is the tell that nobody designed this:

1. **`sendDocumentTemplateSan`** → `str(response)` straight into `external_ref` (`maptara_provider.py:461`). Truthiness is the only validation. `bool(response)` decides `status` `'sent'` vs `'error'`.
2. **`getDocumentStateByEgekoNumber`** → int, stringified, table lookup (`:542-547`). Correctly guards `response is None or response == ''` rather than `if response:`, because **state `0` is a legal code** (`:536-540`, comment at `:533-535`).
3. **`getResponseDocumentsByEgekoNumber`** → hand-rolled dual-path unmarshalling, `:373-397`: `isinstance(doc, dict)` branch vs `getattr` branch, then `getattr(content, '_value_1', content)` — reaching into a zeep private attribute. base64-decode wrapped in a bare `except Exception: decoded_content = None`, so a corrupt attachment silently becomes `None` and then `doc.get('content', b'')` at `maptara_provider.py:644` writes an empty attachment. Nobody is told.
4. **`getDocumentTemplateSanByEgekoNumber`** → `getattr(response.document, 'fileNumber', None)` etc. (`maptara_provider.py:605-608`). `response.document` is accessed unguarded; a shape change is an `AttributeError` inside a cron.

### Errors, timeouts, retries

- **Timeout: set, single value.** `TIMEOUT = 30` (`:17`), passed as `Client(..., timeout=TIMEOUT)` (`:274`). That is zeep's *operation* timeout. **It is NOT passed to `Transport(...)`** (`:273` — `Transport(session=self.session, cache=WSDL_CACHE)` takes no `timeout=` / `operation_timeout=`). zeep's `Transport` defaults `timeout=300` for the WSDL/XSD **load**. So: **the WSDL fetch can hang for 300 s while the business call is capped at 30 s.** Worth confirming against the installed zeep, but the arg is plainly absent at `:273`.
- **Retries: none. Anywhere.** No backoff, no retry, no `urllib3.Retry` on the session. A single transient 502 fails the send and the user gets a red chatter block.
- **Error handling: uniform and lossy.** All six operations are `try: ... except Exception as e: _logger.error(...); raise UserError(_("Failed to X: %s") % str(e))` (e.g. `:333-335`, `:346-348`, `:400-402`, `:432-434`, `:445-447`). SOAP `Fault` is not distinguished from `ConnectionError` is not distinguished from `TypeError` in our own dict. No HTTP status code is ever inspected — zeep hides it and nothing unwraps `zeep.exceptions.TransportError.status_code`.
- **Idempotency: none.** A `sendDocumentTemplateSan` that times out after the server accepted it leaves no Egekonummer, and the retry allocates a *new* KV-Nr — base's own chatter says so in `_post_send_outcome`: `"A retry allocates the next free KV-Nr; eGeKo accepts each one only once per customer account."`

### Caching

`WSDL_CACHE = InMemoryCache(timeout=WSDL_CACHE_SECONDS)` at module scope (`:102`, `:19` = 3600). Class-attribute-backed, so shared per worker process. Documented at `:85-101` with a real measurement (8 GETs → 1). The one piece of pure transport tuning here, and it is correct. A generic SOAP driver must carry it forward or the cron regresses.

### Logging of payloads

`LogPlugin` (`:199-235`) logs **every** request and response XML at `_logger.info` — i.e. patient names, birth dates, insurance numbers and diagnosis codes at INFO level in production logs. `login` is excluded from the request log (`:211`) but not the response, and the cookie is logged separately at `:299`. For four operations listed in `OPERATION_WRITE_LOG` (`:104-109`) the XML is also base64'd into `request_data`/`response_data` on the activity record.

`ingress` writes `'status': 'sent'` (`:230`) **the moment any response arrives** — including a refusal. Base has a compensating hack that downgrades `sent`→`error` when no `external_ref` came back, and says so explicitly in `_post_send_outcome`'s docstring. A rewrite should fix the cause instead of inheriting the workaround.

---

## 3. SEPARATION TEST

Entry points are the six `_<operation>_egeko` methods plus `_test_connection_egeko`. Bucketing of the 1448 addon lines (docstrings/comments counted with the code they justify, because here they *are* the spec):

| Bucket | ≈ lines | % | Where |
|---|---|---|---|
| **(a) pure transport mechanics** — reusable by a generic SOAP driver unchanged | **~185** | 13% | `egeko_request.py:64-83` (URL resolution, 20), `:85-102` (WSDL cache, 18), `:180-195` (auth decorator + etree helper, 16), `:199-235` (logging plugin, 37), `:245-280` (client construction, 36), `:282-303` (session lifecycle, 22), the 6×~6-line try/except-to-UserError wrappers (~36) |
| **(b) Egeko-specific message format & semantics** | **~470** | 32% | `egeko_request.py:21-61` endpoint lore (41), `:104-109` op list (6), `:111-176` status tables (66), `:305-335`+`:404-434` template assembly (62), `:337-402` response shapes (66), `:436-447` (12), `:451-575` probe + platform naming (125); `maptara_provider.py:27-66` de-DE number formats (40), `:96-139` X01 fallback (44), the ~90 literal wire key names in `:266-310` and `:334-378` |
| **(c) Maptara business logic** | **~660** | 46% | `maptara_provider.py:141-178` doctor sourcing (38), `:180-442` minus wire keys — Odoo field reads, discount/VAT/Mehrkosten arithmetic, appendix + MDM download (~175), `:444-504` send/resend + `insurance_state='waiting'` (60), `:506-590` status→state write-back + chatter (85), `:592-671` approval fields + attachment creation (80), `:673-687` (15), `sale_order.py` (48), the 5 duplicated credential-assembly blocks (~30), plus `_get_state_cost_estimate_egeko`'s decision logic |
| **(d) configuration UI** | **~133** | 9% | `res_config_settings.py` (130) + `views/res_config_settings_views.xml` |

**Verdict: the driver split is cheap, but only in one direction.**

Bucket (a) is genuinely generic and is ~185 lines — a SOAP driver that offers `client(url, wsdl, cache, timeout, plugins)`, `login(...)`, `call(op, *args)` and an XML-logging hook absorbs *all* of it. Nothing in (a) mentions Egeko except the `getClosedMessages` liveness ping at `:290` and the `login(arg0,arg1,arg2)` signature at `:295`, both trivially parameterisable.

But (b)+(c) are entangled, not layered. `generate_document_template` (263 lines, `maptara_provider.py:180-442`) is one method that simultaneously reads Odoo ORM fields, does VAT arithmetic, downloads MDM attachments over a *different* transport, and emits egeko's exact wire vocabulary. There is no intermediate representation. You cannot lift a "canonical eKV document" out of it without writing one.

**Concrete recommendation:** the driver split buys ~185 lines and a testable transport seam. The remaining ~1100 lines need a second seam — a provider-neutral eKV DTO between (c) and (b) — or the "plugin" will just be the same 687-line file with a different import at the top.

---

## 4. The de-facto driver interface — everything taken from `maptara_san_provider_base`

This is the contract the new layer must cover. Every item verified.

### Models inherited

| Model | Where | What is added |
|---|---|---|
| `maptara.service.provider` | `maptara_provider.py:70` | `provider_type` `selection_add=[('egeko','Egeko')]` (`:72-74`); `_test_connection_egeko` (`:76-91`) |
| `maptara.provider.activity` | `maptara_provider.py:94` | new field `egeko_state_code` (`:104-110`); constant `EGEKO_FALLBACK_TYPE_CODE = 'X01'` (`:102`); 10 methods |
| `sale.order` | `sale_order.py:18` | overrides `_get_ekv_position_lines` (`:20`) |
| `res.config.settings` | `res_config_settings.py:17` | endpoint fields |

### The naming-convention contract (the actual dispatch mechanism)

Base resolves implementations by **string interpolation on `provider_type`**, `maptara_san_provider_base/models/maptara_provider.py:608-611`:

```python
provider_type = self.provider_id.provider_type
if not provider_type:
    return None
return getattr(self, f'_{operation}_{provider_type}', None)
```

and for connection tests, `:419-420`: `getattr(self, f'_test_connection_{self.provider_type}', None)`.

So the interface is **7 methods that must exist by exact name**:

| Base entry point | Required method on the egeko module | Signature | Return contract |
|---|---|---|---|
| `send_cost_estimate()` | `_send_cost_estimate_egeko` | `(record)` | ignored |
| `resend_cost_estimate()` | `_resend_cost_estimate_egeko` | `(record)` | ignored |
| `_get_state_cost_estimate()` | `_get_state_cost_estimate_egeko` | `(record)` | **truthy iff the insurance decision moved** — the cron branches on it |
| `_get_cost_estimate()` | `_get_cost_estimate_egeko` | `(record)` | ignored |
| `_get_cost_estimate_document()` | `_get_cost_estimate_document_egeko` | `(record)` | ignored |
| `_send_message(subject, message)` | `_send_message_egeko` | `(record, subject, message)` | ignored |
| `action_test_connection()` | `_test_connection_egeko` | `(self)` on the **provider**, not the activity | dict with keys `ok`, `stage`, `url`, `message` |

The `_test_connection_*` result dict is read at base `:423-438` (`result.get('ok')`, `result.get('message')`) and produced at `egeko_request.py:483-561`. `stage` ∈ `{'config','endpoint','credentials'}` is currently only consumed by a log line (`maptara_provider.py:87-90`) and the test suite — but it is the useful half, so specify it.

Also required, though *not* dispatched: `generate_document_template()` on the activity (`maptara_provider.py:180`). Called only from within this module (`:456`, `:477`) but referenced by name in base's docstrings and tests (`maptara_san_provider_base/models/sale_order.py:77`, `tests/test_send_ekv_image_warning.py:6`).

### Fields assumed to exist

On `maptara.service.provider` — `username`, `password`, `client_key` (base `:163-165`), `uom_ids` (`:174`), `display_name`, `name`; method `media_type_code(document_type)` (base `:192-201`, returns `False` when unmapped — deliberately, so the provider module supplies the fallback).

On `maptara.provider.activity` — `order_id`, `provider_id`, `status`, `error_msg`, `external_ref`, `internal_ref`, `document_type`, `request_data`, `request_data_filename`, `response_data`, `response_data_filename` (base `:476-542`).

On `sale.order` — `_get_ekv_position_lines()` (base `sale_order.py:72`), `insurance_state`, `insurance_payment_ids`, `insurance_payment_option`, `show_additional_payment`, `ekv_ik`, `insurance_id`, `prescription_order_id`, `claim_request_ids`, `company_id.ik_no`, `user_id.partner_id.firstname/lastname/phone`, `partner_id.birth_date/firstname/lastname/patient_insurance_number`. Optional/guarded: `supply_window_id`, `case_id` (`maptara_provider.py:167,169` — explicitly field-guarded because `maptara_case_sale_glue` may not be installed), `task_id` on lines (`sale_order.py:45`).

Constant imported from base: `EKV_APPENDIX_MIMETYPES` (`maptara_provider.py:17-19`; defined base `:22-25` as pdf/jpeg/tiff/png).

### Context keys the wizard injects

`selected_document_ids` and `selected_mdm_attachment_ids`, set at `maptara_san_provider_base/wizard/maptara_send_ekv_wizard.py:408-409` and applied at `:572-573`, `:608`. Read at `maptara_provider.py:402` and `:424`. **Undocumented, untyped, and the payload silently sends no appendix if they're absent.** A new driver interface should make attachments an explicit argument, not context smuggling.

---

## 5. Configuration

### `res.config.settings` fields (`models/res_config_settings.py`)

| Field | Line | Stored? |
|---|---|---|
| `egeko_url` | `:19-32` | **Yes** — `config_parameter=EGEKO_URL_PARAM` |
| `egeko_url_effective` | `:33-38` | computed |
| `egeko_url_from_env` | `:39-41` | computed |
| `egeko_url_missing` | `:42-46` | computed |
| `egeko_url_is_production` | `:47-49` | computed |
| `egeko_url_is_dead_test` | `:50-52` | computed |
| `egeko_url_is_test` | `:53-55` | computed |
| `egeko_url_prod` / `egeko_url_test` | `:56-57` | computed, constants echoed to the view |
| `egeko_url_preset` | `:58-70` | computed+`readonly=False`, onchange at `:100-124` |

Public model method `egeko_url_in_use()` (`:126-130`) for shell/diagnostics.

### `ir.config_parameter` keys — exactly one

`maptara_san_provider_egeko.egeko_url` (`egeko_request.py:40`).

### Environment variables — exactly one

`MAPTARA_EGEKO_URL` (`egeko_request.py:39`). Read in two places: `egeko_request.py:76` and, deliberately duplicated for the settings panel, `res_config_settings.py:77` (with a good reason documented at `:74-76`).

### Hardcoded values that should be config

| Value | Line | Why it matters |
|---|---|---|
| `TIMEOUT = 30` | `egeko_request.py:17` | Not overridable. An eKV with 4 large PDF appendices against a slow payer will blow it and there is no knob. |
| `SESSION_TIMEOUT_MINUTES = 10` | `:18` | opta data's real session TTL is a vendor fact, not ours. If they change it, code change. |
| `WSDL_CACHE_SECONDS = 3600` | `:19` | Defensible, documented at `:100-101`. |
| `document.state = 0` | `:321`, `:420` | Hardcoded on both send **and resend**. |
| `'serviceType': '01'` | `maptara_provider.py:301` | Magic string, no comment, no reference. |
| `'serviceIndicator': ... or '00'` | `:300` | ditto |
| `'ownContribution': '0,00'` | `:297` | Literal string, not run through `de_decimal` |
| `'serviceText': 'k.A.'` | `:376` | German literal in the payload |
| `'measureCode': ''` | `:296` | Deliberately blank, 9 lines of justification at `:290-295`. Keep the reasoning. |
| `'costUnitType': 'GK'/'PV'` | `:352` | Two-branch mapping inline in a dict literal |
| `EGEKO_FALLBACK_TYPE_CODE = 'X01'` | `:102` | Correctly a constant, correctly in the egeko module |
| `'priorityCode': 0`, `'deviceAttendedTime': 0`, `'isVatInclusive': 0`, `'hardwareNumber': ''`, `'dhpIdentNumer': ''` | `:362`, `:269`, `:274`, `:281-282` | Constants baked into the payload. Note `dhpIdentNumer` — **that is optadata's typo in the WSDL, not ours.** Preserve it verbatim or the send fails. |
| Cron interval 15 min | `maptara_san_provider_base/hooks.py:58-59` | Lives in base, created by post-init hook, not XML data |

---

## 6. Behaviour that MUST be preserved

### Flow 1 — Send cost estimate (user-initiated)
Wizard → `activity.with_context(selected_document_ids=…, selected_mdm_attachment_ids=…).send_cost_estimate()` (base wizard `:572-573`) → base `_require_provider_method` → `_send_cost_estimate_egeko` (`maptara_provider.py:444`).
1. Hard precondition: `order.company_id.ik_no` must be set, else `UserError("Please configure the Institution Code for the Company!")` (`:446-447`).
2. `generate_document_template()` → `sendDocumentTemplateSan`.
3. `status = 'sent' if response else 'error'`; `external_ref = str(response)`; `error_msg = 'No response from Egeko service'` (`:459-463`).
4. **`order.insurance_state = 'waiting'` unconditionally** (`:464-466`) — including when the send returned nothing. See P1.
5. Base wraps the whole thing in `_chatter_on_send` → posts an HTML outcome block, and downgrades `sent`→`error` if no `external_ref`.

### Flow 2 — Resend (`_resend_cost_estimate_egeko`, `:468-504`)
Same, plus: `internPrescriptionId` is **deleted** from the payload (`:480-481`) — "no element" ≠ "empty string", justified at `:324-330`; and the original Egekonummer is looked up from the most recent `document_type='cost_estimate'` activity (`:484-488`) and passed as `arg0`. No such activity → `UserError("No cost estimate activity found to resend!")` (`:504`).

Subtle: `generate_document_template` already omits the key when `internal_ref` is blank (`:379-380`), so `:480-481` is the belt to that braces. Keep both; they guard different states.

### Flow 3 — Status poll (cron, every 15 min)
`sale.order._cron_process_cost_estimates` → newest `cost_estimate` activity → if `insurance_state == 'waiting'`, `_get_state_cost_estimate()` → `_get_state_cost_estimate_egeko` (`:506-581`).

**The four-bucket rule is the single most important behaviour in this module** and must survive verbatim:

- `STATUS_DECIDED` (`egeko_request.py:131-140`) — **the only bucket allowed to write `insurance_state`.** `2`→full, `3`→partial, `4`→rejected, `5`→modify, `6`→rejected, `501`→cancel.
- `STATUS_PENDING` (`:143-147`) — `0`, `1`, `32`. Silent. Nothing written, nothing posted.
- `STATUS_FAILED` (`:152-155`) — `-1`, `42`. `_logger.warning`, chatter post, **`insurance_state` untouched**.
- `STATUS_INFORMATIONAL` (`:160-168`) — `11`, `12`, `13`, `31`, `33`, `41`, `71`. Posted once, never acted on.
- **Unknown code** → warning listing all known codes, posted as `"Unknown status code -- not acted on"`, nothing written (`:571-578`).
- Empty/None response → warning, treated as pending, returns `False` (`:536-540`).

Two regressions this encodes and which a rewrite will re-introduce if it isn't careful (`tests/test_document_state.py:1-18` is the receipt): code `41` "delivered after cost approval" used to write `rejected`; code `-1` "Error" used to sit in the pending bucket and be re-polled forever.

**Chatter de-duplication:** `egeko_state_code` (`maptara_provider.py:104-110`) stores the last seen code; `first_sighting = record.egeko_state_code != status_str` (`:543`) gates every `message_post`. Without this the cron posts an identical chatter line every 15 minutes for the life of the order. Return value is `decision_moved` (`:549`) — whether `insurance_state` *changed* — not whether the code changed. Those differ and the cron depends on the former.

### Flow 4 — Approval detail fetch
Triggered only when flow 3 returned truthy and no `cost_estimate_status` activity exists yet (base `sale_order.py:446-456`). `_get_cost_estimate_egeko` (`:592-624`) → `getDocumentTemplateSanByEgekoNumber` → writes `approval_no` (from `fileNumber`), `approval_date` (via `convert_german_date`), `approval_amount` (`float(value.replace(',', '.'))`, `:616`), and posts `refusalText` to chatter as "Reason for rejection from …". Idempotency guard is "does a `cost_estimate_status` activity already exist" — record existence, not a flag.

### Flow 5 — Response document fetch
Triggered when `insurance_state in ('full','partial','rejected')` and no `cost_estimate_documents` activity exists (base `sale_order.py:467-497`). `_get_cost_estimate_document_egeko` (`:626-671`) → `getResponseDocumentsByEgekoNumber` → creates `ir.attachment` rows on `maptara.claim.request` and posts them.

Empty response → **`status='error'` on the activity, and the activity is deliberately NOT deleted**, with the reason spelled out at `:657-660`: deleting it would break the "does an activity exist" idempotency guard and cause infinite retries. This looks like a bug ("a poll that ran too early marks itself errored") and is actually load-bearing. Preserve it or replace the guard with something better, but do not just "fix" the error status.

### Flow 6 — Free-text message
`_send_message_egeko` (`:673-687`) → `sendMessageByEgekoNumber`, returns `bool(response)`. Called from base wizard `:621`.

### Flow 7 — Connection test
Provider form button → `action_test_connection` (base `:404`) → `_test_connection_egeko` (`:76-91`) → `probe_egeko_credentials` (`egeko_request.py:451`). Three distinguishable failure stages, never raises on a rejected login (returns a sticky red notification instead), creates no Vorgang so it is safe against production.

### Flow 8 — Endpoint configuration UX
Settings panel with radio preset (test/prod/custom), four mutually-exclusive coloured alerts (`views/res_config_settings_views.xml:30-61`), fields become readonly when the env var is set (`:23`, `:26`). The onchange-instead-of-button decision is documented at `res_config_settings.py:102-119` and the reason is real — object buttons on a settings form get their assignments discarded by the reload.

### Flow 9 — Position selection
`sale_order.py:44-48`: base predicate ∧ `product_id.sale_ok` ∧ ¬`task_id` (field-guarded because `sale_project` is not in the dep closure — explicitly checked, 41 modules, `:38-40`). This is a *seam*: the payload and the eKV warning banner both read it, which is the fix for the banner counting section headers as positions.

Positions are then sorted `create_date` **descending** (`maptara_provider.py:393`) and indexed from 1. Newest first. Odd, tested (`test_position_selection.py:109`), preserve it.

### Flow 10 — Number formatting on the wire
`de_decimal` (`:27-39`) — comma decimal separator, always. `de_price` (`:42-66`) — shortest exact representation, 2 to 4 decimals, **per field independently**. The justification at `:47-61` derives this from three optadata reference documents and reference 129 is the one that proves net and gross do *not* share a precision (`2,75` and `3,2725` on the same position). 20 tests guard it.

Related invariants also encoded, each with an explanation of the bug it replaced:
- net and gross both post-discount (`price_subtotal/qty`, `price_total/qty`) so they imply the stated VAT rate (`:216-229`)
- zero-quantity lines do not raise ZeroDivisionError (`:225-226`)
- `additionalFee` is Mehrkosten, gated on `order.show_additional_payment`, computed via `_additional_payment_netto()` not the stored column, **never** the discount percentage (`:231-258`)
- `serialNumber` never leaks Python `False` onto the wire (`:202-208`)

---

## 7. Licence / authorship surface

### baotnp — exactly 1 occurrence

`__manifest__.py:25` — `"author": "baotnp"`. That is the only hit in the entire module tree (case-insensitive grep over all files).

### Copyright headers — zero

**No `.py` file in this module carries a copyright header.** Every one starts with `# -*- coding: utf-8 -*-` (or, for `tools/ekv_body_diff.py:1`, `#!/usr/bin/env python3`) followed straight by a docstring or imports. Grep for `copyright|(c) 20|Copyright` over the module returns exactly **one** hit, and it is not ours:

- `tools/fixtures/Egeko.dtd:5` — `copyright (c)2007 - 2015 by optadata.com`

### Licence declarations

`__manifest__.py:30` — `"license": 'OPL-1'`. No `LICENSE` file, no `COPYING`, no SPDX identifiers anywhere.

### Vendored third-party material — this is the real exposure, and it is not the Python

| Path | Size / lines | Provenance |
|---|---|---|
| `tools/fixtures/Egeko.dtd` | — | **optadata's own DTD, explicit copyright notice on line 5.** `tools/README.md:36`: "Supplied by Liviu 2026-08-13; no GET was ever spent on it." |
| `tools/fixtures/ref-125-feedback-template-san.xml` | — | optadata reference doc, ticket #768 attachment 125 |
| `tools/fixtures/ref-128-send-toilettensitz.xml` | — | optadata attachment 128, **extracted from a `.docx`**, U+00A0 replaced with spaces (`README.md:34`) |
| `tools/fixtures/ref-129-send-microsoftbc.xml` | — | **a real `sendDocumentTemplate` captured from Microsoft BC** — a third party's production output (`README.md:33`) |
| `tools/fixtures/ours-S07001-20260813.xml` | — | our own capture, "Captured verbatim from the odoo log, 2026-08-13 09:31:36" — **contains live-ish patient payload** |
| `docs/guides/egeko-EKV-DE-TechSpec-LE-soap-V_1_1_6 (1).pdf` | 1.19 MB | **optadata's technical specification, vendored whole.** Filename still carries the browser's ` (1)` duplicate-download suffix |
| `docs/guides/translated_egeko-ekv-provider-soap-interface-v1.1.6_…_en_translation.pdf` | 6.79 MB | **machine-translated copy of the same vendor spec** — a derivative work of a third-party document, committed to the repo |

**8.0 MB of vendor documentation and one DTD carrying an explicit optadata copyright are in this module.** None of it is `baotnp`'s and none of it is ours. The rewrite's licence problem is not the Python — it is `docs/guides/*.pdf` and `tools/fixtures/`. Ship the rewrite without them; keep them in an internal reference repo instead.

### Authorship in git

26 commits touching this path. `Liviu Staniloiu` 13, `soniacristea-cappsai` 6, `Liviuxyz-ctrl` 3, `liviuxyz-ctrl` 1, `liviustaniloiu-cappsai` 1, `Doru Ambrus` 1, `bogdanbozga-cappsai` 1. **Zero commits by `baotnp` in this repo's history for this path.** The manifest attribution appears to be inherited boilerplate rather than a record of who wrote the current code — but the history here may be truncated (26 commits for a 3930-line module at version 19.0.0.15 suggests earlier history lives elsewhere). Confirm against the original repo before relying on this for a clean-room claim.

---

## 8. Tests — the safety net

**83 test methods across 9 files, 1382 lines (35% of the module).** For once, a real net rather than coverage-metric decoration.

Every test is `@tagged('post_install', '-at_install')` `TransactionCase`. **No test touches the network** — `Client` and `Transport` are patched at the module level (`test_request_transport.py:35`), and for the model-level suites `EgekoRequest` itself is patched at the import site (`test_document_state.py:28`: `'odoo.addons.maptara_san_provider_egeko.models.maptara_provider.EgekoRequest'`) precisely because constructing it fetches a WSDL.

| File | Tests | What it actually asserts |
|---|---|---|
| `test_document_state.py` | 15 | Every one of the four status buckets, code by code. The two named regressions (`41`→not-rejected, `-1`→not-pending). State `0` is not mistaken for a dead connection. Chatter posted once per distinct state. Unknown code invents no decision. |
| `test_wire_format.py` | 17 | No decimal dot anywhere in a position. `serialNumber` is never a Python bool. `de_price` reproduces all three optadata reference prices exactly. Discount never sent as `additionalFee`. Discounted and undiscounted lines both imply their stated VAT. Zero quantity does not raise. Quantity-4 keeps the fourth decimal. |
| `test_connection_probe.py` | 17 | All three `stage` values. WSDL failure blames the host, not the password. The dead host gets told it is the dead host; a working host does not get the lecture. Failure is a sticky red notification, not an exception. The button passes *this* row's credentials. |
| `test_endpoint_config.py` | 15 | No default endpoint exists. Nothing configured refuses to build a request. Env var beats system parameter. Whitespace is not a URL. The three hosts are three different hosts. Saving the field lands in `ir.config_parameter`. Platform picker fills the URL. |
| `test_position_selection.py` | 5 | Withdrawn products dropped, display rows never reach the payload, payload and warning banner agree, positions still newest-first. |
| `test_media_type_code.py` | 5 | Mapped type travels as its own code, unmapped and typeless travel as `X01`, the fallback logs and the non-fallback does not. |
| `test_provider_routing_egeko.py` | 5 | All 7 `_<op>_egeko` methods exist. Each base entry point reaches its implementation. `_send_message` passes both arguments. `_provider_method('send_cost_estimate').__name__ == '_send_cost_estimate_egeko'`. **An archived provider still routes** (so in-flight documents stay pollable). |
| `test_request_transport.py` | 4 | `Transport` gets `cache=WSDL_CACHE`, the cache is shared across instances, `session` is still passed, timeout is 3600. |
| `tests/common.py` | — | `egeko_test_provider()` — creates a throwaway `res.company` and scopes the provider to it, so the suite cannot corrupt a real configured row. Documented against an actual incident (staging row id 246, 7 orphaned activities, `common.py:20-32`). |

### What the net covers, and what it does not

**Covered, and a rewrite can lean on it hard:** status bucketing (15 tests), wire number format (17), endpoint resolution (15), the routing convention (5). These are pure-function or thin-model tests and they will port to a new implementation almost unchanged — the best single asset in this module.

**NOT covered — the gaps a rewrite will fall into:**
- **No test ever constructs a real SOAP envelope.** `self.factory.document()` / `setattr(position, field, value)` is never exercised; every suite patches above it. A misspelled wire key name is caught by nothing in this suite.
- `_send_cost_estimate_egeko` / `_resend_cost_estimate_egeko` have **no direct tests** — only `test_provider_routing_egeko.py:62-69`, which patches them out and asserts they were called. The `insurance_state = 'waiting'` write, the `'sent' if response else 'error'` branch, and the resend's activity lookup are untested.
- `_get_cost_estimate_egeko` and `_get_cost_estimate_document_egeko` have **no tests at all** beyond routing. The attachment creation path, the `float(x.replace(',','.'))` parse, and the "empty response marks error but does not delete" behaviour are unguarded.
- `get_response_documents_by_egeko_id`'s dual-path unmarshalling (`egeko_request.py:373-397`) — untested, including the `except Exception: decoded_content = None` swallow.
- `_web_authenticated` session lifecycle — untested. Nothing asserts the 10-minute window or the `getClosedMessages` ping.
- `LogPlugin` egress/ingress, including the `status='sent'` write from `ingress` — untested.
- No contract test against `Egeko.dtd`, despite the DTD being vendored and `tools/ekv_body_diff.py` knowing how to use it. The 630-line tool that could gate this **is not wired into the test suite** (`tools/README.md:13` says it exits 1 so it "can gate a pipeline" — nothing does).

**Verdict:** a strong net on *semantics* (status codes, number formats, config resolution) and effectively none on *transport and payload assembly*. Which is exactly backwards for a driver-layer refactor. Before rewriting, add 2–3 tests that build a real payload and assert against `tools/fixtures/ref-128-send-toilettensitz.xml` — the machinery already exists in `ekv_body_diff.py`, it just needs a `TransactionCase` around it.

---

## Parking Lot

**P1 — `insurance_state = 'waiting'` is written even when the send failed.**
Evidence: `models/maptara_provider.py:459-466` — `status` is set to `'error'` on a falsy response, and then `:464-466` writes `'waiting'` unconditionally in the next statement. Same shape at `:495-502`.
Impact: an order that was never accepted enters the 15-minute poll loop with an empty `external_ref` and polls `getDocumentStateByEgekoNumber('')` forever. Base's `_post_send_outcome` corrects `status` but not `insurance_state`.
Confidence: high. Next action: move the `insurance_state` write inside the success branch; check for existing stuck orders with `insurance_state='waiting'` and empty `external_ref`.

**P2 — WSDL load timeout is not set; only the operation timeout is.**
Evidence: `models/egeko_request.py:273` `Transport(session=self.session, cache=WSDL_CACHE)` vs `:274` `Client(..., timeout=TIMEOUT)`. `TIMEOUT = 30` at `:17`.
Impact: on a cold cache with an unresponsive host, a cron worker can block for zeep's 300 s Transport default instead of 30 s.
Confidence: medium-high (the argument is plainly absent; zeep's default should be confirmed against the installed version). Next action: pass `timeout=` and `operation_timeout=` to `Transport` in the new driver.

**P3 — Session cookie and full patient payload logged at INFO.**
Evidence: `models/egeko_request.py:299` logs `self.session.cookies.get_dict()`; `:212` and `:224` log the entire request/response XML at `_logger.info`.
Impact: names, dates of birth, insurance numbers and ICD-10 codes in production log files and in any log shipper. GDPR-relevant.
Confidence: high. Next action: DEBUG-gate the XML, drop the cookie log entirely, or redact.

**P4 — `_web_authenticated` costs an extra round trip on every call inside the session window.**
Evidence: `models/egeko_request.py:288-293` — `getClosedMessages()` is invoked as a liveness ping before each decorated operation.
Impact: doubles the request count for the poll cron. Also: `getClosedMessages` has business meaning at opta data; using it as a ping may have side effects nobody checked.
Confidence: high on the mechanics, UNKNOWN on the side effects. Next action: check the vendor spec (`docs/guides/…TechSpec…pdf`) for whether `getClosedMessages` is read-only.

**P5 — `wizard/` package exists and is empty.**
Evidence: `wizard/__init__.py` is 1 line (`# -*- coding: utf-8 -*-`), imported by `__init__.py:3`. A stale `wizard/__pycache__/__init__.cpython-312.pyc` is present.
Impact: none functionally; noise. Confidence: certain. Next action: drop it from the rewrite.

**P6 — `__pycache__` directories present in the working tree.**
Evidence: `models/__pycache__/*.pyc`, `__pycache__/__init__.cpython-312.pyc`, `wizard/__pycache__/*.pyc`. `tools/.gitignore` ignores `__pycache__` but only under `tools/`.
Impact: cosmetic, but if tracked it means a stale `.pyc` can ship. Confidence: high that the files exist; UNKNOWN whether git-tracked (not checked). Next action: `git ls-files | grep pycache`.

**P7 — `dhpIdentNumer` is misspelled on the wire.**
Evidence: `models/maptara_provider.py:282` — `'dhpIdentNumer': ''`.
Impact: almost certainly optadata's typo in the WSDL, in which case it MUST be preserved verbatim or `setattr` on the zeep type fails. Flagged only so a well-meaning rewrite does not "fix" it.
Confidence: medium. Next action: grep the WSDL/DTD before touching it.

**P8 — `ekv_body_diff.py` is a working payload gate that nothing runs.**
Evidence: `tools/README.md:13` — "Exit code 1 if any `BUG` or `GAP` survives, so it can gate a pipeline." No CI reference found in the module.
Impact: 630 lines of correctness tooling sitting idle while the test suite has zero payload coverage.
Confidence: high. Next action: wire it into the rewrite's CI, or port its signature comparison into a `TransactionCase`.
