diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/api/AdminEdaSettingsController.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/api/AdminEdaSettingsController.java new file mode 100644 index 0000000..e8ed0c2 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/api/AdminEdaSettingsController.java @@ -0,0 +1,35 @@ +package at.mueller.eeg.backend.mako.api; + +import at.mueller.eeg.backend.mako.api.dto.EdaSettingsDto; +import at.mueller.eeg.backend.mako.domain.EdaSettings; +import at.mueller.eeg.backend.mako.service.EdaSettingsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping(value = "/api/admin/eda-settings", produces = MediaType.APPLICATION_JSON_VALUE) +@PreAuthorize("hasRole('ADMIN')") +@Tag(name = "Admin EDA Settings", description = "Konfiguration des EDA-Kommunikationswegs") +@RequiredArgsConstructor +public class AdminEdaSettingsController { + + private final EdaSettingsService edaSettingsService; + + @GetMapping + @Operation(summary = "Aktuelle EDA-Einstellungen abrufen") + public EdaSettingsDto getSettings() { + EdaSettings settings = edaSettingsService.getSettings(); + return new EdaSettingsDto(settings.getCommunicationMethod()); + } + + @PutMapping + @Operation(summary = "EDA-Kommunikationsweg aktualisieren") + public EdaSettingsDto updateSettings(@RequestBody EdaSettingsDto dto) { + EdaSettings updated = edaSettingsService.updateCommunicationMethod(dto.communicationMethod()); + return new EdaSettingsDto(updated.getCommunicationMethod()); + } +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/api/dto/EdaSettingsDto.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/api/dto/EdaSettingsDto.java new file mode 100644 index 0000000..9f13b07 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/api/dto/EdaSettingsDto.java @@ -0,0 +1,6 @@ +package at.mueller.eeg.backend.mako.api.dto; + +import at.mueller.eeg.backend.mako.domain.EdaCommunicationMethod; + +public record EdaSettingsDto(EdaCommunicationMethod communicationMethod) { +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/domain/EdaCommunicationMethod.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/domain/EdaCommunicationMethod.java new file mode 100644 index 0000000..e56fb04 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/domain/EdaCommunicationMethod.java @@ -0,0 +1,6 @@ +package at.mueller.eeg.backend.mako.domain; + +public enum EdaCommunicationMethod { + EMAIL, + MESSENGER +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/domain/EdaSettings.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/domain/EdaSettings.java new file mode 100644 index 0000000..ac60e51 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/domain/EdaSettings.java @@ -0,0 +1,22 @@ +package at.mueller.eeg.backend.mako.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.UUID; + +@Entity +@Table(name = "eda_settings") +@Getter +@Setter +public class EdaSettings { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private EdaCommunicationMethod communicationMethod = EdaCommunicationMethod.EMAIL; +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/EdaCommunicationEventListener.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/EdaCommunicationEventListener.java index 641d7e3..16dcb86 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/EdaCommunicationEventListener.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/EdaCommunicationEventListener.java @@ -2,7 +2,7 @@ package at.mueller.eeg.backend.mako.event.listener; import at.mueller.eeg.backend.common.event.*; import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse; -import at.mueller.eeg.backend.mako.service.EdaConsentService; +import at.mueller.eeg.backend.mako.service.EdaCommunicationRouter; import at.mueller.eeg.backend.mako.service.EdaTopologyService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -18,7 +18,7 @@ import org.springframework.stereotype.Component; public class EdaCommunicationEventListener { private final EdaTopologyService edaTopologyService; - private final EdaConsentService edaConsentService; + private final EdaCommunicationRouter edaCommunicationRouter; private final ApplicationEventPublisher eventPublisher; @EventListener @@ -59,7 +59,7 @@ public class EdaCommunicationEventListener { event.atNumber(), event.pointType()); try { - edaConsentService.sendConsentRequest(event.atNumber(), event.pointType()); + edaCommunicationRouter.resolve().sendConsentRequest(event.atNumber(), event.pointType()); log.info("MaKo-Domain: Consent-Request erfolgreich an EDA übermittelt für {}", event.atNumber()); diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/repository/EdaSettingsRepository.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/repository/EdaSettingsRepository.java new file mode 100644 index 0000000..fe1f4da --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/repository/EdaSettingsRepository.java @@ -0,0 +1,11 @@ +package at.mueller.eeg.backend.mako.repository; + +import at.mueller.eeg.backend.mako.domain.EdaSettings; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface EdaSettingsRepository extends JpaRepository { + Optional findFirstByOrderByIdAsc(); +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/EdaCommunicationRouter.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/EdaCommunicationRouter.java new file mode 100644 index 0000000..981b22c --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/EdaCommunicationRouter.java @@ -0,0 +1,52 @@ +package at.mueller.eeg.backend.mako.service; + +import at.mueller.eeg.backend.mako.domain.EdaCommunicationMethod; +import at.mueller.eeg.backend.mako.domain.EdaSettings; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; + +import java.util.Arrays; + +@Slf4j +@Service +public class EdaCommunicationRouter { + + private final EdaSettingsService settingsService; + private final Environment environment; + private final EdaConsentService simulatedService; + private final ObjectProvider emailServiceProvider; + private final ObjectProvider messengerServiceProvider; + + public EdaCommunicationRouter( + EdaSettingsService settingsService, + Environment environment, + @Qualifier("simulatedEdaConsentService") EdaConsentService simulatedService, + @Qualifier("emailEdaConsentService") ObjectProvider emailServiceProvider, + @Qualifier("messengerEdaConsentService") ObjectProvider messengerServiceProvider) { + this.settingsService = settingsService; + this.environment = environment; + this.simulatedService = simulatedService; + this.emailServiceProvider = emailServiceProvider; + this.messengerServiceProvider = messengerServiceProvider; + } + + public EdaConsentService resolve() { + if (Arrays.asList(environment.getActiveProfiles()).contains("dev")) { + log.debug("[EDA-ROUTER] Dev-Modus: Simulation wird verwendet."); + return simulatedService; + } + + EdaSettings settings = settingsService.getSettings(); + EdaCommunicationMethod method = settings.getCommunicationMethod(); + + log.info("[EDA-ROUTER] Kommunikationsweg = {}", method); + + return switch (method) { + case EMAIL -> emailServiceProvider.getObject(); + case MESSENGER -> messengerServiceProvider.getObject(); + }; + } +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/EdaSettingsService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/EdaSettingsService.java new file mode 100644 index 0000000..dc01daf --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/EdaSettingsService.java @@ -0,0 +1,38 @@ +package at.mueller.eeg.backend.mako.service; + +import at.mueller.eeg.backend.mako.domain.EdaCommunicationMethod; +import at.mueller.eeg.backend.mako.domain.EdaSettings; +import at.mueller.eeg.backend.mako.repository.EdaSettingsRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EdaSettingsService { + + private final EdaSettingsRepository repository; + + @Transactional(readOnly = true) + public EdaSettings getSettings() { + return repository.findFirstByOrderByIdAsc() + .orElseGet(this::createDefault); + } + + @Transactional + public EdaSettings updateCommunicationMethod(EdaCommunicationMethod method) { + EdaSettings settings = getSettings(); + settings.setCommunicationMethod(method); + EdaSettings saved = repository.save(settings); + log.info("EDA-Kommunikationsweg geaendert auf: {}", method); + return saved; + } + + private EdaSettings createDefault() { + EdaSettings defaultSettings = new EdaSettings(); + defaultSettings.setCommunicationMethod(EdaCommunicationMethod.EMAIL); + return repository.save(defaultSettings); + } +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/impl/MessengerEdaConsentService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/impl/MessengerEdaConsentService.java new file mode 100644 index 0000000..a45b4b7 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/service/impl/MessengerEdaConsentService.java @@ -0,0 +1,16 @@ +package at.mueller.eeg.backend.mako.service.impl; + +import at.mueller.eeg.backend.mako.service.EdaConsentService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service("messengerEdaConsentService") +public class MessengerEdaConsentService implements EdaConsentService { + + @Override + public void sendConsentRequest(String atNumber, String pointType) { + log.warn("[EDA-MESSENGER] Messenger-Interface noch nicht implementiert. AT-Nummer: {}, Typ: {}", atNumber, pointType); + throw new UnsupportedOperationException("Messenger-Interface fuer EDA-Kommunikation noch nicht implementiert."); + } +} diff --git a/eeg_frontend/openapi.yaml b/eeg_frontend/openapi.yaml index 3cd0687..5b2714f 100644 --- a/eeg_frontend/openapi.yaml +++ b/eeg_frontend/openapi.yaml @@ -10,6 +10,8 @@ tags: description: Public Endpoints für Login und Registrierung - name: User Profile description: Profilverwaltung und Passwort-Reset +- name: Admin EDA Settings + description: Konfiguration des EDA-Kommunikationswegs - name: Admin IAM description: User-Verwaltung und Freigabe-Workflow paths: @@ -305,6 +307,37 @@ paths: application/json: schema: $ref: "#/components/schemas/CommunityTariffResponse" + /api/admin/eda-settings: + get: + tags: + - Admin EDA Settings + summary: Aktuelle EDA-Einstellungen abrufen + operationId: getSettings + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EdaSettingsDto" + put: + tags: + - Admin EDA Settings + summary: EDA-Kommunikationsweg aktualisieren + operationId: updateSettings + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EdaSettingsDto" + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EdaSettingsDto" /api/tariffs: post: tags: @@ -1235,6 +1268,14 @@ components: updatedAt: type: string format: date-time + EdaSettingsDto: + type: object + properties: + communicationMethod: + type: string + enum: + - EMAIL + - MESSENGER TariffInviteRequest: type: object properties: @@ -1339,6 +1380,7 @@ components: type: string enum: - EMAIL_XLSX + - EMAIL_AUTO - API - MANUAL fileName: @@ -1630,6 +1672,7 @@ components: type: string enum: - EMAIL_XLSX + - EMAIL_AUTO - API - MANUAL MembershipResponse: diff --git a/eeg_frontend/src/app/app.routes.ts b/eeg_frontend/src/app/app.routes.ts index 662c156..890e11c 100644 --- a/eeg_frontend/src/app/app.routes.ts +++ b/eeg_frontend/src/app/app.routes.ts @@ -22,6 +22,7 @@ import {ConsumptionDashboardComponent} from './pages/consumption-dashboard/consu import {UploadMeteringDataComponent} from './pages/upload-metering-data/upload-metering-data'; import {ProducerInvitesComponent} from './pages/producer-invites/producer-invites'; import {ConsumerInvitesComponent} from './pages/consumer-invites/consumer-invites'; +import {AdminEdaSettingsComponent} from './pages/admin-eda-settings/admin-eda-settings'; export const routes: Routes = [ { path: '', component: LandingPage }, @@ -76,6 +77,11 @@ export const routes: Routes = [ component: UploadMeteringDataComponent, canActivate: [roleGuard(['ADMIN'])] }, + { + path: 'admin-eda-settings', + component: AdminEdaSettingsComponent, + canActivate: [roleGuard(['ADMIN'])] + }, { path: 'my-tariffs', component: UserTariffComponent, diff --git a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts index 769cb4b..aeacd2c 100644 --- a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts +++ b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts @@ -29,4 +29,5 @@ export const NAV_ITEMS: NavItem[] = [ {label: 'Energiegemeinschaften', path: '/dashboard/energy-communities', roles: ['ADMIN'], section: 'Verwaltung'}, {label: 'Tarife verwalten', path: '/dashboard/tariffs', roles: ['ADMIN'], section: 'Verwaltung'}, {label: 'Messdaten-Upload', path: '/dashboard/upload-metering-data', roles: ['ADMIN'], section: 'Verwaltung'}, + {label: 'EDA-Einstellungen', path: '/dashboard/admin-eda-settings', roles: ['ADMIN'], section: 'Verwaltung'}, ]; diff --git a/eeg_frontend/src/app/pages/admin-eda-settings/admin-eda-settings.html b/eeg_frontend/src/app/pages/admin-eda-settings/admin-eda-settings.html new file mode 100644 index 0000000..ee1e84d --- /dev/null +++ b/eeg_frontend/src/app/pages/admin-eda-settings/admin-eda-settings.html @@ -0,0 +1,54 @@ +
+
+

EDA-Einstellungen

+

Konfiguriere den Kommunikationsweg fuer die EDA-Zustimmungsabfrage.

+
+ +
+ @if (isLoading()) { +
Lade Einstellungen...
+ } @else { +
+
+ +
+ + + +
+
+ +
+ +
+
+ } +
+
diff --git a/eeg_frontend/src/app/pages/admin-eda-settings/admin-eda-settings.spec.ts b/eeg_frontend/src/app/pages/admin-eda-settings/admin-eda-settings.spec.ts new file mode 100644 index 0000000..f3c271b --- /dev/null +++ b/eeg_frontend/src/app/pages/admin-eda-settings/admin-eda-settings.spec.ts @@ -0,0 +1,28 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {provideHttpClient} from '@angular/common/http'; +import {provideHttpClientTesting} from '@angular/common/http/testing'; +import {AdminEdaSettingsComponent} from './admin-eda-settings'; +import {describe, expect, it} from 'vitest'; + +describe('AdminEdaSettingsComponent', () => { + let component: AdminEdaSettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminEdaSettingsComponent], + providers: [provideHttpClient(), provideHttpClientTesting()] + }).compileComponents(); + + fixture = TestBed.createComponent(AdminEdaSettingsComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have default method EMAIL', () => { + expect(component.selectedMethod()).toBe('EMAIL'); + }); +}); diff --git a/eeg_frontend/src/app/pages/admin-eda-settings/admin-eda-settings.ts b/eeg_frontend/src/app/pages/admin-eda-settings/admin-eda-settings.ts new file mode 100644 index 0000000..779e2ba --- /dev/null +++ b/eeg_frontend/src/app/pages/admin-eda-settings/admin-eda-settings.ts @@ -0,0 +1,58 @@ +import {Component, inject, OnInit, signal} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormsModule} from '@angular/forms'; +import {ToastService} from '../../services/toast'; +import {AdminEDASettingsService, EdaSettingsDto} from '../../api'; + +@Component({ + selector: 'app-admin-eda-settings', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './admin-eda-settings.html', +}) +export class AdminEdaSettingsComponent implements OnInit { + private edaSettingsService = inject(AdminEDASettingsService); + private toastService = inject(ToastService); + + selectedMethod = signal('EMAIL'); + isLoading = signal(false); + isSaving = signal(false); + + ngOnInit() { + this.loadSettings(); + } + + loadSettings() { + this.isLoading.set(true); + this.edaSettingsService.getSettings().subscribe({ + next: (settings) => { + if (settings.communicationMethod) { + this.selectedMethod.set(settings.communicationMethod); + } + this.isLoading.set(false); + }, + error: (err) => { + this.toastService.error(err.error?.message || 'Fehler beim Laden der EDA-Einstellungen.'); + this.isLoading.set(false); + } + }); + } + + saveSettings() { + this.isSaving.set(true); + const dto: EdaSettingsDto = {communicationMethod: this.selectedMethod()}; + this.edaSettingsService.updateSettings(dto).subscribe({ + next: (settings) => { + if (settings.communicationMethod) { + this.selectedMethod.set(settings.communicationMethod); + } + this.toastService.success('EDA-Einstellungen erfolgreich gespeichert.'); + this.isSaving.set(false); + }, + error: (err) => { + this.toastService.error(err.error?.message || 'Fehler beim Speichern der EDA-Einstellungen.'); + this.isSaving.set(false); + } + }); + } +} diff --git a/eeg_frontend/src/app/pages/user-tariff/user-tariff.ts b/eeg_frontend/src/app/pages/user-tariff/user-tariff.ts index 767ad11..65e0d6a 100644 --- a/eeg_frontend/src/app/pages/user-tariff/user-tariff.ts +++ b/eeg_frontend/src/app/pages/user-tariff/user-tariff.ts @@ -195,7 +195,7 @@ export class UserTariffComponent implements OnInit, OnDestroy { return; } - this.userTariffService.deleteUserTariff(tariff.id!, userId).subscribe({ + this.userTariffService.deleteUserTariff(tariff.id!).subscribe({ next: () => { this.toastService.success('Tarif erfolgreich gelöscht.'); this.loadUserTariffs(this.selectedCommunityId());