Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@

package org.greenbuttonalliance.espi.common.dto.usage;

import org.greenbuttonalliance.espi.common.dto.atom.LinkDto;

import jakarta.xml.bind.annotation.*;
import java.time.OffsetDateTime;
import java.util.List;

/**
* MeterReading DTO record for JAXB XML marshalling/unmarshalling.
*
* Represents a meter reading containing interval blocks and reading types.
* Supports Atom protocol XML wrapping.
*
* Represents a meter reading - a set of values obtained from the meter.
* Per ESPI 4.0 specification, MeterReading extends IdentifiedObject but
* contains NO child elements. Relationships to ReadingType and IntervalBlock
* are expressed via Atom links, not embedded XML elements.
*
* Complies with espi.xsd MeterReading complexType definition.
*
* @see <a href="https://www.naesb.org/ESPI_Standards.asp">NAESB ESPI 4.0</a>
*/
@XmlRootElement(name = "MeterReading", namespace = "http://naesb.org/espi")
@XmlAccessorType(XmlAccessType.FIELD)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,20 @@
import org.greenbuttonalliance.espi.common.domain.usage.MeterReadingEntity;
import org.greenbuttonalliance.espi.common.dto.usage.MeterReadingDto;
import org.greenbuttonalliance.espi.common.mapper.BaseIdentifiedObjectMapper;
import org.greenbuttonalliance.espi.common.mapper.BaseMapperUtils;
import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;

/**
* MapStruct mapper for converting between MeterReadingEntity and MeterReadingDto.
*
* Handles the conversion between the JPA entity used for persistence and the DTO
*
* Per ESPI 4.0 specification, MeterReading has NO child elements - only relationships
* expressed via Atom links. The DTO contains NO fields beyond id/uuid.
*
* Handles the conversion between the JPA entity used for persistence and the DTO
* used for JAXB XML marshalling in the Green Button API.
*/
@Mapper(componentModel = "spring", uses = {
DateTimeMapper.class,
BaseMapperUtils.class,
IntervalBlockMapper.class,
ReadingTypeMapper.class
})
@Mapper(componentModel = "spring", uses = {DateTimeMapper.class})
public interface MeterReadingMapper {

/**
Expand All @@ -55,33 +51,21 @@ public interface MeterReadingMapper {

/**
* Converts a MeterReadingDto to a MeterReadingEntity.
* Maps all related DTOs to their corresponding entities.
*
* Since MeterReading has no child elements, only audit and relationship fields are managed.
*
* @param dto the meter reading DTO
* @return the meter reading entity
*/
@Mapping(target = "id", source = "uuid", qualifiedByName = "stringToUuid")
@Mapping(target = "usagePoint", ignore = true)
@Mapping(target = "readingType", ignore = true)
@Mapping(target = "intervalBlocks", ignore = true)
@Mapping(target = "relatedLinks", ignore = true)
@Mapping(target = "selfLink", ignore = true)
@Mapping(target = "upLink", ignore = true)
@Mapping(target = "created", ignore = true) // Audit field managed by persistence
@Mapping(target = "updated", ignore = true) // Audit field managed by persistence
@Mapping(target = "published", ignore = true) // Audit field managed by persistence
@Mapping(target = "description", ignore = true) // Managed separately
@Mapping(target = "selfLink", ignore = true) // Link managed separately
@Mapping(target = "upLink", ignore = true) // Link managed separately
@Mapping(target = "relatedLinks", ignore = true) // Links managed separately
@Mapping(target = "usagePoint", ignore = true) // Relationship managed separately
@Mapping(target = "readingType", ignore = true) // Relationship managed separately
@Mapping(target = "intervalBlocks", ignore = true) // Relationship managed separately
MeterReadingEntity toEntity(MeterReadingDto dto);

/**
* Updates an existing MeterReadingEntity with data from a MeterReadingDto.
* Useful for merge operations where the entity ID should be preserved.
*
* @param dto the source DTO
* @param entity the target entity to update
*/
@Mapping(target = "id", ignore = true)
@Mapping(target = "usagePoint", ignore = true)
@Mapping(target = "readingType", ignore = true)
@Mapping(target = "intervalBlocks", ignore = true)
@Mapping(target = "relatedLinks", ignore = true)
@Mapping(target = "selfLink", ignore = true)
@Mapping(target = "upLink", ignore = true)
void updateEntity(MeterReadingDto dto, @MappingTarget MeterReadingEntity entity);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,50 +21,20 @@

import org.greenbuttonalliance.espi.common.domain.usage.MeterReadingEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Repository
public interface MeterReadingRepository extends JpaRepository<MeterReadingEntity, UUID> {

// JpaRepository provides: save(), findById(), findAll(), deleteById(), etc.

@Modifying
@Transactional
@Query("DELETE FROM MeterReadingEntity m WHERE m.id = :id")
void deleteById(@Param("id") UUID id);

@Query("SELECT m.id FROM MeterReadingEntity m")
List<UUID> findAllIds();

// findById is already provided by JpaRepository<MeterReadingEntity, UUID>
// Optional<MeterReadingEntity> findById(UUID id) is inherited

// deleteById is already provided by JpaRepository<MeterReadingEntity, UUID>
// void deleteById(UUID id) is inherited

// Custom method for createOrReplaceByUUID - should be implemented in service layer

@Query("SELECT m FROM MeterReadingEntity m join m.relatedLinks link WHERE link.href = :href")
List<MeterReadingEntity> findByRelatedHref(@Param("href") String href);

@Query("SELECT readingType FROM ReadingTypeEntity readingType WHERE readingType.selfLink.href in (:relatedLinkHrefs)")
List<Object> findAllRelated(@Param("relatedLinkHrefs") List<String> relatedLinkHrefs);

@Query("SELECT m.id FROM MeterReadingEntity m WHERE m.usagePoint.id = :usagePointId")
List<UUID> findAllIdsByUsagePointId(@Param("usagePointId") UUID usagePointId);

@Query("SELECT DISTINCT m.id FROM UsagePointEntity u, MeterReadingEntity m WHERE u.retailCustomer.id = :o1Id AND m.usagePoint.id = :o2Id")
List<UUID> findAllIdsByXpath2(@Param("o1Id") UUID o1Id, @Param("o2Id") UUID o2Id);

@Query("SELECT DISTINCT m.id FROM UsagePointEntity u, MeterReadingEntity m WHERE u.retailCustomer.id = :o1Id AND m.usagePoint.id = :o2Id AND m.id = :o3Id")
Optional<UUID> findIdByXpath(@Param("o1Id") UUID o1Id, @Param("o2Id") UUID o2Id, @Param("o3Id") UUID o3Id);

}
Original file line number Diff line number Diff line change
Expand Up @@ -267,41 +267,6 @@ void shouldFindAllMeterReadingIds() {
);
}

@Test
@DisplayName("Should find meter readings by related href")
void shouldFindMeterReadingsByRelatedHref() {
// Arrange - Create a simple meter reading without related links for now
MeterReadingEntity meterReading = TestDataBuilders.createValidMeterReading();
meterReading.setDescription("Meter Reading for Related Href Test");
meterReadingRepository.save(meterReading);
flushAndClear();

// Act - Test the query method with a non-existent href (should return empty)
List<MeterReadingEntity> results = meterReadingRepository.findByRelatedHref("/espi/1_1/resource/IntervalBlock/123");

// Assert - Should return empty list since no meter reading has this related href
assertThat(results).isEmpty();
}

@Test
@DisplayName("Should find all related entities")
void shouldFindAllRelatedEntities() {
// Arrange
List<String> relatedLinkHrefs = List.of(
"/espi/1_1/resource/ReadingType/1",
"/espi/1_1/resource/ReadingType/2",
"/espi/1_1/resource/ReadingType/3"
);

// Act
List<Object> results = meterReadingRepository.findAllRelated(relatedLinkHrefs);

// Assert
assertThat(results).isNotNull();
// Note: This query looks for ReadingType entities with matching self links
// The actual results depend on existing data in the test database
}

@Test
@DisplayName("Should find all meter reading IDs by usage point ID")
void shouldFindAllMeterReadingIdsByUsagePointId() {
Expand All @@ -326,73 +291,11 @@ void shouldFindAllMeterReadingIdsByUsagePointId() {
assertThat(meterReadingIds).contains(savedMeterReadings.get(0).getId(), savedMeterReadings.get(1).getId());
}

@Test
@DisplayName("Should find all meter reading IDs by xpath2")
void shouldFindAllMeterReadingIdsByXpath2() {
// Arrange
RetailCustomerEntity retailCustomer = TestDataBuilders.createValidRetailCustomer();
retailCustomer.setUsername("customer@xpath2.com");
RetailCustomerEntity savedCustomer = retailCustomerRepository.save(retailCustomer);

UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint();
usagePoint.setDescription("Usage Point for Xpath2");
usagePoint.setRetailCustomer(savedCustomer);
UsagePointEntity savedUsagePoint = usagePointRepository.save(usagePoint);

MeterReadingEntity meterReading1 = TestDataBuilders.createValidMeterReadingWithUsagePoint(savedUsagePoint);
meterReading1.setDescription("Meter Reading 1 for Xpath2");
MeterReadingEntity meterReading2 = TestDataBuilders.createValidMeterReadingWithUsagePoint(savedUsagePoint);
meterReading2.setDescription("Meter Reading 2 for Xpath2");

List<MeterReadingEntity> savedMeterReadings = meterReadingRepository.saveAll(List.of(meterReading1, meterReading2));
flushAndClear();

// Act
List<UUID> meterReadingIds = meterReadingRepository.findAllIdsByXpath2(savedCustomer.getId(), savedUsagePoint.getId());

// Assert
assertThat(meterReadingIds).hasSize(2);
assertThat(meterReadingIds).contains(savedMeterReadings.get(0).getId(), savedMeterReadings.get(1).getId());
}

@Test
@DisplayName("Should find meter reading ID by xpath")
void shouldFindMeterReadingIdByXpath() {
// Arrange
RetailCustomerEntity retailCustomer = TestDataBuilders.createValidRetailCustomer();
retailCustomer.setUsername("customer@xpath.com");
RetailCustomerEntity savedCustomer = retailCustomerRepository.save(retailCustomer);

UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint();
usagePoint.setDescription("Usage Point for Xpath");
usagePoint.setRetailCustomer(savedCustomer);
UsagePointEntity savedUsagePoint = usagePointRepository.save(usagePoint);

MeterReadingEntity meterReading = TestDataBuilders.createValidMeterReadingWithUsagePoint(savedUsagePoint);
meterReading.setDescription("Meter Reading for Xpath");
MeterReadingEntity savedMeterReading = meterReadingRepository.save(meterReading);
flushAndClear();

// Act
Optional<UUID> result = meterReadingRepository.findIdByXpath(
savedCustomer.getId(),
savedUsagePoint.getId(),
savedMeterReading.getId()
);

// Assert
assertThat(result).isPresent();
assertThat(result.get()).isEqualTo(savedMeterReading.getId());
}

@Test
@DisplayName("Should handle empty results gracefully")
void shouldHandleEmptyResultsGracefully() {
// Act & Assert
assertThat(meterReadingRepository.findByRelatedHref("nonexistent-href")).isEmpty();
assertThat(meterReadingRepository.findAllIdsByUsagePointId(UUID.randomUUID())).isEmpty();
assertThat(meterReadingRepository.findAllIdsByXpath2(UUID.randomUUID(), UUID.randomUUID())).isEmpty();
assertThat(meterReadingRepository.findIdByXpath(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())).isEmpty();
}
}

Expand Down
Loading