diff --git a/compendium_v2/db/presentation_models.py b/compendium_v2/db/presentation_models.py
index 5297ed4ed6c555265b453bf01d79d0690fa51037..513860e1165d87035ab52fc8c2ad69f1fe33e677 100644
--- a/compendium_v2/db/presentation_models.py
+++ b/compendium_v2/db/presentation_models.py
@@ -18,7 +18,6 @@ from compendium_v2.db.presentation_model_enums import CarryMechanism, Commercial
 
 logger = logging.getLogger(__name__)
 
-
 str128 = Annotated[str, 128]
 str128_pk = Annotated[str, mapped_column(String(128), primary_key=True)]
 str256_pk = Annotated[str, mapped_column(String(256), primary_key=True)]
@@ -48,13 +47,16 @@ RemoteCampus = TypedDict(
 # See https://github.com/pallets-eco/flask-sqlalchemy/issues/1140
 # mypy: disable-error-code="name-defined"
 
+class PresentationModel(db.Model):
+    __abstract__ = True
+
 
-class PreviewYear(db.Model):
+class PreviewYear(PresentationModel):
     __tablename__ = 'preview_year'
     year: Mapped[int_pk]
 
 
-class NREN(db.Model):
+class NREN(PresentationModel):
     __tablename__ = 'nren'
     id: Mapped[int_pk]
     name: Mapped[str128]
@@ -64,7 +66,7 @@ class NREN(db.Model):
         return f'<NREN {self.id} | {self.name}>'
 
 
-class BudgetEntry(db.Model):
+class BudgetEntry(PresentationModel):
     __tablename__ = 'budgets'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -72,7 +74,7 @@ class BudgetEntry(db.Model):
     budget: Mapped[Decimal]
 
 
-class FundingSource(db.Model):
+class FundingSource(PresentationModel):
     __tablename__ = 'funding_source'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -84,7 +86,7 @@ class FundingSource(db.Model):
     other: Mapped[Decimal]
 
 
-class ChargingStructure(db.Model):
+class ChargingStructure(PresentationModel):
     __tablename__ = 'charging_structure'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -92,7 +94,7 @@ class ChargingStructure(db.Model):
     fee_type: Mapped[Optional[FeeType]]
 
 
-class NrenStaff(db.Model):
+class NrenStaff(PresentationModel):
     __tablename__ = 'nren_staff'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -103,7 +105,7 @@ class NrenStaff(db.Model):
     non_technical_fte: Mapped[Decimal]
 
 
-class ParentOrganization(db.Model):
+class ParentOrganization(PresentationModel):
     __tablename__ = 'parent_organization'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -111,7 +113,7 @@ class ParentOrganization(db.Model):
     organization: Mapped[str128]
 
 
-class SubOrganization(db.Model):
+class SubOrganization(PresentationModel):
     __tablename__ = 'sub_organization'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -120,7 +122,7 @@ class SubOrganization(db.Model):
     role: Mapped[str128_pk]
 
 
-class ECProject(db.Model):
+class ECProject(PresentationModel):
     __tablename__ = 'ec_project'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -128,7 +130,7 @@ class ECProject(db.Model):
     project: Mapped[str256_pk]
 
 
-class Policy(db.Model):
+class Policy(PresentationModel):
     __tablename__ = 'policy'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -143,7 +145,7 @@ class Policy(db.Model):
     gender_equality: Mapped[str]
 
 
-class TrafficVolume(db.Model):
+class TrafficVolume(PresentationModel):
     __tablename__ = 'traffic_volume'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -154,7 +156,7 @@ class TrafficVolume(db.Model):
     from_external: Mapped[Decimal]
 
 
-class InstitutionURLs(db.Model):
+class InstitutionURLs(PresentationModel):
     __tablename__ = 'institution_urls'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -162,7 +164,7 @@ class InstitutionURLs(db.Model):
     urls: Mapped[json_str_list]
 
 
-class CentralProcurement(db.Model):
+class CentralProcurement(PresentationModel):
     __tablename__ = 'central_procurement'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -171,7 +173,7 @@ class CentralProcurement(db.Model):
     amount: Mapped[Optional[Decimal]]
 
 
-class ServiceManagement(db.Model):
+class ServiceManagement(PresentationModel):
     __tablename__ = 'service_management'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -180,7 +182,7 @@ class ServiceManagement(db.Model):
     service_level_targets: Mapped[Optional[bool]]
 
 
-class ServiceUserTypes(db.Model):
+class ServiceUserTypes(PresentationModel):
     __tablename__ = 'service_user_types'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -189,7 +191,7 @@ class ServiceUserTypes(db.Model):
     service_category: Mapped[ServiceCategory] = mapped_column(primary_key=True)
 
 
-class EOSCListings(db.Model):
+class EOSCListings(PresentationModel):
     __tablename__ = 'eosc_listings'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -197,7 +199,7 @@ class EOSCListings(db.Model):
     service_names: Mapped[json_str_list]
 
 
-class Standards(db.Model):
+class Standards(PresentationModel):
     __tablename__ = 'standards'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -209,7 +211,7 @@ class Standards(db.Model):
     crisis_management_procedure: Mapped[Optional[bool]]
 
 
-class CrisisExercises(db.Model):
+class CrisisExercises(PresentationModel):
     __tablename__ = 'crisis_exercises'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -217,7 +219,7 @@ class CrisisExercises(db.Model):
     exercise_descriptions: Mapped[json_str_list]
 
 
-class SecurityControls(db.Model):
+class SecurityControls(PresentationModel):
     __tablename__ = 'security_controls'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -225,7 +227,7 @@ class SecurityControls(db.Model):
     security_control_descriptions: Mapped[json_str_list]
 
 
-class ConnectedProportion(db.Model):
+class ConnectedProportion(PresentationModel):
     __tablename__ = 'connected_proportion'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -237,7 +239,7 @@ class ConnectedProportion(db.Model):
     users_served: Mapped[Optional[int]]
 
 
-class ConnectivityLevel(db.Model):
+class ConnectivityLevel(PresentationModel):
     __tablename__ = 'connectivity_level'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -248,7 +250,7 @@ class ConnectivityLevel(db.Model):
     highest_speed_proportion: Mapped[Optional[Decimal]]
 
 
-class ConnectionCarrier(db.Model):
+class ConnectionCarrier(PresentationModel):
     __tablename__ = 'connection_carrier'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -257,7 +259,7 @@ class ConnectionCarrier(db.Model):
     carry_mechanism: Mapped[CarryMechanism]
 
 
-class ConnectivityLoad(db.Model):
+class ConnectivityLoad(PresentationModel):
     __tablename__ = 'connectivity_load'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -269,7 +271,7 @@ class ConnectivityLoad(db.Model):
     peak_load_to_institutions: Mapped[Optional[int]]
 
 
-class ConnectivityGrowth(db.Model):
+class ConnectivityGrowth(PresentationModel):
     __tablename__ = 'connectivity_growth'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -278,7 +280,7 @@ class ConnectivityGrowth(db.Model):
     growth: Mapped[Decimal]
 
 
-class CommercialConnectivity(db.Model):
+class CommercialConnectivity(PresentationModel):
     __tablename__ = 'commercial_connectivity'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -290,7 +292,7 @@ class CommercialConnectivity(db.Model):
     university_spin_off: Mapped[Optional[CommercialConnectivityCoverage]]
 
 
-class CommercialChargingLevel(db.Model):
+class CommercialChargingLevel(PresentationModel):
     __tablename__ = 'commercial_charging_level'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -300,7 +302,7 @@ class CommercialChargingLevel(db.Model):
     direct_peering: Mapped[Optional[CommercialCharges]]
 
 
-class RemoteCampuses(db.Model):
+class RemoteCampuses(PresentationModel):
     __tablename__ = 'remote_campuses'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -309,7 +311,7 @@ class RemoteCampuses(db.Model):
     connections: Mapped[List[RemoteCampus]] = mapped_column(JSON)
 
 
-class DarkFibreLease(db.Model):
+class DarkFibreLease(PresentationModel):
     __tablename__ = 'dark_fibre_lease'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -320,7 +322,7 @@ class DarkFibreLease(db.Model):
     iru_duration: Mapped[Optional[Decimal]]
 
 
-class DarkFibreInstalled(db.Model):
+class DarkFibreInstalled(PresentationModel):
     __tablename__ = 'dark_fibre_installed'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -329,7 +331,7 @@ class DarkFibreInstalled(db.Model):
     fibre_length_in_country: Mapped[Optional[int]]
 
 
-class FibreLight(db.Model):
+class FibreLight(PresentationModel):
     __tablename__ = 'fibre_light'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -337,7 +339,7 @@ class FibreLight(db.Model):
     light_description: Mapped[str]
 
 
-class NetworkMapUrls(db.Model):
+class NetworkMapUrls(PresentationModel):
     __tablename__ = 'network_map_urls'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -345,7 +347,7 @@ class NetworkMapUrls(db.Model):
     urls: Mapped[json_str_list]
 
 
-class MonitoringTools(db.Model):
+class MonitoringTools(PresentationModel):
     __tablename__ = 'monitoring_tools'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -354,7 +356,7 @@ class MonitoringTools(db.Model):
     netflow_processing_description: Mapped[str]
 
 
-class PassiveMonitoring(db.Model):
+class PassiveMonitoring(PresentationModel):
     __tablename__ = 'passive_monitoring'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -363,7 +365,7 @@ class PassiveMonitoring(db.Model):
     method: Mapped[Optional[MonitoringMethod]]
 
 
-class TrafficStatistics(db.Model):
+class TrafficStatistics(PresentationModel):
     __tablename__ = 'traffic_statistics'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -372,7 +374,7 @@ class TrafficStatistics(db.Model):
     urls: Mapped[json_str_list]
 
 
-class SiemVendors(db.Model):
+class SiemVendors(PresentationModel):
     __tablename__ = 'siem_vendors'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -380,7 +382,7 @@ class SiemVendors(db.Model):
     vendor_names: Mapped[json_str_list]
 
 
-class CertificateProviders(db.Model):
+class CertificateProviders(PresentationModel):
     __tablename__ = 'certificate_providers'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -388,7 +390,7 @@ class CertificateProviders(db.Model):
     provider_names: Mapped[json_str_list]
 
 
-class WeatherMap(db.Model):
+class WeatherMap(PresentationModel):
     __tablename__ = 'weather_map'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -397,7 +399,7 @@ class WeatherMap(db.Model):
     url: Mapped[str]
 
 
-class PertTeam(db.Model):
+class PertTeam(PresentationModel):
     __tablename__ = 'pert_team'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -405,7 +407,7 @@ class PertTeam(db.Model):
     pert_team: Mapped[YesNoPlanned]
 
 
-class AlienWave(db.Model):
+class AlienWave(PresentationModel):
     __tablename__ = 'alien_wave'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -415,7 +417,7 @@ class AlienWave(db.Model):
     alien_wave_internal: Mapped[Optional[bool]]
 
 
-class Capacity(db.Model):
+class Capacity(PresentationModel):
     __tablename__ = 'capacity'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -424,7 +426,7 @@ class Capacity(db.Model):
     typical_backbone_capacity: Mapped[Optional[Decimal]]
 
 
-class ExternalConnections(db.Model):
+class ExternalConnections(PresentationModel):
     __tablename__ = 'external_connections'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -432,7 +434,7 @@ class ExternalConnections(db.Model):
     connections: Mapped[List[ExternalConnection]] = mapped_column(JSON)
 
 
-class NonREPeers(db.Model):
+class NonREPeers(PresentationModel):
     __tablename__ = 'non_r_and_e_peers'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -440,7 +442,7 @@ class NonREPeers(db.Model):
     nr_of_non_r_and_e_peers: Mapped[int]
 
 
-class TrafficRatio(db.Model):
+class TrafficRatio(PresentationModel):
     __tablename__ = 'traffic_ratio'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -449,7 +451,7 @@ class TrafficRatio(db.Model):
     commodity_percentage: Mapped[Decimal]
 
 
-class OpsAutomation(db.Model):
+class OpsAutomation(PresentationModel):
     __tablename__ = 'ops_automation'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -458,7 +460,7 @@ class OpsAutomation(db.Model):
     ops_automation_specifics: Mapped[str]
 
 
-class NetworkFunctionVirtualisation(db.Model):
+class NetworkFunctionVirtualisation(PresentationModel):
     __tablename__ = 'network_function_virtualisation'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -467,7 +469,7 @@ class NetworkFunctionVirtualisation(db.Model):
     nfv_specifics: Mapped[json_str_list]
 
 
-class NetworkAutomation(db.Model):
+class NetworkAutomation(PresentationModel):
     __tablename__ = 'network_automation'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
@@ -476,7 +478,7 @@ class NetworkAutomation(db.Model):
     network_automation_specifics: Mapped[json_str_list]
 
 
-class Service(db.Model):
+class Service(PresentationModel):
     __tablename__ = 'service'
     name_key: Mapped[str128_pk]
     name: Mapped[str128]
@@ -484,7 +486,7 @@ class Service(db.Model):
     description: Mapped[str]
 
 
-class NRENService(db.Model):
+class NRENService(PresentationModel):
     __tablename__ = 'nren_service'
     nren_id: Mapped[int_pk_fkNREN]
     nren: Mapped[NREN] = relationship(lazy='joined')
diff --git a/compendium_v2/publishers/survey_publisher.py b/compendium_v2/publishers/survey_publisher.py
index a2a0cdff0fb719aca5dee317c8955d5a63792e40..8acd5d885b2f8761dc2d53e4aae3aa3767f6243d 100644
--- a/compendium_v2/publishers/survey_publisher.py
+++ b/compendium_v2/publishers/survey_publisher.py
@@ -9,546 +9,27 @@ Usage:
     Used in publish_survey API in compendium_v2/routes/survey.py
 """
 
-from decimal import Decimal
-from typing import List
+from typing import Sequence, Dict, Any
+from sqlalchemy import select, delete
 
-from sqlalchemy import delete, select
 
 from compendium_v2.db import db
-from compendium_v2.db.presentation_models import BudgetEntry, ChargingStructure, ECProject, ExternalConnections, \
-    InstitutionURLs, NrenStaff, ParentOrganization, Policy, SubOrganization, TrafficVolume, ExternalConnection, \
-    FundingSource, CentralProcurement, ServiceManagement, ServiceUserTypes, EOSCListings, \
-    Standards, CrisisExercises, SecurityControls, ConnectedProportion, ConnectivityLevel, \
-    ConnectionCarrier, ConnectivityLoad, ConnectivityGrowth, CommercialConnectivity, \
-    CommercialChargingLevel, RemoteCampuses, DarkFibreLease, DarkFibreInstalled, FibreLight, \
-    NetworkMapUrls, MonitoringTools, PassiveMonitoring, TrafficStatistics, SiemVendors, \
-    CertificateProviders, WeatherMap, PertTeam, AlienWave, Capacity, NonREPeers, TrafficRatio, \
-    OpsAutomation, NetworkFunctionVirtualisation, NetworkAutomation, NRENService
-from compendium_v2.db.presentation_model_enums import CarryMechanism, CommercialCharges, YesNoPlanned, \
-    CommercialConnectivityCoverage, ConnectivityCoverage, FeeType, MonitoringMethod, ServiceCategory, UserCategory
-from compendium_v2.db.survey_models import ResponseStatus, SurveyResponse
-
-
-def int_or_none(answers_dict, key):
-    if key in answers_dict:
-        value = answers_dict[key]
-        if isinstance(value, str):
-            value = value.replace(",", ".")
-        return int(answers_dict[key])
-    return None
-
-
-def decimal_or_none(answers_dict, key):
-    if key in answers_dict:
-        value = answers_dict[key]
-        if isinstance(value, str):
-            value = value.replace(",", ".")
-        return Decimal(value)
-    return None
-
-
-def decimal_or_zero(answers_dict, key):
-    value = answers_dict.get(key, 0)
-    if isinstance(value, str):
-        value = value.replace(",", ".")
-    return Decimal(value)
-
-
-def bool_or_none(answer, key=None):
-    if key:
-        answer = answer.get(key)
-    if answer:
-        return answer == "Yes"
-    return None
-
-
-def _map_2023(nren, answers) -> None:
-    year = 2023
-
-    for table_class in [BudgetEntry, ChargingStructure, ECProject, ExternalConnections,
-                        InstitutionURLs, NrenStaff, ParentOrganization, Policy, SubOrganization, TrafficVolume,
-                        FundingSource, CentralProcurement, ServiceManagement, ServiceUserTypes, EOSCListings,
-                        Standards, CrisisExercises, SecurityControls, ConnectedProportion, ConnectivityLevel,
-                        ConnectionCarrier, ConnectivityLoad, ConnectivityGrowth, CommercialConnectivity,
-                        CommercialChargingLevel, RemoteCampuses, DarkFibreLease, DarkFibreInstalled, FibreLight,
-                        NetworkMapUrls, MonitoringTools, PassiveMonitoring, TrafficStatistics, SiemVendors,
-                        CertificateProviders, WeatherMap, PertTeam, AlienWave, Capacity, NonREPeers, TrafficRatio,
-                        OpsAutomation, NetworkFunctionVirtualisation, NetworkAutomation, NRENService]:
-        db.session.execute(delete(table_class).where(table_class.year == year,  # type: ignore
-                           table_class.nren_id == nren.id))  # type: ignore
-
-    answers = answers["data"]
-    budget = answers.get("budget")
-    if budget:
-        # replace comma with dot for decimal, surveyjs allows commas for decimals (common in EU countries)
-        budget = budget.replace(",", ".")
-        db.session.add(BudgetEntry(nren_id=nren.id, nren=nren, year=year, budget=Decimal(budget)))
-
-    funding_source = answers.get("income_sources")
-    if funding_source:
-        db.session.add(FundingSource(
-            nren_id=nren.id, nren=nren, year=year,
-            client_institutions=decimal_or_zero(funding_source, "client_institutions"),
-            european_funding=decimal_or_zero(funding_source, "european_funding"),
-            gov_public_bodies=decimal_or_zero(funding_source, "gov_public_bodies"),
-            commercial=decimal_or_zero(funding_source, "commercial"),
-            other=decimal_or_zero(funding_source, "other")
-        ))
-
-    charging = answers.get("charging_mechanism")
-    if charging:
-        db.session.add(ChargingStructure(nren_id=nren.id, nren=nren, year=year, fee_type=FeeType[charging]))
-
-    staff_roles = answers.get("staff_roles", {})
-    staff_employment_type = answers.get("staff_employment_type", {})
-    if staff_roles or staff_employment_type:
-        db.session.add(NrenStaff(
-            nren_id=nren.id, nren=nren, year=year,
-            permanent_fte=decimal_or_zero(staff_employment_type, "permanent_fte"),
-            subcontracted_fte=decimal_or_zero(staff_employment_type, "subcontracted_fte"),
-            technical_fte=decimal_or_zero(staff_roles, "technical_fte"),
-            non_technical_fte=decimal_or_zero(staff_roles, "nontechnical_fte")
-        ))
-
-    has_parent = answers.get("parent_organization") == "Yes"
-    parent = answers.get("parent_organization_name")
-    if has_parent and parent:
-        db.session.add(ParentOrganization(nren_id=nren.id, nren=nren, year=year, organization=parent))
-
-    has_subs = answers.get("suborganizations") == 'Yes'
-    subs = answers.get("suborganization_details")
-    if has_subs and subs:
-        for sub in subs:
-            role = sub.get("suborganization_role", "")
-            if role == "other":
-                role = sub.get("suborganization_role-Comment", "")
-            db.session.add(SubOrganization(
-                nren_id=nren.id, nren=nren, year=year,
-                organization=sub.get("suborganization_name"),
-                role=role
-            ))
-    has_ec_projects = answers.get("ec_projects") == "Yes"
-    ec_projects = answers.get("ec_project_names")
-    if has_ec_projects and ec_projects:
-        for ec_project in ec_projects:
-            if ec_project:
-                db.session.add(
-                    ECProject(nren_id=nren.id, nren=nren, year=year, project=ec_project.get("ec_project_name"))
-                )
-
-    strategy = answers.get("corporate_strategy_url", "")
-    policies = answers.get("policies", {})
-    if strategy or policies:
-        db.session.add(Policy(
-            nren_id=nren.id, nren=nren, year=year,
-            strategic_plan=strategy,
-            environmental=policies.get("environmental_policy", {}).get("url", ""),
-            equal_opportunity=policies.get("equal_opportunity_policy", {}).get("url", ""),
-            connectivity=policies.get("connectivity_policy", {}).get("url", ""),
-            acceptable_use=policies.get("acceptable_use_policy", {}).get("url", ""),
-            privacy_notice=policies.get("privacy_notice", {}).get("url", ""),
-            data_protection=policies.get("data_protection_contact", {}).get("url", ""),
-            gender_equality=policies.get("gender_equality_policy", {}).get("url", "")
-        ))
-
-    traffic_estimate = answers.get("traffic_estimate")
-    if traffic_estimate:
-        db.session.add(TrafficVolume(
-            nren_id=nren.id, nren=nren, year=year,
-            to_customers=decimal_or_zero(traffic_estimate, "to_customers"),
-            from_customers=decimal_or_zero(traffic_estimate, "from_customers"),
-            to_external=decimal_or_zero(traffic_estimate, "to_external"),
-            from_external=decimal_or_zero(traffic_estimate, "from_external")
-        ))
-
-    institution_urls = answers.get("connected_sites_lists")
-    if institution_urls:
-        urls = [i.get("connected_sites_url", "") for i in institution_urls if i.get("connected_sites_url", "") != ""]
-        if urls:
-            db.session.add(InstitutionURLs(
-                nren_id=nren.id, nren=nren, year=year,
-                urls=urls
-            ))
-
-    central_procurement = answers.get("central_software_procurement") == "Yes"
-    if central_procurement:
-        central_procurement_amount = decimal_or_none(answers, "central_procurement_amount")
-    else:
-        central_procurement_amount = None
-    db.session.add(CentralProcurement(
-        nren_id=nren.id, nren=nren, year=year,
-        central_procurement=central_procurement,
-        amount=central_procurement_amount
-    ))
-
-    formal_service_management_framework = answers.get("formal_service_management_framework")
-    service_level_targets = answers.get("service_level_targets")
-    if formal_service_management_framework or service_level_targets:
-        db.session.add(ServiceManagement(
-            nren_id=nren.id, nren=nren, year=year,
-            service_management_framework=bool_or_none(formal_service_management_framework),
-            service_level_targets=bool_or_none(service_level_targets)
-        ))
-
-    service_type_matrix = answers.get("service_matrix", {})
-    for user_type, service_types in service_type_matrix.items():
-        user_type = UserCategory[user_type]
-        service_types = service_types["service_types"]
-        for service_type in service_types:
-            service_type = ServiceCategory[service_type]
-            db.session.add(ServiceUserTypes(
-                nren_id=nren.id, nren=nren, year=year,
-                user_category=user_type,
-                service_category=service_type
-            ))
-
-    has_eosc_listings = answers.get("service_portfolio_eosc_portal") == "Yes"
-    services_on_eosc_portal_list = answers.get("services_on_eosc_portal_list", [])
-    eosc_list = [i.get("service_name") for i in services_on_eosc_portal_list]
-    eosc_list = [i for i in eosc_list if i]
-    if has_eosc_listings and eosc_list:
-        db.session.add(EOSCListings(
-            nren_id=nren.id, nren=nren, year=year,
-            service_names=eosc_list
-        ))
-
-    audits = answers.get("audits")
-    business_continuity_plans = answers.get("business_continuity_plans")
-    crisis_management_procedure = answers.get("crisis_management_procedure")
-    if audits or business_continuity_plans or crisis_management_procedure:
-        db.session.add(Standards(
-            nren_id=nren.id, nren=nren, year=year,
-            audits=bool_or_none(audits),
-            audit_specifics=answers.get("audit_specifics", ""),
-            business_continuity_plans=bool_or_none(business_continuity_plans),
-            business_continuity_plans_specifics=answers.get("business_continuity_plans_specifics", ""),
-            crisis_management_procedure=bool_or_none(crisis_management_procedure)
-        ))
-
-    crisis_exercises = answers.get("crisis_exercises")
-    if crisis_exercises:
-        db.session.add(CrisisExercises(
-            nren_id=nren.id, nren=nren, year=year,
-            exercise_descriptions=crisis_exercises
-        ))
-
-    security_controls = answers.get("security_controls")
-    if security_controls:
-        if "other" in security_controls:
-            security_controls.remove("other")
-            security_controls.append(answers.get("security_controls-Comment", "other"))
-        db.session.add(SecurityControls(
-            nren_id=nren.id, nren=nren, year=year,
-            security_control_descriptions=security_controls
-        ))
 
-    connectivity_proportions = answers.get("connectivity_proportions", {})
-    for user_type, connectivity_proportion in connectivity_proportions.items():
-        user_type = UserCategory[user_type]
-        coverage = connectivity_proportion.get("covered")
-        coverage = ConnectivityCoverage[coverage] if coverage else None
-        number_connected = int_or_none(connectivity_proportion, "nr_connected")
-        market_share = decimal_or_none(connectivity_proportion, "market_share_percentage")
-        users_served = int_or_none(connectivity_proportion, "nr_of_users")
-        db.session.add(ConnectedProportion(
-            nren_id=nren.id, nren=nren, year=year,
-            user_category=user_type,
-            coverage=coverage,
-            number_connected=number_connected,
-            market_share=market_share,
-            users_served=users_served
-        ))
-
-    connectivity_levels = answers.get("connectivity_level", {})
-    for user_type, connectivity_level in connectivity_levels.items():
-        user_type = UserCategory[user_type]
-        db.session.add(ConnectivityLevel(
-            nren_id=nren.id, nren=nren, year=year,
-            user_category=user_type,
-            typical_speed=int_or_none(connectivity_level, "typical_speed"),
-            highest_speed=int_or_none(connectivity_level, "highest_speed"),
-            highest_speed_proportion=decimal_or_none(connectivity_level, "highest_speed_connection_percentage")
-        ))
-
-    traffic_carriers = answers.get("traffic_carriers", {})
-    for user_type, traffic_carrier in traffic_carriers.items():
-        user_type = UserCategory[user_type]
-        traffic_carrier = traffic_carrier.get("carry_mechanism")
-        if traffic_carrier:
-            db.session.add(ConnectionCarrier(
-                nren_id=nren.id, nren=nren, year=year,
-                user_category=user_type,
-                carry_mechanism=CarryMechanism[traffic_carrier]
-            ))
-
-    traffic_loads = answers.get("traffic_load", {})
-    for user_type, traffic_load in traffic_loads.items():
-        user_type = UserCategory[user_type]
-        db.session.add(ConnectivityLoad(
-            nren_id=nren.id, nren=nren, year=year,
-            user_category=user_type,
-            average_load_from_institutions=int_or_none(traffic_load, "average_from_institutions_to_network"),
-            average_load_to_institutions=int_or_none(traffic_load, "average_to_institutions_from_network"),
-            peak_load_from_institutions=int_or_none(traffic_load, "peak_from_institutions_to_network"),
-            peak_load_to_institutions=int_or_none(traffic_load, "peak_to_institutions_from_network")
-        ))
-
-    traffic_growths = answers.get("traffic_growth", {})
-    for user_type, traffic_growth in traffic_growths.items():
-        user_type = UserCategory[user_type]
-        db.session.add(ConnectivityGrowth(
-            nren_id=nren.id, nren=nren, year=year,
-            user_category=user_type,
-            growth=decimal_or_zero(traffic_growth, "growth_rate")
-        ))
-
-    commercial_organizations = answers.get("commercial_organizations")
-    if commercial_organizations:
-        c1 = commercial_organizations.get("commercial_r_e", {}).get("connection")
-        c2 = commercial_organizations.get("commercial_general", {}).get("connection")
-        c3 = commercial_organizations.get("commercial_collaboration", {}).get("connection")
-        c4 = commercial_organizations.get("commercial_service_provider", {}).get("connection")
-        c5 = commercial_organizations.get("university_spin_off", {}).get("connection")
-        db.session.add(CommercialConnectivity(
-            nren_id=nren.id, nren=nren, year=year,
-            commercial_r_and_e=CommercialConnectivityCoverage[c1] if c1 else None,
-            commercial_general=CommercialConnectivityCoverage[c2] if c2 else None,
-            commercial_collaboration=CommercialConnectivityCoverage[c3] if c3 else None,
-            commercial_service_provider=CommercialConnectivityCoverage[c4] if c4 else None,
-            university_spin_off=CommercialConnectivityCoverage[c5] if c5 else None,
-        ))
-
-    commercial_charging_levels = answers.get("commercial_charging_levels")
-    if commercial_charging_levels:
-        c1 = commercial_charging_levels.get("collaboration", {}).get("charging_level")
-        c2 = commercial_charging_levels.get("services", {}).get("charging_level")
-        c3 = commercial_charging_levels.get("peering", {}).get("charging_level")
-        db.session.add(CommercialChargingLevel(
-            nren_id=nren.id, nren=nren, year=year,
-            collaboration=CommercialCharges[c1] if c1 else None,
-            service_supplier=CommercialCharges[c2] if c2 else None,
-            direct_peering=CommercialCharges[c3] if c3 else None,
-        ))
-
-    remote_campuses = answers.get("remote_campuses")
-    if remote_campuses:
-        remote_campuses = remote_campuses == "Yes"
-
-        if remote_campuses:
-            remote_campuses_specifics = answers.get("remote_campuses_specifics", [])
-            remote_campuses_specifics = [
-                {"country": i.get("country", ""), "local_r_and_e_connection": bool_or_none(i, "connected")}
-                for i in remote_campuses_specifics
-            ]
-        else:
-            remote_campuses_specifics = []
-        db.session.add(RemoteCampuses(
-            nren_id=nren.id, nren=nren, year=year,
-            remote_campus_connectivity=remote_campuses,
-            connections=remote_campuses_specifics
-        ))
-
-    dark_fibre_lease = answers.get("dark_fibre_lease") == "Yes"
-    if dark_fibre_lease:
-        db.session.add(DarkFibreLease(
-            nren_id=nren.id, nren=nren, year=year,
-            iru_or_lease=dark_fibre_lease,
-            fibre_length_in_country=int_or_none(answers, "dark_fibre_lease_kilometers_inside_country"),
-            fibre_length_outside_country=int_or_none(answers, "dark_fibre_lease_kilometers_outside_country"),
-            iru_duration=decimal_or_none(answers, "dark_fibre_lease_duration")
-        ))
-
-    dark_fibre_nren = answers.get("dark_fibre_nren") == "Yes"
-    if dark_fibre_nren:
-        db.session.add(DarkFibreInstalled(
-            nren_id=nren.id, nren=nren, year=year,
-            installed=dark_fibre_nren,
-            fibre_length_in_country=int_or_none(answers, "dark_fibre_nren_kilometers_inside_country")
-        ))
-
-    fibre_light = answers.get("fibre_light")
-    if fibre_light:
-        if fibre_light == "other":
-            fibre_light = answers.get("fibre_light-Comment", "other")
-        db.session.add(FibreLight(
-            nren_id=nren.id, nren=nren, year=year,
-            light_description=fibre_light
-        ))
-
-    network_map_urls = answers.get("network_map_urls", [])
-    urls = [i.get("network_map_url", "") for i in network_map_urls if i.get("network_map_url", "") != ""]
-    if urls:
-        db.session.add(NetworkMapUrls(
-            nren_id=nren.id, nren=nren, year=year,
-            urls=urls
-        ))
-
-    monitoring_tools = answers.get("monitoring_tools", [])
-    netflow_vendors = answers.get("netflow_vendors", "")
-    if monitoring_tools or netflow_vendors:
-        if "other" in monitoring_tools:
-            monitoring_tools.remove("other")
-            monitoring_tools.append(answers.get("monitoring_tools-Comment", "other"))
-        db.session.add(MonitoringTools(
-            nren_id=nren.id, nren=nren, year=year,
-            tool_descriptions=monitoring_tools,
-            netflow_processing_description=netflow_vendors
-        ))
-
-    passive_monitoring = answers.get("passive_monitoring")
-    if passive_monitoring:
-        passive_monitoring = passive_monitoring == "Yes"
-        passive_monitoring_tech = answers.get("passive_monitoring_tech")
-        db.session.add(PassiveMonitoring(
-            nren_id=nren.id, nren=nren, year=year,
-            monitoring=passive_monitoring,
-            method=MonitoringMethod[passive_monitoring_tech] if passive_monitoring_tech else None
-        ))
-
-    traffic_statistics = answers.get("traffic_statistics")
-    if traffic_statistics:
-        traffic_statistics = traffic_statistics == "Yes"
-        urls = answers.get("traffic_statistics_urls", [])
-        urls = [i.get("traffic_statistics_url", "") for i in urls if i.get("traffic_statistics_url")]
-        db.session.add(TrafficStatistics(
-            nren_id=nren.id, nren=nren, year=year,
-            traffic_statistics=traffic_statistics,
-            urls=urls
-        ))
-
-    siem_soc_vendor = answers.get("siem_soc_vendor")
-    if siem_soc_vendor:
-        if "other" in siem_soc_vendor:
-            siem_soc_vendor.remove("other")
-            siem_soc_vendor.append(answers.get("siem_soc_vendor-Comment", "other"))
-        db.session.add(SiemVendors(
-            nren_id=nren.id, nren=nren, year=year,
-            vendor_names=siem_soc_vendor
-        ))
-
-    certificate_service = answers.get("certificate_service")
-    if certificate_service:
-        if "other" in certificate_service:
-            certificate_service.remove("other")
-            certificate_service.append(answers.get("certificate_service-Comment", "other"))
-        db.session.add(CertificateProviders(
-            nren_id=nren.id, nren=nren, year=year,
-            provider_names=certificate_service
-        ))
-
-    network_weather = answers.get("network_weather")
-    if network_weather:
-        network_weather = network_weather == "Yes"
-        db.session.add(WeatherMap(
-            nren_id=nren.id, nren=nren, year=year,
-            weather_map=network_weather,
-            url=answers.get("network_weather_url", "")
-        ))
-
-    pert_team = answers.get("pert_team")
-    if pert_team:
-        pert_team = YesNoPlanned[pert_team.lower()]
-        db.session.add(PertTeam(
-            nren_id=nren.id, nren=nren, year=year,
-            pert_team=pert_team
-        ))
-
-    alienwave_services = answers.get("alienwave_services")
-    alienwave_internal = answers.get("alienwave_internal")
-    if alienwave_services or alienwave_internal:
-        alienwave_services = YesNoPlanned[alienwave_services.lower()] if alienwave_services else None
-        alienwave_internal = alienwave_internal == "Yes" if alienwave_internal else None
-        db.session.add(AlienWave(
-            nren_id=nren.id, nren=nren, year=year,
-            alien_wave_third_pary=alienwave_services,
-            nr_of_alien_wave_third_party_services=int_or_none(answers, "alienwave_services_number"),
-            alien_wave_internal=alienwave_internal
-        ))
-
-    max_capacity = answers.get("max_capacity")
-    typical_capacity = answers.get("typical_capacity")
-    if max_capacity or typical_capacity:
-        db.session.add(Capacity(
-            nren_id=nren.id, nren=nren, year=year,
-            largest_link_capacity=decimal_or_none(answers, "max_capacity"),
-            typical_backbone_capacity=decimal_or_none(answers, "typical_capacity")
-        ))
-
-    external_connections = answers.get("external_connections")
-    if external_connections:
-        connections: List[ExternalConnection] = []
-        for connection in external_connections:
-            connections.append({
-                'link_name': connection.get('link_name', ''),
-                'capacity': connection.get('capacity'),
-                'from_organization': connection.get('from_organization', ''),
-                'to_organization': connection.get('to_organization', ''),
-                'interconnection_method': connection.get('interconnection_method')
-            })
-        db.session.add(ExternalConnections(
-            nren_id=nren.id, nren=nren, year=year,
-            connections=connections
-        ))
-
-    non_r_and_e_peers = answers.get("non_r_and_e_peers")
-    if non_r_and_e_peers:
-        db.session.add(NonREPeers(
-            nren_id=nren.id, nren=nren, year=year,
-            nr_of_non_r_and_e_peers=int(non_r_and_e_peers)
-        ))
-
-    commodity_vs_r_e = answers.get("commodity_vs_r_e")
-    if commodity_vs_r_e:
-        db.session.add(TrafficRatio(
-            nren_id=nren.id, nren=nren, year=year,
-            r_and_e_percentage=decimal_or_zero(commodity_vs_r_e, "r_e"),
-            commodity_percentage=decimal_or_zero(commodity_vs_r_e, "commodity"),
-        ))
-
-    operational_process_automation = answers.get("operational_process_automation")
-    if operational_process_automation:
-        db.session.add(OpsAutomation(
-            nren_id=nren.id, nren=nren, year=year,
-            ops_automation=YesNoPlanned[operational_process_automation.lower()],
-            ops_automation_specifics=answers.get("operational_process_automation_tools", "")
-        ))
-
-    nfv = answers.get("nfv")
-    if nfv:
-        nfv_types = answers.get("nfv_types", [])
-        if "other" in nfv_types:
-            nfv_types.remove("other")
-            nfv_types.append(answers.get("nfv_types-Comment", "other"))
-        db.session.add(NetworkFunctionVirtualisation(
-            nren_id=nren.id, nren=nren, year=year,
-            nfv=YesNoPlanned[nfv.lower()],
-            nfv_specifics=nfv_types
-        ))
+from compendium_v2.db.survey_models import ResponseStatus, SurveyResponse
+from compendium_v2.publishers.year.map_2023 import map_2023
+from compendium_v2.db.presentation_models import PresentationModel
 
-    network_automation = answers.get("network_automation")
-    if network_automation:
-        network_automation_tasks = answers.get("network_automation_tasks", [])
-        db.session.add(NetworkAutomation(
-            nren_id=nren.id, nren=nren, year=year,
-            network_automation=YesNoPlanned[network_automation.lower()],
-            network_automation_specifics=network_automation_tasks
-        ))
 
-    all_services = {}
-    for question_name in ["services_collaboration", "services_hosting", "services_identity", "services_isp",
-                          "services_multimedia", "services_network", "services_professional", "services_security"]:
-        all_services.update(answers.get(question_name, {}))
+def save_data(year: int, data: Dict[PresentationModel, Sequence[Dict[str, Any]]]):
+    table_classes = list(data.keys())
+    if not all(issubclass(table, db.Model) for table in table_classes):
+        raise ValueError("All tables must be subclasses of db.Model")
 
-    for service_key, answers in all_services.items():
-        offered = answers.get("offered")
-        if offered == ["yes"]:
-            db.session.add(NRENService(
-                nren_id=nren.id, nren=nren, year=year,
-                service_key=service_key,
-                product_name=answers.get("name", ""),
-                additional_information=answers.get("additional_information", ""),
-                official_description=answers.get("description", "")
-            ))
+    for table_class in table_classes:
+        db.session.execute(delete(table_class).where(table_class.year == year))
+    for model, rows in data.items():
+        db.session.bulk_insert_mappings(model, rows)  # type: ignore
+        db.session.commit()
 
 
 def publish(year):
@@ -558,7 +39,7 @@ def publish(year):
     ).unique()
 
     question_mapping = {
-        2023: _map_2023
+        2023: map_2023
     }
 
     mapping_function = question_mapping.get(year)
@@ -566,5 +47,6 @@ def publish(year):
     if not mapping_function:
         raise ValueError(f"No publishing function found for the {year} survey.")
 
-    for response in responses:
-        mapping_function(response.nren, response.answers)
+    data = mapping_function(responses)
+
+    save_data(year, data)
diff --git a/compendium_v2/publishers/utils.py b/compendium_v2/publishers/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..04b1f66d1447522d64b8e1708e111125b850b46a
--- /dev/null
+++ b/compendium_v2/publishers/utils.py
@@ -0,0 +1,34 @@
+from decimal import Decimal
+
+
+def int_or_none(answers_dict, key):
+    if key in answers_dict:
+        value = answers_dict[key]
+        if isinstance(value, str):
+            value = value.replace(",", ".")
+        return int(answers_dict[key])
+    return None
+
+
+def decimal_or_none(answers_dict, key):
+    if key in answers_dict:
+        value = answers_dict[key]
+        if isinstance(value, str):
+            value = value.replace(",", ".")
+        return Decimal(value)
+    return None
+
+
+def decimal_or_zero(answers_dict, key):
+    value = answers_dict.get(key, 0)
+    if isinstance(value, str):
+        value = value.replace(",", ".")
+    return Decimal(value)
+
+
+def bool_or_none(answer, key=None):
+    if key:
+        answer = answer.get(key)
+    if answer:
+        return answer == "Yes"
+    return None
diff --git a/compendium_v2/publishers/year/__init__.py b/compendium_v2/publishers/year/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/compendium_v2/publishers/year/map_2023.py b/compendium_v2/publishers/year/map_2023.py
new file mode 100644
index 0000000000000000000000000000000000000000..8a17ef5f4308b951ca817664aab3204c635b2d93
--- /dev/null
+++ b/compendium_v2/publishers/year/map_2023.py
@@ -0,0 +1,826 @@
+from decimal import Decimal
+from typing import List, Dict, Any, Type
+from collections import defaultdict
+from compendium_v2.db.presentation_models import BudgetEntry, ChargingStructure, ECProject, ExternalConnections, \
+    InstitutionURLs, NrenStaff, ParentOrganization, Policy, SubOrganization, TrafficVolume, ExternalConnection, \
+    FundingSource, CentralProcurement, ServiceManagement, ServiceUserTypes, EOSCListings, \
+    Standards, CrisisExercises, SecurityControls, ConnectedProportion, ConnectivityLevel, \
+    ConnectionCarrier, ConnectivityLoad, ConnectivityGrowth, CommercialConnectivity, \
+    CommercialChargingLevel, RemoteCampuses, DarkFibreLease, DarkFibreInstalled, FibreLight, \
+    NetworkMapUrls, MonitoringTools, PassiveMonitoring, TrafficStatistics, SiemVendors, \
+    CertificateProviders, WeatherMap, PertTeam, AlienWave, Capacity, NonREPeers, TrafficRatio, \
+    OpsAutomation, NetworkFunctionVirtualisation, NetworkAutomation, NRENService, NREN, PresentationModel
+from compendium_v2.db.presentation_model_enums import CarryMechanism, CommercialCharges, YesNoPlanned, \
+    CommercialConnectivityCoverage, ConnectivityCoverage, FeeType, MonitoringMethod, ServiceCategory, UserCategory
+from compendium_v2.db.survey_models import SurveyResponse
+from compendium_v2.publishers.utils import decimal_or_zero, decimal_or_none, bool_or_none, int_or_none
+
+
+def map_budget_entry(year, nren, answers):
+    budget = answers.get("budget")
+    if budget:
+        # replace comma with dot for decimal, surveyjs allows commas for decimals (common in EU countries)
+        budget = budget.replace(",", ".")
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'budget': Decimal(budget)
+        }
+
+
+def map_fundingsource_entry(year, nren, answers):
+    funding_source = answers.get("income_sources")
+    if funding_source:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'client_institutions': decimal_or_zero(funding_source, "client_institutions"),
+            'european_funding': decimal_or_zero(funding_source, "european_funding"),
+            'gov_public_bodies': decimal_or_zero(funding_source, "gov_public_bodies"),
+            'commercial': decimal_or_zero(funding_source, "commercial"),
+            'other': decimal_or_zero(funding_source, "other")
+        }
+
+
+def map_charging_structure_entry(year, nren, answers):
+    charging = answers.get("charging_mechanism")
+    if charging:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'fee_type': FeeType[charging]
+        }
+
+
+def map_nren_staff(year, nren, answers):
+    staff_roles = answers.get("staff_roles", {})
+    staff_employment_type = answers.get("staff_employment_type", {})
+    if staff_roles or staff_employment_type:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'permanent_fte': decimal_or_zero(staff_employment_type, "permanent_fte"),
+            'subcontracted_fte': decimal_or_zero(staff_employment_type, "subcontracted_fte"),
+            'technical_fte': decimal_or_zero(staff_roles, "technical_fte"),
+            'non_technical_fte': decimal_or_zero(staff_roles, "nontechnical_fte")
+        }
+
+
+def map_parent_orgs(nren: NREN, year: int, answers: Dict[str, Any]):
+    has_parent = answers.get("parent_organization") == "Yes"
+    parent = answers.get("parent_organization_name")
+    if has_parent and parent:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'organization': parent
+        }
+
+
+def map_sub_orgs(nren: NREN, year: int, answers: Dict[str, Any]):
+    has_subs = answers.get("suborganizations") == 'Yes'
+    subs = answers.get("suborganization_details")
+    if has_subs and subs:
+        for sub in subs:
+            role = sub.get("suborganization_role", "")
+            if role == "other":
+                role = sub.get("suborganization_role-Comment", "")
+            yield {
+                'nren_id': nren.id,
+                'year': year,
+                'organization': sub.get("suborganization_name"),
+                'role': role
+            }
+
+
+def map_ec_projects(nren: NREN, year: int, answers: Dict[str, Any]):
+    has_ec_projects = answers.get("ec_projects") == "Yes"
+    ec_projects = answers.get("ec_project_names")
+    if has_ec_projects and ec_projects:
+        for ec_project in ec_projects:
+            if ec_project:
+                yield {
+                    'nren_id': nren.id,
+                    'year': year,
+                    'project': ec_project.get("ec_project_name")
+                }
+
+
+def map_policy(nren: NREN, year: int, answers: Dict[str, Any]):
+    strategy = answers.get("corporate_strategy_url", "")
+    policies = answers.get("policies", {})
+    if strategy or policies:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'strategic_plan': strategy,
+            'environmental': policies.get("environmental_policy", {}).get("url", ""),
+            'equal_opportunity': policies.get("equal_opportunity_policy", {}).get("url", ""),
+            'connectivity': policies.get("connectivity_policy", {}).get("url", ""),
+            'acceptable_use': policies.get("acceptable_use_policy", {}).get("url", ""),
+            'privacy_notice': policies.get("privacy_notice", {}).get("url", ""),
+            'data_protection': policies.get("data_protection_contact", {}).get("url", ""),
+            'gender_equality': policies.get("gender_equality_policy", {}).get("url", "")
+        }
+
+
+def map_traffic_volume(nren: NREN, year: int, answers: Dict[str, Any]):
+    traffic_estimate = answers.get("traffic_estimate")
+    if traffic_estimate:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'to_customers': decimal_or_zero(traffic_estimate, "to_customers"),
+            'from_customers': decimal_or_zero(traffic_estimate, "from_customers"),
+            'to_external': decimal_or_zero(traffic_estimate, "to_external"),
+            'from_external': decimal_or_zero(traffic_estimate, "from_external")
+        }
+
+
+def map_institution_urls(nren: NREN, year: int, answers: Dict[str, Any]):
+    institution_urls = answers.get("connected_sites_lists")
+    if institution_urls:
+        urls = [i.get("connected_sites_url", "")
+                for i in institution_urls if i.get("connected_sites_url", "")]
+        if urls:
+            return {
+                'nren_id': nren.id,
+                'year': year,
+                'urls': urls
+            }
+
+
+def map_central_procurement(nren: NREN, year: int, answers: Dict[str, Any]):
+    central_procurement = answers.get("central_software_procurement") == "Yes"
+    if central_procurement:
+        central_procurement_amount = decimal_or_none(answers, "central_procurement_amount")
+    else:
+        central_procurement_amount = None
+    return {
+        'nren_id': nren.id,
+        'year': year,
+        'central_procurement': central_procurement,
+        'amount': central_procurement_amount
+    }
+
+
+def map_service_management(nren: NREN, year: int, answers: Dict[str, Any]):
+    formal_service_management_framework = answers.get("formal_service_management_framework")
+    service_level_targets = answers.get("service_level_targets")
+    if formal_service_management_framework or service_level_targets:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'service_management_framework': bool_or_none(formal_service_management_framework),
+            'service_level_targets': bool_or_none(service_level_targets)
+        }
+
+
+def map_service_user_types(nren: NREN, year: int, answers: Dict[str, Any]):
+    service_type_matrix = answers.get("service_matrix", {})
+    for user_type, service_types in service_type_matrix.items():
+        user_type_enum = UserCategory[user_type]
+        service_types_list = service_types["service_types"]
+        for service_type in service_types_list:
+            service_type_enum = ServiceCategory[service_type]
+            yield {
+                'nren_id': nren.id,
+                'year': year,
+                'user_category': user_type_enum,
+                'service_category': service_type_enum
+            }
+
+
+def map_eosc_listings(nren: NREN, year: int, answers: Dict[str, Any]):
+    has_eosc_listings = answers.get("service_portfolio_eosc_portal") == "Yes"
+    services_on_eosc_portal_list = answers.get("services_on_eosc_portal_list", [])
+    eosc_list = [i.get("service_name") for i in services_on_eosc_portal_list if i.get("service_name")]
+    if has_eosc_listings and eosc_list:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'service_names': eosc_list
+        }
+
+
+def map_standards(nren: NREN, year: int, answers: Dict[str, Any]):
+    audits = answers.get("audits")
+    business_continuity_plans = answers.get("business_continuity_plans")
+    crisis_management_procedure = answers.get("crisis_management_procedure")
+    if audits or business_continuity_plans or crisis_management_procedure:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'audits': bool_or_none(audits),
+            'audit_specifics': answers.get("audit_specifics", ""),
+            'business_continuity_plans': bool_or_none(business_continuity_plans),
+            'business_continuity_plans_specifics': answers.get("business_continuity_plans_specifics", ""),
+            'crisis_management_procedure': bool_or_none(crisis_management_procedure)
+        }
+
+
+def map_crisis_exercises(nren: NREN, year: int, answers: Dict[str, Any]):
+    crisis_exercises = answers.get("crisis_exercises")
+    if crisis_exercises:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'exercise_descriptions': crisis_exercises
+        }
+
+
+def map_security_controls(nren: NREN, year: int, answers: Dict[str, Any]):
+    security_controls = answers.get("security_controls")
+    if security_controls:
+        if "other" in security_controls:
+            security_controls.remove("other")
+            security_controls.append(answers.get("security_controls-Comment", "other"))
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'security_control_descriptions': security_controls
+        }
+
+
+def map_connected_proportion(nren: NREN, year: int, answers: Dict[str, Any]):
+    connectivity_proportions = answers.get("connectivity_proportions", {})
+    for user_type, connectivity_proportion in connectivity_proportions.items():
+        user_type_enum = UserCategory[user_type]
+        coverage = connectivity_proportion.get("covered")
+        coverage_enum = ConnectivityCoverage[coverage] if coverage else None
+        number_connected = int_or_none(connectivity_proportion, "nr_connected")
+        market_share = decimal_or_none(connectivity_proportion, "market_share_percentage")
+        users_served = int_or_none(connectivity_proportion, "nr_of_users")
+        yield {
+            'nren_id': nren.id,
+            'year': year,
+            'user_category': user_type_enum,
+            'coverage': coverage_enum,
+            'number_connected': number_connected,
+            'market_share': market_share,
+            'users_served': users_served
+        }
+
+
+def map_connectivity_levels(nren: NREN, year: int, answers: Dict[str, Any]):
+    connectivity_levels = answers.get("connectivity_level", {})
+    for user_type, connectivity_level in connectivity_levels.items():
+        user_type_enum = UserCategory[user_type]
+        typical_speed = int_or_none(connectivity_level, "typical_speed")
+        highest_speed = int_or_none(connectivity_level, "highest_speed")
+        highest_speed_proportion = decimal_or_none(connectivity_level, "highest_speed_connection_percentage")
+        yield {
+            'nren_id': nren.id,
+            'year': year,
+            'user_category': user_type_enum,
+            'typical_speed': typical_speed,
+            'highest_speed': highest_speed,
+            'highest_speed_proportion': highest_speed_proportion
+        }
+
+
+def map_connection_carriers(nren: NREN, year: int, answers: Dict[str, Any]):
+    traffic_carriers = answers.get("traffic_carriers", {})
+    for user_type, traffic_carrier in traffic_carriers.items():
+        user_type_enum = UserCategory[user_type]
+        carry_mechanism = traffic_carrier.get("carry_mechanism")
+        if carry_mechanism:
+            yield {
+                'nren_id': nren.id,
+                'year': year,
+                'user_category': user_type_enum,
+                'carry_mechanism': CarryMechanism[carry_mechanism]
+            }
+
+
+def map_connectivity_loads(nren: NREN, year: int, answers: Dict[str, Any]):
+    traffic_loads = answers.get("traffic_load", {})
+    for user_type, traffic_load in traffic_loads.items():
+        user_type_enum = UserCategory[user_type]
+        yield {
+            'nren_id': nren.id,
+            'year': year,
+            'user_category': user_type_enum,
+            'average_load_from_institutions': int_or_none(traffic_load, "average_from_institutions_to_network"),
+            'average_load_to_institutions': int_or_none(traffic_load, "average_to_institutions_from_network"),
+            'peak_load_from_institutions': int_or_none(traffic_load, "peak_from_institutions_to_network"),
+            'peak_load_to_institutions': int_or_none(traffic_load, "peak_to_institutions_from_network")
+        }
+
+
+def map_connectivity_growth(nren: NREN, year: int, answers: Dict[str, Any]):
+    traffic_growths = answers.get("traffic_growth", {})
+    for user_type, traffic_growth in traffic_growths.items():
+        user_type_enum = UserCategory[user_type]
+        yield {
+            'nren_id': nren.id,
+            'year': year,
+            'user_category': user_type_enum,
+            'growth': decimal_or_zero(traffic_growth, "growth_rate")
+        }
+
+
+def map_commercial_connectivity(nren: NREN, year: int, answers: Dict[str, Any]):
+    commercial_organizations = answers.get("commercial_organizations")
+    if commercial_organizations:
+        c1 = commercial_organizations.get("commercial_r_e", {}).get("connection")
+        c2 = commercial_organizations.get("commercial_general", {}).get("connection")
+        c3 = commercial_organizations.get("commercial_collaboration", {}).get("connection")
+        c4 = commercial_organizations.get("commercial_service_provider", {}).get("connection")
+        c5 = commercial_organizations.get("university_spin_off", {}).get("connection")
+        yield {
+            'nren_id': nren.id,
+            'year': year,
+            'commercial_r_and_e': CommercialConnectivityCoverage[c1] if c1 else None,
+            'commercial_general': CommercialConnectivityCoverage[c2] if c2 else None,
+            'commercial_collaboration': CommercialConnectivityCoverage[c3] if c3 else None,
+            'commercial_service_provider': CommercialConnectivityCoverage[c4] if c4 else None,
+            'university_spin_off': CommercialConnectivityCoverage[c5] if c5 else None,
+        }
+
+
+def map_commercial_charging_levels(nren: NREN, year: int, answers: Dict[str, Any]):
+    commercial_charging_levels = answers.get("commercial_charging_levels")
+    if commercial_charging_levels:
+        c1 = commercial_charging_levels.get("collaboration", {}).get("charging_level")
+        c2 = commercial_charging_levels.get("services", {}).get("charging_level")
+        c3 = commercial_charging_levels.get("peering", {}).get("charging_level")
+        yield {
+            'nren_id': nren.id,
+            'year': year,
+            'collaboration': CommercialCharges[c1] if c1 else None,
+            'service_supplier': CommercialCharges[c2] if c2 else None,
+            'direct_peering': CommercialCharges[c3] if c3 else None,
+        }
+
+
+def map_remote_campuses(nren: NREN, year: int, answers: Dict[str, Any]):
+    remote_campuses = answers.get("remote_campuses")
+    if remote_campuses:
+        remote_campuses = remote_campuses == "Yes"
+        remote_campuses_specifics = []
+        if remote_campuses:
+            remote_campuses_specifics = [
+                {"country": i.get("country", ""), "local_r_and_e_connection": bool_or_none(i, "connected")}
+                for i in answers.get("remote_campuses_specifics", [])
+            ]
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'remote_campus_connectivity': remote_campuses,
+            'connections': remote_campuses_specifics
+        }
+
+
+def map_dark_fibre_lease(nren: NREN, year: int, answers: Dict[str, Any]):
+    dark_fibre_lease = answers.get("dark_fibre_lease") == "Yes"
+    if dark_fibre_lease:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'iru_or_lease': dark_fibre_lease,
+            'fibre_length_in_country': int_or_none(answers, "dark_fibre_lease_kilometers_inside_country"),
+            'fibre_length_outside_country': int_or_none(answers, "dark_fibre_lease_kilometers_outside_country"),
+            'iru_duration': decimal_or_none(answers, "dark_fibre_lease_duration")
+        }
+
+
+def map_dark_fibre_installed(nren: NREN, year: int, answers: Dict[str, Any]):
+    dark_fibre_nren = answers.get("dark_fibre_nren") == "Yes"
+    if dark_fibre_nren:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'installed': dark_fibre_nren,
+            'fibre_length_in_country': int_or_none(answers, "dark_fibre_nren_kilometers_inside_country")
+        }
+
+
+def map_fibre_light(nren: NREN, year: int, answers: Dict[str, Any]):
+    fibre_light = answers.get("fibre_light")
+    if fibre_light:
+        if fibre_light == "other":
+            fibre_light = answers.get("fibre_light-Comment", "other")
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'light_description': fibre_light
+        }
+
+
+def map_monitoring_tools(nren: NREN, year: int, answers: Dict[str, Any]):
+    monitoring_tools = answers.get("monitoring_tools", [])
+    netflow_vendors = answers.get("netflow_vendors", "")
+    if monitoring_tools or netflow_vendors:
+        if "other" in monitoring_tools:
+            monitoring_tools.remove("other")
+            monitoring_tools.append(answers.get("monitoring_tools-Comment", "other"))
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'tool_descriptions': monitoring_tools,
+            'netflow_processing_description': netflow_vendors
+        }
+
+
+def map_passive_monitoring(nren: NREN, year: int, answers: Dict[str, Any]):
+    passive_monitoring = answers.get("passive_monitoring")
+    if passive_monitoring:
+        passive_monitoring = passive_monitoring == "Yes"
+        passive_monitoring_tech = answers.get("passive_monitoring_tech")
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'monitoring': passive_monitoring,
+            'method': MonitoringMethod[passive_monitoring_tech] if passive_monitoring_tech else None
+        }
+
+
+def map_traffic_statistics(nren: NREN, year: int, answers: Dict[str, Any]):
+    traffic_statistics = answers.get("traffic_statistics")
+    if traffic_statistics:
+        traffic_statistics = traffic_statistics == "Yes"
+        urls = answers.get("traffic_statistics_urls", [])
+        urls = [i.get("traffic_statistics_url", "") for i in urls if i.get("traffic_statistics_url")]
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'traffic_statistics': traffic_statistics,
+            'urls': urls
+        }
+
+
+def map_siem_vendors(nren: NREN, year: int, answers: Dict[str, Any]):
+    siem_soc_vendor = answers.get("siem_soc_vendor")
+    if siem_soc_vendor:
+        if "other" in siem_soc_vendor:
+            siem_soc_vendor.remove("other")
+            siem_soc_vendor.append(answers.get("siem_soc_vendor-Comment", "other"))
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'vendor_names': siem_soc_vendor
+        }
+
+
+def map_network_map_urls(nren: NREN, year: int, answers: Dict[str, Any]):
+    network_map_urls = answers.get("network_map_urls", [])
+    urls = [i.get("network_map_url", "") for i in network_map_urls if i.get("network_map_url", "")]
+    if urls:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'urls': urls
+        }
+
+
+def map_certificate_providers(nren: NREN, year: int, answers: Dict[str, Any]):
+    certificate_service = answers.get("certificate_service")
+    if certificate_service:
+        if "other" in certificate_service:
+            certificate_service.remove("other")
+            certificate_service.append(answers.get("certificate_service-Comment", "other"))
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'provider_names': certificate_service
+        }
+
+
+def map_weathermap_entry(nren: NREN, year: int, answers: Dict[str, Any]):
+    network_weather = answers.get("network_weather")
+    if network_weather:
+        network_weather = network_weather == "Yes"
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'weather_map': network_weather,
+            'url': answers.get("network_weather_url", "")
+        }
+
+
+def map_pert_team(nren: NREN, year: int, answers: Dict[str, Any]):
+    pert_team = answers.get("pert_team")
+    if pert_team:
+        pert_team = YesNoPlanned[pert_team.lower()]
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'pert_team': pert_team
+        }
+
+
+def map_alienwave_entry(nren: NREN, year: int, answers: Dict[str, Any]):
+    alienwave_services = answers.get("alienwave_services")
+    alienwave_internal = answers.get("alienwave_internal")
+    if alienwave_services or alienwave_internal:
+        alienwave_services = YesNoPlanned[alienwave_services.lower()] if alienwave_services else None
+        alienwave_internal = alienwave_internal == "Yes" if alienwave_internal else None
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'alien_wave_third_pary': alienwave_services,
+            'nr_of_alien_wave_third_party_services': int_or_none(answers, "alienwave_services_number"),
+            'alien_wave_internal': alienwave_internal
+        }
+
+
+def map_capacity_entry(nren: NREN, year: int, answers: Dict[str, Any]):
+    max_capacity = answers.get("max_capacity")
+    typical_capacity = answers.get("typical_capacity")
+    if max_capacity or typical_capacity:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'largest_link_capacity': decimal_or_none(answers, "max_capacity"),
+            'typical_backbone_capacity': decimal_or_none(answers, "typical_capacity")
+        }
+
+
+def map_external_connections(nren: NREN, year: int, answers: Dict[str, Any]):
+    external_connections = answers.get("external_connections")
+    if external_connections:
+        connections: List[ExternalConnection] = [{
+            'link_name': connection.get('link_name', ''),
+            'capacity': connection.get('capacity'),
+            'from_organization': connection.get('from_organization', ''),
+            'to_organization': connection.get('to_organization', ''),
+            'interconnection_method': connection.get('interconnection_method')
+        } for connection in external_connections]
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'connections': connections
+        }
+
+
+def map_non_re_peers(nren: NREN, year: int, answers: Dict[str, Any]):
+    non_r_and_e_peers = answers.get("non_r_and_e_peers")
+    if non_r_and_e_peers:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'nr_of_non_r_and_e_peers': int(non_r_and_e_peers)
+        }
+
+
+def map_traffic_ratio(nren: NREN, year: int, answers: Dict[str, Any]):
+    commodity_vs_r_e = answers.get("commodity_vs_r_e")
+    if commodity_vs_r_e:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'r_and_e_percentage': decimal_or_zero(commodity_vs_r_e, "r_e"),
+            'commodity_percentage': decimal_or_zero(commodity_vs_r_e, "commodity"),
+        }
+
+
+def map_ops_automation(nren: NREN, year: int, answers: Dict[str, Any]):
+    operational_process_automation = answers.get("operational_process_automation")
+    if operational_process_automation:
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'ops_automation': YesNoPlanned[operational_process_automation.lower()],
+            'ops_automation_specifics': answers.get("operational_process_automation_tools", "")
+        }
+
+
+def map_network_function_virtualisation(nren: NREN, year: int, answers: Dict[str, Any]):
+    nfv = answers.get("nfv")
+    if nfv:
+        nfv_types = answers.get("nfv_types", [])
+        if "other" in nfv_types:
+            nfv_types.remove("other")
+            nfv_types.append(answers.get("nfv_types-Comment", "other"))
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'nfv': YesNoPlanned[nfv.lower()],
+            'nfv_specifics': nfv_types
+        }
+
+
+def map_network_automation(nren: NREN, year: int, answers: Dict[str, Any]):
+    network_automation = answers.get("network_automation")
+    if network_automation:
+        network_automation_tasks = answers.get("network_automation_tasks", [])
+        return {
+            'nren_id': nren.id,
+            'year': year,
+            'network_automation': YesNoPlanned[network_automation.lower()],
+            'network_automation_specifics': network_automation_tasks
+        }
+
+
+def map_nren_services(nren: NREN, year: int, answers: Dict[str, Any]):
+    all_services = {}
+    for question_name in [
+        "services_collaboration", "services_hosting", "services_identity", "services_isp",
+        "services_multimedia", "services_network", "services_professional", "services_security"
+    ]:
+        all_services.update(answers.get(question_name, {}))
+
+    for service_key, service_answers in all_services.items():
+        offered = service_answers.get("offered")
+        if offered == ["yes"]:
+            yield {
+                'nren_id': nren.id,
+                'year': year,
+                'service_key': service_key,
+                'product_name': service_answers.get("name", ""),
+                'additional_information': service_answers.get("additional_information", ""),
+                'official_description': service_answers.get("description", "")
+            }
+
+
+def map_2023(responses: List[SurveyResponse]) -> Dict[Type[PresentationModel], List[Dict]]:
+    year = 2023
+
+    result = defaultdict(list)  # {model: [data]}
+
+    for response in responses:
+        nren = response.nren
+        answers = response.answers['data']
+
+        budget = map_budget_entry(year, nren, answers)
+        if budget:
+            result[BudgetEntry].append(budget)
+
+        funding = map_fundingsource_entry(year, nren, answers)
+        if funding:
+            result[FundingSource].append(funding)
+
+        charging = map_charging_structure_entry(year, nren, answers)
+        if charging:
+            result[ChargingStructure].append(charging)
+
+        nren_staff = map_nren_staff(year, nren, answers)
+        if nren_staff:
+            result[NrenStaff].append(nren_staff)
+
+        parent = map_parent_orgs(nren, year, answers)
+        if parent:
+            result[ParentOrganization].append(parent)
+
+        subs = map_sub_orgs(nren, year, answers)
+        for sub in subs:
+            result[SubOrganization].append(sub)
+
+        ec_projects = map_ec_projects(nren, year, answers)
+        for ec_project in ec_projects:
+            result[ECProject].append(ec_project)
+
+        policy = map_policy(nren, year, answers)
+        if policy:
+            result[Policy].append(policy)
+
+        traffic_volume = map_traffic_volume(nren, year, answers)
+        if traffic_volume:
+            result[TrafficVolume].append(traffic_volume)
+
+        institution_urls = map_institution_urls(nren, year, answers)
+        if institution_urls:
+            result[InstitutionURLs].append(institution_urls)
+
+        central_procurement = map_central_procurement(nren, year, answers)
+        if central_procurement:
+            result[CentralProcurement].append(central_procurement)
+
+        service_management = map_service_management(nren, year, answers)
+        if service_management:
+            result[ServiceManagement].append(service_management)
+
+        service_user_types = map_service_user_types(nren, year, answers)
+        for service_user_type in service_user_types:
+            result[ServiceUserTypes].append(service_user_type)
+
+        eosc_listings = map_eosc_listings(nren, year, answers)
+        if eosc_listings:
+            result[EOSCListings].append(eosc_listings)
+
+        standards = map_standards(nren, year, answers)
+        if standards:
+            result[Standards].append(standards)
+
+        crisis_exercises = map_crisis_exercises(nren, year, answers)
+        if crisis_exercises:
+            result[CrisisExercises].append(crisis_exercises)
+
+        security_controls = map_security_controls(nren, year, answers)
+        if security_controls:
+            result[SecurityControls].append(security_controls)
+
+        connected_proportions = map_connected_proportion(nren, year, answers)
+        for connected_proportion in connected_proportions:
+            result[ConnectedProportion].append(connected_proportion)
+
+        connectivity_levels = map_connectivity_levels(nren, year, answers)
+        for connectivity_level in connectivity_levels:
+            result[ConnectivityLevel].append(connectivity_level)
+
+        connection_carriers = map_connection_carriers(nren, year, answers)
+        for connection_carrier in connection_carriers:
+            result[ConnectionCarrier].append(connection_carrier)
+
+        connectivity_loads = map_connectivity_loads(nren, year, answers)
+        for connectivity_load in connectivity_loads:
+            result[ConnectivityLoad].append(connectivity_load)
+
+        connectivity_growths = map_connectivity_growth(nren, year, answers)
+        for connectivity_growth in connectivity_growths:
+            result[ConnectivityGrowth].append(connectivity_growth)
+
+        commercial_connectivities = map_commercial_connectivity(nren, year, answers)
+        for commercial_connectivity in commercial_connectivities:
+            result[CommercialConnectivity].append(commercial_connectivity)
+
+        commercial_charging_levels = map_commercial_charging_levels(nren, year, answers)
+        for commercial_charging_level in commercial_charging_levels:
+            result[CommercialChargingLevel].append(commercial_charging_level)
+
+        remote_campuses = map_remote_campuses(nren, year, answers)
+        if remote_campuses:
+            result[RemoteCampuses].append(remote_campuses)
+
+        dark_fibre_lease = map_dark_fibre_lease(nren, year, answers)
+        if dark_fibre_lease:
+            result[DarkFibreLease].append(dark_fibre_lease)
+
+        dark_fibre_installed = map_dark_fibre_installed(nren, year, answers)
+        if dark_fibre_installed:
+            result[DarkFibreInstalled].append(dark_fibre_installed)
+
+        fibre_light = map_fibre_light(nren, year, answers)
+        if fibre_light:
+            result[FibreLight].append(fibre_light)
+
+        network_map_urls = map_network_map_urls(nren, year, answers)
+        if network_map_urls:
+            result[NetworkMapUrls].append(network_map_urls)
+
+        monitoring_tools = map_monitoring_tools(nren, year, answers)
+        if monitoring_tools:
+            result[MonitoringTools].append(monitoring_tools)
+
+        passive_monitoring = map_passive_monitoring(nren, year, answers)
+        if passive_monitoring:
+            result[PassiveMonitoring].append(passive_monitoring)
+
+        traffic_statistics = map_traffic_statistics(nren, year, answers)
+        if traffic_statistics:
+            result[TrafficStatistics].append(traffic_statistics)
+
+        siem_vendors = map_siem_vendors(nren, year, answers)
+        if siem_vendors:
+            result[SiemVendors].append(siem_vendors)
+
+        certificate_providers = map_certificate_providers(nren, year, answers)
+        if certificate_providers:
+            result[CertificateProviders].append(certificate_providers)
+
+        weathermap = map_weathermap_entry(nren, year, answers)
+        if weathermap:
+            result[WeatherMap].append(weathermap)
+
+        pert_team = map_pert_team(nren, year, answers)
+        if pert_team:
+            result[PertTeam].append(pert_team)
+
+        alienwave = map_alienwave_entry(nren, year, answers)
+        if alienwave:
+            result[AlienWave].append(alienwave)
+
+        capacity = map_capacity_entry(nren, year, answers)
+        if capacity:
+            result[Capacity].append(capacity)
+
+        external_connections = map_external_connections(nren, year, answers)
+        if external_connections:
+            result[ExternalConnections].append(external_connections)
+
+        non_re_peers = map_non_re_peers(nren, year, answers)
+        if non_re_peers:
+            result[NonREPeers].append(non_re_peers)
+
+        traffic_ratio = map_traffic_ratio(nren, year, answers)
+        if traffic_ratio:
+            result[TrafficRatio].append(traffic_ratio)
+
+        ops_automation = map_ops_automation(nren, year, answers)
+        if ops_automation:
+            result[OpsAutomation].append(ops_automation)
+
+        nfv = map_network_function_virtualisation(nren, year, answers)
+        if nfv:
+            result[NetworkFunctionVirtualisation].append(nfv)
+
+        network_automation = map_network_automation(nren, year, answers)
+        if network_automation:
+            result[NetworkAutomation].append(network_automation)
+
+        nren_services = map_nren_services(nren, year, answers)
+        for nren_service in nren_services:
+            result[NRENService].append(nren_service)
+
+    return result
diff --git a/compendium_v2/publishers/year/map_2024.py b/compendium_v2/publishers/year/map_2024.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/test/test_survey_publisher.py b/test/test_survey_publisher.py
index 140356e22e7db89e5e68a3dc0e3f7b9d6d24999a..5b9eb9d3c4a92d12522a2ee7f73157a9a9be38c6 100644
--- a/test/test_survey_publisher.py
+++ b/test/test_survey_publisher.py
@@ -6,7 +6,8 @@ from sqlalchemy import func, select
 
 from compendium_v2 import db
 from compendium_v2.db import presentation_model_enums as model_enums, presentation_models
-from compendium_v2.publishers.survey_publisher import _map_2023
+from compendium_v2.db.survey_models import Survey, SurveyResponse, ResponseStatus, SurveyStatus
+from compendium_v2.publishers.survey_publisher import publish
 
 
 JSON_FILE = os.path.join(os.path.dirname(__file__), "data", "2023_all_questions_answered.json")
@@ -17,13 +18,13 @@ def test_v2_publisher_empty(app):
 
     with app.app_context():
         nren = presentation_models.NREN(name='name', country='country')
+        db.session.add(nren)
+        survey = Survey(year=2023, survey={}, status=SurveyStatus.open)
+        response = SurveyResponse(survey=survey, nren=nren, answers={"data": data}, status=ResponseStatus.completed)
+        db.session.add(survey)
+        db.session.add(response)
         db.session.commit()
 
-    with app.app_context():
-        _map_2023(nren, {"data": data})
-        db.session.commit()
-
-    with app.app_context():
         budget_count = db.session.scalar(select(func.count(presentation_models.BudgetEntry.year)))
         assert budget_count == 0
         # the main thing is actually that it doesnt crash
@@ -35,13 +36,15 @@ def test_v2_publisher_full(app):
 
     with app.app_context():
         nren = presentation_models.NREN(name='name', country='country')
+        db.session.add(nren)
+        survey = Survey(year=2023, survey={}, status=SurveyStatus.open)
+        response = SurveyResponse(survey=survey, nren=nren, answers={"data": data}, status=ResponseStatus.completed)
+        db.session.add(survey)
+        db.session.add(response)
         db.session.commit()
 
-    with app.app_context():
-        _map_2023(nren, {"data": data})
-        db.session.commit()
+        publish(2023)
 
-    with app.app_context():
         budget = db.session.scalar(select(presentation_models.BudgetEntry.budget))
         assert budget == Decimal("124.76")