Repository: bo4e/BO4E-python (language: python)

Issue:
Implement COM RegionalerAufAbschlag
https://www.bo4e.de/dokumentation/komponenten/com-regionaleraufabschlag
requires 
- #128 
- #83 
- #125 
- #162
- #163
- #164
- #158 
- #152 (see #292)
Implement COM RegionalePreisstaffel
https://www.bo4e.de/dokumentation/komponenten/com-regionalepreisstaffel

Requires
- #140 
- #151


Discussion / hints:

Fix patch:
diff --git a/src/bo4e/com/regionaleraufabschlag.py b/src/bo4e/com/regionaleraufabschlag.py
new file mode 100644
index 000000000..92850b33c
--- /dev/null
+++ b/src/bo4e/com/regionaleraufabschlag.py
@@ -0,0 +1,157 @@
+"""
+Contains RegionalerAufAbschlag class and corresponding marshmallow schema for de-/serialization
+"""
+
+from typing import List, Optional
+
+import attr
+from marshmallow import fields
+from marshmallow_enum import EnumField  # type:ignore[import]
+
+from bo4e.com.com import COM, COMSchema
+from bo4e.com.energiemix import Energiemix, EnergiemixSchema
+from bo4e.com.preisgarantie import Preisgarantie, PreisgarantieSchema
+from bo4e.com.regionalepreisstaffel import RegionalePreisstaffel, RegionalePreisstaffelSchema
+from bo4e.com.tarifeinschraenkung import Tarifeinschraenkung, TarifeinschraenkungSchema
+from bo4e.com.vertragskonditionen import Vertragskonditionen, VertragskonditionenSchema
+from bo4e.com.zeitraum import Zeitraum, ZeitraumSchema
+from bo4e.enum.aufabschlagstyp import AufAbschlagstyp
+from bo4e.enum.aufabschlagsziel import AufAbschlagsziel
+from bo4e.enum.waehrungseinheit import Waehrungseinheit
+from bo4e.validators import check_list_length_at_least_one
+
+
+# pylint: disable=too-few-public-methods, too-many-instance-attributes
+@attr.s(auto_attribs=True, kw_only=True)
+class RegionalerAufAbschlag(COM):
+    """
+    Mit dieser Komponente können Auf- und Abschläge verschiedener Typen im Zusammenhang mit regionalen Gültigkeiten
+    abgebildet werden.
+    Hier sind auch die Auswirkungen auf verschiedene Tarifparameter modelliert, die sich durch die Auswahl eines Auf-
+    oder Abschlags ergeben.
+    """
+
+    # required attributes
+    #: Bezeichnung des Auf-/Abschlags
+    bezeichnung: str = attr.ib(validator=attr.validators.instance_of(str))
+
+    #: Werte für die gestaffelten Auf/Abschläge mit regionaler Eingrenzung
+    staffeln: List[RegionalePreisstaffel] = attr.ib(
+        validator=attr.validators.deep_iterable(
+            member_validator=attr.validators.instance_of(RegionalePreisstaffel),
+            iterable_validator=check_list_length_at_least_one,
+        )
+    )
+
+    # optional attributes
+    #: Beschreibung des Auf-/Abschlags
+    beschreibung: Optional[str] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(str)), default=None
+    )
+
+    #: Typ des Aufabschlages (z.B. absolut oder prozentual)
+    auf_abschlagstyp: Optional[AufAbschlagstyp] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(AufAbschlagstyp)), default=None
+    )
+
+    #: Diesem Preis oder den Kosten ist der Auf/Abschlag zugeordnet. Z.B. Arbeitspreis, Gesamtpreis etc.
+    auf_abschlagsziel: Optional[AufAbschlagsziel] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(AufAbschlagsziel)), default=None
+    )
+
+    #: Gibt an in welcher Währungseinheit der Auf/Abschlag berechnet wird (nur im Falle absoluter Aufschlagstypen).
+    einheit: Optional[Waehrungseinheit] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Waehrungseinheit)), default=None
+    )
+
+    #: Internetseite, auf der die Informationen zum Auf-/Abschlag veröffentlicht sind
+    website: Optional[str] = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(str)), default=None)
+
+    #: Zusatzprodukte, die nur in Kombination mit diesem AufAbschlag erhältlich sind
+    zusatzprodukte: Optional[List[str]] = attr.ib(
+        validator=attr.validators.optional(
+            attr.validators.deep_iterable(
+                member_validator=attr.validators.instance_of(str), iterable_validator=attr.validators.instance_of(list)
+            )
+        ),
+        default=None,
+    )
+
+    #: Voraussetzungen, die erfüllt sein müssen, damit dieser AufAbschlag zur Anwendung kommen kann
+    voraussetzungen: Optional[List[str]] = attr.ib(
+        validator=attr.validators.optional(
+            attr.validators.deep_iterable(
+                member_validator=attr.validators.instance_of(str), iterable_validator=attr.validators.instance_of(list)
+            )
+        ),
+        default=None,
+    )
+
+    #: Durch die Anwendung des Auf/Abschlags kann eine Änderung des Tarifnamens auftreten
+    tarifnamensaenderungen: Optional[str] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(str)), default=None
+    )
+
+    #: Zeitraum, in dem der Abschlag zur Anwendung kommen kann
+    gueltigkeitszeitraum: Optional[Zeitraum] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Zeitraum)), default=None
+    )
+
+    energiemixaenderung: Optional[Energiemix] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Energiemix)), default=None
+    )
+    """
+    Der Energiemix kann sich durch einen AufAbschlag ändern (z.B. zwei Cent Aufschlag für Ökostrom).
+    Sollte dies der Fall sein, wird hier die neue Zusammensetzung des Energiemix angegeben.
+    """
+
+    vertagskonditionsaenderung: Optional[Vertragskonditionen] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Vertragskonditionen)), default=None
+    )
+    """
+    Änderungen in den Vertragskonditionen;
+    Falls in dieser Komponenten angegeben, werden die Tarifparameter hiermit überschrieben.
+    """
+
+    garantieaenderung: Optional[Preisgarantie] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Preisgarantie)), default=None
+    )
+    """
+    Änderungen in den Garantievereinbarungen;
+    Falls in dieser Komponenten angegeben, werden die Tarifparameter hiermit überschrieben.
+    """
+
+    einschraenkungsaenderung: Optional[Tarifeinschraenkung] = attr.ib(
+        validator=attr.validators.optional(attr.validators.instance_of(Tarifeinschraenkung)), default=None
+    )
+    """
+    Änderungen in den Einschränkungen zum Tarif;
+    Falls in dieser Komponenten angegeben, werden die Tarifparameter hiermit überschrieben.
+    """
+
+
+class RegionalerAufAbschlagSchema(COMSchema):
+    """
+    Schema for de-/serialization of RegionalerAufAbschlag
+    """
+
+    class_name = RegionalerAufAbschlag
+    # required attributes
+    bezeichnung = fields.Str()
+    staffeln = fields.List(fields.Nested(RegionalePreisstaffelSchema))
+
+    # optional attributes
+    beschreibung = fields.Str(default=None)
+    auf_abschlagstyp = EnumField(AufAbschlagstyp, default=None)
+    auf_abschlagsziel = EnumField(AufAbschlagsziel, default=None)
+    einheit = EnumField(Waehrungseinheit, default=None)
+    website = fields.Str(default=None)
+    zusatzprodukte = fields.List(fields.Str(), default=None)
+    voraussetzungen = fields.List(fields.Str(), default=None)
+
+    tarifnamensaenderungen = fields.Str(default=None)
+    gueltigkeitszeitraum = fields.Nested(ZeitraumSchema, default=None)
+    energiemixaenderung = fields.Nested(EnergiemixSchema, default=None)
+    vertagskonditionsaenderung = fields.Nested(VertragskonditionenSchema, default=None)
+    garantieaenderung = fields.Nested(PreisgarantieSchema, default=None)
+    einschraenkungsaenderung = fields.Nested(TarifeinschraenkungSchema, default=None)


Test patch:
diff --git a/tests/test_energiemix.py b/tests/test_energiemix.py
index 7c9d15a47..5a77fb90a 100644
--- a/tests/test_energiemix.py
+++ b/tests/test_energiemix.py
@@ -10,24 +10,26 @@
 from bo4e.enum.sparte import Sparte
 from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
 
+example_energiemix = Energiemix(
+    energiemixnummer=2,
+    energieart=Sparte.STROM,
+    bezeichnung="foo",
+    gueltigkeitsjahr=2021,
+    anteil=[
+        Energieherkunft(
+            erzeugungsart=Erzeugungsart.BIOGAS,
+            anteil_prozent=Decimal(40),
+        ),
+    ],
+)
+
 
 class TestEnergiemix:
     @pytest.mark.parametrize(
         "energiemix, expected_json_dict",
         [
             pytest.param(
-                Energiemix(
-                    energiemixnummer=2,
-                    energieart=Sparte.STROM,
-                    bezeichnung="foo",
-                    gueltigkeitsjahr=2021,
-                    anteil=[
-                        Energieherkunft(
-                            erzeugungsart=Erzeugungsart.BIOGAS,
-                            anteil_prozent=Decimal(40),
-                        ),
-                    ],
-                ),
+                example_energiemix,
                 {
                     "energiemixnummer": 2,
                     "energieart": "STROM",
diff --git a/tests/test_preisgarantie.py b/tests/test_preisgarantie.py
index f1fe51bd3..fa9731b1a 100644
--- a/tests/test_preisgarantie.py
+++ b/tests/test_preisgarantie.py
@@ -7,19 +7,21 @@
 from bo4e.enum.preisgarantietyp import Preisgarantietyp
 from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
 
+example_preisgarantie = Preisgarantie(
+    preisgarantietyp=Preisgarantietyp.NUR_ENERGIEPREIS,
+    zeitliche_gueltigkeit=Zeitraum(
+        startdatum=datetime(2020, 1, 1, tzinfo=timezone.utc),
+        enddatum=datetime(2020, 4, 1, tzinfo=timezone.utc),
+    ),
+)
+
 
 class TestPreisgarantie:
     @pytest.mark.parametrize(
         "preisgarantie, expected_json_dict",
         [
             pytest.param(
-                Preisgarantie(
-                    preisgarantietyp=Preisgarantietyp.NUR_ENERGIEPREIS,
-                    zeitliche_gueltigkeit=Zeitraum(
-                        startdatum=datetime(2020, 1, 1, tzinfo=timezone.utc),
-                        enddatum=datetime(2020, 4, 1, tzinfo=timezone.utc),
-                    ),
-                ),
+                example_preisgarantie,
                 {
                     "beschreibung": None,
                     "preisgarantietyp": "NUR_ENERGIEPREIS",
diff --git a/tests/test_regionalepreisstaffel.py b/tests/test_regionalepreisstaffel.py
index b2a8ba5d5..21da01112 100644
--- a/tests/test_regionalepreisstaffel.py
+++ b/tests/test_regionalepreisstaffel.py
@@ -11,22 +11,24 @@
 from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
 from tests.test_sigmoidparameter import example_sigmoidparameter  # type:ignore[import]
 
+example_regionale_preisstaffel = RegionalePreisstaffel(
+    einheitspreis=Decimal(40.0),
+    staffelgrenze_von=Decimal(12.5),
+    staffelgrenze_bis=Decimal(25.0),
+    sigmoidparameter=example_sigmoidparameter,
+    regionale_gueltigkeit=RegionaleGueltigkeit(
+        gueltigkeitstyp=Gueltigkeitstyp.NUR_IN,
+        kriteriums_werte=[KriteriumWert(kriterium=Tarifregionskriterium.POSTLEITZAHL, wert="01069")],
+    ),
+)
+
 
 class TestRegionalePreisstaffel:
     @pytest.mark.parametrize(
         "regionale_preisstaffel, expected_json_dict",
         [
             pytest.param(
-                RegionalePreisstaffel(
-                    einheitspreis=Decimal(40.0),
-                    staffelgrenze_von=Decimal(12.5),
-                    staffelgrenze_bis=Decimal(25.0),
-                    sigmoidparameter=example_sigmoidparameter,
-                    regionale_gueltigkeit=RegionaleGueltigkeit(
-                        gueltigkeitstyp=Gueltigkeitstyp.NUR_IN,
-                        kriteriums_werte=[KriteriumWert(kriterium=Tarifregionskriterium.POSTLEITZAHL, wert="01069")],
-                    ),
-                ),
+                example_regionale_preisstaffel,
                 {
                     "regionaleGueltigkeit": {
                         "gueltigkeitstyp": "NUR_IN",
@@ -37,7 +39,8 @@ class TestRegionalePreisstaffel:
                     "staffelgrenzeVon": "12.5",
                     "staffelgrenzeBis": "25",
                 },
-                id="maximal attributes",  # the messing sigmoidparameter is tested in the Preisstaffel tests
+                id="maximal attributes"
+                # the messing sigmoidparameter is tested in the Preisstaffel tests
             ),
         ],
     )
diff --git a/tests/test_regionaleraufabschlag.py b/tests/test_regionaleraufabschlag.py
new file mode 100644
index 000000000..c4b0bbc5a
--- /dev/null
+++ b/tests/test_regionaleraufabschlag.py
@@ -0,0 +1,51 @@
+import pytest  # type:ignore[import]
+
+from bo4e.com.regionaleraufabschlag import RegionalerAufAbschlag, RegionalerAufAbschlagSchema
+from bo4e.com.tarifeinschraenkung import Tarifeinschraenkung
+from bo4e.enum.aufabschlagstyp import AufAbschlagstyp
+from bo4e.enum.aufabschlagsziel import AufAbschlagsziel
+from bo4e.enum.waehrungseinheit import Waehrungseinheit
+from tests.serialization_helper import assert_serialization_roundtrip  # type:ignore[import]
+from tests.test_energiemix import example_energiemix  # type:ignore[import]
+from tests.test_preisgarantie import example_preisgarantie  # type:ignore[import]
+from tests.test_regionalepreisstaffel import example_regionale_preisstaffel  # type:ignore[import]
+from tests.test_vertragskonditionen import example_vertragskonditionen  # type:ignore[import]
+from tests.test_zeitraum import example_zeitraum  # type:ignore[import]
+
+
+class TestRegionalerAufAbschlag:
+    @pytest.mark.parametrize(
+        "regionaler_auf_abschlag",
+        [
+            pytest.param(
+                RegionalerAufAbschlag(
+                    bezeichnung="Foo",
+                    beschreibung="Bar",
+                    auf_abschlagstyp=AufAbschlagstyp.RELATIV,
+                    auf_abschlagsziel=AufAbschlagsziel.ARBEITSPREIS_HT,
+                    einheit=Waehrungseinheit.CT,
+                    website="https://www.hochfrequenz.de",
+                    zusatzprodukte=["ein standmixer", "ein thermomix"],
+                    voraussetzungen=["lecker essen", "mit ökostrom gekocht"],
+                    tarifnamensaenderungen="Super-Duper Tarif",
+                    staffeln=[example_regionale_preisstaffel],
+                    gueltigkeitszeitraum=example_zeitraum,
+                    energiemixaenderung=example_energiemix,
+                    vertagskonditionsaenderung=example_vertragskonditionen,
+                    garantieaenderung=example_preisgarantie,
+                    einschraenkungsaenderung=Tarifeinschraenkung(),
+                ),
+                id="maximal attributes",
+            ),
+        ],
+    )
+    def test_serialization_roundtrip(self, regionaler_auf_abschlag: RegionalerAufAbschlag):
+        """
+        Test de-/serialisation
+        """
+        assert_serialization_roundtrip(regionaler_auf_abschlag, RegionalerAufAbschlagSchema())
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = RegionalerAufAbschlag()
+        assert "missing 2 required" in str(excinfo.value)
diff --git a/tests/test_vertragskonditionen.py b/tests/test_vertragskonditionen.py
index cf21cef4c..0d374a712 100644
--- a/tests/test_vertragskonditionen.py
+++ b/tests/test_vertragskonditionen.py
@@ -5,23 +5,25 @@
 from bo4e.com.zeitraum import Zeitraum
 from bo4e.enum.zeiteinheit import Zeiteinheit
 
+example_vertragskonditionen = Vertragskonditionen(
+    beschreibung="Foobar",
+    anzahl_abschlaege=Decimal(3),
+    vertragslaufzeit=Zeitraum(
+        startdatum=datetime(2012, 9, 21, tzinfo=timezone.utc),
+        enddatum=datetime(2013, 10, 11, tzinfo=timezone.utc),
+    ),
+    kuendigungsfrist=Zeitraum(einheit=Zeiteinheit.WOCHE, dauer=Decimal(3)),
+    vertragsverlaengerung=Zeitraum(einheit=Zeiteinheit.TAG, dauer=Decimal(14)),
+    abschlagszyklus=Zeitraum(einheit=Zeiteinheit.TAG, dauer=Decimal(5)),
+)
+
 
 class TestVertragskonditionen:
     def test_vertragskonditionen_with_optional_attributes(self):
         """
         Test de-/serialisation of Vertragskonditionen (only has optional attributes).
         """
-        vertragskonditionen = Vertragskonditionen(
-            beschreibung="Foobar",
-            anzahl_abschlaege=Decimal(3),
-            vertragslaufzeit=Zeitraum(
-                startdatum=datetime(2012, 9, 21, tzinfo=timezone.utc),
-                enddatum=datetime(2013, 10, 11, tzinfo=timezone.utc),
-            ),
-            kuendigungsfrist=Zeitraum(einheit=Zeiteinheit.WOCHE, dauer=Decimal(3)),
-            vertragsverlaengerung=Zeitraum(einheit=Zeiteinheit.TAG, dauer=Decimal(14)),
-            abschlagszyklus=Zeitraum(einheit=Zeiteinheit.TAG, dauer=Decimal(5)),
-        )
+        vertragskonditionen = example_vertragskonditionen
 
         schema = VertragskonditionenSchema()
         json_string = schema.dumps(vertragskonditionen, ensure_ascii=False)