refactoring, adaption, community membership handing start implementing

This commit is contained in:
Bernhard Müller 2026-06-15 10:10:33 +02:00
parent ab28328408
commit 5f845833cb
26 changed files with 408 additions and 50 deletions

View File

@ -5,9 +5,9 @@ Content-Type: application/json
{ {
"firstName": "Admin", "firstName": "Admin",
"lastName": "Admin", "lastName": "Admin",
"email": "admin@admin.at", "email": "test10@qwer.as",
"password": "SuperAdmin", "password": "testtest",
"atNumber": "AT0010000000000000000000001234567" "atNumber": "AT0010000000000000000000001234568"
} }
### User Login ### User Login

View File

@ -2,8 +2,10 @@ package at.mueller.eeg.backend;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication @SpringBootApplication
@EnableAsync
public class EegPortalApplication { public class EegPortalApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -1,8 +1,9 @@
package at.mueller.eeg.backend.common.config.event; package at.mueller.eeg.backend.common.event;
import java.util.UUID; import java.util.UUID;
public record EdaTopologyCheckEvent( public record EdaTopologyCheckEvent(
UUID userId, UUID userId,
UUID meteringPointId,
String atNumber String atNumber
) {} ) {}

View File

@ -0,0 +1,13 @@
package at.mueller.eeg.backend.common.event;
import java.util.UUID;
public record EdaTopologyCompletedEvent(
UUID userId,
UUID meteringPointId,
boolean isEligible,
String gridOperatorId,
String substationId,
String transformerId,
String gridLevel
) {}

View File

@ -0,0 +1,9 @@
package at.mueller.eeg.backend.common.event;
import java.util.UUID;
public record EdaTopologyFailedEvent(
UUID userId,
UUID meteringPointId,
String errorMessage
) {}

View File

@ -1,4 +1,4 @@
package at.mueller.eeg.backend.common.config.event; package at.mueller.eeg.backend.common.event;
import java.util.UUID; import java.util.UUID;

View File

@ -1,3 +1,3 @@
package at.mueller.eeg.backend.common.config.exception; package at.mueller.eeg.backend.common.exception;
public record ErrorResponse(String message, int status) {} public record ErrorResponse(String message, int status) {}

View File

@ -1,4 +1,4 @@
package at.mueller.eeg.backend.common.config.exception; package at.mueller.eeg.backend.common.exception;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;

View File

@ -0,0 +1,51 @@
package at.mueller.eeg.backend.community.api;
import at.mueller.eeg.backend.community.api.dto.EligibleCommunityResponse;
import at.mueller.eeg.backend.community.api.dto.MembershipRequest;
import at.mueller.eeg.backend.community.service.MembershipService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/api/community")
@RequiredArgsConstructor
public class MembershipController {
private final MembershipService membershipService;
// --- USER ENDPUNKTE ---
@GetMapping("/metering-points/{id}/eligible-communities")
public ResponseEntity<List<EligibleCommunityResponse>> getEligibleCommunities(@PathVariable UUID id) {
return ResponseEntity.ok(membershipService.getEligibleCommunities(id));
}
@PostMapping("/memberships/request")
public ResponseEntity<UUID> requestMembership(@Valid @RequestBody MembershipRequest request) {
UUID membershipId = membershipService.requestMembership(request);
return ResponseEntity.status(HttpStatus.CREATED).body(membershipId);
}
// --- ADMIN ENDPUNKTE ---
@PutMapping("/admin/memberships/{id}/approve")
@PreAuthorize("hasRole('ADMIN')") // Verknüpfung mit deiner UserRole.ADMIN aus dem iam-Modul
public ResponseEntity<Void> approveMembership(@PathVariable UUID id) {
membershipService.approveMembership(id);
return ResponseEntity.ok().build();
}
@PutMapping("/admin/memberships/{id}/reject")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> rejectMembership(@PathVariable UUID id) {
membershipService.rejectMembership(id);
return ResponseEntity.ok().build();
}
}

View File

@ -0,0 +1,11 @@
package at.mueller.eeg.backend.community.api.dto;
import at.mueller.eeg.backend.community.domain.CommunityType;
import java.util.UUID;
public record EligibleCommunityResponse(
UUID id,
String name,
CommunityType type,
String edaEcId
) {}

View File

@ -0,0 +1,12 @@
package at.mueller.eeg.backend.community.api.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.util.UUID;
public record MembershipRequest(
@NotNull UUID meteringPointId,
@NotNull UUID energyCommunityId,
@NotNull @Min(1) @Max(5) Integer priorityLevel
) {}

View File

@ -16,10 +16,19 @@ public class EnergyCommunity {
private UUID id; private UUID id;
@Column(unique = true, nullable = false) @Column(unique = true, nullable = false)
private String edaEcId; // z.B. CC123456 private String edaEcId;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private CommunityType type; // EEG_LOKAL, EEG_REGIONAL, BEG private CommunityType type;
private String name; private String name;
@Column(name = "grid_operator_id", length = 50)
private String gridOperatorId;
@Column(name = "substation_id", length = 50)
private String substationId;
@Column(name = "transformer_id", length = 50)
private String transformerId;
} }

View File

@ -1,5 +1,7 @@
package at.mueller.eeg.backend.community.domain; package at.mueller.eeg.backend.community.domain;
import at.mueller.eeg.backend.community.domain.state.MakoState;
import at.mueller.eeg.backend.community.domain.state.MakoTrigger;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -36,8 +38,49 @@ public class MeteringPoint {
private String transformerId; private String transformerId;
@Column(name = "grid_level") @Column(name = "grid_level")
private Integer gridLevel; private String gridLevel;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private MakoState makoState = MakoState.NEW;
@OneToMany(mappedBy = "meteringPoint", cascade = CascadeType.ALL) @OneToMany(mappedBy = "meteringPoint", cascade = CascadeType.ALL)
private List<Membership> memberships; private List<Membership> memberships;
public void fireTrigger(MakoTrigger trigger) {
this.makoState = switch (this.makoState) {
case NEW ->
trigger == MakoTrigger.SEND_REQUEST ? MakoState.WAITING_FOR_CONSENT : invalid(trigger);
case WAITING_FOR_CONSENT -> switch (trigger) {
case CONSENT_APPROVED -> MakoState.CONSENT_GRANTED;
case CONSENT_DENIED -> MakoState.REJECTED;
case TECHNICAL_FAILURE -> MakoState.ERROR;
default -> invalid(trigger);
};
case CONSENT_GRANTED -> switch (trigger) {
case TOPOLOGY_VALID -> MakoState.ACTIVE;
case TOPOLOGY_INVALID -> MakoState.REJECTED;
case TECHNICAL_FAILURE -> MakoState.ERROR;
default -> invalid(trigger);
};
// Aus einem Fehlerzustand heraus kann man den Prozess z.B. neu starten
case ERROR ->
trigger == MakoTrigger.SEND_REQUEST ? MakoState.WAITING_FOR_CONSENT : invalid(trigger);
// Endzustände (ACTIVE, REJECTED) erlauben standardmäßig keine weiteren Trigger
default -> invalid(trigger);
};
}
private MakoState invalid(MakoTrigger trigger) {
throw new IllegalStateException(
String.format("Ungültiger EDA-Zustandsübergang: Von Status '%s' mit Trigger '%s' nicht erlaubt.",
this.makoState, trigger)
);
}
} }

View File

@ -0,0 +1,10 @@
package at.mueller.eeg.backend.community.domain.state;
public enum MakoState {
NEW, // Neu angelegt, noch nichts passiert
WAITING_FOR_CONSENT, // CMRequest an EDA gesendet, warten auf Kunden
CONSENT_GRANTED, // Kunde hat beim Netzbetreiber zugestimmt
ACTIVE, // Topologie passt, Zählpunkt ist bereit für die EEG
REJECTED, // Kunde hat abgelehnt oder Zählpunkt passt nicht
ERROR // Technischer Fehler (Timeout, falsche AT-Nummer)
}

View File

@ -0,0 +1,10 @@
package at.mueller.eeg.backend.community.domain.state;
public enum MakoTrigger {
SEND_REQUEST, // Wir schicken den Consent-Request raus
CONSENT_APPROVED, // Netzbetreiber meldet Zustimmung
CONSENT_DENIED, // Netzbetreiber meldet Ablehnung
TOPOLOGY_VALID, // Topologie-Abfrage war erfolgreich und passend
TOPOLOGY_INVALID, // Topologie passt nicht zur EEG
TECHNICAL_FAILURE // EDA-Schnittstelle liefert Fehler
}

View File

@ -1,24 +1,35 @@
package at.mueller.eeg.backend.community.event.listener; package at.mueller.eeg.backend.community.event.listener;
import at.mueller.eeg.backend.common.config.event.EdaTopologyCheckEvent; import at.mueller.eeg.backend.common.event.EdaTopologyCheckEvent;
import at.mueller.eeg.backend.common.config.event.UserRegisteredEvent; import at.mueller.eeg.backend.common.event.EdaTopologyCompletedEvent;
import at.mueller.eeg.backend.common.event.EdaTopologyFailedEvent;
import at.mueller.eeg.backend.common.event.UserRegisteredEvent;
import at.mueller.eeg.backend.community.domain.MeteringPoint; import at.mueller.eeg.backend.community.domain.MeteringPoint;
import at.mueller.eeg.backend.community.domain.PointType; import at.mueller.eeg.backend.community.domain.PointType;
import at.mueller.eeg.backend.community.domain.state.MakoTrigger;
import at.mueller.eeg.backend.community.repository.MeteringPointRepository; import at.mueller.eeg.backend.community.repository.MeteringPointRepository;
import at.mueller.eeg.backend.mako.service.EdaTopologyService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.event.TransactionalEventListener;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class MeteringPointEventListener { public class MeteringPointEventListener {
private final MeteringPointRepository meteringPointRepository; private final MeteringPointRepository meteringPointRepository;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
private final EdaTopologyService edaTopologyService;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleUserRegistered(UserRegisteredEvent event) { public void handleUserRegistered(UserRegisteredEvent event) {
MeteringPoint point = new MeteringPoint(); MeteringPoint point = new MeteringPoint();
point.setUserId(event.userId()); point.setUserId(event.userId());
@ -26,6 +37,42 @@ public class MeteringPointEventListener {
point.setType(PointType.CONSUMER); point.setType(PointType.CONSUMER);
meteringPointRepository.save(point); meteringPointRepository.save(point);
eventPublisher.publishEvent(new EdaTopologyCheckEvent(point.getId(), event.atNumber())); eventPublisher.publishEvent(new EdaTopologyCheckEvent(event.userId(), point.getId(), event.atNumber()));
}
@EventListener
@Transactional
public void handleEdaTopologyCompleted(EdaTopologyCompletedEvent event) {
meteringPointRepository.findById(event.meteringPointId()).ifPresent(point -> {
var data = event.topologyData();
try {
if (data.isEligible()) {
point.fireTrigger(MakoTrigger.TOPOLOGY_VALID);
point.setGridOperatorId(data.gridOperatorName());
point.setGridLevel(data.gridLevel());
point.setSubstationId(data.substationId());
point.setTransformerId(data.transformerId());
} else {
point.fireTrigger(MakoTrigger.TOPOLOGY_INVALID);
}
meteringPointRepository.save(point);
log.info("Zählpunkt {} ist nun im Status: {}", point.getId(), point.getMakoState());
} catch (IllegalStateException ex) {
log.error("Fehler beim Verarbeiten der EDA-Antwort: {}", ex.getMessage());
}
});
}
@EventListener
@Transactional
public void handleEdaTopologyFailed(EdaTopologyFailedEvent event) {
meteringPointRepository.findById(event.meteringPointId()).ifPresent(point -> {
point.fireTrigger(MakoTrigger.TECHNICAL_FAILURE);
meteringPointRepository.save(point);
});
} }
} }

View File

@ -0,0 +1,24 @@
package at.mueller.eeg.backend.community.repository;
import at.mueller.eeg.backend.community.domain.EnergyCommunity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.UUID;
public interface EnergyCommunityRepository extends JpaRepository<EnergyCommunity, UUID> {
@Query("""
SELECT c FROM EnergyCommunity c
WHERE c.type = 'BEG'
OR (c.type = 'EEG_REGIONAL' AND c.substationId = :substationId AND c.gridOperatorId = :gridOperatorId)
OR (c.type = 'EEG_LOKAL' AND c.transformerId = :transformerId AND c.gridOperatorId = :gridOperatorId)
""")
List<EnergyCommunity> findEligibleCommunities(
@Param("substationId") String substationId,
@Param("transformerId") String transformerId,
@Param("gridOperatorId") String gridOperatorId
);
}

View File

@ -0,0 +1,21 @@
package at.mueller.eeg.backend.community.repository;
import at.mueller.eeg.backend.community.domain.Membership;
import at.mueller.eeg.backend.community.domain.MembershipStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface MembershipRepository extends JpaRepository<Membership, UUID> {
boolean existsByMeteringPointIdAndEnergyCommunityId(UUID meteringPointId, UUID energyCommunityId);
List<Membership> findByMeteringPointId(UUID meteringPointId);
boolean existsByMeteringPointIdAndPriorityLevelAndStatusIn(
UUID meteringPointId,
Integer priorityLevel,
List<MembershipStatus> statuses
);
}

View File

@ -3,5 +3,7 @@ package at.mueller.eeg.backend.community.repository;
import at.mueller.eeg.backend.community.domain.MeteringPoint; import at.mueller.eeg.backend.community.domain.MeteringPoint;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
public interface MeteringPointRepository extends JpaRepository<MeteringPoint, String> { import java.util.UUID;
public interface MeteringPointRepository extends JpaRepository<MeteringPoint, UUID> {
} }

View File

@ -42,8 +42,8 @@ public interface IamDtos {
String firstName, String firstName,
String lastName, String lastName,
String email, String email,
String status, // PENDING, APPROVED, REJECTED String status,
String primaryAtNumber // Zur schnellen Anzeige in der Liste String primaryAtNumber
) {} ) {}
record ErrorResponse(String message, int status) {} record ErrorResponse(String message, int status) {}

View File

@ -49,39 +49,22 @@ public class SecurityConfig {
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http http
// 1. CSRF deaktivieren: Da wir stateless mit JWTs arbeiten und keine
// klassischen Session-Cookies verwenden, ist CSRF-Schutz hier nicht nötig.
.cors(cors -> cors.configurationSource(corsConfigurationSource())) .cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
// 2. Stateless Session Management: Spring Security darf keine HTTP-Session anlegen
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 3. Routing & Autorisierung
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
// Erlaubt interne Forwards (wichtig für deinen SpaRoutingController -> /index.html)
.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
// Erlaubt den Zugriff auf statische Frontend-Dateien im Root
.requestMatchers("/", "/index.html", "/assets/**", "/static/**", "/*.js", "/*.css", "/*.ico").permitAll() .requestMatchers("/", "/index.html", "/assets/**", "/static/**", "/*.js", "/*.css", "/*.ico").permitAll()
// OpenAPI / Swagger Docs für das Frontend-Tooling freigeben
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
// Deine öffentlichen API-Endpunkte
.requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/auth/**").permitAll()
.requestMatchers(
// Admin-Routen auf URL-Ebene absichern (zusätzlich zur @PreAuthorize Annotation) "/api/community/admin/**",
.requestMatchers("/api/admin/**").hasRole("ADMIN") "/api/iam/admin/**",
"/api/mako/admin/**"
// Alle anderen /api/ Anfragen erfordern zumindest einen gültigen Login ).hasRole("ADMIN")
.requestMatchers("/api/**").authenticated() .requestMatchers("/api/**").authenticated()
// Alles andere (SPA-Routen, die nicht von obigen Matches gefangen wurden) freigeben
.anyRequest().permitAll() .anyRequest().permitAll()
) )
// 4. JWT als Authentifizierungsmethode aktivieren
.oauth2ResourceServer(oauth2 -> oauth2 .oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
); );
@ -110,20 +93,10 @@ public class SecurityConfig {
@Bean @Bean
public CorsConfigurationSource corsConfigurationSource() { public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration(); CorsConfiguration configuration = new CorsConfiguration();
// Erlaube exakt dein Angular-Frontend
configuration.setAllowedOrigins(List.of("http://localhost:4200")); configuration.setAllowedOrigins(List.of("http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
// Erlaube die gängigen HTTP-Methoden
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
// Erlaube Header, die Angular mitsendet (besonders wichtig für Content-Type und später Authorization/Token)
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type")); configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
// Wichtig, wenn Anmeldedaten (Credentials) oder Cookies im Spiel sind
configuration.setAllowCredentials(true); configuration.setAllowCredentials(true);
// Wende diese Regeln auf alle Endpunkte (/**) an
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); source.registerCorsConfiguration("/**", configuration);

View File

@ -1,6 +1,6 @@
package at.mueller.eeg.backend.iam.service; package at.mueller.eeg.backend.iam.service;
import at.mueller.eeg.backend.common.config.event.UserRegisteredEvent; import at.mueller.eeg.backend.common.event.UserRegisteredEvent;
import at.mueller.eeg.backend.iam.domain.User; import at.mueller.eeg.backend.iam.domain.User;
import at.mueller.eeg.backend.iam.mapper.UserMapper; import at.mueller.eeg.backend.iam.mapper.UserMapper;
import at.mueller.eeg.backend.iam.repository.UserRepository; import at.mueller.eeg.backend.iam.repository.UserRepository;
@ -16,6 +16,7 @@ import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
@ -53,6 +54,7 @@ public class AuthService {
/** /**
* Verarbeitet die Registrierung des neuen EEG-Mitglieds. * Verarbeitet die Registrierung des neuen EEG-Mitglieds.
*/ */
@Transactional
public void register(RegistrationRequest request) { public void register(RegistrationRequest request) {
userRepository.findByEmail(request.email()).ifPresent(user -> { userRepository.findByEmail(request.email()).ifPresent(user -> {

View File

@ -0,0 +1,11 @@
package at.mueller.eeg.backend.mako.api.dto;
public record EdaTopologyResponse(
String atNumber,
boolean isEligible,
String gridOperatorName,
String gridLevel,
String substationId,
String transformerId
) {
}

View File

@ -0,0 +1,54 @@
package at.mueller.eeg.backend.mako.event.listener;
import at.mueller.eeg.backend.common.event.EdaTopologyCheckEvent;
import at.mueller.eeg.backend.common.event.EdaTopologyCompletedEvent;
import at.mueller.eeg.backend.common.event.EdaTopologyFailedEvent;
import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse;
import at.mueller.eeg.backend.mako.service.EdaTopologyService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class EdaCommuncationEventListener {
private final EdaTopologyService edaTopologyService;
private final ApplicationEventPublisher eventPublisher;
@EventListener
@Async
public void handleEdaTopologyCheckEvent(EdaTopologyCheckEvent event) {
log.info("Asynchroner Topologie-Check gestartet für ID: {}", event.meteringPointId());
try {
EdaTopologyResponse edaResponse = edaTopologyService.checkTopology(event.atNumber());
eventPublisher.publishEvent(new EdaTopologyCompletedEvent(
event.userId(),
event.meteringPointId(),
edaResponse.isEligible(),
edaResponse.gridOperatorName(),
edaResponse.substationId(),
edaResponse.transformerId(),
edaResponse.gridLevel()
));
log.info("MeteringPoint erfolgreich mit EDA-Daten aktualisiert.");
} catch (Exception e) {
log.error("Fehler bei der EDA Topologie-Abfrage für AT-Nummer {}: {}",
event.atNumber(), e.getMessage());
eventPublisher.publishEvent(new EdaTopologyFailedEvent(
event.userId(),
event.meteringPointId(),
e.getMessage()
));
}
}
}

View File

@ -0,0 +1,10 @@
package at.mueller.eeg.backend.mako.service;
import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse;
public interface EdaTopologyService {
/**
* Führt die Topologieabfrage für einen Zählpunkt durch.
*/
EdaTopologyResponse checkTopology(String atNumber);
}

View File

@ -0,0 +1,43 @@
package at.mueller.eeg.backend.mako.service.impl;
import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse;
import at.mueller.eeg.backend.mako.service.EdaTopologyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class SimulatedEdaTopologyService implements EdaTopologyService {
@Override
public EdaTopologyResponse checkTopology(String atNumber) {
log.info("Starte simulierte EDA-Topologieabfrage für Zählpunkt: {}", atNumber);
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("EDA Simulation unterbrochen", e);
}
if (atNumber == null || !atNumber.startsWith("AT")) {
throw new IllegalArgumentException("Ungültiges Zählpunkt-Format (muss mit AT beginnen)");
}
boolean isEligible = !atNumber.endsWith("999");
String operator = atNumber.contains("001000") ? "Wiener Netze GmbH" : "Netz NÖ GmbH";
log.info("Simulierte EDA-Abfrage abgeschlossen für: {}", atNumber);
return new EdaTopologyResponse(
atNumber,
isEligible,
operator,
"7",
"UW_WAP_21_STREBERSDORF",
"TRAFO_21_042_BISAMBERG"
);
}
}