Metadata-Version: 2.5
Name: kommunalassist-core
Version: 0.5.0
Summary: Kern der Kommunalassist-Familie: Mandant, Termin-Engine (BRMS-Regelwerk mit Freigabe-Workflow), Adapter-Ports
Author-email: Achim Dehnert <achim.dehnert@iil.gmbh>
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: django<6.0,>=5.0
Requires-Dist: pyyaml>=6.0
Provides-Extra: dev
Requires-Dist: import-linter>=2.0; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-django; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

# kommunalassist-core

Der gemeinsame Kern der **Kommunalassist**-Familie: **Mandant**, **Termin-Engine**
(Regelwerk mit Freigabe-Workflow) und die **Ports** zu Fachverfahren und DMS.

> **Namenswechsel 2026-09-10.** Das Paket hiess bis 0.4.0 `iil-assist-core`. Der
> Verteilname trug den Namen des Anbieters, obwohl das Produkt in einer
> Kommunalverwaltung laeuft — und er kollidierte mit `iil-assist`, dem
> Plattform-Mechanismus aus `platform:KONZ-platform-058`. **Der Importname
> bleibt `assist_core`**, ebenso App-Label und Tabellennamen: dort steht kein
> Anbietername, eine Umbenennung haette nur Migrationen gekostet und nichts
> geloest. Aendern muss ein Adopter genau eine Zeile in `requirements.txt`.

Grundlage: `meiki:ADR-044` (Paket-Topologie) · `meiki:ADR-022` v1.1 (die
Termin-Engine gehört in den Kern) · `meiki:ADR-025` (Mandantenfähigkeit).

## Warum es dieses Paket gibt

Die Termin-Engine hat **zwei** Konsumenten: FristAssist führt Wiedervorlagen,
StatistikAssist führt Meldetermine. Ein Meldetermin ist eine Frist mit Stichtag —
dieselbe Rechnung, zwei Zwecke. Läge die Engine hinter FristAssist, bräuchte
StatistikAssist FristAssist. Genau das schließt der Produktschnitt aus.

## Was drin ist

| Modul | Inhalt |
|---|---|
| `models` | `Regelfreigabe` (Regelwerk mit Zustandsmodell `entwurf → eingereicht → freigegeben → abgelehnt`), `RegelAuditEintrag`, `MandantEinstellung`, `TenantAuditEintrag`, Registries (`Verfahren`, `Quellsystem`, `AdapterBindung`, `Feiertagskalender`) |
| `tenancy` | `TenantModel`/`TenantManager` (fail-closed), `tenant_context()`, GUC `app.tenant_id` |
| `registry`/`adapters` | Validierung an der Grenze gegen die Registry-Tabelle; Adapter-Auflösung gegen eine Allowlist |
| `mandant` | Hauspraxis als Konfiguration — ohne gepflegte Einstellung wird **nichts geraten**, die Renderer liefern `None` |
| `regelkatalog` | einmaliger Import von Startbeständen. **Keine Betriebsquelle** — im Betrieb gilt allein die Tabelle |
| `engine` | die Termin-Engine — `berechne_frist()`, Regel-Resolver, `serie()` |
| `ports` | Schnittstellen zu Fachverfahren und DMS; die konkreten Adapter liegen beim Konsumenten |

## Engine

`assist_core.engine` ist die **eine Rechenstelle** für Termine (meiki:ADR-022
v1.1) — zwei Konsumenten (FristAssist, StatistikAssist), eine Implementierung.
`berechne_frist()` rechnet Fristen nach § 187/188/193 BGB mit Bekanntgabefiktion
und Bayern-Kalender-Korrektur; `serie()` leitet daraus wiederkehrende
Meldetermine (monatlich/quartalsweise/jährlich) für StatistikAssist ab. Die
bayerischen Feiertage (`feiertage_bayern()`, inkl. Mariä Himmelfahrt) sind seit
`0.4.0` **Startbestand statt Konstante**: Mariä Himmelfahrt gilt nur in Gemeinden
mit katholischer Mehrheit, das Augsburger Friedensfest nur in Augsburg. Der
Kalender ist deshalb ein Profil je Kennung (`Feiertagskalender`, Import über
`seed_feiertage`), das ein Haus kopieren und abweichend pflegen kann
(KONZ-meiki-009 § 3.2). Der Regel-Resolver liest Fristwerte
aus der Governance-Tabelle `Regelfreigabe`, mit Code-Katalog-Fallback, solange
keine Regel gepflegt ist.

## Mandant

Jede mandantengebundene Tabelle erbt von `assist_core.tenancy.TenantModel`
(`tenant_id` als `BigIntegerField`, platform:ADR-109). Der Manager ist
**fail-closed** — ohne aktiven Mandanten `none()`, nicht ungefiltert:

```python
with transaction.atomic(), tenant_context(4711):
    Verfahren.objects.all()  # Manager + Policy filtern beide
# Einziger Cross-Tenant-Pfad, schreibt TenantAuditEintrag:
Verfahren.objects.all_tenants(grund="Statistik")
```

## RLS

`manage.py assist_rls --apply [--app-rolle assist_app]` legt je Tabelle mit
Mandanten-Spalte eine Policy an — `USING (tenant_id = NULLIF(current_setting(
'app.tenant_id', true), '')::bigint)`, **ohne** OR-Ausweich-Zweige. Kein Mandant
heißt keine Zeile; nur `Regelfreigabe` öffnet über `tenant_scope_id IS NULL` die
globalen Regeln, nie fremde Mandanten. Der GUC ist transaktionslokal, Aufrufer
brauchen also `transaction.atomic()`. `--app-rolle` setzt `NOBYPASSRLS` und
entzieht `UPDATE`/`DELETE` auf den Audit-Tabellen; `--check` endet mit Exit ≠ 0,
wenn eine Policy fehlt oder einen aufmachenden Zweig trägt.

## Was **nicht** drin ist

Kein Hausinhalt. Keine Regelwerke, keine Vorlagen, keine Mandantenwerte, keine
Team- oder Fachbereichsbezeichnungen. Nach `meiki:ADR-044` ist alles, was sich je
Haus unterscheidet, **eine Zeile in Postgres** — nicht Code, nicht YAML.

Prüfbar: *ein zweites Haus geht durch Konfiguration und Uploads in Betrieb, nicht
durch einen Commit.*

## App-Label und Tabellennamen

`assist_core`, also `assist_core_*`. Kein `db_table` auf die Namen des
Herkunfts-Repos — siehe `docs/adr/ADR-001`.

Entschieden am 2026-09-09 zugunsten eines **echten Renames** statt einer
Zustands-Migration, weil es noch keinen produktiven Datenbestand gibt. Für das
Repo, das die Tabellen heute hält (`meiki-lra/frist-hub`), bedeutet das beim
Umstellen eine Umbenennungs-Migration — beim jetzigen Stand risikoarm, mit
produktiven Daten wäre sie es nicht. **Das Zeitfenster ist begrenzt.**

## CI

Jeder PR und jeder Push auf `main` läuft gegen Postgres 16: `ruff check`,
`ruff format --check`, `pytest` und `makemigrations --check` (siehe
`.github/workflows/ci.yml`).

## Stand

`0.4.0` — Tenancy-Basis, RLS-Erzeuger, Registries (KONZ-meiki-009 U3). Noch von
keinem Repo als Abhängigkeit gebunden. `tenant_id` bleibt `BigIntegerField`;
der Vorschlag, in der ganzen Familie auf UUID zu wechseln (KONZ-meiki-009 § 4.1,
D-2), ist offen und hier nicht vorweggenommen.
