Compare commits
No commits in common. "83b6d503cc624d0d99db20bc228d06296485031f" and "b1f38e752bdb12d8f9c6afedd9ef4f525d318794" have entirely different histories.
83b6d503cc
...
b1f38e752b
38 changed files with 3221 additions and 2570 deletions
|
|
@ -6,19 +6,11 @@
|
||||||
# ---- Stage 1: Build Angular Frontend ----
|
# ---- Stage 1: Build Angular Frontend ----
|
||||||
FROM node:24-slim AS frontend-build
|
FROM node:24-slim AS frontend-build
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
default-jre-headless \
|
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app/eeg_frontend
|
WORKDIR /app/eeg_frontend
|
||||||
|
|
||||||
COPY eeg_frontend/package.json eeg_frontend/package-lock.json* ./
|
COPY eeg_frontend/package.json eeg_frontend/package-lock.json* ./
|
||||||
RUN npm ci --prefer-offline
|
RUN npm ci --prefer-offline
|
||||||
|
|
||||||
COPY eeg_frontend/openapi.yaml ./
|
|
||||||
RUN npx openapi-generator-cli generate -i ./openapi.yaml -g typescript-angular -o ./src/app/api
|
|
||||||
|
|
||||||
COPY eeg_frontend/ ./
|
COPY eeg_frontend/ ./
|
||||||
RUN npm run build -- --configuration production
|
RUN npm run build -- --configuration production
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
# Plan: E-Mail-basierte EDA-Kommunikation
|
|
||||||
|
|
||||||
## Ziel
|
|
||||||
|
|
||||||
Ersetze die aktuelle vollständige EDA-Simulation durch eine reale E-Mail-basierte Kommunikation mit dem EDA-Netzwerk (Netzbetreiber). Simulator bleibt als `@Profile("!prod")`-Fallback. Architektur erlaubt später einfachen Wechsel zu Messenger-API.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Aktueller Stand
|
|
||||||
|
|
||||||
### Was existiert
|
|
||||||
|
|
||||||
- `EdaConsentService` (Interface) + `SimulatedEdaConsentService` (immer aktiv, kein @Profile)
|
|
||||||
- `EdaTopologyService` (Interface) + `SimulatedEdaTopologyService` (immer aktiv)
|
|
||||||
- `MailService` (Interface) für User-Verifizierung, nicht für EDA
|
|
||||||
- `spring-boot-starter-restclient` im pom.xml (nicht verwendet)
|
|
||||||
- `DataSource`-Enum hat `EMAIL_XLSX` aber kein automatisches E-Mail-Polling
|
|
||||||
- Kommentar in `EdaCommunicationEventListener:66`: "entweder via Webhook oder Mailbox"
|
|
||||||
|
|
||||||
### Was fehlt
|
|
||||||
|
|
||||||
- Keine reale EDA-Kommunikation (kein HTTP, kein E-Mail-Versand an EDA)
|
|
||||||
- Keine automatische Verbrauchsdaten-Ingestion via E-Mail
|
|
||||||
- Simulator hat kein `@Profile` - immer aktiv, auch in prod
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architektur-Entscheidung: Strategy Pattern
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────┐
|
|
||||||
│ EdaConsentService │ (Interface - bleibt unverändert)
|
|
||||||
│ EdaTopologyService │
|
|
||||||
└─────────┬───────────────┘
|
|
||||||
│
|
|
||||||
┌───────────────────┼───────────────────┐
|
|
||||||
│ │ │
|
|
||||||
┌─────────▼──────────┐ ┌─────▼───────────┐ ┌────▼──────────────┐
|
|
||||||
│ SimulatedEda* │ │ EmailEda* │ │ MessengerEda* │
|
|
||||||
│ @Profile("!prod") │ │ @Profile("prod")│ │ (später) │
|
|
||||||
└────────────────────┘ └─────────────────┘ └───────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Umsetzungs-Schritte
|
|
||||||
|
|
||||||
### Schritt 1: Simulator mit @Profile versehen
|
|
||||||
|
|
||||||
**Dateien:**
|
|
||||||
- `eeg_backend/.../mako/service/impl/SimulatedEdaConsentService.java`
|
|
||||||
- `eeg_backend/.../mako/service/impl/SimulatedEdaTopologyService.java`
|
|
||||||
|
|
||||||
**Änderung:**
|
|
||||||
```java
|
|
||||||
// Vorher:
|
|
||||||
@Service
|
|
||||||
public class SimulatedEdaConsentService implements EdaConsentService {
|
|
||||||
|
|
||||||
// Nachher:
|
|
||||||
@Profile("!prod")
|
|
||||||
@Service
|
|
||||||
public class SimulatedEdaConsentService implements EdaConsentService {
|
|
||||||
```
|
|
||||||
|
|
||||||
**Hinweis:** `@Profile("!prod")` statt `@Profile("dev")` - so sind die Simulatoren auch ohne Profil (z.B. in Tests) verfügbar. Die `EdaCommunicationEventListener` hat kein Profil und braucht immer einen `EdaConsentService` + `EdaTopologyService`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Schritt 2: EDA E-Mail-Konfiguration
|
|
||||||
|
|
||||||
**application-prod.yml (ergänzen):**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
eda:
|
|
||||||
email:
|
|
||||||
gateway-address: ${EDA_GATEWAY_EMAIL:eda-gateway@example.com}
|
|
||||||
poll-interval-ms: ${EDA_POLL_INTERVAL_MS:900000}
|
|
||||||
imap:
|
|
||||||
host: ${EDA_IMAP_HOST:}
|
|
||||||
port: ${EDA_IMAP_PORT:993}
|
|
||||||
username: ${EDA_IMAP_USERNAME:}
|
|
||||||
password: ${EDA_IMAP_PASSWORD:}
|
|
||||||
folder: INBOX
|
|
||||||
protocol: imaps
|
|
||||||
smtp:
|
|
||||||
host: ${EDA_SMTP_HOST:}
|
|
||||||
port: ${EDA_SMTP_PORT:587}
|
|
||||||
username: ${EDA_SMTP_USERNAME:}
|
|
||||||
password: ${EDA_SMTP_PASSWORD:}
|
|
||||||
```
|
|
||||||
|
|
||||||
**application-dev.yml (ergänzen):**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
eda:
|
|
||||||
email:
|
|
||||||
gateway-address: test-eda@example.com
|
|
||||||
poll-interval-ms: 900000
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Schritt 3: E-Mail-basierte Consent-Implementierung
|
|
||||||
|
|
||||||
#### EDA-E-Mail-Format (Hinweis)
|
|
||||||
|
|
||||||
Das EDA-Netzwerk (Elektrizitäts-Daten-Austausch) in Österreich nutzt für E-Mail-Kommunikation typischerweise:
|
|
||||||
|
|
||||||
- **Betreff:** Strukturiert mit AT-Nummer + Nachrichtentyp (z.B. `CMRequest|AT...` für Consent)
|
|
||||||
- **Body:** XML oder strukturiertes Textformat mit AT-Nummer, Entscheidung, Referenz-ID
|
|
||||||
- **Attachments:** XLSX/CSV für Verbrauchsdaten
|
|
||||||
- **Zentraler Empfänger:** EDA-Gateway leitet an Netzbetreiber weiter (wie beim Messenger)
|
|
||||||
|
|
||||||
Die genaue Spezifikation muss vom EDA-Gateway-Anbieter bestätigt werden. Die Architektur ist flexibel gehalten.
|
|
||||||
|
|
||||||
#### Neue Dateien
|
|
||||||
|
|
||||||
**EmailEdaConsentService.java** - Prod Consent via SMTP:
|
|
||||||
- `@Profile("prod")`
|
|
||||||
- Implementiert `EdaConsentService`
|
|
||||||
- `sendConsentRequest()`:
|
|
||||||
1. Baut Consent-E-Mail auf (AT-Nummer, Typ, Portal-Referenz-ID)
|
|
||||||
2. Versendet an `eda.email.gateway-address` via SMTP
|
|
||||||
3. Speichert Request-Metadaten für Zuordnung der Antwort
|
|
||||||
4. Logging
|
|
||||||
|
|
||||||
**EdaMailSender.java** - E-Mail-Versand:
|
|
||||||
- Hilfsklasse zum Aufbau und Versand von EDA-E-Mails
|
|
||||||
- Consent-Request-E-Mail Vorlage (XML-Format)
|
|
||||||
- `@Async` für nicht-blockierenden Versand
|
|
||||||
|
|
||||||
**EdaMailParser.java** - E-Mail-Parsing:
|
|
||||||
- Hilfsklasse zum Parsen von EDA-E-Mails
|
|
||||||
- Erkennt E-Mail-Typ (Consent-Antwort vs. Verbrauchsdaten)
|
|
||||||
- Extrahiert AT-Nummer, Entscheidung, Referenz-ID
|
|
||||||
- Flexibel: E-Mail-Format-Konfiguration via Properties
|
|
||||||
|
|
||||||
**EdaMailPoller.java** - IMAP-Polling:
|
|
||||||
- `@Profile("prod")`
|
|
||||||
- `@Scheduled(fixedDelayString = "${eda.email.poll-interval-ms:900000}")` - alle 15 Minuten
|
|
||||||
- Verbindet sich via IMAP zum EDA-Postfach
|
|
||||||
- Parst eingehende E-Mails:
|
|
||||||
- **Consent-Antwort:** Erkennt AT-Nummer + Entscheidung (approved/rejected)
|
|
||||||
- **Verbrauchsdaten-Attachment:** Erkennt XLSX/CSV
|
|
||||||
- Für Consent-Antworten:
|
|
||||||
- Veröffentlicht `MakoConsentApprovedEvent` / `MakoConsentRejectedEvent` / `MakoConsentTechnicalFailedEvent`
|
|
||||||
- Für Verbrauchsdaten:
|
|
||||||
- Nutzt bestehenden `XlsxMeteringDataParser` für XLSX
|
|
||||||
- Speichert via `MeteringDataService`
|
|
||||||
- Erstellt `MeteringDataUpload`-Audit-Eintrag
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Schritt 4: Verbrauchsdaten-Ingestion via E-Mail
|
|
||||||
|
|
||||||
**Integration in EdaMailPoller:**
|
|
||||||
|
|
||||||
Der Poller erkennt eingehende E-Mails mit Verbrauchsdaten-Attachments:
|
|
||||||
|
|
||||||
1. **Mail-Scan:** Durchsucht Postfach nach neuen E-Mails mit Attachments
|
|
||||||
2. **Attachment-Erkennung:** XLSX, CSV (je nach EDA-Standard)
|
|
||||||
3. **Parsing:** Nutzt `XlsxMeteringDataParser` (bestehend)
|
|
||||||
4. **Speicherung:** Ruft `MeteringDataService.uploadMeteringData()` auf
|
|
||||||
5. **Audit:** Erstellt `MeteringDataUpload` mit `DataSource.EMAIL_AUTO`
|
|
||||||
|
|
||||||
**DataSource erweitern:**
|
|
||||||
- `DataSource.java` - `EMAIL_AUTO` hinzugefügt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Schritt 5: Tests
|
|
||||||
|
|
||||||
**Neue/angepasste Dateien:**
|
|
||||||
- `EmailEdaConsentServiceTest.java` - Mock SMTP, verify E-Mail-Inhalt
|
|
||||||
- `EdaMailParserTest.java` - Test verschiedener E-Mail-Formate
|
|
||||||
- `EdaMailPollerTest.java` - Unit-Test für Polling-Logik
|
|
||||||
- `SimulatorProfileTest.java` - Verifiziere @Profile("!prod")
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Datei-Übersicht
|
|
||||||
|
|
||||||
| Aktion | Datei |
|
|
||||||
|--------|-------|
|
|
||||||
| Ändern | `SimulatedEdaConsentService.java` - @Profile("!prod") |
|
|
||||||
| Ändern | `SimulatedEdaTopologyService.java` - @Profile("!prod") |
|
|
||||||
| Ändern | `application-prod.yml` - EDA E-Mail-Konfiguration |
|
|
||||||
| Ändern | `application-dev.yml` - EDA Dev-Konfiguration |
|
|
||||||
| Ändern | `DataSource.java` - EMAIL_AUTO hinzufügen |
|
|
||||||
| Neu | `EmailEdaConsentService.java` - Prod Consent via SMTP |
|
|
||||||
| Neu | `EdaMailSender.java` - E-Mail-Versand |
|
|
||||||
| Neu | `EdaMailParser.java` - E-Mail-Parsing |
|
|
||||||
| Neu | `EdaMailPoller.java` - IMAP-Polling (15 Min) für Antworten + Verbrauchsdaten |
|
|
||||||
| Neu | Tests |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verifizierung
|
|
||||||
|
|
||||||
1. **Dev-Modus:** `@Profile("!prod")` aktiv → Simulator wird geladen, kein EDA-Mail-Poller
|
|
||||||
2. **Prod-Modus:** `@Profile("prod")` aktiv → `EmailEdaConsentService` + `EdaMailPoller` werden geladen
|
|
||||||
3. **Unit-Tests:** `mvn test -pl eeg_backend` - alle Tests grün (232 Tests)
|
|
||||||
4. **Integrationstest:**
|
|
||||||
- Dev: Konsistenter Flow mit Simulator (AT-Nummer endet mit REJECT/ERROR)
|
|
||||||
- Prod: Echte SMTP/IMAP-Verbindung testen (kann mit Test-Server)
|
|
||||||
5. **Manuell prüfen:**
|
|
||||||
- Consent-Flow: User registrieren → Admin genehmigen → MeteringPoint wechselt zu WAITING_FOR_CONSENT
|
|
||||||
- Simulator: AT-Nummer "AT0010001234567890123456789012345" → Consent Approved nach Delay
|
|
||||||
- E-Mail: Echte E-Mail wird an Gateway gesendet
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Offene Fragen
|
|
||||||
|
|
||||||
1. **EDA-Spezifikation:** Die genaue E-Mail-Format-Spezifikation muss vom EDA-Gateway-Anbieter bezogen werden. Die Architektur ist flexibel gehalten (Konfiguration via Properties).
|
|
||||||
2. **Polling-Intervall:** **Entschieden: alle 15 Minuten** (Standardwert in Konfiguration, konfigurierbar).
|
|
||||||
3. **Fehlerbehandlung:** Was passiert bei nicht zuordenbaren E-Mails? (Log + Flag) - Muss noch definiert werden.
|
|
||||||
4. **Verbrauchsdaten-Format:** XLSX oder CSV? (XLSX-Parsing existiert bereits) - Muss mit EDA-Gateway-Anbieter abgestimmt werden.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementierungsstand
|
|
||||||
|
|
||||||
**Status: ABGESCHLOSSEN**
|
|
||||||
|
|
||||||
Alle 6 Schritte wurden implementiert und alle 232 Tests bestehen.
|
|
||||||
|
|
@ -2,7 +2,6 @@ package at.mueller.eeg.backend.community.domain;
|
||||||
|
|
||||||
public enum DataSource {
|
public enum DataSource {
|
||||||
EMAIL_XLSX,
|
EMAIL_XLSX,
|
||||||
EMAIL_AUTO,
|
|
||||||
API,
|
API,
|
||||||
MANUAL
|
MANUAL
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package at.mueller.eeg.backend.mako.api.dto;
|
|
||||||
|
|
||||||
import at.mueller.eeg.backend.mako.domain.EdaCommunicationMethod;
|
|
||||||
|
|
||||||
public record EdaSettingsDto(EdaCommunicationMethod communicationMethod) {
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package at.mueller.eeg.backend.mako.domain;
|
|
||||||
|
|
||||||
public enum EdaCommunicationMethod {
|
|
||||||
EMAIL,
|
|
||||||
MESSENGER
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
package at.mueller.eeg.backend.mako.email;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.context.annotation.Profile;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.mail.internet.MimeMessage;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Profile("prod")
|
|
||||||
@Component
|
|
||||||
public class EdaMailParser {
|
|
||||||
|
|
||||||
public record ConsentResponse(String atNumber, ConsentDecision decision, String reason) {
|
|
||||||
public enum ConsentDecision {
|
|
||||||
APPROVED, REJECTED, TECHNICAL_FAILURE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record MeteringDataMail(String atNumber, byte[] attachment, String fileName) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<ConsentResponse> parseConsentResponse(MimeMessage message) {
|
|
||||||
try {
|
|
||||||
String subject = message.getSubject();
|
|
||||||
if (subject == null || !subject.startsWith("CMResponse|")) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
String atNumber = subject.substring("CMResponse|".length()).trim();
|
|
||||||
String body = extractTextContent(message);
|
|
||||||
|
|
||||||
ConsentResponse.ConsentDecision decision = extractDecision(body);
|
|
||||||
String reason = extractReason(body);
|
|
||||||
|
|
||||||
return Optional.of(new ConsentResponse(atNumber, decision, reason));
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("[EDA-PARSER] Fehler beim Parsen der Consent-Antwort: {}", e.getMessage());
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMeteringDataMail(MimeMessage message) {
|
|
||||||
try {
|
|
||||||
String subject = message.getSubject();
|
|
||||||
return subject != null && subject.startsWith("Verbrauchsdaten|");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ConsentResponse.ConsentDecision extractDecision(String body) {
|
|
||||||
String upperBody = body.toUpperCase();
|
|
||||||
if (upperBody.contains("APPROVED") || upperBody.contains("ZUSTIMMUNG_ERT") || upperBody.contains("ZUSTIMMUNG_ERTEILT")) {
|
|
||||||
return ConsentResponse.ConsentDecision.APPROVED;
|
|
||||||
} else if (upperBody.contains("REJECTED") || upperBody.contains("ABGELEHNT")) {
|
|
||||||
return ConsentResponse.ConsentDecision.REJECTED;
|
|
||||||
}
|
|
||||||
return ConsentResponse.ConsentDecision.TECHNICAL_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractReason(String body) {
|
|
||||||
int reasonStart = body.indexOf("<REASON>");
|
|
||||||
int reasonEnd = body.indexOf("</REASON>");
|
|
||||||
if (reasonStart >= 0 && reasonEnd > reasonStart) {
|
|
||||||
return body.substring(reasonStart + 8, reasonEnd).trim();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractTextContent(MimeMessage message) {
|
|
||||||
try {
|
|
||||||
Object content = message.getContent();
|
|
||||||
if (content instanceof String text) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
} catch (IOException | jakarta.mail.MessagingException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
package at.mueller.eeg.backend.mako.email;
|
|
||||||
|
|
||||||
import at.mueller.eeg.backend.common.event.MakoConsentApprovedEvent;
|
|
||||||
import at.mueller.eeg.backend.common.event.MakoConsentRejectedEvent;
|
|
||||||
import at.mueller.eeg.backend.common.event.MakoConsentTechnicalFailedEvent;
|
|
||||||
import at.mueller.eeg.backend.community.api.dto.MeteringDataRecordDto;
|
|
||||||
import at.mueller.eeg.backend.community.api.dto.MeteringDataUploadRequest;
|
|
||||||
import at.mueller.eeg.backend.community.api.dto.MeteringDataUploadResponse;
|
|
||||||
import at.mueller.eeg.backend.community.domain.DataSource;
|
|
||||||
import at.mueller.eeg.backend.community.service.MeteringDataService;
|
|
||||||
import at.mueller.eeg.backend.community.service.XlsxMeteringDataParser;
|
|
||||||
import jakarta.mail.*;
|
|
||||||
import jakarta.mail.internet.MimeMessage;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
|
||||||
import org.springframework.context.annotation.Profile;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Profile("prod")
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class EdaMailPoller {
|
|
||||||
|
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
|
||||||
private final EdaMailParser edaMailParser;
|
|
||||||
private final MeteringDataService meteringDataService;
|
|
||||||
private final XlsxMeteringDataParser xlsxMeteringDataParser;
|
|
||||||
|
|
||||||
@Value("${eda.email.imap.host}")
|
|
||||||
private String imapHost;
|
|
||||||
|
|
||||||
@Value("${eda.email.imap.port}")
|
|
||||||
private int imapPort;
|
|
||||||
|
|
||||||
@Value("${eda.email.imap.username}")
|
|
||||||
private String imapUsername;
|
|
||||||
|
|
||||||
@Value("${eda.email.imap.password}")
|
|
||||||
private String imapPassword;
|
|
||||||
|
|
||||||
@Value("${eda.email.imap.folder:INBOX}")
|
|
||||||
private String folderName;
|
|
||||||
|
|
||||||
@Value("${eda.email.imap.protocol:imaps}")
|
|
||||||
private String protocol;
|
|
||||||
|
|
||||||
@Scheduled(fixedDelayString = "${eda.email.poll-interval-ms:900000}")
|
|
||||||
public void pollMailbox() {
|
|
||||||
if (imapHost == null || imapHost.isBlank()) {
|
|
||||||
log.debug("[EDA-POLLER] Kein IMAP-Host konfiguriert, Poller uebersprungen.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("[EDA-POLLER] Starte Mailbox-Polling...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
Properties props = new Properties();
|
|
||||||
props.put("mail.store.protocol", protocol);
|
|
||||||
props.put("mail." + protocol + ".host", imapHost);
|
|
||||||
props.put("mail." + protocol + ".port", imapPort);
|
|
||||||
props.put("mail." + protocol + ".ssl.enable", "true");
|
|
||||||
|
|
||||||
Session session = Session.getInstance(props, null);
|
|
||||||
Store store = session.getStore(protocol);
|
|
||||||
store.connect(imapHost, imapPort, imapUsername, imapPassword);
|
|
||||||
|
|
||||||
Folder folder = store.getFolder(folderName);
|
|
||||||
folder.open(Folder.READ_WRITE);
|
|
||||||
|
|
||||||
Message[] messages = folder.getMessages();
|
|
||||||
log.info("[EDA-POLLER] {} neue Nachrichten gefunden.", messages.length);
|
|
||||||
|
|
||||||
for (Message message : messages) {
|
|
||||||
processMessage(message);
|
|
||||||
message.setFlag(Flags.Flag.SEEN, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
folder.close(true);
|
|
||||||
store.close();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[EDA-POLLER] Fehler beim Polling: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processMessage(Message message) {
|
|
||||||
try {
|
|
||||||
if (message instanceof MimeMessage mimeMessage) {
|
|
||||||
if (edaMailParser.isMeteringDataMail(mimeMessage)) {
|
|
||||||
processMeteringDataMail(mimeMessage);
|
|
||||||
} else {
|
|
||||||
processPotentialConsentResponse(mimeMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("[EDA-POLLER] Fehler beim Verarbeiten einer Nachricht: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processPotentialConsentResponse(MimeMessage message) {
|
|
||||||
edaMailParser.parseConsentResponse(message).ifPresent(response -> {
|
|
||||||
log.info("[EDA-POLLER] Consent-Antwort empfangen: AT-Nummer={}, Entscheidung={}",
|
|
||||||
response.atNumber(), response.decision());
|
|
||||||
|
|
||||||
switch (response.decision()) {
|
|
||||||
case APPROVED -> eventPublisher.publishEvent(new MakoConsentApprovedEvent(response.atNumber()));
|
|
||||||
case REJECTED -> eventPublisher.publishEvent(new MakoConsentRejectedEvent(
|
|
||||||
response.atNumber(), response.reason()));
|
|
||||||
case TECHNICAL_FAILURE -> eventPublisher.publishEvent(new MakoConsentTechnicalFailedEvent(
|
|
||||||
response.atNumber(), response.reason()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processMeteringDataMail(MimeMessage message) {
|
|
||||||
try {
|
|
||||||
String subject = message.getSubject();
|
|
||||||
String atNumber = subject != null && subject.startsWith("Verbrauchsdaten|")
|
|
||||||
? subject.substring("Verbrauchsdaten|".length()).trim()
|
|
||||||
: "UNKNOWN";
|
|
||||||
|
|
||||||
log.info("[EDA-POLLER] Verarbeite Verbrauchsdaten-Email fuer AT-Nummer: {}", atNumber);
|
|
||||||
|
|
||||||
if (message.getContent() instanceof Multipart multipart) {
|
|
||||||
for (int i = 0; i < multipart.getCount(); i++) {
|
|
||||||
BodyPart bodyPart = multipart.getBodyPart(i);
|
|
||||||
if (Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition())) {
|
|
||||||
String fileName = bodyPart.getFileName();
|
|
||||||
if (fileName != null && (fileName.endsWith(".xlsx") || fileName.endsWith(".xls"))) {
|
|
||||||
processXlsxAttachment(bodyPart.getInputStream(), fileName, atNumber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[EDA-POLLER] Fehler bei der Verarbeitung der Verbrauchsdaten-Email: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processXlsxAttachment(InputStream inputStream, String fileName, String atNumber) {
|
|
||||||
try {
|
|
||||||
log.info("[EDA-POLLER] Verarbeite XLSX-Attachment: {}", fileName);
|
|
||||||
|
|
||||||
List<MeteringDataRecordDto> records = xlsxMeteringDataParser.parse(inputStream);
|
|
||||||
|
|
||||||
if (records.isEmpty()) {
|
|
||||||
log.warn("[EDA-POLLER] Keine gueltigen Datensaetze in der Datei: {}", fileName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MeteringDataUploadRequest request = new MeteringDataUploadRequest(
|
|
||||||
DataSource.EMAIL_AUTO,
|
|
||||||
fileName,
|
|
||||||
records
|
|
||||||
);
|
|
||||||
|
|
||||||
MeteringDataUploadResponse response = meteringDataService.uploadMeteringData(request, null);
|
|
||||||
|
|
||||||
if (response.validationErrors() != null && !response.validationErrors().isEmpty()) {
|
|
||||||
log.warn("[EDA-POLLER] Validierungsfehler beim Import: {}", response.validationErrors());
|
|
||||||
} else {
|
|
||||||
log.info("[EDA-POLLER] Verbrauchsdaten erfolgreich importiert: {} Datensaetze aus Datei {}",
|
|
||||||
response.recordCount(), fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[EDA-POLLER] Fehler beim Import der XLSX-Datei {}: {}", fileName, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
package at.mueller.eeg.backend.mako.email;
|
|
||||||
|
|
||||||
import jakarta.mail.MessagingException;
|
|
||||||
import jakarta.mail.internet.MimeMessage;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Profile;
|
|
||||||
import org.springframework.mail.javamail.JavaMailSender;
|
|
||||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
|
||||||
import org.springframework.scheduling.annotation.Async;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Profile("prod")
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class EdaMailSender {
|
|
||||||
|
|
||||||
private final JavaMailSender javaMailSender;
|
|
||||||
|
|
||||||
@Value("${eda.email.gateway-address}")
|
|
||||||
private String gatewayAddress;
|
|
||||||
|
|
||||||
@Async
|
|
||||||
public void sendConsentRequest(String atNumber, String pointType) {
|
|
||||||
try {
|
|
||||||
MimeMessage message = javaMailSender.createMimeMessage();
|
|
||||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
|
||||||
|
|
||||||
helper.setFrom(gatewayAddress);
|
|
||||||
helper.setTo(gatewayAddress);
|
|
||||||
helper.setSubject("CMRequest|" + atNumber);
|
|
||||||
|
|
||||||
String body = buildConsentRequestBody(atNumber, pointType);
|
|
||||||
helper.setText(body, false);
|
|
||||||
|
|
||||||
javaMailSender.send(message);
|
|
||||||
|
|
||||||
log.info("[EDA-MAIL] Consent-Request gesendet an {}: AT-Nummer={}, Typ={}",
|
|
||||||
gatewayAddress, atNumber, pointType);
|
|
||||||
|
|
||||||
} catch (MessagingException e) {
|
|
||||||
log.error("[EDA-MAIL] Fehler beim Senden des Consent-Requests fuer AT-Nummer {}: {}",
|
|
||||||
atNumber, e.getMessage());
|
|
||||||
throw new RuntimeException("EDA-Email-Versand fehlgeschlagen", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildConsentRequestBody(String atNumber, String pointType) {
|
|
||||||
return """
|
|
||||||
<EDA_REQUEST>
|
|
||||||
<TYPE>CONSENT</TYPE>
|
|
||||||
<AT_NUMBER>%s</AT_NUMBER>
|
|
||||||
<POINT_TYPE>%s</POINT_TYPE>
|
|
||||||
<TIMESTAMP>%s</TIMESTAMP>
|
|
||||||
</EDA_REQUEST>
|
|
||||||
""".formatted(atNumber, pointType, java.time.Instant.now());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@ package at.mueller.eeg.backend.mako.event.listener;
|
||||||
|
|
||||||
import at.mueller.eeg.backend.common.event.*;
|
import at.mueller.eeg.backend.common.event.*;
|
||||||
import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse;
|
import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse;
|
||||||
import at.mueller.eeg.backend.mako.service.EdaCommunicationRouter;
|
import at.mueller.eeg.backend.mako.service.EdaConsentService;
|
||||||
import at.mueller.eeg.backend.mako.service.EdaTopologyService;
|
import at.mueller.eeg.backend.mako.service.EdaTopologyService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
@ -18,7 +18,7 @@ import org.springframework.stereotype.Component;
|
||||||
public class EdaCommunicationEventListener {
|
public class EdaCommunicationEventListener {
|
||||||
|
|
||||||
private final EdaTopologyService edaTopologyService;
|
private final EdaTopologyService edaTopologyService;
|
||||||
private final EdaCommunicationRouter edaCommunicationRouter;
|
private final EdaConsentService edaConsentService;
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
@EventListener
|
@EventListener
|
||||||
|
|
@ -59,7 +59,7 @@ public class EdaCommunicationEventListener {
|
||||||
event.atNumber(), event.pointType());
|
event.atNumber(), event.pointType());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
edaCommunicationRouter.resolve().sendConsentRequest(event.atNumber(), event.pointType());
|
edaConsentService.sendConsentRequest(event.atNumber(), event.pointType());
|
||||||
|
|
||||||
log.info("MaKo-Domain: Consent-Request erfolgreich an EDA übermittelt für {}", event.atNumber());
|
log.info("MaKo-Domain: Consent-Request erfolgreich an EDA übermittelt für {}", event.atNumber());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
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<EdaSettings, UUID> {
|
|
||||||
Optional<EdaSettings> findFirstByOrderByIdAsc();
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
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<EdaConsentService> emailServiceProvider;
|
|
||||||
private final ObjectProvider<EdaConsentService> messengerServiceProvider;
|
|
||||||
|
|
||||||
public EdaCommunicationRouter(
|
|
||||||
EdaSettingsService settingsService,
|
|
||||||
Environment environment,
|
|
||||||
@Qualifier("simulatedEdaConsentService") EdaConsentService simulatedService,
|
|
||||||
@Qualifier("emailEdaConsentService") ObjectProvider<EdaConsentService> emailServiceProvider,
|
|
||||||
@Qualifier("messengerEdaConsentService") ObjectProvider<EdaConsentService> 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();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package at.mueller.eeg.backend.mako.service.impl;
|
|
||||||
|
|
||||||
import at.mueller.eeg.backend.mako.email.EdaMailSender;
|
|
||||||
import at.mueller.eeg.backend.mako.service.EdaConsentService;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service("emailEdaConsentService")
|
|
||||||
public class EmailEdaConsentService implements EdaConsentService {
|
|
||||||
|
|
||||||
private final EdaMailSender edaMailSender;
|
|
||||||
|
|
||||||
public EmailEdaConsentService(@Lazy EdaMailSender edaMailSender) {
|
|
||||||
this.edaMailSender = edaMailSender;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendConsentRequest(String atNumber, String pointType) {
|
|
||||||
log.info("[EDA-EMAIL] Sende Consent-Request fuer AT-Nummer: {} (Typ: {})", atNumber, pointType);
|
|
||||||
|
|
||||||
if (atNumber == null || atNumber.isBlank()) {
|
|
||||||
throw new IllegalArgumentException("AT-Nummer darf nicht leer sein fuer EDA-Request.");
|
|
||||||
}
|
|
||||||
|
|
||||||
edaMailSender.sendConsentRequest(atNumber, pointType);
|
|
||||||
|
|
||||||
log.info("[EDA-EMAIL] Consent-Request erfolgreich an EDA-Gateway gesendet fuer {}", atNumber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -16,7 +16,7 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service("simulatedEdaConsentService")
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SimulatedEdaConsentService implements EdaConsentService {
|
public class SimulatedEdaConsentService implements EdaConsentService {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,11 @@ import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse;
|
||||||
import at.mueller.eeg.backend.mako.service.EdaTopologyService;
|
import at.mueller.eeg.backend.mako.service.EdaTopologyService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Profile;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Profile("!prod")
|
|
||||||
@Service
|
@Service
|
||||||
public class SimulatedEdaTopologyService implements EdaTopologyService {
|
public class SimulatedEdaTopologyService implements EdaTopologyService {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,3 @@ eda:
|
||||||
simulation:
|
simulation:
|
||||||
consent-delay-ms: 3000
|
consent-delay-ms: 3000
|
||||||
topology-delay-ms: 1000
|
topology-delay-ms: 1000
|
||||||
email:
|
|
||||||
gateway-address: test-eda@example.com
|
|
||||||
poll-interval-ms: 900000
|
|
||||||
|
|
|
||||||
|
|
@ -52,20 +52,3 @@ logging:
|
||||||
at.mueller.eeg: INFO
|
at.mueller.eeg: INFO
|
||||||
org.springframework.security: WARN
|
org.springframework.security: WARN
|
||||||
org.hibernate.SQL: WARN
|
org.hibernate.SQL: WARN
|
||||||
|
|
||||||
eda:
|
|
||||||
email:
|
|
||||||
gateway-address: ${EDA_GATEWAY_EMAIL:eda-gateway@example.com}
|
|
||||||
poll-interval-ms: ${EDA_POLL_INTERVAL_MS:900000}
|
|
||||||
imap:
|
|
||||||
host: ${EDA_IMAP_HOST:}
|
|
||||||
port: ${EDA_IMAP_PORT:993}
|
|
||||||
username: ${EDA_IMAP_USERNAME:}
|
|
||||||
password: ${EDA_IMAP_PASSWORD:}
|
|
||||||
folder: INBOX
|
|
||||||
protocol: imaps
|
|
||||||
smtp:
|
|
||||||
host: ${EDA_SMTP_HOST:}
|
|
||||||
port: ${EDA_SMTP_PORT:587}
|
|
||||||
username: ${EDA_SMTP_USERNAME:}
|
|
||||||
password: ${EDA_SMTP_PASSWORD:}
|
|
||||||
|
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
package at.mueller.eeg.backend.dashboard.service;
|
|
||||||
|
|
||||||
import at.mueller.eeg.backend.community.api.dto.MeteringDataRecordDto;
|
|
||||||
import at.mueller.eeg.backend.community.api.dto.MeteringDataUploadRequest;
|
|
||||||
import at.mueller.eeg.backend.community.api.dto.MeteringDataUploadResponse;
|
|
||||||
import at.mueller.eeg.backend.community.domain.*;
|
|
||||||
import at.mueller.eeg.backend.community.domain.state.MakoState;
|
|
||||||
import at.mueller.eeg.backend.community.repository.MeteringDataRepository;
|
|
||||||
import at.mueller.eeg.backend.community.repository.MeteringPointRepository;
|
|
||||||
import at.mueller.eeg.backend.community.service.MeteringDataService;
|
|
||||||
import at.mueller.eeg.backend.dashboard.api.dto.DashboardStatsDto;
|
|
||||||
import at.mueller.eeg.backend.iam.domain.ParticipantType;
|
|
||||||
import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
|
|
||||||
import at.mueller.eeg.backend.iam.domain.User;
|
|
||||||
import at.mueller.eeg.backend.iam.domain.UserRole;
|
|
||||||
import at.mueller.eeg.backend.iam.repository.UserRepository;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
@ActiveProfiles("test")
|
|
||||||
@Transactional
|
|
||||||
class DashboardServiceIntegrationTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private DashboardService dashboardService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MeteringDataService meteringDataService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MeteringPointRepository meteringPointRepository;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MeteringDataRepository meteringDataRepository;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository;
|
|
||||||
|
|
||||||
private User testUser;
|
|
||||||
private MeteringPoint testMeteringPoint;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
testUser = new User();
|
|
||||||
testUser.setEmail("test-dashboard-" + UUID.randomUUID() + "@example.com");
|
|
||||||
testUser.setPasswordHash("encoded");
|
|
||||||
testUser.setFirstName("Test");
|
|
||||||
testUser.setLastName("Dashboard");
|
|
||||||
testUser.setRole(UserRole.MEMBER);
|
|
||||||
testUser.setStatus(RegistrationStatus.APPROVED);
|
|
||||||
testUser.setParticipantType(ParticipantType.PRIVATE);
|
|
||||||
testUser.setEnabled(true);
|
|
||||||
testUser = userRepository.save(testUser);
|
|
||||||
|
|
||||||
testMeteringPoint = new MeteringPoint();
|
|
||||||
testMeteringPoint.setUserId(testUser.getId());
|
|
||||||
testMeteringPoint.setAtNumber("AT0010000000000000000000001234567");
|
|
||||||
testMeteringPoint.setType(PointType.CONSUMER);
|
|
||||||
testMeteringPoint.setMakoState(MakoState.ACTIVE);
|
|
||||||
testMeteringPoint = meteringPointRepository.save(testMeteringPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getMeteringDataOverview_afterUpload_returnsNonEmptyTimeSeries() {
|
|
||||||
// Upload hourly data for 3 days
|
|
||||||
List<MeteringDataRecordDto> records = new ArrayList<>();
|
|
||||||
Instant base = Instant.parse("2026-06-01T00:00:00Z");
|
|
||||||
for (int i = 0; i < 72; i++) { // 3 days * 24 hours
|
|
||||||
Instant start = base.plusSeconds(i * 3600L);
|
|
||||||
Instant end = start.plusSeconds(3600);
|
|
||||||
records.add(new MeteringDataRecordDto(
|
|
||||||
testMeteringPoint.getAtNumber(), MeteringDataType.CONSUMPTION,
|
|
||||||
start, end, 0.5 + (i % 10) * 0.1, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
MeteringDataUploadRequest request = new MeteringDataUploadRequest(
|
|
||||||
DataSource.EMAIL_XLSX, "test.xlsx", records);
|
|
||||||
MeteringDataUploadResponse uploadResponse = meteringDataService.uploadMeteringData(request, testUser.getId());
|
|
||||||
assertEquals(UploadStatus.COMPLETED, uploadResponse.status());
|
|
||||||
|
|
||||||
// Now query via dashboard service
|
|
||||||
Instant from = Instant.parse("2026-06-01T00:00:00Z");
|
|
||||||
Instant to = Instant.parse("2026-06-04T00:00:00Z");
|
|
||||||
|
|
||||||
DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(testUser.getId(), from, to);
|
|
||||||
|
|
||||||
assertNotNull(overview);
|
|
||||||
assertFalse(overview.meteringPoints().isEmpty(), "Should have metering points");
|
|
||||||
assertFalse(overview.timeSeries().isEmpty(), "Should have time series data");
|
|
||||||
// Data spans 3 full days, but timezone grouping may add 1 day
|
|
||||||
assertTrue(overview.timeSeries().size() >= 3, "Should have at least 3 days of data");
|
|
||||||
assertEquals(1, overview.meteringPoints().size());
|
|
||||||
assertTrue(overview.meteringPoints().get(0).totalKwh() > 0, "Total kWh should be > 0");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getMeteringDataOverview_fullYearData_returnsTimeSeries() {
|
|
||||||
// Upload full year hourly data
|
|
||||||
List<MeteringDataRecordDto> records = new ArrayList<>();
|
|
||||||
Instant base = Instant.parse("2026-01-01T00:00:00Z");
|
|
||||||
for (int i = 0; i < 8760; i++) {
|
|
||||||
Instant start = base.plusSeconds(i * 3600L);
|
|
||||||
Instant end = start.plusSeconds(3600);
|
|
||||||
records.add(new MeteringDataRecordDto(
|
|
||||||
testMeteringPoint.getAtNumber(), MeteringDataType.CONSUMPTION,
|
|
||||||
start, end, 0.5 + (i % 10) * 0.1, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
MeteringDataUploadRequest request = new MeteringDataUploadRequest(
|
|
||||||
DataSource.EMAIL_XLSX, "fullyear.xlsx", records);
|
|
||||||
MeteringDataUploadResponse uploadResponse = meteringDataService.uploadMeteringData(request, testUser.getId());
|
|
||||||
assertEquals(UploadStatus.COMPLETED, uploadResponse.status());
|
|
||||||
|
|
||||||
// Query last 30 days
|
|
||||||
Instant from = Instant.parse("2026-06-24T00:00:00Z");
|
|
||||||
Instant to = Instant.parse("2026-07-24T00:00:00Z");
|
|
||||||
|
|
||||||
DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(testUser.getId(), from, to);
|
|
||||||
|
|
||||||
assertNotNull(overview);
|
|
||||||
assertFalse(overview.meteringPoints().isEmpty(), "Should have metering points");
|
|
||||||
assertFalse(overview.timeSeries().isEmpty(), "Should have time series data for the queried range");
|
|
||||||
assertTrue(overview.timeSeries().size() >= 29, "Should have ~30 days of data");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +1,21 @@
|
||||||
package at.mueller.eeg.backend.dashboard.service;
|
package at.mueller.eeg.backend.dashboard.service;
|
||||||
|
|
||||||
import at.mueller.eeg.backend.community.domain.*;
|
import at.mueller.eeg.backend.community.domain.MembershipStatus;
|
||||||
import at.mueller.eeg.backend.community.domain.state.MakoState;
|
|
||||||
import at.mueller.eeg.backend.community.repository.EnergyCommunityRepository;
|
import at.mueller.eeg.backend.community.repository.EnergyCommunityRepository;
|
||||||
import at.mueller.eeg.backend.community.repository.MeteringDataRepository;
|
|
||||||
import at.mueller.eeg.backend.community.repository.MeteringPointRepository;
|
|
||||||
import at.mueller.eeg.backend.community.repository.MembershipRepository;
|
import at.mueller.eeg.backend.community.repository.MembershipRepository;
|
||||||
|
import at.mueller.eeg.backend.community.repository.MeteringPointRepository;
|
||||||
import at.mueller.eeg.backend.dashboard.api.dto.DashboardStatsDto;
|
import at.mueller.eeg.backend.dashboard.api.dto.DashboardStatsDto;
|
||||||
import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
|
import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
|
||||||
import at.mueller.eeg.backend.iam.repository.UserRepository;
|
import at.mueller.eeg.backend.iam.repository.UserRepository;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.util.UUID;
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.time.ZoneOffset;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
|
@ -31,131 +23,45 @@ class DashboardServiceTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private EnergyCommunityRepository energyCommunityRepository;
|
private EnergyCommunityRepository energyCommunityRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private UserRepository userRepository;
|
private UserRepository userRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private MembershipRepository membershipRepository;
|
private MembershipRepository membershipRepository;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private MeteringPointRepository meteringPointRepository;
|
private MeteringPointRepository meteringPointRepository;
|
||||||
@Mock
|
|
||||||
private MeteringDataRepository meteringDataRepository;
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private DashboardService dashboardService;
|
private DashboardService dashboardService;
|
||||||
|
|
||||||
private UUID userId;
|
@Test
|
||||||
private UUID meteringPointId;
|
void getAdminStats_returnsCorrectCounts() {
|
||||||
private MeteringPoint meteringPoint;
|
when(energyCommunityRepository.count()).thenReturn(5L);
|
||||||
|
when(userRepository.countByStatusAndEmailVerified(RegistrationStatus.PENDING, true)).thenReturn(3L);
|
||||||
|
when(membershipRepository.countByStatus(MembershipStatus.PENDING)).thenReturn(2L);
|
||||||
|
when(membershipRepository.countByStatus(MembershipStatus.ACTIVE)).thenReturn(10L);
|
||||||
|
|
||||||
@BeforeEach
|
DashboardStatsDto.AdminStats stats = dashboardService.getAdminStats();
|
||||||
void setUp() {
|
|
||||||
userId = UUID.randomUUID();
|
|
||||||
meteringPointId = UUID.randomUUID();
|
|
||||||
|
|
||||||
meteringPoint = new MeteringPoint();
|
assertEquals(5, stats.totalCommunities());
|
||||||
meteringPoint.setId(meteringPointId);
|
assertEquals(3, stats.pendingUsers());
|
||||||
meteringPoint.setUserId(userId);
|
assertEquals(2, stats.pendingMemberships());
|
||||||
meteringPoint.setAtNumber("AT0010000000000000000000001234567");
|
assertEquals(10, stats.totalActiveMemberships());
|
||||||
meteringPoint.setType(PointType.CONSUMER);
|
|
||||||
meteringPoint.setMakoState(MakoState.ACTIVE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getMeteringDataOverview_withData_returnsTimeSeries() {
|
void getUserStats_returnsCorrectCounts() {
|
||||||
Instant from = Instant.parse("2026-01-01T00:00:00Z");
|
UUID userId = UUID.randomUUID();
|
||||||
Instant to = Instant.parse("2026-01-03T00:00:00Z");
|
when(meteringPointRepository.countByUserId(userId)).thenReturn(2L);
|
||||||
|
when(membershipRepository.countByUserIdAndStatus(userId, MembershipStatus.ACTIVE)).thenReturn(1L);
|
||||||
|
when(membershipRepository.countByUserIdAndStatus(userId, MembershipStatus.PENDING)).thenReturn(1L);
|
||||||
|
|
||||||
when(meteringPointRepository.findAllByUserId(userId)).thenReturn(List.of(meteringPoint));
|
DashboardStatsDto.UserStats stats = dashboardService.getUserStats(userId);
|
||||||
|
|
||||||
ZoneId systemZone = ZoneId.systemDefault();
|
assertEquals(2, stats.totalMeteringPoints());
|
||||||
List<MeteringData> meteringData = List.of(
|
assertEquals(1, stats.activeMemberships());
|
||||||
createMeteringData(meteringPointId, Instant.parse("2026-01-01T10:00:00Z"), 5.0),
|
assertEquals(1, stats.pendingMemberships());
|
||||||
createMeteringData(meteringPointId, Instant.parse("2026-01-02T10:00:00Z"), 3.0)
|
|
||||||
);
|
|
||||||
when(meteringDataRepository.findByMeteringPointIdAndIntervalStartBetween(meteringPointId, from, to))
|
|
||||||
.thenReturn(meteringData);
|
|
||||||
|
|
||||||
DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(userId, from, to);
|
|
||||||
|
|
||||||
assertEquals(1, overview.meteringPoints().size());
|
|
||||||
assertEquals("AT0010000000000000000000001234567", overview.meteringPoints().get(0).atNumber());
|
|
||||||
assertEquals(8.0, overview.meteringPoints().get(0).totalKwh(), 0.01);
|
|
||||||
assertEquals(2, overview.timeSeries().size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getMeteringDataOverview_noMeteringPoints_returnsEmptyOverview() {
|
|
||||||
Instant from = Instant.parse("2026-01-01T00:00:00Z");
|
|
||||||
Instant to = Instant.parse("2026-01-03T00:00:00Z");
|
|
||||||
|
|
||||||
when(meteringPointRepository.findAllByUserId(userId)).thenReturn(Collections.emptyList());
|
|
||||||
|
|
||||||
DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(userId, from, to);
|
|
||||||
|
|
||||||
assertTrue(overview.meteringPoints().isEmpty());
|
|
||||||
assertTrue(overview.timeSeries().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getMeteringDataOverview_noDataInTimeRange_returnsEmptyTimeSeries() {
|
|
||||||
Instant from = Instant.parse("2026-01-01T00:00:00Z");
|
|
||||||
Instant to = Instant.parse("2026-01-03T00:00:00Z");
|
|
||||||
|
|
||||||
when(meteringPointRepository.findAllByUserId(userId)).thenReturn(List.of(meteringPoint));
|
|
||||||
when(meteringDataRepository.findByMeteringPointIdAndIntervalStartBetween(meteringPointId, from, to))
|
|
||||||
.thenReturn(Collections.emptyList());
|
|
||||||
|
|
||||||
DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(userId, from, to);
|
|
||||||
|
|
||||||
assertEquals(1, overview.meteringPoints().size());
|
|
||||||
assertEquals(0.0, overview.meteringPoints().get(0).totalKwh(), 0.01);
|
|
||||||
assertTrue(overview.timeSeries().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getMeteringDataOverview_multipleMeteringPoints_aggregatesCorrectly() {
|
|
||||||
Instant from = Instant.parse("2026-06-01T00:00:00Z");
|
|
||||||
Instant to = Instant.parse("2026-06-02T00:00:00Z");
|
|
||||||
|
|
||||||
MeteringPoint mp2 = new MeteringPoint();
|
|
||||||
mp2.setId(UUID.randomUUID());
|
|
||||||
mp2.setUserId(userId);
|
|
||||||
mp2.setAtNumber("AT0010000000000000000000001234568");
|
|
||||||
mp2.setType(PointType.PRODUCER);
|
|
||||||
mp2.setMakoState(MakoState.ACTIVE);
|
|
||||||
|
|
||||||
when(meteringPointRepository.findAllByUserId(userId)).thenReturn(List.of(meteringPoint, mp2));
|
|
||||||
|
|
||||||
List<MeteringData> data1 = List.of(createMeteringData(meteringPointId, Instant.parse("2026-06-01T10:00:00Z"), 5.0));
|
|
||||||
List<MeteringData> data2 = List.of(createMeteringData(mp2.getId(), Instant.parse("2026-06-01T10:00:00Z"), 3.0));
|
|
||||||
|
|
||||||
when(meteringDataRepository.findByMeteringPointIdAndIntervalStartBetween(meteringPointId, from, to))
|
|
||||||
.thenReturn(data1);
|
|
||||||
when(meteringDataRepository.findByMeteringPointIdAndIntervalStartBetween(mp2.getId(), from, to))
|
|
||||||
.thenReturn(data2);
|
|
||||||
|
|
||||||
DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(userId, from, to);
|
|
||||||
|
|
||||||
assertEquals(2, overview.meteringPoints().size());
|
|
||||||
assertEquals(1, overview.timeSeries().size());
|
|
||||||
|
|
||||||
DashboardStatsDto.TimeSeriesEntry entry = overview.timeSeries().get(0);
|
|
||||||
assertEquals(5.0, entry.values().get("AT0010000000000000000000001234567"), 0.01);
|
|
||||||
assertEquals(3.0, entry.values().get("AT0010000000000000000000001234568"), 0.01);
|
|
||||||
}
|
|
||||||
|
|
||||||
private MeteringData createMeteringData(UUID meteringPointId, Instant intervalStart, double kwh) {
|
|
||||||
MeteringPoint mp = new MeteringPoint();
|
|
||||||
mp.setId(meteringPointId);
|
|
||||||
|
|
||||||
MeteringData data = new MeteringData();
|
|
||||||
data.setMeteringPoint(mp);
|
|
||||||
data.setDataType(MeteringDataType.CONSUMPTION);
|
|
||||||
data.setIntervalStart(intervalStart);
|
|
||||||
data.setIntervalEnd(intervalStart.plusSeconds(3600));
|
|
||||||
data.setKwh(kwh);
|
|
||||||
data.setSource(DataSource.EMAIL_XLSX);
|
|
||||||
data.setUploadId(UUID.randomUUID());
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
package at.mueller.eeg.backend.mako.email;
|
|
||||||
|
|
||||||
import jakarta.mail.internet.MimeMessage;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
class EdaMailParserTest {
|
|
||||||
|
|
||||||
private EdaMailParser edaMailParser;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
edaMailParser = new EdaMailParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isMeteringDataMail_true() throws Exception {
|
|
||||||
MimeMessage message = new MimeMessage((jakarta.mail.Session) null);
|
|
||||||
message.setSubject("Verbrauchsdaten|AT1234567890123456789012345678901");
|
|
||||||
|
|
||||||
assertTrue(edaMailParser.isMeteringDataMail(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void isMeteringDataMail_false() throws Exception {
|
|
||||||
MimeMessage message = new MimeMessage((jakarta.mail.Session) null);
|
|
||||||
message.setSubject("CMResponse|AT1234567890123456789012345678901");
|
|
||||||
|
|
||||||
assertFalse(edaMailParser.isMeteringDataMail(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parseConsentResponse_approved() throws Exception {
|
|
||||||
MimeMessage message = new MimeMessage((jakarta.mail.Session) null);
|
|
||||||
message.setSubject("CMResponse|AT1234567890123456789012345678901");
|
|
||||||
message.setText("<EDA_RESPONSE><DECISION>APPROVED</DECISION></EDA_RESPONSE>");
|
|
||||||
|
|
||||||
Optional<EdaMailParser.ConsentResponse> response = edaMailParser.parseConsentResponse(message);
|
|
||||||
|
|
||||||
assertTrue(response.isPresent());
|
|
||||||
assertEquals("AT1234567890123456789012345678901", response.get().atNumber());
|
|
||||||
assertEquals(EdaMailParser.ConsentResponse.ConsentDecision.APPROVED, response.get().decision());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parseConsentResponse_rejected() throws Exception {
|
|
||||||
MimeMessage message = new MimeMessage((jakarta.mail.Session) null);
|
|
||||||
message.setSubject("CMResponse|AT1234567890123456789012345678901");
|
|
||||||
message.setText("<EDA_RESPONSE><DECISION>REJECTED</DECISION><REASON>Kunde abgelehnt</REASON></EDA_RESPONSE>");
|
|
||||||
|
|
||||||
Optional<EdaMailParser.ConsentResponse> response = edaMailParser.parseConsentResponse(message);
|
|
||||||
|
|
||||||
assertTrue(response.isPresent());
|
|
||||||
assertEquals(EdaMailParser.ConsentResponse.ConsentDecision.REJECTED, response.get().decision());
|
|
||||||
assertEquals("Kunde abgelehnt", response.get().reason());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parseConsentResponse_notAConsentResponse() throws Exception {
|
|
||||||
MimeMessage message = new MimeMessage((jakarta.mail.Session) null);
|
|
||||||
message.setSubject("Some Other Subject");
|
|
||||||
message.setText("Random content");
|
|
||||||
|
|
||||||
Optional<EdaMailParser.ConsentResponse> response = edaMailParser.parseConsentResponse(message);
|
|
||||||
|
|
||||||
assertFalse(response.isPresent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
package at.mueller.eeg.backend.mako.email;
|
|
||||||
|
|
||||||
import at.mueller.eeg.backend.common.event.MakoConsentApprovedEvent;
|
|
||||||
import at.mueller.eeg.backend.community.service.MeteringDataService;
|
|
||||||
import at.mueller.eeg.backend.community.service.XlsxMeteringDataParser;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class EdaMailPollerTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private EdaMailParser edaMailParser;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private MeteringDataService meteringDataService;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private XlsxMeteringDataParser xlsxMeteringDataParser;
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private EdaMailPoller edaMailPoller;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void pollMailbox_skipsWhenNoImapHostConfigured() {
|
|
||||||
ReflectionTestUtils.setField(edaMailPoller, "imapHost", "");
|
|
||||||
|
|
||||||
edaMailPoller.pollMailbox();
|
|
||||||
|
|
||||||
// Should not throw any exception
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void pollMailbox_skipsWhenImapHostIsNull() {
|
|
||||||
ReflectionTestUtils.setField(edaMailPoller, "imapHost", null);
|
|
||||||
|
|
||||||
edaMailPoller.pollMailbox();
|
|
||||||
|
|
||||||
// Should not throw any exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
package at.mueller.eeg.backend.mako.service.impl;
|
|
||||||
|
|
||||||
import at.mueller.eeg.backend.mako.email.EdaMailSender;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class EmailEdaConsentServiceTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private EdaMailSender edaMailSender;
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private EmailEdaConsentService emailEdaConsentService;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void sendConsentRequest_sendsEmailSuccessfully() {
|
|
||||||
String atNumber = "AT1234567890123456789012345678901";
|
|
||||||
String pointType = "CONSUMER";
|
|
||||||
|
|
||||||
emailEdaConsentService.sendConsentRequest(atNumber, pointType);
|
|
||||||
|
|
||||||
verify(edaMailSender).sendConsentRequest(atNumber, pointType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void sendConsentRequest_throwsExceptionForBlankAtNumber() {
|
|
||||||
assertThrows(IllegalArgumentException.class,
|
|
||||||
() -> emailEdaConsentService.sendConsentRequest("", "CONSUMER"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void sendConsentRequest_throwsExceptionForNullAtNumber() {
|
|
||||||
assertThrows(IllegalArgumentException.class,
|
|
||||||
() -> emailEdaConsentService.sendConsentRequest(null, "CONSUMER"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package at.mueller.eeg.backend.mako.service.impl;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.context.annotation.Profile;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
class SimulatorProfileTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void simulatedEdaTopologyService_hasNotProdProfile() {
|
|
||||||
Profile profile = SimulatedEdaTopologyService.class.getAnnotation(Profile.class);
|
|
||||||
assertNotNull(profile, "SimulatedEdaTopologyService should have @Profile annotation");
|
|
||||||
assertArrayEquals(new String[]{"!prod"}, profile.value(),
|
|
||||||
"SimulatedEdaTopologyService should have @Profile(\"!prod\")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,8 +10,6 @@ tags:
|
||||||
description: Public Endpoints für Login und Registrierung
|
description: Public Endpoints für Login und Registrierung
|
||||||
- name: User Profile
|
- name: User Profile
|
||||||
description: Profilverwaltung und Passwort-Reset
|
description: Profilverwaltung und Passwort-Reset
|
||||||
- name: Admin EDA Settings
|
|
||||||
description: Konfiguration des EDA-Kommunikationswegs
|
|
||||||
- name: Admin IAM
|
- name: Admin IAM
|
||||||
description: User-Verwaltung und Freigabe-Workflow
|
description: User-Verwaltung und Freigabe-Workflow
|
||||||
paths:
|
paths:
|
||||||
|
|
@ -307,37 +305,6 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/CommunityTariffResponse"
|
$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:
|
/api/tariffs:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -1268,14 +1235,6 @@ components:
|
||||||
updatedAt:
|
updatedAt:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
EdaSettingsDto:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
communicationMethod:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- EMAIL
|
|
||||||
- MESSENGER
|
|
||||||
TariffInviteRequest:
|
TariffInviteRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -1380,7 +1339,6 @@ components:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- EMAIL_XLSX
|
- EMAIL_XLSX
|
||||||
- EMAIL_AUTO
|
|
||||||
- API
|
- API
|
||||||
- MANUAL
|
- MANUAL
|
||||||
fileName:
|
fileName:
|
||||||
|
|
@ -1672,7 +1630,6 @@ components:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- EMAIL_XLSX
|
- EMAIL_XLSX
|
||||||
- EMAIL_AUTO
|
|
||||||
- API
|
- API
|
||||||
- MANUAL
|
- MANUAL
|
||||||
MembershipResponse:
|
MembershipResponse:
|
||||||
|
|
|
||||||
4312
eeg_frontend/package-lock.json
generated
4312
eeg_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -19,7 +19,7 @@
|
||||||
"@angular/forms": "^21.2.0",
|
"@angular/forms": "^21.2.0",
|
||||||
"@angular/material": "^21.2.12",
|
"@angular/material": "^21.2.12",
|
||||||
"@angular/platform-browser": "^21.2.0",
|
"@angular/platform-browser": "^21.2.0",
|
||||||
"@angular/platform-browser-dynamic": "^21.2.0",
|
"@angular/platform-browser-dynamic": "^20.0.7",
|
||||||
"@angular/router": "^21.2.0",
|
"@angular/router": "^21.2.0",
|
||||||
"@tailwindcss/postcss": "^4.3.0",
|
"@tailwindcss/postcss": "^4.3.0",
|
||||||
"echarts": "^6.1.0",
|
"echarts": "^6.1.0",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {BASE_PATH} from './api';
|
||||||
import {jwtInterceptor} from './interceptors/jwt.interceptor';
|
import {jwtInterceptor} from './interceptors/jwt.interceptor';
|
||||||
import {errorInterceptor} from './interceptors/error.interceptor';
|
import {errorInterceptor} from './interceptors/error.interceptor';
|
||||||
import {environment} from '../environments/environment';
|
import {environment} from '../environments/environment';
|
||||||
import {provideEcharts} from 'ngx-echarts';
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
|
|
@ -14,7 +13,6 @@ export const appConfig: ApplicationConfig = {
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideHttpClient(withFetch(),
|
provideHttpClient(withFetch(),
|
||||||
withInterceptors([jwtInterceptor, errorInterceptor])),
|
withInterceptors([jwtInterceptor, errorInterceptor])),
|
||||||
{ provide: BASE_PATH, useValue: environment.apiUrl },
|
{ provide: BASE_PATH, useValue: environment.apiUrl }
|
||||||
provideEcharts()
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import {ConsumptionDashboardComponent} from './pages/consumption-dashboard/consu
|
||||||
import {UploadMeteringDataComponent} from './pages/upload-metering-data/upload-metering-data';
|
import {UploadMeteringDataComponent} from './pages/upload-metering-data/upload-metering-data';
|
||||||
import {ProducerInvitesComponent} from './pages/producer-invites/producer-invites';
|
import {ProducerInvitesComponent} from './pages/producer-invites/producer-invites';
|
||||||
import {ConsumerInvitesComponent} from './pages/consumer-invites/consumer-invites';
|
import {ConsumerInvitesComponent} from './pages/consumer-invites/consumer-invites';
|
||||||
import {AdminEdaSettingsComponent} from './pages/admin-eda-settings/admin-eda-settings';
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: LandingPage },
|
{ path: '', component: LandingPage },
|
||||||
|
|
@ -77,11 +76,6 @@ export const routes: Routes = [
|
||||||
component: UploadMeteringDataComponent,
|
component: UploadMeteringDataComponent,
|
||||||
canActivate: [roleGuard(['ADMIN'])]
|
canActivate: [roleGuard(['ADMIN'])]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'admin-eda-settings',
|
|
||||||
component: AdminEdaSettingsComponent,
|
|
||||||
canActivate: [roleGuard(['ADMIN'])]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'my-tariffs',
|
path: 'my-tariffs',
|
||||||
component: UserTariffComponent,
|
component: UserTariffComponent,
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,4 @@ export const NAV_ITEMS: NavItem[] = [
|
||||||
{label: 'Energiegemeinschaften', path: '/dashboard/energy-communities', roles: ['ADMIN'], section: 'Verwaltung'},
|
{label: 'Energiegemeinschaften', path: '/dashboard/energy-communities', roles: ['ADMIN'], section: 'Verwaltung'},
|
||||||
{label: 'Tarife verwalten', path: '/dashboard/tariffs', 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: 'Messdaten-Upload', path: '/dashboard/upload-metering-data', roles: ['ADMIN'], section: 'Verwaltung'},
|
||||||
{label: 'EDA-Einstellungen', path: '/dashboard/admin-eda-settings', roles: ['ADMIN'], section: 'Verwaltung'},
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
<div class="max-w-2xl mx-auto p-4 md:p-6">
|
|
||||||
<div class="mb-6">
|
|
||||||
<h2 class="text-2xl font-bold text-slate-800">EDA-Einstellungen</h2>
|
|
||||||
<p class="text-sm text-slate-500 mt-1">Konfiguriere den Kommunikationsweg fuer die EDA-Zustimmungsabfrage.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
|
||||||
@if (isLoading()) {
|
|
||||||
<div class="text-center py-4 text-slate-500">Lade Einstellungen...</div>
|
|
||||||
} @else {
|
|
||||||
<div class="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-slate-700 mb-3">Kommunikationsweg</label>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<label class="flex items-start gap-3 p-4 border border-slate-200 rounded-xl cursor-pointer transition-colors"
|
|
||||||
[class.border-blue-500]="selectedMethod() === 'EMAIL'"
|
|
||||||
[class.bg-blue-50]="selectedMethod() === 'EMAIL'"
|
|
||||||
[class.hover:border-slate-300]="selectedMethod() !== 'EMAIL'">
|
|
||||||
<input type="radio" name="edaMethod" value="EMAIL"
|
|
||||||
[checked]="selectedMethod() === 'EMAIL'"
|
|
||||||
(change)="selectedMethod.set('EMAIL')"
|
|
||||||
class="mt-0.5 text-blue-600 focus:ring-blue-500">
|
|
||||||
<div>
|
|
||||||
<div class="font-medium text-slate-800">E-Mail</div>
|
|
||||||
<div class="text-sm text-slate-500 mt-0.5">Consent-Requests werden per E-Mail an das EDA-Gateway gesendet.</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="flex items-start gap-3 p-4 border border-slate-200 rounded-xl cursor-pointer transition-colors"
|
|
||||||
[class.border-blue-500]="selectedMethod() === 'MESSENGER'"
|
|
||||||
[class.bg-blue-50]="selectedMethod() === 'MESSENGER'"
|
|
||||||
[class.hover:border-slate-300]="selectedMethod() !== 'MESSENGER'">
|
|
||||||
<input type="radio" name="edaMethod" value="MESSENGER"
|
|
||||||
[checked]="selectedMethod() === 'MESSENGER'"
|
|
||||||
(change)="selectedMethod.set('MESSENGER')"
|
|
||||||
class="mt-0.5 text-blue-600 focus:ring-blue-500">
|
|
||||||
<div>
|
|
||||||
<div class="font-medium text-slate-800">Messenger</div>
|
|
||||||
<div class="text-sm text-slate-500 mt-0.5">Consent-Requests werden ueber den Messenger gesendet (noch nicht implementiert).</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<button (click)="saveSettings()" [disabled]="isSaving()"
|
|
||||||
class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-xl font-medium transition-colors shadow-sm disabled:opacity-50 disabled:cursor-not-allowed">
|
|
||||||
{{ isSaving() ? 'Speichern...' : 'Speichern' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
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<AdminEdaSettingsComponent>;
|
|
||||||
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
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<EdaSettingsDto.CommunicationMethodEnum>('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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -63,8 +63,8 @@ export class ConsumptionDashboardComponent implements OnInit {
|
||||||
this.isLoading.set(true);
|
this.isLoading.set(true);
|
||||||
this.error.set(null);
|
this.error.set(null);
|
||||||
|
|
||||||
const from = this.dateFrom() + 'T00:00:00Z';
|
const from = this.dateFrom() + 'T00:00:00';
|
||||||
const to = this.dateTo() + 'T23:59:59Z';
|
const to = this.dateTo() + 'T23:59:59';
|
||||||
|
|
||||||
this.consumptionService.getDashboard(mpId, from, to).subscribe({
|
this.consumptionService.getDashboard(mpId, from, to).subscribe({
|
||||||
next: (data) => {
|
next: (data) => {
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,7 @@ export class DashboardOverview implements OnInit {
|
||||||
this.meteringData.set(data);
|
this.meteringData.set(data);
|
||||||
this.buildChartOptions(data);
|
this.buildChartOptions(data);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: () => {}
|
||||||
console.error('Fehler beim Laden der Messdaten:', err);
|
|
||||||
this.error.set('Fehler beim Laden der Messdaten.');
|
|
||||||
this.isLoading.set(false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ export class UserTariffComponent implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userTariffService.deleteUserTariff(tariff.id!).subscribe({
|
this.userTariffService.deleteUserTariff(tariff.id!, userId).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.success('Tarif erfolgreich gelöscht.');
|
this.toastService.success('Tarif erfolgreich gelöscht.');
|
||||||
this.loadUserTariffs(this.selectedCommunityId());
|
this.loadUserTariffs(this.selectedCommunityId());
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue