# C — Blast radius & prior art

All paths relative to `/home/liviuxyz/workspace/OdooProjects/maptara-demo/`.
Produced: 2026-09-18. Read-only survey. Every claim carries file:line.

## Q1 — BLAST RADIUS

### 1. Dependency tree (derived by `ast.literal_eval` over all 104 `__manifest__.py`)

**3 modules depend on `maptara_san_provider_base` directly:**

| Module | Manifest line |
|---|---|
| `maptara_san_provider_egeko` | `maptara_san_provider_egeko/__manifest__.py:20` |
| `maptara_codexa_api` | `maptara_codexa_api/__manifest__.py:13` |
| `maptara_pos_sale_glue` | `maptara_pos_sale_glue/__manifest__.py:38` |

**0 modules depend on `maptara_san_provider_egeko`.** Nothing. It is a leaf.

**22 modules total in the transitive closure** (incl. egeko; 19 are indirect-only). The two indirect fan-outs:

- via `maptara_codexa_api` (17): `maptara_codexa`, `maptara_asset_obligation`, `maptara_asset_obligation_codexa_glue`, `maptara_case_codexa_glue`, `maptara_case_codexa_demo`, `maptara_case_delivery_glue`, `maptara_case_invoicing_glue`, `maptara_case_mdm_glue`, `maptara_codexa_document_manager_glue`, `maptara_codexa_pricing_glue`, `maptara_codexa_snapshot`, `maptara_codexa_version_diff`, `maptara_codexa_vdms`, `maptara_codexa_vdms_preview`, `maptara_hodey_playground` (+ the two above).
- via `maptara_pos_sale_glue` (4): `maptara_pos_appointment_sale_glue`, `maptara_pos_purchase_glue`, `maptara_pos_shop_sale_glue`, `maptara_pos_shop_purchase_glue`.

The entire Codexa tree (15 modules) hangs off provider_base through **one field and one migration**. See #4.

### 2. Cross-module references into the provider layer

Real code references from outside the two modules (excluding `docs/`, `openspec/`, comments-only):

| File | Refs | What |
|---|---|---|
| `maptara_pos_sale_glue/models/maptara_pos_visit.py` | **5** | `ece_provider_id` (:332), `provider_activity_ids` (:336, :388), `action_resend_insurance()` (:389), `action_send_insurance()` (:391) |
| `maptara_codexa_api/models/provider_site.py` | **2** | M2o `'maptara.service.provider'` (:20); `create({... 'name': ...})` (:73) — `name` is added by provider_base at `maptara_san_provider_base/models/maptara_provider.py:159` |
| `maptara_codexa_api/migrations/19.0.1.11.0/post-migrate.py` | **1** | raw SQL `UPDATE maptara_service_provider SET active = TRUE` (:26) — `active` added by provider_base at `maptara_provider.py:165` |
| `maptara_base/models/res_partner.py` | **1** | defensive `if 'name' in Provider._fields` (:1082) |
| `maptara_case_sale_glue/tests/test_doctor_from_involved_partner.py` | **2** | `hasattr(Activity, '_ekv_doctor_source')` (:118), call (:126) — the only external caller of an **egeko-private** method |
| `maptara_stock/tests/test_consumer_contracts.py` | **1** | `_require_installed("maptara_san_provider_egeko")` (:92) |
| `scripts/codexa-pq-demo/seed_pq_demo.py` | **1** | passes `name` when seeding providers (:113) |
| `maptara_patient_connect_hub/controllers/patient_service_view.py:23`, `patient_patient_view.py:639` | **2** | `search()` on `maptara.service.provider` — inherits provider_base's `active` default filter implicitly |

**Total: 15 live code references, across 8 files, in 6 modules.**

Zero XML view inheritance into provider-layer views from outside (`grep "maptara_san_provider" --include=*.xml` outside the two modules: **0 hits**). Zero external `ir.model.access` rows against the 5 provider models (all 12 ACL rows live in `maptara_san_provider_base/security/ir.model.access.csv`). Zero external references to `maptara_service_provider_action` / `maptara_provider_activity_action`. One JS asset, owned by the consumer not the provider: `maptara_pos_sale_glue/static/src/js/ekv_docs_action.js:42` calls `action_open_send_ekv` on `maptara.pos.visit` (which then delegates), and registers into `registry.category("maptara_pos.docs_panel_actions")` at :54.

`maptara_case_mdm_glue/models/sale_order.py:118-144` is a 26-line commented-out block waiting for a `sale.order.action_send_ekv` hook that does not exist. Not a dependency; a note-to-self.

### 3. The eKV fields — 4 of the 12 named are actually fields

| Name | What it really is | Defined at | Modules that read it |
|---|---|---|---|
| `ekv_ik` | `fields.Char` | `maptara_san_sale/models/sale_order.py:127` | **4**: `maptara_san_sale`, `maptara_case_sale_glue` (:342,:345), `maptara_san_industry_fsm` (`repair.py:142`), `maptara_san_sale/views/sale_order_views.xml:115`. **Not provider-layer.** |
| `ekv_warning_count` | `fields.Integer`, stored compute | `maptara_san_provider_base/models/sale_order.py:44` | **1** (itself). Zero external readers. |
| `ekv_warnings_text` | `fields.Text`, stored compute | `maptara_san_provider_base/models/sale_order.py:52` | **1** (itself). Zero external readers. |
| `ekv_state` | `fields.Selection` | `maptara_san_claim/models/maptara_claim_request.py:102` | **1** (`maptara_san_claim` only). **Not provider-layer.** |
| `ekv_position_lines` | **does not exist.** Method `_get_ekv_position_lines` | `maptara_san_provider_base/models/sale_order.py:72`, overridden `maptara_san_provider_egeko/models/sale_order.py:20` | 2 modules, both provider-layer |
| `ekv_warning_lines` | **does not exist.** Method `_get_ekv_warning_lines` | `maptara_san_provider_base/models/sale_order.py:100` | 1 module. Documented as "the extension seam" (`sale_order.py:34`) with **no external extender today** |
| `ekv_lines` | **does not exist.** Local variable name | `maptara_san_provider_base/models/sale_order.py:124`, `maptara_san_provider_egeko/tools/ekv_doctor.py:246` | n/a |
| `ekv_forced_qty` | **does not exist.** Local dict | `maptara_san_sale/models/sale_order.py:2752` | n/a |
| `ekv_not_invoiced` | **does not exist.** Local variable | `maptara_san_sale/models/sale_order.py:2757` | n/a |
| `ekv_doctor_source` | **does not exist.** Static method `_ekv_doctor_source` | `maptara_san_provider_egeko/models/maptara_provider.py:142` | 2: egeko + `maptara_case_sale_glue` test (guarded by `hasattr`) |
| `ekv_body_diff` | **does not exist.** A CLI script | `maptara_san_provider_egeko/tools/ekv_body_diff.py` | 0 |
| `ekv_available` | **does not exist.** Method `_ekv_available` | `maptara_pos_sale_glue/models/maptara_pos_visit.py:302` | 1, and it belongs to the consumer, not the provider |

**Public API surface of the provider layer, measured:** four `sale.order` fields (`ece_provider_id`, `ece_provider_locked`, `cost_estimate_completed`, `provider_activity_ids` — `maptara_san_provider_base/models/sale_order.py:15-28`), two actions (`action_send_insurance`, `action_resend_insurance`), and the `name`/`active`/`provider_type` columns grafted onto `maptara.service.provider`. Everything with "ekv" in the name is internal or lives in a different module.

### 4. Verdict per consumer

| Consumer | Breaks? | How loudly |
|---|---|---|
| `maptara_san_provider_egeko` | Yes, totally | It is being rewritten. It imports `from ...maptara_san_provider_base.models.maptara_provider import ...` (`maptara_provider.py:17`) and defines 8 `_<op>_egeko` methods bound by name. |
| `maptara_pos_sale_glue` | Yes if the four `sale.order` fields or the two actions move | **Field missing / AttributeError at runtime.** Keep `ece_provider_id`, `provider_activity_ids`, `action_send_insurance`, `action_resend_insurance` and it survives untouched — its own docstring (`maptara_pos_visit.py:373`) says it only delegates. |
| `maptara_codexa_api` (and the 15 modules behind it) | Only if `name` or `active` stop existing on `maptara.service.provider` | **Loud: `create()` fails** (`name` is `required=True`) and the `19.0.1.11.0` migration's raw SQL throws `column "active" does not exist`. The entire Codexa tree's dependency on the provider layer is *two column names*. Move them into `maptara_base` and 15 of the 22 modules stop caring. |
| `maptara_patient_connect_hub` | **Silent behaviour change.** Its two `search()` calls have no `active` domain; with provider_base installed, `active=True` filters them. Remove `active`, or change its default, and the portal starts listing archived providers. No error, no test. |
| `maptara_case_sale_glue` test | `hasattr`-guarded (`test_doctor_from_involved_partner.py:118`) | Silently **skips**. A rewrite that drops `_ekv_doctor_source` turns a green test into a green non-test. |
| `maptara_stock` test | `_require_installed` | Skips, loudly enough to notice. |
| `maptara_base/res_partner.py:1082` | Guarded by `'name' in Provider._fields` | No break. Someone already thought about this. |

---

## Q2 — PRIOR ART

### 5. The `*_api` service-boundary pattern (best template available)

Shape, verbatim from `maptara_codexa_api/models/codexa_service_api.py`:

- **Boundary declared** as `models.AbstractModel` with `_name = 'codexa.service'` (:12). Twelve `@api.model` methods, all **keyword-only** (`def service_find_contract(self, *, case_id=None, ...)` :16).
- **Default body raises**: `raise NotImplementedError("maptara_codexa implementation module is not installed. Install it to use service_find_contract.")` (:64-67). Every method. The API module ships no behaviour.
- **Implementation binds by `_inherit`**, not registration: `maptara_codexa/models/codexa_service_api.py:18` — `class CodexaServiceImpl(models.AbstractModel): _inherit = 'codexa.service'`, and each override is a one-line delegation to a plain-Python class `CodexaContractService(self.env)`.
- **Calls** are `self.env['codexa.service'].service_find_contract(...)`.
- **Errors**: `NotImplementedError` for "not installed"; business failures return dict envelopes (`{'match': False, 'reason': 'no_insurance_ik' | 'no_provider_site' | 'no_matching_item'}`, :113-115).
- **No registry.** Odoo's `_inherit` MRO *is* the registry. Exactly one implementation may exist per `_name`.

The same pattern is repeated 12 times: `maptara_document_manager_api/models/document_service.py:62` (`maptara.document.service`, raises `UserError(self._no_provider_message())` instead of `NotImplementedError` — and has a genuinely good module-level docstring documenting the `filters` dict contract at :44-57), `maptara_case_api/models/case_service.py:69`, `maptara_pos_api/models/pos_visit_service.py:45`, `maptara_asset_ownership_api/models/ownership_service_api.py:11`, `maptara_pricing_api/services/pricing_service_mixin.py:29`, `maptara_pos_register_api/models/pos_register_service.py:101`, `maptara_san_prescription_extract_api/models/prescription_extract_service.py:83`.

`architecture-rules.json:779` tags `maptara_codexa_api` `"tier": "tier1a_api"` with an explicit `forbidden_depends` list — there is machine-readable tier enforcement already.

### 6. VDMS / RHVital importer

Two modules. `maptara_vdms_connector` (transport, generic, "does NOT depend on any consumer module" — `__manifest__.py:17`), `maptara_codexa_vdms` (projection into Codexa).

`maptara_vdms_connector/services/client.py` — a **plain Python class**, `class VdmsClient` (:31), not an Odoo model. Imports:
```python
import logging, time
from datetime import datetime, timedelta, timezone
from typing import Any
from urllib.parse import urljoin
import requests
from .exceptions import (VdmsAuthError, VdmsConfigError, VdmsHttpError, VdmsRateLimitError)
```
- **Constants** :21-28: `DEFAULT_TIMEOUT = 30`, `TOKEN_REFRESH_MARGIN_SECONDS = 60`, `MAX_RETRIES_5XX = 3`, `DEFAULT_LIFE_TIME = 900`.
- **Auth**: `login()` (:96) POSTs to `/restApi/login`, caches `access_token` + expiry; `_token_is_fresh()` (:90) applies the 60s margin; `ensure_token()` (:154).
- **Retries** in `_request()` (:158): transport errors and 5xx → `time.sleep(2 ** (attempt - 1))`, 3 attempts; 401 → one re-login and one retry, then `VdmsAuthError("Still 401 after re-login")` (:205); 429 → `VdmsRateLimitError` immediately, no backoff.
- **Pagination**: `iter_versorgung()` (:254) is a generator walking `page` until `data.get('maxPage', 0)` (:274), with `start_page` and `max_pages` for resume/cap.
- **Credentials live in the ORM**, per-company, not config: `vdms.backend` (`models/vdms_backend.py:21`) with `username`/`password` `fields.Char(groups='maptara_vdms_connector.group_vdms_manager')` (:43-49). Token is persisted to a separate `vdms.session` via `_persist_token` (:114). `_get_client()` (:91) reads via `sudo()` and raises when unset.
- `external_dependencies: {'python': ['requests']}` — `maptara_vdms_connector/__manifest__.py:26-28`. **The only module in the repo that declares any.**

Contrast: egeko's endpoint is **not** in the ORM. `resolve_egeko_url()` (`maptara_san_provider_egeko/models/egeko_request.py:63`) resolves env var `MAPTARA_EGEKO_URL` first, then `ir.config_parameter` key `maptara_san_provider_egeko.egeko_url` (:39-40), with **no default** — and a 25-line comment (:42-62) explaining that a default would file real eKVs with real Krankenkassen from a staging clone. Credentials are on the `maptara.service.provider` row (`username`/`password`/`client_key`, `maptara_provider.py:163-165`). Two different credential-location conventions in one codebase; pick one.

### 7. Document storage / naming / retrieval

- Interface: `maptara_document_manager_api/models/document_service.py` — `upload(document_id, file_data, filename, mimetype=None, notes=None)` → int, `download(attachment_id)` → `{'data': base64, 'filename', 'mimetype'}`, `submit_document(...)` atomic create+attach.
- Implementation: `maptara_document_manager/models/document_service_impl.py` — "upload / download against `ir.attachment` as the local backend" (:7). `upload` (:59) rejects a missing filename (:65-66), then `self.env['ir.attachment'].create({'name': filename, 'datas': file_data, 'res_model': 'maptara.document', ...})` (:74-76). `download` (:94) returns `att.datas` and logs an audit row with `outcome='denied'` when the `ir.attachment` is gone (:128-131).
- Metadata model `maptara.document.attachment` (`maptara_document_manager_api/models/document_attachment.py:32`) carries `version` (:43), `is_current` (:48), `provider_code` (:115) + `provider_ref` (:122) — **already designed for non-local backends** — plus `filename`/`mimetype`/`size` (:134-136).

**What the provider layer does instead, today:** `maptara.provider.activity` stores wire traffic as raw `fields.Binary(attachment=True)` on itself — `request_data`/`request_data_filename`, `response_data`/`response_data_filename`, `file_data`/`file_name`, `export_zip_data`/`export_zip_filename` (`maptara_san_provider_base/models/maptara_provider.py:521-550`), with `action_export()` (:554) zipping them. Naming is done inline in the zeep plugin: `f'EGEKO_REQ_{operation.name}_{datetime.now().strftime("%Y%m%d_%H%M%S")}.xml'` (`egeko_request.py:214`) and `EGEKO_RES_...` (:228). It does not touch MDM at all.

### 8. Background jobs — the one-shot dynamic `ir.cron`

**There is no `product_catalog_import` module in this repo.** The pattern lives in `maptara_codexa_vdms/models/vdms_sync_run.py`. Copy this:

- Model `vdms.sync.run` (:29) holds the job: `state` draft/running/done/error (:55), `next_page` (:61), `page_total`, `contracts_seen`, `log` Text, `error_message`, `started_at`/`finished_at`.
- `_schedule_job(method_name, description)` (:84):
```python
self.env['ir.cron'].sudo().create({
    'name': f'[VDMS Sync] {description} — #{self.id}',
    'model_id': self.env['ir.model']._get_id(self._name),
    'state': 'code',
    'code': f"env['{self._name}'].browse({self.id})._do_sync()",
    'interval_number': 99,
    'interval_type': 'months',
    'active': True,
    'user_id': self.env.uid,
})
```
The 99-month interval is the trick: it fires once on the next cron tick, outside any HTTP request, so `limit-time-real` does not apply (:86-89).
- `_checkpoint()` (:120): `if not config['test_enable']: self.env.cr.commit()` — commits per page so the run survives a kill and resumes. Guarded because Odoo forbids commit on the test cursor.
- `action_start` (:130) writes state + `next_page=0`, calls `_checkpoint()` **before** `_schedule_job` so the cron worker sees a committed record (:147-148), returns a `display_notification` client action with a `next` back to the form (:100).
- `action_continue` (:153) resumes from `next_page`.

Second, simpler pattern for a *recurring* cron created from Python rather than XML: `maptara_san_provider_base/hooks.py` — `post_init_hook` (manifest :18) → `_ensure_egeko_cron` (:25), idempotent via `env.ref(CRON_XMLID, raise_if_not_found=False)` (:35), then `env['ir.cron'].create(...)` (:53) followed by an explicit `env['ir.model.data'].create({... 'noupdate': True})` (:66-72) to mint the external ID. The stated rationale: "XML data files are unreliable across upgrades" (:16).

### 9. Pluggable-behaviour patterns — the best example is inside the module being rewritten

**`maptara_san_provider_base`'s name-based dispatch is genuinely the best plugin point in the repo for this problem**, and the comment above it (`models/maptara_provider.py:580-597`) is a 17-line incident report explaining why:

```python
provider_type = fields.Selection([], string="Provider Type")          # :162, base
provider_type = fields.Selection(selection_add=[('egeko', 'Egeko')])  # egeko/models/maptara_provider.py:72
...
def _provider_method(self, operation):        # :599
    provider_type = self.provider_id.provider_type
    if not provider_type: return None
    return getattr(self, f'_{operation}_{provider_type}', None)
def _require_provider_method(self, operation):  # :613 — raises, 3 distinct UserErrors
```
Empty `Selection([])` + `selection_add` in each driver module is the registry; `getattr(self, f'_{op}_{type}')` is the lookup; `_require_provider_method` (:613) for human-triggered paths raises three distinguishable `UserError`s (no provider / no type / module not installed), and `_log_unroutable` (:644) for cron paths writes `error_msg` without raising, because "the poll cron walks every waiting order in one transaction" (:648-650). `action_test_connection` uses the same shape independently: `getattr(self, f'_test_connection_{self.provider_type}', None)` (:419).

Other candidates, ranked: the `*_api` AbstractModel pattern (§5) — 12 instances, but it is one-implementation-per-name, which is the wrong shape for "three drivers, pick one at runtime"; `models.AbstractModel` service objects with no API split (`maptara_pos_register/models/fiskaly_client.py:46`, all HTTP funnelled through one patchable `_kassensichv_rpc` at :64 — good testability idea); the OWL-side `registry.category("maptara_pos.docs_panel_actions")` (`maptara_pos_sale_glue/.../ekv_docs_action.js:54`), 3 entries, frontend only.

### 10. Dependency reality check

| Package | Declared anywhere? | Used where |
|---|---|---|
| `zeep` | **NO.** Not in `requirements.txt` (9 lines, PyPDF2/zxing-cpp/pypdfium2/img2pdf/py-zerox/aioshutil/google-genai), not in `requirements-custom.txt` (22 lines), not in `packages-custom.txt` (1 line: `poppler-utils`), not in `pyproject.toml` (`dependencies = []`, :9), not in `uv.lock` (0 hits), and `maptara_san_provider_egeko/__manifest__.py` has **no `external_dependencies` key at all**. | `maptara_san_provider_egeko/models/egeko_request.py:2` `from odoo.tools.zeep import Client, Transport, Plugin` (Odoo 19 vendors a shim at `odoo/tools/zeep/`), and :7 `from zeep.cache import InMemoryCache` — a **direct** import of upstream zeep, bypassing the shim. |
| `paramiko` | **NO** | 0 occurrences in the entire repo |
| `pysftp` | **NO** | 0 occurrences |
| `sftp` (any casing) | — | 0 occurrences outside `docs/` |
| `xmlschema` | **NO** | 0 occurrences |
| `lxml` | **NO** (Odoo core dep, so it happens to be there) | `egeko_request.py:5` + 11 test files |
| `requests` | **YES**, exactly once: `maptara_vdms_connector/__manifest__.py:26-28` `'external_dependencies': {'python': ['requests']}` | also imported bare by `egeko_request.py:6` (`from requests import Session`) with no declaration |

Nine manifests declare `'external_dependencies': {}` — an empty dict — including `maptara_san_sale`, `maptara_san_claim`, `maptara_base`. Only `maptara_vdms_connector` declares anything real.

**Correction to the brief's "3 hits for `from zeep.cache`":** in this worktree there is **1** (`egeko_request.py:7`). `grep -rn "from zeep"` across all `.py` returns exactly one line. The finding stands regardless: zeep is undeclared, and one of its imports sidesteps Odoo's vendored shim, so a `pip install odoo` without the SOAP extra gives an `ImportError` at module load, not a graceful degrade.

**Implication for azh/NOVENTI:** SFTP has zero prior art here — no library, no declaration, no credential store for host keys. That is net-new.

---

## Parking Lot

1. **An OpenSpec proposal already covers part of this.** `openspec/changes/consolidate-ece-provider-settings/proposal.md` — :86-87 names the exact same three reverse dependencies derived above (`maptara_san_provider_egeko`, `maptara_pos_sale_glue`, `maptara_codexa_api`), and :29-31 says "Six methods in `maptara_san_provider_egeko/models/maptara_provider.py` currently branch on…". *Impact:* risk of duplicating or colliding with in-flight work (CR #1174). *Confidence:* high. *Next:* read that proposal before designing.

2. **PCH portal controllers get an implicit `active` filter from provider_base.** `maptara_patient_connect_hub/controllers/patient_service_view.py:23` and `patient_patient_view.py:639` `search()` `maptara.service.provider` with no `active` clause; the field only exists because provider_base adds it (`maptara_provider.py:165`). *Impact:* installing/uninstalling provider_base silently changes what a public portal lists. No test covers it. *Confidence:* high (field ownership verified). *Next:* move `active` to `maptara_base` regardless of the driver work.

3. **`maptara_case_mdm_glue/models/sale_order.py:118-144`** is a commented-out `action_send_ekv` override waiting on a hook that was never built; the comment at :143-144 offers two options, one of which is "overrides the wizard's `_send_ekv` method". *Impact:* a driver rewrite is the moment to either build the hook or delete the comment. *Confidence:* high. *Next:* decide, don't inherit the ambiguity.

4. **`maptara_san_provider_egeko/tools/ekv_doctor.py:44-45` hardcodes container paths** (`/opt/odoo/custom-addons/...`, `/mnt/extra-addons/...`) and :21 hardcodes a *different developer's worktree* (`maptara-demo-ece-1173`). *Impact:* diagnostic tooling silently degrades depending on deploy layout. *Confidence:* high. *Next:* low priority, but don't copy that into the driver layer.

5. **`maptara.provider.activity` archives wire traffic to `fields.Binary` on itself, not to MDM**, despite provider_base already depending on `maptara_document_manager_api` (manifest :30) and `maptara.document.attachment` having `provider_code`/`provider_ref` columns built for exactly this. *Impact:* request/response archives are invisible to the document system, have no retention policy, and `action_export()` (`maptara_provider.py:554`) exists because of it. *Confidence:* high. *Next:* decide whether the driver layer inherits this or fixes it.
