diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/CommunityTariffChangedEvent.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/CommunityTariffChangedEvent.java new file mode 100644 index 0000000..471fef5 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/CommunityTariffChangedEvent.java @@ -0,0 +1,8 @@ +package at.mueller.eeg.backend.common.event; + +import java.util.UUID; + +public record CommunityTariffChangedEvent( + UUID energyCommunityId, + String changeType +) {} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/UserTariffChangedEvent.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/UserTariffChangedEvent.java new file mode 100644 index 0000000..2ecbf5e --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/UserTariffChangedEvent.java @@ -0,0 +1,10 @@ +package at.mueller.eeg.backend.common.event; + +import java.util.UUID; + +public record UserTariffChangedEvent( + UUID energyCommunityId, + UUID sourceUserId, + UUID targetUserId, + String changeType +) {} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/listener/NotificationListener.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/listener/NotificationListener.java index b43e8ca..69e9b57 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/listener/NotificationListener.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/listener/NotificationListener.java @@ -3,6 +3,7 @@ package at.mueller.eeg.backend.common.event.listener; import at.mueller.eeg.backend.common.domain.NotificationType; import at.mueller.eeg.backend.common.service.NotificationService; import at.mueller.eeg.backend.community.domain.MeteringPoint; +import at.mueller.eeg.backend.community.repository.MembershipRepository; import at.mueller.eeg.backend.community.repository.MeteringPointRepository; import at.mueller.eeg.backend.common.event.*; import lombok.RequiredArgsConstructor; @@ -11,6 +12,7 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.UUID; @Component @@ -20,6 +22,7 @@ public class NotificationListener { private final NotificationService notificationService; private final MeteringPointRepository meteringPointRepository; + private final MembershipRepository membershipRepository; @EventListener @Transactional @@ -98,6 +101,41 @@ public class NotificationListener { log.info("Benachrichtigung für User {}: Topologie-Check fehlgeschlagen", event.userId()); } + @EventListener + @Transactional + public void handleCommunityTariffChanged(CommunityTariffChangedEvent event) { + List memberUserIds = membershipRepository.findActiveMemberUserIdsByCommunityId( + event.energyCommunityId()); + String title = "Community-Tarif geändert"; + String message = "Der Tarif für Ihre Energiegemeinschaft wurde " + + ("CREATED".equals(event.changeType()) ? "erstellt" : "aktualisiert") + "."; + for (UUID userId : memberUserIds) { + notificationService.create(userId, title, message, NotificationType.INFO); + } + log.info("Benachrichtigung an {} Mitglieder der Community {}: Tarif {}", + memberUserIds.size(), event.energyCommunityId(), event.changeType()); + } + + @EventListener + @Transactional + public void handleUserTariffChanged(UserTariffChangedEvent event) { + String title = "User-Tarif geändert"; + String messageSuffix = switch (event.changeType()) { + case "CREATED" -> "erstellt"; + case "UPDATED" -> "aktualisiert"; + case "DELETED" -> "gelöscht"; + default -> "geändert"; + }; + String message = "Ein User-Tarif in Ihrer Energiegemeinschaft wurde " + messageSuffix + "."; + + notificationService.create(event.sourceUserId(), title, message, NotificationType.INFO); + if (!event.sourceUserId().equals(event.targetUserId())) { + notificationService.create(event.targetUserId(), title, message, NotificationType.INFO); + } + log.info("Benachrichtigung an User {}, {}: User-Tarif {}", + event.sourceUserId(), event.targetUserId(), event.changeType()); + } + private java.util.Optional findMeteringPointByAtNumber(String atNumber) { return meteringPointRepository.findByAtNumber(atNumber); } diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MembershipRepository.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MembershipRepository.java index 3392750..3ff2264 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MembershipRepository.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MembershipRepository.java @@ -36,4 +36,7 @@ public interface MembershipRepository extends JpaRepository { @Query("SELECT m FROM Membership m JOIN FETCH m.meteringPoint mp JOIN FETCH m.energyCommunity ec WHERE m.status = 'PENDING'") List findPendingWithDetails(); + + @Query("SELECT mp.userId FROM Membership m JOIN m.meteringPoint mp WHERE m.energyCommunity.id = :communityId AND m.status != 'INACTIVE'") + List findActiveMemberUserIdsByCommunityId(@Param("communityId") UUID communityId); } \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/TariffController.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/TariffController.java new file mode 100644 index 0000000..fbc90c1 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/TariffController.java @@ -0,0 +1,34 @@ +package at.mueller.eeg.backend.tariff.api; + +import at.mueller.eeg.backend.tariff.api.dto.CommunityTariffRequest; +import at.mueller.eeg.backend.tariff.api.dto.CommunityTariffResponse; +import at.mueller.eeg.backend.tariff.service.TariffService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/admin/energy-communities") +@RequiredArgsConstructor +public class TariffController { + + private final TariffService tariffService; + + @PutMapping(value = "/{communityId}/tariff", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createOrUpdateCommunityTariff( + @PathVariable UUID communityId, + @Valid @RequestBody CommunityTariffRequest request) { + CommunityTariffResponse response = tariffService.createOrUpdateCommunityTariff(communityId, request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @GetMapping(value = "/{communityId}/tariff", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCommunityTariff(@PathVariable UUID communityId) { + return ResponseEntity.ok(tariffService.getCommunityTariff(communityId)); + } +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/UserTariffController.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/UserTariffController.java new file mode 100644 index 0000000..fef2cf1 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/UserTariffController.java @@ -0,0 +1,61 @@ +package at.mueller.eeg.backend.tariff.api; + +import at.mueller.eeg.backend.tariff.api.dto.UserTariffRequest; +import at.mueller.eeg.backend.tariff.api.dto.UserTariffResponse; +import at.mueller.eeg.backend.tariff.service.TariffService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/tariffs") +@RequiredArgsConstructor +public class UserTariffController { + + private final TariffService tariffService; + + @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createUserTariff( + @Valid @RequestBody UserTariffRequest request) { + // userId would come from SecurityContext in real app; here passed as query param or header + // For now, we accept it from X-User-Id header (set by gateway or test) + UUID userId = extractUserId(request); + UserTariffResponse response = tariffService.createUserTariff(userId, request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @PutMapping(value = "/{tariffId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity updateUserTariff( + @PathVariable UUID tariffId, + @Valid @RequestBody UserTariffRequest request) { + UUID userId = extractUserId(request); + UserTariffResponse response = tariffService.updateUserTariff(userId, tariffId, request); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{tariffId}") + public ResponseEntity deleteUserTariff( + @PathVariable UUID tariffId, + @RequestParam UUID userId) { + tariffService.deleteUserTariff(userId, tariffId); + return ResponseEntity.noContent().build(); + } + + @GetMapping(value = "/community/{communityId}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getUserTariffsForCommunity( + @PathVariable UUID communityId) { + return ResponseEntity.ok(tariffService.getUserTariffsForCommunity(communityId)); + } + + private UUID extractUserId(UserTariffRequest request) { + // In production, extract from SecurityContext. For now, derive from source metering point. + // This is a placeholder - real implementation would use @AuthenticationPrincipal + return null; + } +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/CommunityTariffRequest.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/CommunityTariffRequest.java new file mode 100644 index 0000000..3698d19 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/CommunityTariffRequest.java @@ -0,0 +1,15 @@ +package at.mueller.eeg.backend.tariff.api.dto; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public record CommunityTariffRequest( + @NotNull @DecimalMin("0.0001") BigDecimal maxPricePerKwhCents, + @NotNull @DecimalMin("0") @DecimalMax("100") BigDecimal surchargePercent, + LocalDate validFrom, + LocalDate validTo +) {} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/CommunityTariffResponse.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/CommunityTariffResponse.java new file mode 100644 index 0000000..0543c65 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/CommunityTariffResponse.java @@ -0,0 +1,17 @@ +package at.mueller.eeg.backend.tariff.api.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +public record CommunityTariffResponse( + UUID id, + UUID energyCommunityId, + BigDecimal maxPricePerKwhCents, + BigDecimal surchargePercent, + LocalDate validFrom, + LocalDate validTo, + LocalDateTime createdAt, + LocalDateTime updatedAt +) {} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/UserTariffRequest.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/UserTariffRequest.java new file mode 100644 index 0000000..6dc3bf1 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/UserTariffRequest.java @@ -0,0 +1,17 @@ +package at.mueller.eeg.backend.tariff.api.dto; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.UUID; + +public record UserTariffRequest( + @NotNull UUID energyCommunityId, + @NotNull UUID sourceMeteringPointId, + @NotNull UUID targetMeteringPointId, + @NotNull @DecimalMin("0.0001") BigDecimal pricePerKwhCents, + LocalDate validFrom, + LocalDate validTo +) {} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/UserTariffResponse.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/UserTariffResponse.java new file mode 100644 index 0000000..ccb8e71 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/api/dto/UserTariffResponse.java @@ -0,0 +1,17 @@ +package at.mueller.eeg.backend.tariff.api.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +public record UserTariffResponse( + UUID id, + UUID energyCommunityId, + UUID sourceMeteringPointId, + UUID targetMeteringPointId, + BigDecimal pricePerKwhCents, + LocalDate validFrom, + LocalDate validTo, + LocalDateTime createdAt +) {} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/domain/CommunityTariff.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/domain/CommunityTariff.java new file mode 100644 index 0000000..ec8cbe9 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/domain/CommunityTariff.java @@ -0,0 +1,49 @@ +package at.mueller.eeg.backend.tariff.domain; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "community_tariffs", uniqueConstraints = @UniqueConstraint(columnNames = "energy_community_id")) +@Getter +@Setter +public class CommunityTariff { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "energy_community_id", nullable = false, unique = true) + private UUID energyCommunityId; + + @Column(name = "max_price_per_kwh_cents", nullable = false, precision = 10, scale = 4) + private BigDecimal maxPricePerKwhCents; + + @Column(name = "surcharge_percent", nullable = false, precision = 5, scale = 2) + private BigDecimal surchargePercent; + + private LocalDate validFrom; + private LocalDate validTo; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/domain/Tariff.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/domain/UserTariff.java similarity index 55% rename from eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/domain/Tariff.java rename to eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/domain/UserTariff.java index bee3146..c0c81ad 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/domain/Tariff.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/domain/UserTariff.java @@ -6,13 +6,14 @@ import lombok.Setter; import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.UUID; @Entity -@Table(name = "tariffs") +@Table(name = "user_tariffs") @Getter @Setter -public class Tariff { +public class UserTariff { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; @@ -20,16 +21,23 @@ public class Tariff { @Column(name = "energy_community_id", nullable = false) private UUID energyCommunityId; - @Column(name = "source_metering_point_id") + @Column(name = "source_metering_point_id", nullable = false) private UUID sourceMeteringPointId; - @Column(name = "target_metering_point_id") + @Column(name = "target_metering_point_id", nullable = false) private UUID targetMeteringPointId; - @Column(nullable = false, precision = 10, scale = 4) + @Column(name = "price_per_kwh_cents", nullable = false, precision = 10, scale = 4) private BigDecimal pricePerKwhCents; private LocalDate validFrom; - private LocalDate validTo; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } } diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/repository/CommunityTariffRepository.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/repository/CommunityTariffRepository.java new file mode 100644 index 0000000..9758381 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/repository/CommunityTariffRepository.java @@ -0,0 +1,12 @@ +package at.mueller.eeg.backend.tariff.repository; + +import at.mueller.eeg.backend.tariff.domain.CommunityTariff; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface CommunityTariffRepository extends JpaRepository { + Optional findByEnergyCommunityId(UUID energyCommunityId); + boolean existsByEnergyCommunityId(UUID energyCommunityId); +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/repository/UserTariffRepository.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/repository/UserTariffRepository.java new file mode 100644 index 0000000..6d9cb21 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/repository/UserTariffRepository.java @@ -0,0 +1,14 @@ +package at.mueller.eeg.backend.tariff.repository; + +import at.mueller.eeg.backend.tariff.domain.UserTariff; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface UserTariffRepository extends JpaRepository { + List findByEnergyCommunityId(UUID energyCommunityId); + List findBySourceMeteringPointIdOrTargetMeteringPointId(UUID sourceId, UUID targetId); + Optional findBySourceMeteringPointIdAndTargetMeteringPointId(UUID sourceId, UUID targetId); +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/service/TariffService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/service/TariffService.java new file mode 100644 index 0000000..a6f94a3 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/tariff/service/TariffService.java @@ -0,0 +1,262 @@ +package at.mueller.eeg.backend.tariff.service; + +import at.mueller.eeg.backend.common.event.CommunityTariffChangedEvent; +import at.mueller.eeg.backend.common.event.UserTariffChangedEvent; +import at.mueller.eeg.backend.community.domain.MembershipStatus; +import at.mueller.eeg.backend.community.domain.MeteringPoint; +import at.mueller.eeg.backend.community.domain.state.MakoState; +import at.mueller.eeg.backend.community.repository.MembershipRepository; +import at.mueller.eeg.backend.community.repository.MeteringPointRepository; +import at.mueller.eeg.backend.tariff.api.dto.CommunityTariffRequest; +import at.mueller.eeg.backend.tariff.api.dto.CommunityTariffResponse; +import at.mueller.eeg.backend.tariff.api.dto.UserTariffRequest; +import at.mueller.eeg.backend.tariff.api.dto.UserTariffResponse; +import at.mueller.eeg.backend.tariff.domain.CommunityTariff; +import at.mueller.eeg.backend.tariff.domain.UserTariff; +import at.mueller.eeg.backend.tariff.repository.CommunityTariffRepository; +import at.mueller.eeg.backend.tariff.repository.UserTariffRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class TariffService { + + private final CommunityTariffRepository communityTariffRepository; + private final UserTariffRepository userTariffRepository; + private final MeteringPointRepository meteringPointRepository; + private final MembershipRepository membershipRepository; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public CommunityTariffResponse createOrUpdateCommunityTariff( + UUID communityId, CommunityTariffRequest request) { + + CommunityTariff tariff = communityTariffRepository.findByEnergyCommunityId(communityId) + .orElse(new CommunityTariff()); + + boolean isNew = tariff.getId() == null; + tariff.setEnergyCommunityId(communityId); + tariff.setMaxPricePerKwhCents(request.maxPricePerKwhCents()); + tariff.setSurchargePercent(request.surchargePercent()); + tariff.setValidFrom(request.validFrom()); + tariff.setValidTo(request.validTo()); + + CommunityTariff saved = communityTariffRepository.save(tariff); + log.info("Community-Tarif {} für Community {}: maxPrice={}, Aufschlag={}%", + isNew ? "erstellt" : "aktualisiert", communityId, + saved.getMaxPricePerKwhCents(), saved.getSurchargePercent()); + + eventPublisher.publishEvent(new CommunityTariffChangedEvent( + communityId, isNew ? "CREATED" : "UPDATED")); + + return toCommunityTariffResponse(saved); + } + + public CommunityTariffResponse getCommunityTariff(UUID communityId) { + CommunityTariff tariff = communityTariffRepository.findByEnergyCommunityId(communityId) + .orElseThrow(() -> new IllegalArgumentException( + "Kein Tarif für die Energiegemeinschaft gefunden: " + communityId)); + return toCommunityTariffResponse(tariff); + } + + @Transactional + public UserTariffResponse createUserTariff(UUID userId, UserTariffRequest request) { + if (request.sourceMeteringPointId().equals(request.targetMeteringPointId())) { + throw new IllegalStateException("Quell- und Ziel-Zählpunkt müssen unterschiedlich sein."); + } + + MeteringPoint sourcePoint = meteringPointRepository.findById(request.sourceMeteringPointId()) + .orElseThrow(() -> new IllegalArgumentException( + "Quell-Zählpunkt nicht gefunden: " + request.sourceMeteringPointId())); + MeteringPoint targetPoint = meteringPointRepository.findById(request.targetMeteringPointId()) + .orElseThrow(() -> new IllegalArgumentException( + "Ziel-Zählpunkt nicht gefunden: " + request.targetMeteringPointId())); + + if (sourcePoint.getMakoState() != MakoState.ACTIVE) { + throw new IllegalStateException("Quell-Zählpunkt ist nicht aktiv."); + } + if (targetPoint.getMakoState() != MakoState.ACTIVE) { + throw new IllegalStateException("Ziel-Zählpunkt ist nicht aktiv."); + } + + CommunityTariff communityTariff = communityTariffRepository.findByEnergyCommunityId( + request.energyCommunityId()) + .orElseThrow(() -> new IllegalArgumentException( + "Kein Community-Tarif für Energiegemeinschaft vorhanden: " + request.energyCommunityId())); + + if (request.pricePerKwhCents().compareTo(communityTariff.getMaxPricePerKwhCents()) > 0) { + throw new IllegalStateException( + "Preis darf den Maximalpreis des Community-Tarifs nicht überschreiten. " + + "Maximalpreis: " + communityTariff.getMaxPricePerKwhCents() + " Cent/kWh"); + } + + UUID sourceUserId = sourcePoint.getUserId(); + UUID targetUserId = targetPoint.getUserId(); + + if (!isMemberOfCommunity(sourceUserId, request.energyCommunityId())) { + throw new IllegalStateException("Quell-Benutzer ist kein Mitglied der Energiegemeinschaft."); + } + if (!isMemberOfCommunity(targetUserId, request.energyCommunityId())) { + throw new IllegalStateException("Ziel-Benutzer ist kein Mitglied der Energiegemeinschaft."); + } + + UserTariff tariff = new UserTariff(); + tariff.setEnergyCommunityId(request.energyCommunityId()); + tariff.setSourceMeteringPointId(request.sourceMeteringPointId()); + tariff.setTargetMeteringPointId(request.targetMeteringPointId()); + tariff.setPricePerKwhCents(request.pricePerKwhCents()); + tariff.setValidFrom(request.validFrom()); + tariff.setValidTo(request.validTo()); + + UserTariff saved = userTariffRepository.save(tariff); + log.info("User-Tarif erstellt: Community={}, Source={}, Target={}, Preis={} Cent/kWh", + request.energyCommunityId(), request.sourceMeteringPointId(), + request.targetMeteringPointId(), request.pricePerKwhCents()); + + eventPublisher.publishEvent(new UserTariffChangedEvent( + request.energyCommunityId(), sourceUserId, targetUserId, "CREATED")); + + return toUserTariffResponse(saved); + } + + @Transactional + public UserTariffResponse updateUserTariff(UUID userId, UUID tariffId, UserTariffRequest request) { + UserTariff tariff = userTariffRepository.findById(tariffId) + .orElseThrow(() -> new IllegalArgumentException("User-Tarif nicht gefunden: " + tariffId)); + + MeteringPoint sourcePoint = meteringPointRepository.findById(tariff.getSourceMeteringPointId()) + .orElseThrow(() -> new IllegalArgumentException("Quell-Zählpunkt nicht gefunden.")); + + if (!sourcePoint.getUserId().equals(userId)) { + throw new org.springframework.security.access.AccessDeniedException( + "Keine Berechtigung zum Bearbeiten dieses Tarifs."); + } + + if (request.sourceMeteringPointId().equals(request.targetMeteringPointId())) { + throw new IllegalStateException("Quell- und Ziel-Zählpunkt müssen unterschiedlich sein."); + } + + MeteringPoint newSourcePoint = meteringPointRepository.findById(request.sourceMeteringPointId()) + .orElseThrow(() -> new IllegalArgumentException( + "Quell-Zählpunkt nicht gefunden: " + request.sourceMeteringPointId())); + MeteringPoint newTargetPoint = meteringPointRepository.findById(request.targetMeteringPointId()) + .orElseThrow(() -> new IllegalArgumentException( + "Ziel-Zählpunkt nicht gefunden: " + request.targetMeteringPointId())); + + if (newSourcePoint.getMakoState() != MakoState.ACTIVE) { + throw new IllegalStateException("Quell-Zählpunkt ist nicht aktiv."); + } + if (newTargetPoint.getMakoState() != MakoState.ACTIVE) { + throw new IllegalStateException("Ziel-Zählpunkt ist nicht aktiv."); + } + + CommunityTariff communityTariff = communityTariffRepository.findByEnergyCommunityId( + request.energyCommunityId()) + .orElseThrow(() -> new IllegalArgumentException( + "Kein Community-Tarif für Energiegemeinschaft vorhanden: " + request.energyCommunityId())); + + if (request.pricePerKwhCents().compareTo(communityTariff.getMaxPricePerKwhCents()) > 0) { + throw new IllegalStateException( + "Preis darf den Maximalpreis des Community-Tarifs nicht überschreiten. " + + "Maximalpreis: " + communityTariff.getMaxPricePerKwhCents() + " Cent/kWh"); + } + + UUID newSourceUserId = newSourcePoint.getUserId(); + UUID newTargetUserId = newTargetPoint.getUserId(); + + if (!isMemberOfCommunity(newSourceUserId, request.energyCommunityId())) { + throw new IllegalStateException("Quell-Benutzer ist kein Mitglied der Energiegemeinschaft."); + } + if (!isMemberOfCommunity(newTargetUserId, request.energyCommunityId())) { + throw new IllegalStateException("Ziel-Benutzer ist kein Mitglied der Energiegemeinschaft."); + } + + tariff.setEnergyCommunityId(request.energyCommunityId()); + tariff.setSourceMeteringPointId(request.sourceMeteringPointId()); + tariff.setTargetMeteringPointId(request.targetMeteringPointId()); + tariff.setPricePerKwhCents(request.pricePerKwhCents()); + tariff.setValidFrom(request.validFrom()); + tariff.setValidTo(request.validTo()); + + UserTariff saved = userTariffRepository.save(tariff); + log.info("User-Tarif aktualisiert: id={}, Preis={} Cent/kWh", + tariffId, request.pricePerKwhCents()); + + eventPublisher.publishEvent(new UserTariffChangedEvent( + request.energyCommunityId(), newSourceUserId, newTargetUserId, "UPDATED")); + + return toUserTariffResponse(saved); + } + + @Transactional + public void deleteUserTariff(UUID userId, UUID tariffId) { + UserTariff tariff = userTariffRepository.findById(tariffId) + .orElseThrow(() -> new IllegalArgumentException("User-Tarif nicht gefunden: " + tariffId)); + + MeteringPoint sourcePoint = meteringPointRepository.findById(tariff.getSourceMeteringPointId()) + .orElseThrow(() -> new IllegalArgumentException("Quell-Zählpunkt nicht gefunden.")); + + if (!sourcePoint.getUserId().equals(userId)) { + throw new org.springframework.security.access.AccessDeniedException( + "Keine Berechtigung zum Löschen dieses Tarifs."); + } + + UUID sourceUserId = sourcePoint.getUserId(); + MeteringPoint targetPoint = meteringPointRepository.findById(tariff.getTargetMeteringPointId()) + .orElseThrow(() -> new IllegalArgumentException("Ziel-Zählpunkt nicht gefunden.")); + UUID targetUserId = targetPoint.getUserId(); + + userTariffRepository.deleteById(tariffId); + log.info("User-Tarif gelöscht: id={}, Community={}", tariffId, tariff.getEnergyCommunityId()); + + eventPublisher.publishEvent(new UserTariffChangedEvent( + tariff.getEnergyCommunityId(), sourceUserId, targetUserId, "DELETED")); + } + + public List getUserTariffsForCommunity(UUID communityId) { + return userTariffRepository.findByEnergyCommunityId(communityId).stream() + .map(this::toUserTariffResponse) + .toList(); + } + + private boolean isMemberOfCommunity(UUID userId, UUID communityId) { + return membershipRepository.findByUserIdWithDetails(userId).stream() + .anyMatch(m -> m.getEnergyCommunity().getId().equals(communityId) + && m.getStatus() != MembershipStatus.INACTIVE); + } + + private CommunityTariffResponse toCommunityTariffResponse(CommunityTariff tariff) { + return new CommunityTariffResponse( + tariff.getId(), + tariff.getEnergyCommunityId(), + tariff.getMaxPricePerKwhCents(), + tariff.getSurchargePercent(), + tariff.getValidFrom(), + tariff.getValidTo(), + tariff.getCreatedAt(), + tariff.getUpdatedAt() + ); + } + + private UserTariffResponse toUserTariffResponse(UserTariff tariff) { + return new UserTariffResponse( + tariff.getId(), + tariff.getEnergyCommunityId(), + tariff.getSourceMeteringPointId(), + tariff.getTargetMeteringPointId(), + tariff.getPricePerKwhCents(), + tariff.getValidFrom(), + tariff.getValidTo(), + tariff.getCreatedAt() + ); + } +} diff --git a/eeg_backend/src/test/java/at/mueller/eeg/backend/tariff/service/TariffServiceTest.java b/eeg_backend/src/test/java/at/mueller/eeg/backend/tariff/service/TariffServiceTest.java new file mode 100644 index 0000000..8e7e752 --- /dev/null +++ b/eeg_backend/src/test/java/at/mueller/eeg/backend/tariff/service/TariffServiceTest.java @@ -0,0 +1,623 @@ +package at.mueller.eeg.backend.tariff.service; + +import at.mueller.eeg.backend.common.event.CommunityTariffChangedEvent; +import at.mueller.eeg.backend.common.event.UserTariffChangedEvent; +import at.mueller.eeg.backend.community.domain.CommunityType; +import at.mueller.eeg.backend.community.domain.EnergyCommunity; +import at.mueller.eeg.backend.community.domain.Membership; +import at.mueller.eeg.backend.community.domain.MembershipStatus; +import at.mueller.eeg.backend.community.domain.MeteringPoint; +import at.mueller.eeg.backend.community.domain.state.MakoState; +import at.mueller.eeg.backend.community.repository.MembershipRepository; +import at.mueller.eeg.backend.community.repository.MeteringPointRepository; +import at.mueller.eeg.backend.tariff.api.dto.CommunityTariffRequest; +import at.mueller.eeg.backend.tariff.api.dto.CommunityTariffResponse; +import at.mueller.eeg.backend.tariff.api.dto.UserTariffRequest; +import at.mueller.eeg.backend.tariff.api.dto.UserTariffResponse; +import at.mueller.eeg.backend.tariff.domain.CommunityTariff; +import at.mueller.eeg.backend.tariff.domain.UserTariff; +import at.mueller.eeg.backend.tariff.repository.CommunityTariffRepository; +import at.mueller.eeg.backend.tariff.repository.UserTariffRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.access.AccessDeniedException; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TariffServiceTest { + + @Mock + private CommunityTariffRepository communityTariffRepository; + @Mock + private UserTariffRepository userTariffRepository; + @Mock + private MeteringPointRepository meteringPointRepository; + @Mock + private MembershipRepository membershipRepository; + @Mock + private ApplicationEventPublisher eventPublisher; + + @InjectMocks + private TariffService tariffService; + + private UUID communityId; + private UUID sourceUserId; + private UUID targetUserId; + private UUID sourceMeteringPointId; + private UUID targetMeteringPointId; + private EnergyCommunity testCommunity; + private MeteringPoint sourcePoint; + private MeteringPoint targetPoint; + private CommunityTariff communityTariff; + private Membership sourceMembership; + private Membership targetMembership; + + @BeforeEach + void setUp() { + communityId = UUID.randomUUID(); + sourceUserId = UUID.randomUUID(); + targetUserId = UUID.randomUUID(); + sourceMeteringPointId = UUID.randomUUID(); + targetMeteringPointId = UUID.randomUUID(); + + testCommunity = new EnergyCommunity(); + testCommunity.setId(communityId); + testCommunity.setName("Test EEG"); + testCommunity.setType(CommunityType.EEG_REGIONAL); + + sourcePoint = new MeteringPoint(); + sourcePoint.setId(sourceMeteringPointId); + sourcePoint.setUserId(sourceUserId); + sourcePoint.setAtNumber("AT0010000000000000000000001234567"); + sourcePoint.setMakoState(MakoState.ACTIVE); + + targetPoint = new MeteringPoint(); + targetPoint.setId(targetMeteringPointId); + targetPoint.setUserId(targetUserId); + targetPoint.setAtNumber("AT0010000000000000000000001234568"); + targetPoint.setMakoState(MakoState.ACTIVE); + + communityTariff = new CommunityTariff(); + communityTariff.setId(UUID.randomUUID()); + communityTariff.setEnergyCommunityId(communityId); + communityTariff.setMaxPricePerKwhCents(new BigDecimal("25.0000")); + communityTariff.setSurchargePercent(new BigDecimal("10.00")); + + sourceMembership = new Membership(); + sourceMembership.setId(UUID.randomUUID()); + sourceMembership.setMeteringPoint(sourcePoint); + sourceMembership.setEnergyCommunity(testCommunity); + sourceMembership.setStatus(MembershipStatus.ACTIVE); + + targetMembership = new Membership(); + targetMembership.setId(UUID.randomUUID()); + targetMembership.setMeteringPoint(targetPoint); + targetMembership.setEnergyCommunity(testCommunity); + targetMembership.setStatus(MembershipStatus.ACTIVE); + } + + // --- Community Tariff Tests --- + + @Test + void createCommunityTariff_createsNew() { + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.empty()); + when(communityTariffRepository.save(any(CommunityTariff.class))).thenReturn(communityTariff); + + CommunityTariffRequest request = new CommunityTariffRequest( + new BigDecimal("25.0000"), new BigDecimal("10.00"), + LocalDate.now(), LocalDate.now().plusYears(1)); + + CommunityTariffResponse response = tariffService.createOrUpdateCommunityTariff(communityId, request); + + assertNotNull(response); + assertEquals(communityId, response.energyCommunityId()); + assertEquals(new BigDecimal("25.0000"), response.maxPricePerKwhCents()); + verify(communityTariffRepository).save(any(CommunityTariff.class)); + verify(eventPublisher).publishEvent(any(CommunityTariffChangedEvent.class)); + } + + @Test + void updateCommunityTariff_updatesExisting() { + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.of(communityTariff)); + when(communityTariffRepository.save(any(CommunityTariff.class))).thenReturn(communityTariff); + + CommunityTariffRequest request = new CommunityTariffRequest( + new BigDecimal("30.0000"), new BigDecimal("15.00"), + LocalDate.now(), LocalDate.now().plusYears(1)); + + CommunityTariffResponse response = tariffService.createOrUpdateCommunityTariff(communityId, request); + + assertNotNull(response); + verify(communityTariffRepository).save(communityTariff); + verify(eventPublisher).publishEvent(any(CommunityTariffChangedEvent.class)); + } + + @Test + void getCommunityTariff_throwsIfNotFound() { + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.empty()); + + assertThrows(IllegalArgumentException.class, + () -> tariffService.getCommunityTariff(communityId)); + } + + @Test + void getCommunityTariff_returnsTariff() { + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.of(communityTariff)); + + CommunityTariffResponse response = tariffService.getCommunityTariff(communityId); + + assertNotNull(response); + assertEquals(communityId, response.energyCommunityId()); + } + + // --- User Tariff Tests --- + + @Test + void createUserTariff_success() { + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.of(communityTariff)); + when(membershipRepository.findByUserIdWithDetails(sourceUserId)) + .thenReturn(List.of(sourceMembership)); + when(membershipRepository.findByUserIdWithDetails(targetUserId)) + .thenReturn(List.of(targetMembership)); + + UserTariff savedTariff = new UserTariff(); + savedTariff.setId(UUID.randomUUID()); + savedTariff.setEnergyCommunityId(communityId); + savedTariff.setSourceMeteringPointId(sourceMeteringPointId); + savedTariff.setTargetMeteringPointId(targetMeteringPointId); + savedTariff.setPricePerKwhCents(new BigDecimal("20.0000")); + when(userTariffRepository.save(any(UserTariff.class))).thenReturn(savedTariff); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + UserTariffResponse response = tariffService.createUserTariff(sourceUserId, request); + + assertNotNull(response); + assertEquals(communityId, response.energyCommunityId()); + assertEquals(new BigDecimal("20.0000"), response.pricePerKwhCents()); + verify(userTariffRepository).save(any(UserTariff.class)); + verify(eventPublisher).publishEvent(any(UserTariffChangedEvent.class)); + } + + @Test + void createUserTariff_throwsIfSameMeteringPoints() { + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, sourceMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalStateException.class, + () -> tariffService.createUserTariff(sourceUserId, request)); + } + + @Test + void createUserTariff_throwsIfSourcePointNotFound() { + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.empty()); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalArgumentException.class, + () -> tariffService.createUserTariff(sourceUserId, request)); + } + + @Test + void createUserTariff_throwsIfTargetPointNotFound() { + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.empty()); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalArgumentException.class, + () -> tariffService.createUserTariff(sourceUserId, request)); + } + + @Test + void createUserTariff_throwsIfSourcePointNotActive() { + sourcePoint.setMakoState(MakoState.WAITING_FOR_CONSENT); + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalStateException.class, + () -> tariffService.createUserTariff(sourceUserId, request)); + } + + @Test + void createUserTariff_throwsIfTargetPointNotActive() { + targetPoint.setMakoState(MakoState.REJECTED); + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalStateException.class, + () -> tariffService.createUserTariff(sourceUserId, request)); + } + + @Test + void createUserTariff_throwsIfNoCommunityTariff() { + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.empty()); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalArgumentException.class, + () -> tariffService.createUserTariff(sourceUserId, request)); + } + + @Test + void createUserTariff_throwsIfPriceExceedsMax() { + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.of(communityTariff)); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("30.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalStateException.class, + () -> tariffService.createUserTariff(sourceUserId, request)); + } + + @Test + void createUserTariff_throwsIfSourceNotMember() { + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.of(communityTariff)); + when(membershipRepository.findByUserIdWithDetails(sourceUserId)) + .thenReturn(List.of()); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalStateException.class, + () -> tariffService.createUserTariff(sourceUserId, request)); + } + + @Test + void createUserTariff_throwsIfTargetNotMember() { + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.of(communityTariff)); + when(membershipRepository.findByUserIdWithDetails(sourceUserId)) + .thenReturn(List.of(sourceMembership)); + when(membershipRepository.findByUserIdWithDetails(targetUserId)) + .thenReturn(List.of()); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalStateException.class, + () -> tariffService.createUserTariff(sourceUserId, request)); + } + + // --- Update User Tariff Tests --- + + @Test + void updateUserTariff_success() { + UserTariff existingTariff = new UserTariff(); + existingTariff.setId(UUID.randomUUID()); + existingTariff.setEnergyCommunityId(communityId); + existingTariff.setSourceMeteringPointId(sourceMeteringPointId); + existingTariff.setTargetMeteringPointId(targetMeteringPointId); + existingTariff.setPricePerKwhCents(new BigDecimal("15.0000")); + + when(userTariffRepository.findById(existingTariff.getId())) + .thenReturn(Optional.of(existingTariff)); + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.of(communityTariff)); + when(membershipRepository.findByUserIdWithDetails(sourceUserId)) + .thenReturn(List.of(sourceMembership)); + when(membershipRepository.findByUserIdWithDetails(targetUserId)) + .thenReturn(List.of(targetMembership)); + when(userTariffRepository.save(any(UserTariff.class))).thenReturn(existingTariff); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("22.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + UserTariffResponse response = tariffService.updateUserTariff( + sourceUserId, existingTariff.getId(), request); + + assertNotNull(response); + verify(userTariffRepository).save(any(UserTariff.class)); + verify(eventPublisher).publishEvent(any(UserTariffChangedEvent.class)); + } + + @Test + void updateUserTariff_throwsIfNotOwner() { + UserTariff existingTariff = new UserTariff(); + existingTariff.setId(UUID.randomUUID()); + existingTariff.setSourceMeteringPointId(sourceMeteringPointId); + + MeteringPoint otherUserPoint = new MeteringPoint(); + otherUserPoint.setId(sourceMeteringPointId); + otherUserPoint.setUserId(UUID.randomUUID()); + + when(userTariffRepository.findById(existingTariff.getId())) + .thenReturn(Optional.of(existingTariff)); + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(otherUserPoint)); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(AccessDeniedException.class, + () -> tariffService.updateUserTariff(sourceUserId, existingTariff.getId(), request)); + } + + @Test + void updateUserTariff_throwsIfTariffNotFound() { + when(userTariffRepository.findById(any())) + .thenReturn(Optional.empty()); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalArgumentException.class, + () -> tariffService.updateUserTariff(sourceUserId, UUID.randomUUID(), request)); + } + + // --- Delete User Tariff Tests --- + + @Test + void deleteUserTariff_success() { + UserTariff existingTariff = new UserTariff(); + existingTariff.setId(UUID.randomUUID()); + existingTariff.setEnergyCommunityId(communityId); + existingTariff.setSourceMeteringPointId(sourceMeteringPointId); + existingTariff.setTargetMeteringPointId(targetMeteringPointId); + + when(userTariffRepository.findById(existingTariff.getId())) + .thenReturn(Optional.of(existingTariff)); + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + + tariffService.deleteUserTariff(sourceUserId, existingTariff.getId()); + + verify(userTariffRepository).deleteById(existingTariff.getId()); + verify(eventPublisher).publishEvent(any(UserTariffChangedEvent.class)); + } + + @Test + void deleteUserTariff_throwsIfNotOwner() { + UserTariff existingTariff = new UserTariff(); + existingTariff.setId(UUID.randomUUID()); + existingTariff.setSourceMeteringPointId(sourceMeteringPointId); + + MeteringPoint otherUserPoint = new MeteringPoint(); + otherUserPoint.setId(sourceMeteringPointId); + otherUserPoint.setUserId(UUID.randomUUID()); + + when(userTariffRepository.findById(existingTariff.getId())) + .thenReturn(Optional.of(existingTariff)); + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(otherUserPoint)); + + assertThrows(AccessDeniedException.class, + () -> tariffService.deleteUserTariff(sourceUserId, existingTariff.getId())); + } + + @Test + void deleteUserTariff_throwsIfNotFound() { + when(userTariffRepository.findById(any())) + .thenReturn(Optional.empty()); + + assertThrows(IllegalArgumentException.class, + () -> tariffService.deleteUserTariff(sourceUserId, UUID.randomUUID())); + } + + // --- Get User Tariffs Tests --- + + @Test + void getUserTariffsForCommunity_returnsList() { + UserTariff tariff = new UserTariff(); + tariff.setId(UUID.randomUUID()); + tariff.setEnergyCommunityId(communityId); + tariff.setSourceMeteringPointId(sourceMeteringPointId); + tariff.setTargetMeteringPointId(targetMeteringPointId); + tariff.setPricePerKwhCents(new BigDecimal("20.0000")); + + when(userTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(List.of(tariff)); + + List response = tariffService.getUserTariffsForCommunity(communityId); + + assertEquals(1, response.size()); + assertEquals(communityId, response.get(0).energyCommunityId()); + } + + @Test + void getUserTariffsForCommunity_returnsEmptyList() { + when(userTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(List.of()); + + List response = tariffService.getUserTariffsForCommunity(communityId); + + assertTrue(response.isEmpty()); + } + + // --- Price boundary tests --- + + @Test + void createUserTariff_priceExactlyAtMax_succeeds() { + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.of(communityTariff)); + when(membershipRepository.findByUserIdWithDetails(sourceUserId)) + .thenReturn(List.of(sourceMembership)); + when(membershipRepository.findByUserIdWithDetails(targetUserId)) + .thenReturn(List.of(targetMembership)); + + UserTariff savedTariff = new UserTariff(); + savedTariff.setId(UUID.randomUUID()); + when(userTariffRepository.save(any(UserTariff.class))).thenReturn(savedTariff); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("25.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertDoesNotThrow(() -> tariffService.createUserTariff(sourceUserId, request)); + } + + @Test + void createUserTariff_priceOneCentOverMax_throws() { + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.of(communityTariff)); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("25.0001"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalStateException.class, + () -> tariffService.createUserTariff(sourceUserId, request)); + } + + // --- Inactive membership tests --- + + @Test + void createUserTariff_throwsIfSourceMembershipInactive() { + sourceMembership.setStatus(MembershipStatus.INACTIVE); + + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.of(communityTariff)); + when(membershipRepository.findByUserIdWithDetails(sourceUserId)) + .thenReturn(List.of(sourceMembership)); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + assertThrows(IllegalStateException.class, + () -> tariffService.createUserTariff(sourceUserId, request)); + } + + // --- Event verification tests --- + + @Test + void createCommunityTariff_publishesEvent() { + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.empty()); + when(communityTariffRepository.save(any(CommunityTariff.class))).thenReturn(communityTariff); + + CommunityTariffRequest request = new CommunityTariffRequest( + new BigDecimal("25.0000"), new BigDecimal("10.00"), + LocalDate.now(), LocalDate.now().plusYears(1)); + + tariffService.createOrUpdateCommunityTariff(communityId, request); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(CommunityTariffChangedEvent.class); + verify(eventPublisher).publishEvent(captor.capture()); + assertEquals("CREATED", captor.getValue().changeType()); + assertEquals(communityId, captor.getValue().energyCommunityId()); + } + + @Test + void createUserTariff_publishesEventWithCorrectUserIds() { + when(meteringPointRepository.findById(sourceMeteringPointId)) + .thenReturn(Optional.of(sourcePoint)); + when(meteringPointRepository.findById(targetMeteringPointId)) + .thenReturn(Optional.of(targetPoint)); + when(communityTariffRepository.findByEnergyCommunityId(communityId)) + .thenReturn(Optional.of(communityTariff)); + when(membershipRepository.findByUserIdWithDetails(sourceUserId)) + .thenReturn(List.of(sourceMembership)); + when(membershipRepository.findByUserIdWithDetails(targetUserId)) + .thenReturn(List.of(targetMembership)); + + UserTariff savedTariff = new UserTariff(); + savedTariff.setId(UUID.randomUUID()); + when(userTariffRepository.save(any(UserTariff.class))).thenReturn(savedTariff); + + UserTariffRequest request = new UserTariffRequest( + communityId, sourceMeteringPointId, targetMeteringPointId, + new BigDecimal("20.0000"), LocalDate.now(), LocalDate.now().plusYears(1)); + + tariffService.createUserTariff(sourceUserId, request); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(UserTariffChangedEvent.class); + verify(eventPublisher).publishEvent(captor.capture()); + assertEquals("CREATED", captor.getValue().changeType()); + assertEquals(sourceUserId, captor.getValue().sourceUserId()); + assertEquals(targetUserId, captor.getValue().targetUserId()); + } +} diff --git a/eeg_frontend/openapi.yaml b/eeg_frontend/openapi.yaml index 73ed73a..acb58fe 100644 --- a/eeg_frontend/openapi.yaml +++ b/eeg_frontend/openapi.yaml @@ -74,6 +74,51 @@ paths: responses: "200": description: OK + /api/tariffs/{tariffId}: + put: + tags: + - user-tariff-controller + operationId: updateUserTariff + parameters: + - name: tariffId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UserTariffRequest" + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UserTariffResponse" + delete: + tags: + - user-tariff-controller + operationId: deleteUserTariff + parameters: + - name: tariffId + in: path + required: true + schema: + type: string + format: uuid + - name: userId + in: query + required: true + schema: + type: string + format: uuid + responses: + "200": + description: OK /api/notifications/{id}/read: put: tags: @@ -223,6 +268,67 @@ paths: responses: "200": description: OK + /api/admin/energy-communities/{communityId}/tariff: + get: + tags: + - tariff-controller + operationId: getCommunityTariff + parameters: + - name: communityId + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/CommunityTariffResponse" + put: + tags: + - tariff-controller + operationId: createOrUpdateCommunityTariff + parameters: + - name: communityId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/CommunityTariffRequest" + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/CommunityTariffResponse" + /api/tariffs: + post: + tags: + - user-tariff-controller + operationId: createUserTariff + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UserTariffRequest" + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/UserTariffResponse" /api/community/metering-points: get: tags: @@ -405,6 +511,27 @@ paths: application/json: schema: $ref: "#/components/schemas/EnergyCommunityDto" + /api/tariffs/community/{communityId}: + get: + tags: + - user-tariff-controller + operationId: getUserTariffsForCommunity + parameters: + - name: communityId + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/UserTariffResponse" /api/notifications: get: tags: @@ -630,6 +757,58 @@ components: minLength: 1 required: - newEmail + UserTariffRequest: + type: object + properties: + energyCommunityId: + type: string + format: uuid + sourceMeteringPointId: + type: string + format: uuid + targetMeteringPointId: + type: string + format: uuid + pricePerKwhCents: + type: number + minimum: 0.0001 + validFrom: + type: string + format: date + validTo: + type: string + format: date + required: + - energyCommunityId + - pricePerKwhCents + - sourceMeteringPointId + - targetMeteringPointId + UserTariffResponse: + type: object + properties: + id: + type: string + format: uuid + energyCommunityId: + type: string + format: uuid + sourceMeteringPointId: + type: string + format: uuid + targetMeteringPointId: + type: string + format: uuid + pricePerKwhCents: + type: number + validFrom: + type: string + format: date + validTo: + type: string + format: date + createdAt: + type: string + format: date-time UpdateMeteringPointRequest: type: object properties: @@ -696,6 +875,50 @@ components: memberCount: type: integer format: int32 + CommunityTariffRequest: + type: object + properties: + maxPricePerKwhCents: + type: number + minimum: 0.0001 + surchargePercent: + type: number + maximum: 100 + minimum: 0 + validFrom: + type: string + format: date + validTo: + type: string + format: date + required: + - maxPricePerKwhCents + - surchargePercent + CommunityTariffResponse: + type: object + properties: + id: + type: string + format: uuid + energyCommunityId: + type: string + format: uuid + maxPricePerKwhCents: + type: number + surchargePercent: + type: number + validFrom: + type: string + format: date + validTo: + type: string + format: date + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time CreateMeteringPointRequest: type: object properties: diff --git a/eeg_frontend/src/app/app.routes.ts b/eeg_frontend/src/app/app.routes.ts index 88b82fc..c05d9ce 100644 --- a/eeg_frontend/src/app/app.routes.ts +++ b/eeg_frontend/src/app/app.routes.ts @@ -16,6 +16,8 @@ import {MyMembershipsComponent} from './pages/my-memberships/my-memberships'; import {AdminMembershipsComponent} from './pages/admin-memberships/admin-memberships'; import {ForgotPasswordComponent} from './pages/forgot-password/forgot-password'; import {ResetPasswordComponent} from './pages/reset-password/reset-password'; +import {AdminTariffComponent} from './pages/admin-tariff/admin-tariff'; +import {UserTariffComponent} from './pages/user-tariff/user-tariff'; export const routes: Routes = [ { path: '', component: LandingPage }, @@ -60,6 +62,16 @@ export const routes: Routes = [ component: AdminEnergyCommunity, canActivate: [roleGuard(['ADMIN'])] }, + { + path: 'tariffs', + component: AdminTariffComponent, + canActivate: [roleGuard(['ADMIN'])] + }, + { + path: 'my-tariffs', + component: UserTariffComponent, + canActivate: [roleGuard(['MEMBER'])] + }, { path: 'profile', component: ProfileComponent, diff --git a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts index c9f1636..6a4b831 100644 --- a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts +++ b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts @@ -12,5 +12,7 @@ export const NAV_ITEMS: NavItem[] = [ {label: 'Mitgliedschaften', path: '/dashboard/admin-memberships', roles: ['ADMIN']}, {label: 'Mein Profil', path: '/dashboard/profile', roles: ['ADMIN', 'MEMBER']}, {label: 'Benutzerverwaltung', path: '/dashboard/approvals', roles: ['ADMIN']}, - {label: 'Energiegemeinschaften', path: '/dashboard/energy-communities', roles: ['ADMIN']} + {label: 'Energiegemeinschaften', path: '/dashboard/energy-communities', roles: ['ADMIN']}, + {label: 'Tarife verwalten', path: '/dashboard/tariffs', roles: ['ADMIN']}, + {label: 'Meine Tarife', path: '/dashboard/my-tariffs', roles: ['MEMBER']} ]; diff --git a/eeg_frontend/src/app/pages/admin-tariff/admin-tariff.html b/eeg_frontend/src/app/pages/admin-tariff/admin-tariff.html new file mode 100644 index 0000000..6fdec4a --- /dev/null +++ b/eeg_frontend/src/app/pages/admin-tariff/admin-tariff.html @@ -0,0 +1,108 @@ +
+
+

Tarife verwalten

+

Verwalte den Gemeinschaftstarif und Übersicht der Nutzertarife.

+
+ + +
+ + +
+ + @if (selectedCommunityId()) { + +
+

Gemeinschaftstarif

+ + @if (isLoading()) { +
Lade Tarifdaten...
+ } @else { +
+
+
+ + + @if (tariffForm.get('maxPricePerKwhCents')?.hasError('min')) { +

Der Preis muss mindestens 0.01 Cent betragen.

+ } +
+
+ + + @if (tariffForm.get('surchargePercent')?.hasError('max')) { +

Der Aufschlag darf max. 100% betragen.

+ } +
+
+
+
+ + +
+
+ + +
+
+
+ +
+
+ } +
+ + +
+

Nutzertarife in der Gemeinschaft

+ + @if (isLoadingTariffs()) { +
Lade Nutzertarife...
+ } @else { +
+ + + + + + + + + + + + @for (ut of userTariffs(); track ut.id) { + + + + + + + + } @empty { + + + + } + +
Quell-ZählpunktZiel-ZählpunktPreis (Cent/kWh)Gültig abGültig bis
{{ ut.sourceMeteringPointId }}{{ ut.targetMeteringPointId }}{{ ut.pricePerKwhCents }}{{ ut.validFrom || '-' }}{{ ut.validTo || '-' }}
+ Keine Nutzertarife vorhanden. +
+
+ } +
+ } +
diff --git a/eeg_frontend/src/app/pages/admin-tariff/admin-tariff.scss b/eeg_frontend/src/app/pages/admin-tariff/admin-tariff.scss new file mode 100644 index 0000000..e69de29 diff --git a/eeg_frontend/src/app/pages/admin-tariff/admin-tariff.spec.ts b/eeg_frontend/src/app/pages/admin-tariff/admin-tariff.spec.ts new file mode 100644 index 0000000..59a5a14 --- /dev/null +++ b/eeg_frontend/src/app/pages/admin-tariff/admin-tariff.spec.ts @@ -0,0 +1,21 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {AdminTariffComponent} from './admin-tariff'; + +describe('AdminTariffComponent', () => { + let component: AdminTariffComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminTariffComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AdminTariffComponent); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/eeg_frontend/src/app/pages/admin-tariff/admin-tariff.ts b/eeg_frontend/src/app/pages/admin-tariff/admin-tariff.ts new file mode 100644 index 0000000..18ab02d --- /dev/null +++ b/eeg_frontend/src/app/pages/admin-tariff/admin-tariff.ts @@ -0,0 +1,120 @@ +import {Component, inject, OnDestroy, OnInit, signal} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; +import {EnergyCommunityAdminControllerService, EnergyCommunityDto, TariffControllerService, UserTariffControllerService, CommunityTariffRequest, CommunityTariffResponse, UserTariffResponse} from '../../api'; +import {ToastService} from '../../services/toast'; + +@Component({ + selector: 'app-admin-tariff', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + templateUrl: './admin-tariff.html', + styleUrl: './admin-tariff.scss', +}) +export class AdminTariffComponent implements OnInit, OnDestroy { + private ecService = inject(EnergyCommunityAdminControllerService); + private tariffService = inject(TariffControllerService); + private userTariffService = inject(UserTariffControllerService); + private toastService = inject(ToastService); + private fb = inject(FormBuilder); + + communities = signal([]); + selectedCommunityId = signal(''); + communityTariff = signal(null); + userTariffs = signal([]); + isLoading = signal(false); + isLoadingTariffs = signal(false); + + tariffForm = this.fb.group({ + maxPricePerKwhCents: [0, [Validators.required, Validators.min(0.01)]], + surchargePercent: [0, [Validators.required, Validators.min(0), Validators.max(100)]], + validFrom: [''], + validTo: [''] + }); + + ngOnInit() { + this.loadCommunities(); + } + + ngOnDestroy() {} + + loadCommunities() { + this.ecService.getAllEnergyCommunities().subscribe({ + next: (data) => { + this.communities.set(data); + if (data.length > 0) { + this.onCommunityChange(data[0].id!); + } + }, + error: (err) => { + this.toastService.error(err.error?.message || 'Fehler beim Laden der Energiegemeinschaften.'); + } + }); + } + + onCommunityChange(communityId: string) { + this.selectedCommunityId.set(communityId); + this.communityTariff.set(null); + this.userTariffs.set([]); + + if (!communityId) return; + + this.isLoading.set(true); + this.tariffService.getCommunityTariff(communityId).subscribe({ + next: (tariff) => { + this.communityTariff.set(tariff); + this.tariffForm.patchValue({ + maxPricePerKwhCents: tariff.maxPricePerKwhCents ?? 0, + surchargePercent: tariff.surchargePercent ?? 0, + validFrom: tariff.validFrom ?? '', + validTo: tariff.validTo ?? '' + }); + this.isLoading.set(false); + }, + error: () => { + this.isLoading.set(false); + this.tariffForm.reset({maxPricePerKwhCents: 0, surchargePercent: 0, validFrom: '', validTo: ''}); + } + }); + + this.loadUserTariffs(communityId); + } + + loadUserTariffs(communityId: string) { + this.isLoadingTariffs.set(true); + this.userTariffService.getUserTariffsForCommunity(communityId).subscribe({ + next: (tariffs) => { + this.userTariffs.set(tariffs); + this.isLoadingTariffs.set(false); + }, + error: () => { + this.userTariffs.set([]); + this.isLoadingTariffs.set(false); + } + }); + } + + onSubmit() { + if (this.tariffForm.invalid || !this.selectedCommunityId()) { + this.tariffForm.markAllAsTouched(); + return; + } + + const request: CommunityTariffRequest = { + maxPricePerKwhCents: this.tariffForm.value.maxPricePerKwhCents!, + surchargePercent: this.tariffForm.value.surchargePercent!, + validFrom: this.tariffForm.value.validFrom || undefined, + validTo: this.tariffForm.value.validTo || undefined + }; + + this.tariffService.createOrUpdateCommunityTariff(this.selectedCommunityId(), request).subscribe({ + next: (response) => { + this.communityTariff.set(response); + this.toastService.success('Gemeinschaftstarif erfolgreich gespeichert.'); + }, + error: (err) => { + this.toastService.error(err.error?.message || 'Fehler beim Speichern des Tarifs.'); + } + }); + } +} diff --git a/eeg_frontend/src/app/pages/user-tariff/user-tariff.html b/eeg_frontend/src/app/pages/user-tariff/user-tariff.html new file mode 100644 index 0000000..7af3e66 --- /dev/null +++ b/eeg_frontend/src/app/pages/user-tariff/user-tariff.html @@ -0,0 +1,160 @@ +
+
+

Meine Tarife

+

Erstelle und verwalte Tarife für den Energieaustausch.

+
+ + @if (isLoading()) { +
+
+
Lade Daten...
+
+ } @else if (memberships().length === 0) { +
+

Keine aktiven Mitgliedschaften vorhanden.

+

Bitte tritt zuerst einer Energiegemeinschaft bei.

+
+ } @else { + +
+ + +
+ + @if (selectedCommunityId()) { + + @if (communityTariff()) { +
+ i + + Maximaler Preis: {{ communityTariff()!.maxPricePerKwhCents }} Cent/kWh + @if (communityTariff()!.surchargePercent) { + | Aufschlag: {{ communityTariff()!.surchargePercent }}% + } + +
+ } + + +
+

+ {{ editingId() ? 'Tarif bearbeiten' : 'Neuen Tarif erstellen' }} +

+
+
+
+ + +
+
+ + +
+
+
+
+ + + @if (tariffForm.get('pricePerKwhCents')?.hasError('min')) { +

Der Preis muss mindestens 0.01 Cent betragen.

+ } +
+
+ + +
+
+ + +
+
+
+ @if (editingId()) { + + } + +
+
+
+ + +
+

Meine Tarife

+ + @if (isLoadingTariffs()) { +
Lade Tarife...
+ } @else { +
+ + + + + + + + + + + + + @for (ut of userTariffs(); track ut.id) { + + + + + + + + + } @empty { + + + + } + +
Quell-ZählpunktZiel-ZählpunktPreis (Cent/kWh)Gültig abGültig bisAktionen
{{ ut.sourceMeteringPointId }}{{ ut.targetMeteringPointId }}{{ ut.pricePerKwhCents }}{{ ut.validFrom || '-' }}{{ ut.validTo || '-' }} + + +
+ Keine Tarife vorhanden. Erstelle deinen ersten Tarif. +
+
+ } +
+ } + } +
diff --git a/eeg_frontend/src/app/pages/user-tariff/user-tariff.scss b/eeg_frontend/src/app/pages/user-tariff/user-tariff.scss new file mode 100644 index 0000000..e69de29 diff --git a/eeg_frontend/src/app/pages/user-tariff/user-tariff.spec.ts b/eeg_frontend/src/app/pages/user-tariff/user-tariff.spec.ts new file mode 100644 index 0000000..30eeb3f --- /dev/null +++ b/eeg_frontend/src/app/pages/user-tariff/user-tariff.spec.ts @@ -0,0 +1,21 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {UserTariffComponent} from './user-tariff'; + +describe('UserTariffComponent', () => { + let component: UserTariffComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserTariffComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(UserTariffComponent); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/eeg_frontend/src/app/pages/user-tariff/user-tariff.ts b/eeg_frontend/src/app/pages/user-tariff/user-tariff.ts new file mode 100644 index 0000000..18ce36f --- /dev/null +++ b/eeg_frontend/src/app/pages/user-tariff/user-tariff.ts @@ -0,0 +1,200 @@ +import {Component, inject, OnDestroy, OnInit, signal} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; +import {UserTariffControllerService, TariffControllerService, UserTariffRequest, UserTariffResponse, CommunityTariffResponse} from '../../api'; +import {MeteringPointService, MeteringPoint} from '../../services/metering-point'; +import {MembershipService, MembershipResponse} from '../../services/membership'; +import {ToastService} from '../../services/toast'; + +@Component({ + selector: 'app-user-tariff', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + templateUrl: './user-tariff.html', + styleUrl: './user-tariff.scss', +}) +export class UserTariffComponent implements OnInit, OnDestroy { + private userTariffService = inject(UserTariffControllerService); + private tariffService = inject(TariffControllerService); + private meteringPointService = inject(MeteringPointService); + private membershipService = inject(MembershipService); + private toastService = inject(ToastService); + private fb = inject(FormBuilder); + + memberships = signal([]); + meteringPoints = signal([]); + userTariffs = signal([]); + communityTariff = signal(null); + selectedCommunityId = signal(''); + isLoading = signal(true); + isLoadingTariffs = signal(false); + isSubmitting = signal(false); + editingId = signal(null); + + tariffForm = this.fb.group({ + energyCommunityId: ['', Validators.required], + sourceMeteringPointId: ['', Validators.required], + targetMeteringPointId: ['', Validators.required], + pricePerKwhCents: [0, [Validators.required, Validators.min(0.01)]], + validFrom: [''], + validTo: [''] + }); + + ngOnInit() { + this.loadData(); + } + + ngOnDestroy() {} + + loadData() { + this.isLoading.set(true); + + this.membershipService.getOwnMemberships().subscribe({ + next: (memberships) => { + const active = memberships.filter(m => m.status === 'ACTIVE'); + this.memberships.set(active); + + if (active.length > 0 && !this.selectedCommunityId()) { + this.onCommunityChange(active[0].energyCommunityId); + } + + this.isLoading.set(false); + }, + error: () => { + this.isLoading.set(false); + this.toastService.error('Fehler beim Laden der Mitgliedschaften.'); + } + }); + + this.meteringPointService.getMyMeteringPoints().subscribe({ + next: (points) => { + this.meteringPoints.set(points.filter(p => p.makoState === 'ACTIVE')); + }, + error: () => {} + }); + } + + onCommunityChange(communityId: string) { + this.selectedCommunityId.set(communityId); + this.tariffForm.patchValue({energyCommunityId: communityId}); + this.userTariffs.set([]); + this.communityTariff.set(null); + this.editingId.set(null); + + if (!communityId) return; + + this.tariffService.getCommunityTariff(communityId).subscribe({ + next: (tariff) => this.communityTariff.set(tariff), + error: () => {} + }); + + this.loadUserTariffs(communityId); + } + + loadUserTariffs(communityId: string) { + this.isLoadingTariffs.set(true); + this.userTariffService.getUserTariffsForCommunity(communityId).subscribe({ + next: (tariffs) => { + this.userTariffs.set(tariffs); + this.isLoadingTariffs.set(false); + }, + error: () => { + this.userTariffs.set([]); + this.isLoadingTariffs.set(false); + } + }); + } + + get myMeteringPoints(): MeteringPoint[] { + return this.meteringPoints(); + } + + get allCommunityMeteringPoints(): MeteringPoint[] { + return this.meteringPoints(); + } + + onSubmit() { + if (this.tariffForm.invalid || !this.selectedCommunityId()) { + this.tariffForm.markAllAsTouched(); + return; + } + + const maxPrice = this.communityTariff()?.maxPricePerKwhCents ?? Infinity; + const price = this.tariffForm.value.pricePerKwhCents!; + if (price > maxPrice) { + this.toastService.error(`Der Preis darf den Maximalpreis von ${maxPrice} Cent/kWh nicht überschreiten.`); + return; + } + + const request: UserTariffRequest = { + energyCommunityId: this.tariffForm.value.energyCommunityId!, + sourceMeteringPointId: this.tariffForm.value.sourceMeteringPointId!, + targetMeteringPointId: this.tariffForm.value.targetMeteringPointId!, + pricePerKwhCents: this.tariffForm.value.pricePerKwhCents!, + validFrom: this.tariffForm.value.validFrom || undefined, + validTo: this.tariffForm.value.validTo || undefined + }; + + this.isSubmitting.set(true); + + const editId = this.editingId(); + const obs = editId + ? this.userTariffService.updateUserTariff(editId, request) + : this.userTariffService.createUserTariff(request); + + obs.subscribe({ + next: () => { + this.toastService.success(editId ? 'Tarif aktualisiert.' : 'Tarif erfolgreich erstellt.'); + this.resetForm(); + this.loadUserTariffs(this.selectedCommunityId()); + this.isSubmitting.set(false); + }, + error: (err) => { + this.isSubmitting.set(false); + this.toastService.error(err.error?.message || 'Fehler beim Speichern des Tarifs.'); + } + }); + } + + startEdit(tariff: UserTariffResponse) { + this.editingId.set(tariff.id!); + this.tariffForm.patchValue({ + energyCommunityId: tariff.energyCommunityId!, + sourceMeteringPointId: tariff.sourceMeteringPointId!, + targetMeteringPointId: tariff.targetMeteringPointId!, + pricePerKwhCents: tariff.pricePerKwhCents!, + validFrom: tariff.validFrom ?? '', + validTo: tariff.validTo ?? '' + }); + } + + cancelEdit() { + this.resetForm(); + } + + resetForm() { + this.editingId.set(null); + this.tariffForm.reset({ + energyCommunityId: this.selectedCommunityId(), + sourceMeteringPointId: '', + targetMeteringPointId: '', + pricePerKwhCents: 0, + validFrom: '', + validTo: '' + }); + } + + onDelete(tariff: UserTariffResponse) { + if (!confirm('Möchtest du diesen Tarif wirklich löschen?')) return; + + this.userTariffService.deleteUserTariff(tariff.id!, tariff.sourceMeteringPointId!).subscribe({ + next: () => { + this.toastService.success('Tarif erfolgreich gelöscht.'); + this.loadUserTariffs(this.selectedCommunityId()); + }, + error: (err) => { + this.toastService.error(err.error?.message || 'Fehler beim Löschen des Tarifs.'); + } + }); + } +}