Internal · the goal, in one page

One socket, many providers

Every insurance and billing partner we integrate with — Egeko today, Noventi/azh next, MIP and ZHP X3 after, and now a direct GKV-DTA route — should plug into the same socket. Right now each one brings its own wiring, and the "vendor-neutral" base module is named after a vendor.

What this is actually for

CR #1197 — Noventi/azh §302 billing — is the deliverable. This refactor is the thing that stops it becoming integration #3 with its own private SFTP client, and it is scoped to serve that, not the other way round. If the refactor ever looks like it is competing with #1197 for time, the refactor loses.

The wider ECE programme sits under CR #1173, with #1174 (settings consolidation) and #1271–1276 (MIP and inbound test cases) as siblings. Not in scope: eKV via AZH's KVCheck — we already do eKV.

22 Sep — CR #1319 arrived and, honestly, was expected. A Lovable-generated spec and module for §302 that puts two routes in Release 1: azh (the above) and direct GKV-DTA — EDIFACT ESOL/AUF per TA1 v21→v22, per-payer encryption, DAVASO, Dakota, KOTR. The generated module integrates with zero existing models, does not install on Odoo 19, and once fixed runs no billing process — 46 of its 142 documented claims are true, and those are field names. It does not change this architecture: direct DTA is a second plugin on the same contract, and it is Release 2. How it lands: 07. The module measured: 08.

2026-09-19, revised 22 Sep for CR #1319 · overview for the scope/subtask breakdown that follows.
Same architecture drawn two ways in diagram variants — the wide one expands the VDMS import.
Detail lives in the programme report and R1.

Why bother

Plugins on the contract
6

Egeko live · azh · MIP · ZHP X3 · direct DTA (#1319, Release 2)

Transport drivers
4+?

SOAP · SFTP · REST · file-drop (Dakota) — MIP's md5 key may add a fifth

Vendor names in the "neutral" base
106

non-test lines, 10 of 14 files

Transport code in the base
0

all of it lives in one plugin

Today's shape is inverted: the base carries one vendor's vocabulary — its cron, its code list, its field labels — while the plugin carries all the reusable networking. Add a provider and you edit shared code. Add five and you get five retry policies, five credential conventions, and five ways of logging patient data.

This is not a tidiness project

Three concrete things are waiting on it: azh §302 billing (the next ticket, needs SFTP that does not exist anywhere in the repo), MIP and ZHP X3 (committed direction, no dates), and a VDMS Excel/XML import that will want the same transports from a completely different part of the tree.

There is also a licence angle, and it is bigger than these two modules. Across all 104 manifests: 9 modules still carry "author": "baotnp"san_claim, san_flat_rate, san_followup_order, san_industry_fsm, san_prescription_extract, san_provider_base, san_provider_egeko, san_purchase, san_sale — and 17 are OPL-1. This refactor clears two of the nine; it does not finish the 2027 job.

Within these two, the Python exposure is two manifest lines. The real exposure is 8 MB of a vendor's copyrighted PDFs plus a competitor's captured production XML sitting in the repo, which the rewrite is the natural moment to stop shipping.

The rule everything follows

A driver knows nothing.
Not the domain. Not the provider. Not where the password came from.

It moves bytes to a destination and reports what happened. The moment a SoapDriver learns what an eKV is, we have rebuilt today's problem with more files and a nicer diagram.

What we're building

Boxes: unchanged the problem rewritten new later, no date
Lines: Odoo depends new depends on a driver credentials, passed in ⇢ rest driver used All numbers are wc -l / AST counts.
TODAY  ·  8 588 py lines, 57 files AFTER  ·  1 262 rewritten, 3 654 untouched base (Odoo core) base (Odoo core) no transport layer exists SOAP client buried in egeko_request.py · HTTP client buried in VdmsClient zeep imported, declared in no requirements file · paramiko: 0 occurrences repo-wide NEW — transport drivers  ·  depends: ['base'] only driver_soap ~260 connection · soap_driver 1 consumer — outlier driver_sftp ~230 put·list·get·dry-run 2 consumers · net new driver_rest ~200 from VdmsClient 3–4 — the common case + driver_filedrop for Dakota (#1319, Release 2) — ~80 lines, one consumer, it is the seam not the reuse maptara_base  12 124 ln · 30 dependents maptara_base  unchanged maptara_san_sale · maptara_san_claim · maptara_document_manager_api  (provider_base depends on all 3) maptara_san_sale · maptara_san_claim · maptara_document_manager_api maptara_san_provider_base  4 658 ln · 9 models · 70 fields · 77 methods models/maptara_provider.py 819 models/sale_order.py — _process_single_order_estimate 127 ln = Egeko's 3-call protocol 508 wizard/maptara_send_ekv_wizard.py — X01/A01, action_new_egeko_code 784 hooks.py — creates cron 'Egeko: Poll Cost Estimate Statuses' on install of the BASE 74 ref_burn 121 · views 486 · acl 13 tests 2 007 · migrations 265 106 Egeko lines across 10 of 14 non-test files · 0 lines of transport code ACL line 9: base.group_user = 1,1,1,1 on the model holding password + client_key maptara_san_provider_base  the socket · eKV stays put models/provider_driver.py — NEW: 7 outbound + 3 inbound ops, capability flags ~200 maptara_provider.py — de-brand chatter, drop maptara.provider.uom (0 methods) 819 sale_order.py + wizard — strings only; the 3-call protocol moves to the plugin 1 292 tests 2 007 unchanged · migrations 265 frozen · ACL: keep read, drop write/unlink out: probe · send_estimate · resend · poll_state · fetch_decision fetch_documents · send_message  |  IN: fetch_orders · fetch_requests · fetch_messages maptara_san_provider_egeko  3 930 ln · 0 dependents · a leaf models/maptara_provider.py 687 — generate_document_template is ONE 263-line method ORM reads + VAT maths + MDM downloads + wire vocabulary, no intermediate representation models/egeko_request.py — zeep client, 8 ops, all positional arg0/arg1/arg2 575 tests 1 382 · tools 1 070 7.6 MB vendor PDFs + DTD egeko ⇢ soap 1 262 rewritten ~200 → driver azh §302 ⇢ sftp Release 1 · Oct no test env direct DTA ⇢ filedrop #1319 · Release 2 greenfield MIP ⇢ md5? creds LIVE ZHP X3 ⇢ ? no docs each plugin: implement 7 methods, declare capabilities, pick a driver — zero shared edits maptara_codexa_api · maptara_pos_sale_glue  — 15 live refs, 6 modules +15 codexa modules behind codexa_api, coupled by two column names maptara_codexa_api · maptara_pos_sale_glue  untouched FK at codexa_api/provider_site.py:20 is NOT sudo → ACL must keep read separate island — depends: ['base','mail','maptara_masterdata'] vdms_connector → codexa_vdms → codexa.contract ✓ version ✓ item ✓ VdmsClient — the repo's second private HTTP client codexa.contract.rule ✗ — imported contracts arrive with no prices same island — reaches the drivers, never the provider tree vdms_connector ⇢ driver_rest own glue + Excel/XML import  · separate modules · later wanted: the import should produce price rules too — mapping decision first WHERE CREDENTIALS AND ENDPOINTS COME FROM maptara.service.provider  password / client_key = plain Char, no groups= ir.model.access.csv:9 — base.group_user 1,1,1,1: read, edit, DELETE endpoint: env var → ir.config_parameter → refuse, never default …implemented once, inside egeko. Nobody else has the pattern. maptara.service.provider vdms.backend secrets behind groups= · each caller owns its own storage ConnectionConfig — plain Python, no ORM host · user · secret · timeout · retries · endpoint (env beats DB, never defaults) Adding a provider = editing shared code 195 test methods · 3 389 test lines · 0 assert what goes on the wire 9 modules still authored "baotnp" · 17 still OPL-1 Adding a provider = one new module, zero shared edits drivers test with no database · a misspelled wire key turns the suite red ekv_body_diff.py (630 ln, currently called by nothing) becomes the CI oracle The work is four files: provider_base/maptara_provider.py 819 · egeko/maptara_provider.py 687 · egeko_request.py 575 · provider_base/sale_order.py 508 2 589 lines = 68% of the 3 834 runtime lines in scope. Everything else on the plan is string work, one CSV line, and a model with zero methods.
The diff, at file and method level. Every number is wc -l or an AST count taken on 2026-09-19 — see the file map and symbol map for the per-file and per-method verdicts behind each box. Grey is untouched, and most of the graph is grey: 3 654 of 8 588 lines are tests and frozen migrations that nothing in this plan edits.
Design against REST, not against Egeko

Counting consumers changes which protocol is "normal". REST has 3–4 (azhIndex, MIP pending confirmation, the VDMS connector, probably the Excel/XML import). SFTP has 2. SOAP has exactly one, and it is the module we are rewriting.

So Egeko is the outlier, and the contract must not be shaped around it. WSDL type factories, positional arg0/arg1/arg2, a live zeep object graph, German comma decimals, four status buckets, zeep's private _value_1 — none of that generalises to anything else we will ever integrate. Abstracting from the strangest implementation in the set produces an interface that fits one caller badly and every other caller worse.

Shape the contract on request / response / status / retry, and let the SOAP plugin do its own awkward translation on its own side. That is the plugin's job, and it is the only place that should know what a WSDL is.

The honest problem with SoapDriver: it has one consumer

Building a generic driver for a single caller is speculative generality, and normally I would argue against it. Two reasons it still earns its place, neither of which is reuse:

1. It is the test seam. Constructing EgekoRequest today fetches a WSDL, which is why every test mocks the entire class and why zero tests exercise payload assembly. Without a seam, a misspelled wire key is caught by nothing.

2. The code already exists. It is ~200 working lines; moving them is close to free.

The viable alternative is to leave the SOAP code inside the rewritten Egeko module, behind the same contract. That is a defensible call — you lose the test seam, which is the part worth fighting for, not the driver.

Dropped: extracting the eKV domain into its own module

Considered in detail and not doing it. It was 4–6 days for zero behaviour change, and it was the only work in the programme that could delete production records if the ir_model_data migration went wrong. The eKV domain stays inside maptara_san_provider_base. Reasoning kept in R1 §3a so nobody re-proposes it from scratch in six months.

Why drivers sit at base, not maptara_base

maptara_base is 12 124 lines and pulls in sale, account, product, portal, l10n_din5008. A module that opens an SSH session has no business dragging the accounting stack behind it. And maptara_san_base is a hidden compatibility shim — not a home for anything.

Where credentials live

Not in the driver. It takes host, user and key as arguments; the caller owns storage — maptara.service.provider for ECE, vdms.backend for VDMS. The moment a driver grows a credential model it has an opinion about who it is talking to, and it stops being reusable.

The plan

Day figures are judgement, not measurement
Phase0

Foundations do now

Baseline the 112-test harness. Strip the vendor vocabulary out of the base. Fix the ACL that currently gives every internal user delete rights on every stored credential. Delete the model nothing reads. Then declare the driver contract — ~200 lines, purely additive, no migration.

~4 d
Phase1

Rewrite the Egeko provider the actual deliverable

1 448 lines, one 263-line method mixing ORM reads, VAT arithmetic, document downloads and wire vocabulary, with zero tests on payload assembly. Write the characterisation tests first, then rebuild it on the contract as payload-mapping only.

This is the one figure that does not compress. Being fast does not help when nothing tells you the bytes changed.

10–15 d
Phase2

azh enablement when the batch work starts

The SFTP driver (net new — paramiko appears nowhere in the repo today), plus claim-batch → submission wiring. Ships with a local-directory dry-run from day one, because azhDirekt has no test environment and that dry run is the only pre-production test that will ever exist for it.

4–6 d
Phase3

Everything with no date later

MIP (protocol pending confirmation) and ZHP X3 as they arrive · the Excel/XML import as a separate module glued to VDMS · the licence and dependency sweep across the remaining 7 baotnp modules.

Release 2 — direct GKV-DTA (CR #1319). A second plugin, provider_dta, on the unchanged contract: EDIFACT ESOL/AUF per TA1 v21 and v22, per-payer certificate encryption, DAVASO pre-check, Dakota file-drop + mailbox returns, KOTR payer-file import, six IK kinds, IK-Umlenkung, bundling engine, P302 parameter set. Every line greenfield. Not costed here — it is a release, not a phase.

Roughly 4 days before the shape is real

Phase 0 is small and carries almost no migration risk — the only data-touching parts are a cron rename and deleting an unread model, both verifiable against the production-read database now available. After it, every later phase plugs in rather than negotiating.

Explicitly out of scope

Named here so nobody has to ask twice, and so they do not creep back in as "while we are in there".

KVCheck / eKV via AZH a GraphQL driver bulk import in the provider contract rewriting the existing tests multi-provider abstraction inside VDMS §302 record mapping — deferred, not dropped eKV module extraction a SOAP driver — earns its place on testability, not reuse direct GKV-DTA (#1319) — Release 2, not dropped the Lovable-generated maptara_302 module as code
Not doingBecause
KVCheck (eKV via AZH)We already do eKV, across 11 modules. Buying it a second time from a second vendor is not a plan.
A GraphQL driverNo provider in scope uses it. Drivers built for hypothetical providers are maintenance you pay for and never use. If one appears, it is a new class next to the others — that is the point of the contract.
Bulk import through the provider contractPer-order exchange and bulk catalogue ingestion are different domains with different lifecycles. The Excel/XML importer is a parser, not a transport; it belongs in the VDMS extension that owns that data.
Rewriting the 112 base testsTheir docstrings carry real incident dates and row counts. They are the only thing making this refactor verifiable, and they are worth more than our opinion of their style.
The eKV module splitDropped, not deferred. 4–6 days for zero behaviour change, and the only work in the programme that could delete production records via a wrong ir_model_data migration. The eKV domain stays in maptara_san_provider_base.
Shaping the contract around SOAPSOAP has one consumer; REST has three or four. Egeko's WSDL type factory, positional arg0/arg1/arg2 and comma decimals generalise to nothing. Design on request/response/status/retry and let the SOAP plugin translate on its own side.

How we know it worked

Test
1

A new provider ships without editing shared code. New module, selection_add, implement the contract, pick a driver. Zero lines changed in maptara_san_provider_base.

Test
2

A grep for vendor names in the base returns nothing. No egeko, no X01, no Egekonummer, no cron named after a company.

Test
3

A misspelled wire key turns the suite red. Today it does not — which is why the Egeko rewrite starts with tests and not with code.

Test
4

The VDMS tree can open an SFTP session without depending on anything in the provider or eKV tree. If it cannot, the drivers are in the wrong place.

Test
5

A payer-initiated Direktauftrag arrives in Maptara without a human going to look for it, and arriving twice creates one record, not two. Ticket #1274.

Test
6

The direct-DTA plugin ships with zero edits to the contract. If provider_dta needs a new operation on maptara.provider.driver, the contract was shaped around azh rather than around the problem. Ticket #1319.

Next: scope and subtask breakdown per phase. This page is the frame for it; the numbers and evidence behind every claim are in the programme report, R1, and the four surveys under .agent-work/provider-refactor/.

Internal planning document. Nothing described here has been implemented.