Implement complete ESPI 4.0 customer.xsd schema compliance for Meter following Phase 25 (EndDevice) patterns. Meter extends EndDevice and adds meter-specific fields.
Branch: feature/schema-compliance-phase-26-meter
Issue: #28 Phase 26
Scope:
- Phase 26 (Meter): Complete implementation with unit and integration tests
- Remove non-ID queries from MeterRepository and MeterService
- Rewrite MeterDto for strict XSD compliance
- Create MeterMapper for entity-to-DTO conversion
Existing:
- ✅ MeterEntity.java (extends EndDeviceEntity, has 3 Meter fields)
- ✅ MeterRepository.java (HAS NON-ID QUERIES - NEEDS CLEANUP)
- ✅ MeterService.java (HAS NON-ID QUERIES - NEEDS CLEANUP)
- ✅ MeterDto.java (has Atom fields - NEEDS REWRITE)
Missing:
- ❌ MeterMapper.java
- ❌ MeterServiceImpl.java (implementation may exist but needs UUID v5 pattern)
- ❌ MeterDtoTest.java
- ❌ MeterRepositoryTest.java
- ❌ MeterMySQLIntegrationTest.java
- ❌ MeterPostgreSQLIntegrationTest.java
Has 11 non-ID custom query methods (MUST REMOVE ALL):
- ❌ findBySerialNumber()
- ❌ findByFormNumber()
- ❌ findByUtcNumber()
- ❌ findVirtualMeters()
- ❌ findPhysicalMeters()
- ❌ findPanDevices()
- ❌ findByAmrSystem()
- ❌ findByInstallCode()
- ❌ findByIntervalLengthGreaterThan()
- ❌ findCriticalMeters()
- ❌ All other non-ID queries
Target: ONLY inherited JpaRepository methods (findById, findAll, save, delete)
Has 18 non-ID service methods (MUST REMOVE ALL):
- ❌ findByUuid()
- ❌ findBySerialNumber()
- ❌ findByFormNumber()
- ❌ findByUtcNumber()
- ❌ findVirtualMeters()
- ❌ findPhysicalMeters()
- ❌ findPanDevices()
- ❌ findByAmrSystem()
- ❌ findByInstallCode()
- ❌ findByIntervalLengthGreaterThan()
- ❌ findCriticalMeters()
- ❌ existsBySerialNumber()
- ❌ countMeters()
- ❌ countVirtualMeters()
- ❌ countPhysicalMeters()
- ❌ All other non-ID methods
Target: ONLY 6 CRUD methods (findAll, findById, save, deleteById, existsById, count)
Current (WRONG):
- Has Atom fields: id, uuid, published, updated, selfLink, upLink, relatedLinks
- Has description field (goes to AtomEntryDto.title)
- Has serviceLocation embedded DTO (use Atom link)
- Has wrong fields: installDate, removedDate, kh, meterMultiplier (not in XSD)
- Missing ALL 12 Asset fields
- Missing ALL 4 EndDevice fields
Target (CORRECT):
- ONLY 19 XSD fields: 12 Asset + 4 EndDevice + 3 Meter
- NO Atom fields
- NO embedded relationships
Meter extends EndDevice (customer.xsd lines 243-268):
Object → IdentifiedObject → Asset → EndDevice → Meter
<xs:complexType name="Meter">
<xs:extension base="EndDevice">
<xs:sequence>
<xs:element name="formNumber" type="String256" minOccurs="0"/>
<xs:element name="MeterMultipliers" type="MeterMultiplier" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="intervalLength" type="UInt32" minOccurs="0"/>
</xs:sequence>
</xs:extension>
</xs:complexType>- Asset fields: 12 (aliasName, initialCondition, initialLossOfLife, lifecycleDate, serialNumber, type, utcNumber, lotNumber, electronicAddress, acceptanceTest, status, category)
- EndDevice fields: 4 (amrSystem, installCode, isPan, timeZoneOffset)
- Meter fields: 3 (formNumber, meterMultipliers, intervalLength)
- Total: 19 fields
File: openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/MeterRepository.java
REMOVE ALL custom queries (lines 40-99):
// DELETE THESE METHODS:
Optional<MeterEntity> findBySerialNumber(String serialNumber);
List<MeterEntity> findByFormNumber(String formNumber);
List<MeterEntity> findByUtcNumber(String utcNumber);
List<MeterEntity> findVirtualMeters();
List<MeterEntity> findPhysicalMeters();
List<MeterEntity> findPanDevices();
List<MeterEntity> findByAmrSystem(String amrSystem);
List<MeterEntity> findByInstallCode(String installCode);
List<MeterEntity> findByIntervalLengthGreaterThan(Long seconds);
List<MeterEntity> findCriticalMeters();KEEP ONLY:
@Repository
public interface MeterRepository extends JpaRepository<MeterEntity, UUID> {
// ONLY inherited methods - NO custom queries
}File: openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/MeterService.java
REMOVE ALL non-CRUD methods (lines 48-130):
// DELETE THESE METHODS:
Optional<MeterEntity> findByUuid(String uuid);
Optional<MeterEntity> findBySerialNumber(String serialNumber);
List<MeterEntity> findByFormNumber(String formNumber);
List<MeterEntity> findByUtcNumber(String utcNumber);
List<MeterEntity> findVirtualMeters();
List<MeterEntity> findPhysicalMeters();
List<MeterEntity> findPanDevices();
List<MeterEntity> findByAmrSystem(String amrSystem);
List<MeterEntity> findByInstallCode(String installCode);
List<MeterEntity> findByIntervalLengthGreaterThan(Long seconds);
List<MeterEntity> findCriticalMeters();
boolean existsBySerialNumber(String serialNumber);
long countMeters();
long countVirtualMeters();
long countPhysicalMeters();KEEP ONLY 6 CRUD methods:
public interface MeterService {
List<MeterEntity> findAll();
Optional<MeterEntity> findById(UUID id);
MeterEntity save(MeterEntity meter);
void deleteById(UUID id);
boolean existsById(UUID id);
long count();
}File: openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/MeterEntity.java
Verify Structure:
- ✅ Extends EndDeviceEntity
- ✅ Has formNumber field
- ✅ Has intervalLength field
⚠️ Has meterMultipliers TODO comment (line 57-58)
Action: Keep as-is for now. MeterMultiplier collection can be added in future phase.
Update equals/hashCode: Verify uses pattern matching for HibernateProxy
File: openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/MeterDto.java
REMOVE:
- ❌ id, uuid, published, updated, relatedLinks, selfLink, upLink (Atom fields)
- ❌ description (goes to AtomEntryDto.title)
- ❌ serviceLocation (use Atom link)
- ❌ installDate, removedDate (not in XSD)
- ❌ kh, meterMultiplier (not in XSD - wrong names, XSD has MeterMultipliers collection)
- ❌ getSelfHref(), getUpHref() methods
ADD:
- ✅ All 12 Asset fields (from EndDeviceDto)
- ✅ All 4 EndDevice fields (from EndDeviceDto)
- ✅ All 3 Meter fields (formNumber, meterMultipliers, intervalLength)
propOrder:
@XmlType(name = "Meter", namespace = "http://naesb.org/espi/customer", propOrder = {
// Asset fields (12) - inherited from EndDevice
"aliasName", "initialCondition", "initialLossOfLife", "lifecycleDate",
"serialNumber", "type", "utcNumber", "lotNumber",
"electronicAddress", "acceptanceTest", "status", "category",
// EndDevice fields (4) - inherited from EndDevice
"amrSystem", "installCode", "isPan", "timeZoneOffset",
// Meter fields (3)
"formNumber", "meterMultipliers", "intervalLength"
})Structure:
@XmlRootElement(name = "Meter", namespace = "http://naesb.org/espi/customer")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Meter", namespace = "http://naesb.org/espi/customer", propOrder = {...})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MeterDto {
// Asset fields (12) - from customer.xsd Asset type
@XmlElement(name = "aliasName", namespace = "http://naesb.org/espi/customer")
private String aliasName;
@XmlElement(name = "initialCondition", namespace = "http://naesb.org/espi/customer")
private String initialCondition;
@XmlElement(name = "initialLossOfLife", namespace = "http://naesb.org/espi/customer")
private String initialLossOfLife;
@XmlElement(name = "lifecycleDate", namespace = "http://naesb.org/espi/customer")
private LifecycleDateDto lifecycleDate;
@XmlElement(name = "serialNumber", namespace = "http://naesb.org/espi/customer")
private String serialNumber;
@XmlElement(name = "type", namespace = "http://naesb.org/espi/customer")
private String type;
@XmlElement(name = "utcNumber", namespace = "http://naesb.org/espi/customer")
private String utcNumber;
@XmlElement(name = "lotNumber", namespace = "http://naesb.org/espi/customer")
private String lotNumber;
@XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer")
private List<ElectronicAddressDto> electronicAddress;
@XmlElement(name = "acceptanceTest", namespace = "http://naesb.org/espi/customer")
private AcceptanceTestDto acceptanceTest;
@XmlElement(name = "status", namespace = "http://naesb.org/espi/customer")
private StatusDto status;
@XmlElement(name = "category", namespace = "http://naesb.org/espi/customer")
private String category;
// EndDevice fields (4) - from customer.xsd EndDevice type
@XmlElement(name = "amrSystem", namespace = "http://naesb.org/espi/customer")
private String amrSystem;
@XmlElement(name = "installCode", namespace = "http://naesb.org/espi/customer")
private String installCode;
@XmlElement(name = "isPan", namespace = "http://naesb.org/espi/customer")
private Boolean isPan;
@XmlElement(name = "timeZoneOffset", namespace = "http://naesb.org/espi/customer")
private Long timeZoneOffset;
// Meter fields (3) - from customer.xsd Meter type
@XmlElement(name = "formNumber", namespace = "http://naesb.org/espi/customer")
private String formNumber;
@XmlElement(name = "MeterMultipliers", namespace = "http://naesb.org/espi/customer")
private List<MeterMultiplierDto> meterMultipliers;
@XmlElement(name = "intervalLength", namespace = "http://naesb.org/espi/customer")
private Long intervalLength;
}Note: MeterMultiplierDto is a simple embedded object with 2 fields:
public class MeterMultiplierDto {
private String kind; // MeterMultiplierKind enum as String
private Float value;
}File: openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/MeterMapper.java
@Mapper(componentModel = "spring", uses = {
LifecycleDateMapper.class,
AcceptanceTestMapper.class,
ElectronicAddressMapper.class,
StatusMapper.class
})
public interface MeterMapper {
@Mapping(target = "id", ignore = true)
MeterEntity toEntity(MeterDto dto);
MeterDto toDto(MeterEntity entity);
// MeterMultiplier mappings (if implementing collection)
MeterMultiplierDto toDto(MeterMultiplier entity);
MeterMultiplier toEntity(MeterMultiplierDto dto);
}File: openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/MeterServiceImpl.java
Pattern: Follow EndDeviceServiceImpl pattern with UUID v5 generation
@Service
@Slf4j
@RequiredArgsConstructor
public class MeterServiceImpl implements MeterService {
private static final String NAMESPACE = "ESPI-METER";
private final MeterRepository repository;
private final EspiIdGeneratorService idGenerator;
@Override
@Transactional
public MeterEntity save(MeterEntity meter) {
if (meter.getId() == null) {
// ❌ NO random UUID fallback - ESPI requires UUID v5
if (meter.getSerialNumber() == null) {
throw new IllegalArgumentException(
"SerialNumber is required for Meter UUID generation");
}
UUID deterministicId = idGenerator.generateV5UUID(
NAMESPACE, meter.getSerialNumber());
meter.setId(deterministicId);
log.debug("Generated UUID v5 for Meter: {}", deterministicId);
}
return repository.save(meter);
}
@Override
@Transactional(readOnly = true)
public List<MeterEntity> findAll() {
return repository.findAll();
}
@Override
@Transactional(readOnly = true)
public Optional<MeterEntity> findById(UUID id) {
return repository.findById(id);
}
@Override
@Transactional
public void deleteById(UUID id) {
repository.deleteById(id);
}
@Override
@Transactional(readOnly = true)
public boolean existsById(UUID id) {
return repository.existsById(id);
}
@Override
@Transactional(readOnly = true)
public long count() {
return repository.count();
}
}CRITICAL: NO random UUID fallback - ESPI standard requires UUID v5
File: openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java
Add to JAXBContext initialization (line ~264):
org.greenbuttonalliance.espi.common.dto.customer.MeterDto.class,
org.greenbuttonalliance.espi.common.dto.customer.MeterMultiplierDto.class,Verify meters table in V3__Create_additiional_Base_Tables.sql:
- ✅ Inherits all EndDevice columns (12 Asset + 4 EndDevice = 16)
- ✅ Has form_number column
- ✅ Has interval_length column
⚠️ MeterMultipliers collection table (create if needed)
MeterDtoTest.java (6+ tests):
@DisplayName("MeterDto XML Marshalling Tests")
class MeterDtoTest {
// shouldExportMeterWithRealisticData
// shouldVerifyMeterFieldOrder (19 fields: 12 Asset + 4 EndDevice + 3 Meter)
// shouldVerifyMeterInheritsFromEndDevice
// shouldVerifyMeterMultipliersCollection
// shouldExportMeterWithMinimalData
// shouldUseCorrectCustomerNamespace
}MeterRepositoryTest.java (21+ tests):
@DisplayName("Meter Repository Tests")
class MeterRepositoryTest {
@Nested
@DisplayName("CRUD Operations")
class CrudOperationsTest {
// 5 tests: save, retrieve, update, delete, findAll
}
@Nested
@DisplayName("Asset Field Persistence")
class AssetFieldPersistenceTest {
// 5 tests: inherited Asset fields
}
@Nested
@DisplayName("EndDevice Field Persistence")
class EndDeviceFieldPersistenceTest {
// 3 tests: inherited EndDevice fields
}
@Nested
@DisplayName("Meter Field Persistence")
class MeterFieldPersistenceTest {
// 3 tests: formNumber, intervalLength, meterMultipliers
}
@Nested
@DisplayName("Entity Validation")
class EntityValidationTest {
// 2 tests: serialNumber required for UUID generation
}
@Nested
@DisplayName("Base Class Functionality")
class BaseClassFunctionalityTest {
// 3 tests: extends EndDevice correctly
}
}Files to Create:
MeterMySQLIntegrationTest.javaMeterPostgreSQLIntegrationTest.java
Pattern: Follow EndDeviceMySQLIntegrationTest pattern
Location: openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/
MeterMySQLIntegrationTest.java:
@DisplayName("Meter Integration Tests - MySQL")
@ActiveProfiles({"test", "test-mysql"})
class MeterMySQLIntegrationTest extends BaseTestContainersTest {
@Container
private static final MySQLContainer<?> mysql = mysqlContainer;
@Autowired
private MeterRepository meterRepository;
@Nested
@DisplayName("CRUD Operations")
class CrudOperationsTest {
// 5+ tests: save, retrieve, update, delete, findAll
}
@Nested
@DisplayName("Asset Field Persistence")
class AssetFieldPersistenceTest {
// 3+ tests: LifecycleDate, AcceptanceTest, ElectronicAddress, Status
}
@Nested
@DisplayName("EndDevice Field Persistence")
class EndDeviceFieldPersistenceTest {
// 2+ tests: amrSystem, installCode, isPan, timeZoneOffset
}
@Nested
@DisplayName("Meter Field Persistence")
class MeterFieldPersistenceTest {
// 3+ tests: formNumber, intervalLength, meterMultipliers collection
}
}MeterPostgreSQLIntegrationTest.java: Same structure with PostgreSQL container
Expected: 13+ tests per database (26+ total integration tests for Meter)
File: openespi-common/src/test/java/org/greenbuttonalliance/espi/common/test/TestDataBuilders.java
Add helper method:
public static MeterEntity createValidMeter() {
MeterEntity meter = new MeterEntity();
meter.setSerialNumber("METER-" + faker.number().digits(10));
meter.setFormNumber("12S");
meter.setIntervalLength(900L); // 15 minutes in seconds
// Asset fields (inherited from EndDevice)
meter.setType("Smart Meter");
meter.setUtcNumber("UTC-" + faker.number().digits(6));
meter.setLotNumber("LOT-" + faker.number().digits(8));
// EndDevice fields
meter.setAmrSystem("AMR-" + faker.company().name());
meter.setInstallCode("INSTALL-" + faker.number().digits(8));
meter.setIsPan(false);
return meter;
}cd openespi-common
# Run unit tests
mvn test
# Run integration tests
mvn verify -DskipUnitTestsExpected Results:
- Unit tests: 680+ pass (654 existing + 26+ new Meter)
- Integration tests: 125+ pass (99 existing + 26+ new Meter)
- Total: 805+ tests pass
Follow Phase 25 git workflow:
- Create feature branch
- Commit changes with comprehensive message
- Push to remote
- Create PR with detailed description
- Update Issue #28 (do NOT close)
| File | Type | Description |
|---|---|---|
| MeterRepository.java | MODIFY | Remove ALL 11 non-ID query methods |
| MeterService.java | MODIFY | Remove ALL 18 non-ID methods, keep 6 CRUD |
| MeterServiceImpl.java | REWRITE | UUID v5 pattern, 6 CRUD methods only |
| MeterEntity.java | VERIFY | Confirm correct structure (extends EndDevice) |
| MeterDto.java | REWRITE | Remove Atom, add 19 XSD fields |
| MeterMapper.java | CREATE | MapStruct mapper with nested DTOs |
| MeterMultiplierDto.java | CREATE | Embedded object (2 fields) |
| DtoExportServiceImpl.java | MODIFY | Add MeterDto to JAXBContext |
| MeterDtoTest.java | CREATE | 6+ unit tests |
| MeterRepositoryTest.java | CREATE | 21+ unit tests |
| MeterMySQLIntegrationTest.java | CREATE | 13+ integration tests |
| MeterPostgreSQLIntegrationTest.java | CREATE | 13+ integration tests |
| TestDataBuilders.java | MODIFY | Add createValidMeter() |
Total: 13 files (6 modified, 7 created)
Phase 26: Meter
- ✅ Repository: NO non-ID queries (remove all 11)
- ✅ Service: ONLY 6 CRUD methods (remove 18 non-ID methods)
- ✅ DTO: NO Atom fields
- ✅ DTO: All 19 XSD fields (12 Asset + 4 EndDevice + 3 Meter)
- ✅ Mapper: Uses LifecycleDateMapper, AcceptanceTestMapper, etc.
- ✅ UUID v5: NO random fallback
- ✅ Extends: EndDevice correctly
Testing
- ✅ Unit Tests: 680+ pass (654 existing + 26+ new)
- ✅ Integration Tests: 125+ pass (99 existing + 26+ new)
- ✅ Total Tests: 805+ pass
Quality
- ✅ SonarQube: Zero violations
- ✅ CI/CD: All checks pass
-
UUID v5 Generation:
- Use serialNumber as seed
- ❌ NO random UUID fallback
- Throw exception if serialNumber is null
-
Repository Cleanup:
- Remove ALL 11 custom query methods
- Keep ONLY inherited JpaRepository methods
-
Service Cleanup:
- Remove ALL 18 non-CRUD methods
- Keep ONLY 6 CRUD methods (findAll, findById, save, deleteById, existsById, count)
-
Meter Extends EndDevice:
- Inherits 12 Asset fields
- Inherits 4 EndDevice fields
- Adds 3 Meter fields
- Total: 19 fields
-
MeterMultipliers Collection:
- Optional: Can defer to future phase if needed
- If implementing: Create MeterMultiplierDto (2 fields: kind, value)
- XSD allows 0 to unbounded multipliers
-
Field Name Mapping:
- XSD:
MeterMultipliers(capital M, plural) - Entity:
meterMultipliers(camelCase) - DTO:
meterMultipliers(camelCase, maps to XML MeterMultipliers)
- XSD:
Version: 1.0 Created: 2026-01-29 Status: ✅ Ready for Implementation
Change Log:
- v1.0: Initial Meter implementation plan based on Phase 25 EndDevice template