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
+
+### Task 7: Register MeterDto in DtoExportServiceImpl
+
+**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java`
+
+Add to JAXBContext initialization (line ~264):
+```java
+org.greenbuttonalliance.espi.common.dto.customer.MeterDto.class,
+org.greenbuttonalliance.espi.common.dto.customer.MeterMultiplierDto.class,
+```
+
+### Task 8: Verify Flyway Migration
+
+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)
+
+### Task 9: Create Unit Tests
+
+**MeterDtoTest.java** (6+ tests):
+```java
+@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):
+```java
+@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
+ }
+}
+```
+
+### Task 10: Create Integration Tests
+
+**Files to Create**:
+- `MeterMySQLIntegrationTest.java`
+- `MeterPostgreSQLIntegrationTest.java`
+
+**Pattern**: Follow EndDeviceMySQLIntegrationTest pattern
+
+**Location**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/`
+
+**MeterMySQLIntegrationTest.java**:
+```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)
+
+### Task 11: Update TestDataBuilders
+
+**File**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/test/TestDataBuilders.java`
+
+Add helper method:
+```java
+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;
+}
+```
+
+### Task 12: Run All Tests
+
+```bash
+cd openespi-common
+
+# Run unit tests
+mvn test
+
+# Run integration tests
+mvn verify -DskipUnitTests
+```
+
+**Expected Results**:
+- Unit tests: 680+ pass (654 existing + 26+ new Meter)
+- Integration tests: 125+ pass (99 existing + 26+ new Meter)
+- **Total**: 805+ tests pass
+
+### Task 13: Commit, Push, PR
+
+Follow Phase 25 git workflow:
+1. Create feature branch
+2. Commit changes with comprehensive message
+3. Push to remote
+4. Create PR with detailed description
+5. Update Issue #28 (do NOT close)
+
+## Expected File Changes
+
+| 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)
+
+## Success Criteria
+
+**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
+
+## Critical Notes
+
+1. **UUID v5 Generation**:
+ - Use serialNumber as seed
+ - ❌ NO random UUID fallback
+ - Throw exception if serialNumber is null
+
+2. **Repository Cleanup**:
+ - Remove ALL 11 custom query methods
+ - Keep ONLY inherited JpaRepository methods
+
+3. **Service Cleanup**:
+ - Remove ALL 18 non-CRUD methods
+ - Keep ONLY 6 CRUD methods (findAll, findById, save, deleteById, existsById, count)
+
+4. **Meter Extends EndDevice**:
+ - Inherits 12 Asset fields
+ - Inherits 4 EndDevice fields
+ - Adds 3 Meter fields
+ - Total: 19 fields
+
+5. **MeterMultipliers Collection**:
+ - Optional: Can defer to future phase if needed
+ - If implementing: Create MeterMultiplierDto (2 fields: kind, value)
+ - XSD allows 0 to unbounded multipliers
+
+6. **Field Name Mapping**:
+ - XSD: `MeterMultipliers` (capital M, plural)
+ - Entity: `meterMultipliers` (camelCase)
+ - DTO: `meterMultipliers` (camelCase, maps to XML MeterMultipliers)
+
+---
+
+**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
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/ElectronicAddress.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/ElectronicAddress.java
new file mode 100644
index 00000000..6ccf9b63
--- /dev/null
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/ElectronicAddress.java
@@ -0,0 +1,100 @@
+/*
+ *
+ * Copyright (c) 2025 Green Button Alliance, Inc.
+ *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.greenbuttonalliance.espi.common.domain.customer.common;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * Embeddable ElectronicAddress type.
+ *
+ * Electronic address information (email, LAN, MAC, web, radio, etc.).
+ * Per customer.xsd ElectronicAddress type (lines 886-936).
+ *
+ * Extends Object (NOT IdentifiedObject) per ESPI 4.0 specification.
+ * Shared across multiple ESPI resources: Asset, Organisation, and others.
+ */
+@Embeddable
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ElectronicAddress implements Serializable {
+
+ /**
+ * LAN address for this electronic address.
+ * XSD: String256, minOccurs="0"
+ */
+ @Column(name = "lan", length = 256)
+ private String lan;
+
+ /**
+ * MAC address for this electronic address.
+ * XSD: String256, minOccurs="0"
+ */
+ @Column(name = "mac", length = 256)
+ private String mac;
+
+ /**
+ * Primary email address.
+ * XSD: String256, minOccurs="0"
+ */
+ @Column(name = "email1", length = 256)
+ private String email1;
+
+ /**
+ * Secondary email address.
+ * XSD: String256, minOccurs="0"
+ */
+ @Column(name = "email2", length = 256)
+ private String email2;
+
+ /**
+ * Web address (URL).
+ * XSD: String256, minOccurs="0"
+ */
+ @Column(name = "web", length = 256)
+ private String web;
+
+ /**
+ * Radio address.
+ * XSD: String256, minOccurs="0"
+ */
+ @Column(name = "radio", length = 256)
+ private String radio;
+
+ /**
+ * User ID for this electronic address.
+ * XSD: String256, minOccurs="0"
+ */
+ @Column(name = "user_id", length = 256)
+ private String userID;
+
+ /**
+ * Password for this electronic address.
+ * XSD: String256, minOccurs="0"
+ */
+ @Column(name = "password", length = 256)
+ private String password;
+}
\ No newline at end of file
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/MeterMultiplier.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/MeterMultiplier.java
new file mode 100644
index 00000000..04cf24aa
--- /dev/null
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/MeterMultiplier.java
@@ -0,0 +1,55 @@
+/*
+ *
+ * Copyright (c) 2025 Green Button Alliance, Inc.
+ *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.greenbuttonalliance.espi.common.domain.customer.common;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * Embeddable MeterMultiplier type.
+ * Per customer.xsd MeterMultiplier type.
+ * Extends Object (NOT IdentifiedObject) per ESPI 4.0 specification.
+ * Multiplier applied at the meter.
+ */
+@Embeddable
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class MeterMultiplier implements Serializable {
+
+ /**
+ * Kind of multiplier.
+ * Per customer.xsd MeterMultiplierKind enumeration.
+ */
+ @Column(name = "kind", length = 256)
+ private String kind;
+
+ /**
+ * Multiplier value.
+ */
+ @Column(name = "value")
+ private BigDecimal value;
+}
\ No newline at end of file
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/StreetAddress.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/StreetAddress.java
new file mode 100644
index 00000000..fce4db36
--- /dev/null
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/StreetAddress.java
@@ -0,0 +1,63 @@
+/*
+ *
+ * Copyright (c) 2025 Green Button Alliance, Inc.
+ *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.greenbuttonalliance.espi.common.domain.customer.common;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * Embeddable StreetAddress type.
+ *
+ * General purpose street and postal address information.
+ * Per customer.xsd StreetAddress type (lines 1285-1320).
+ *
+ * Extends Object (NOT IdentifiedObject) per ESPI 4.0 specification.
+ * Shared across multiple ESPI resources: Organisation, Location, and others.
+ *
+ * Note: The XSD defines fields (streetDetail, townDetail, status, postalCode, poBox)
+ * but this implementation uses (streetDetail, townDetail, stateOrProvince, postalCode, country)
+ * for practical address representation in use across the codebase.
+ */
+@Embeddable
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class StreetAddress implements Serializable {
+
+ @Column(name = "street_detail", length = 256)
+ private String streetDetail;
+
+ @Column(name = "town_detail", length = 256)
+ private String townDetail;
+
+ @Column(name = "state_or_province", length = 256)
+ private String stateOrProvince;
+
+ @Column(name = "postal_code", length = 256)
+ private String postalCode;
+
+ @Column(name = "country", length = 256)
+ private String country;
+}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/TelephoneNumber.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/TelephoneNumber.java
new file mode 100644
index 00000000..9ad62d2c
--- /dev/null
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/TelephoneNumber.java
@@ -0,0 +1,107 @@
+/*
+ *
+ * Copyright (c) 2025 Green Button Alliance, Inc.
+ *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.greenbuttonalliance.espi.common.domain.customer.common;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * Embeddable TelephoneNumber type.
+ *
+ * Telephone number representation.
+ * Per customer.xsd TelephoneNumber type (lines 1428-1478).
+ *
+ * Extends Object (NOT IdentifiedObject) per ESPI 4.0 specification.
+ * Shared across multiple ESPI resources: Organisation, ServiceLocation, and others.
+ *
+ * 8 fields per ESPI 4.0 specification:
+ * countryCode, areaCode, cityCode, localNumber, ext, dialOut, internationalPrefix, ituPhone
+ */
+@Embeddable
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class TelephoneNumber implements Serializable {
+
+ @Column(name = "country_code", length = 256)
+ private String countryCode;
+
+ @Column(name = "area_code", length = 256)
+ private String areaCode;
+
+ @Column(name = "city_code", length = 256)
+ private String cityCode;
+
+ @Column(name = "local_number", length = 256)
+ private String localNumber;
+
+ @Column(name = "ext", length = 256)
+ private String ext;
+
+ @Column(name = "dial_out", length = 256)
+ private String dialOut;
+
+ @Column(name = "international_prefix", length = 256)
+ private String internationalPrefix;
+
+ @Column(name = "itu_phone", length = 256)
+ private String ituPhone;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TelephoneNumber that = (TelephoneNumber) o;
+ return java.util.Objects.equals(countryCode, that.countryCode) &&
+ java.util.Objects.equals(areaCode, that.areaCode) &&
+ java.util.Objects.equals(cityCode, that.cityCode) &&
+ java.util.Objects.equals(localNumber, that.localNumber) &&
+ java.util.Objects.equals(ext, that.ext) &&
+ java.util.Objects.equals(dialOut, that.dialOut) &&
+ java.util.Objects.equals(internationalPrefix, that.internationalPrefix) &&
+ java.util.Objects.equals(ituPhone, that.ituPhone);
+ }
+
+ @Override
+ public int hashCode() {
+ return java.util.Objects.hash(countryCode, areaCode, cityCode, localNumber, ext, dialOut, internationalPrefix, ituPhone);
+ }
+
+ @Override
+ public String toString() {
+ return "TelephoneNumber{" +
+ "countryCode='" + countryCode + '\'' +
+ ", areaCode='" + areaCode + '\'' +
+ ", cityCode='" + cityCode + '\'' +
+ ", localNumber='" + localNumber + '\'' +
+ ", ext='" + ext + '\'' +
+ ", dialOut='" + dialOut + '\'' +
+ ", internationalPrefix='" + internationalPrefix + '\'' +
+ ", ituPhone='" + ituPhone + '\'' +
+ '}';
+ }
+}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Asset.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Asset.java
index e246c1b4..4341c871 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Asset.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Asset.java
@@ -23,6 +23,7 @@
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import jakarta.persistence.*;
import java.io.Serializable;
@@ -88,17 +89,15 @@ public abstract class Asset implements Serializable {
* Electronic address.
*/
@Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "lan", column = @Column(name = "asset_lan")),
- @AttributeOverride(name = "mac", column = @Column(name = "asset_mac")),
- @AttributeOverride(name = "email1", column = @Column(name = "asset_email1")),
- @AttributeOverride(name = "email2", column = @Column(name = "asset_email2")),
- @AttributeOverride(name = "web", column = @Column(name = "asset_web")),
- @AttributeOverride(name = "radio", column = @Column(name = "asset_radio")),
- @AttributeOverride(name = "userID", column = @Column(name = "asset_user_id")),
- @AttributeOverride(name = "password", column = @Column(name = "asset_password"))
- })
- private Organisation.ElectronicAddress electronicAddress;
+ @AttributeOverride(name = "lan", column = @Column(name = "asset_lan"))
+ @AttributeOverride(name = "mac", column = @Column(name = "asset_mac"))
+ @AttributeOverride(name = "email1", column = @Column(name = "asset_email1"))
+ @AttributeOverride(name = "email2", column = @Column(name = "asset_email2"))
+ @AttributeOverride(name = "web", column = @Column(name = "asset_web"))
+ @AttributeOverride(name = "radio", column = @Column(name = "asset_radio"))
+ @AttributeOverride(name = "userID", column = @Column(name = "asset_user_id"))
+ @AttributeOverride(name = "password", column = @Column(name = "asset_password"))
+ private ElectronicAddress electronicAddress;
/**
* Lifecycle dates for this asset.
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java
index 530129b3..2f58847a 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java
@@ -24,6 +24,7 @@
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.hibernate.proxy.HibernateProxy;
import java.time.OffsetDateTime;
@@ -99,7 +100,7 @@ public class CustomerAccountEntity extends IdentifiedObject {
@AttributeOverride(name = "radio", column = @Column(name = "doc_radio"))
@AttributeOverride(name = "userID", column = @Column(name = "doc_user_id"))
@AttributeOverride(name = "password", column = @Column(name = "doc_password"))
- private Organisation.ElectronicAddress electronicAddress;
+ private ElectronicAddress electronicAddress;
/**
* Subject of this document, intended for this document to be found by a search engine.
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java
index 17bd41c8..2883e487 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java
@@ -22,6 +22,7 @@
import lombok.*;
import org.greenbuttonalliance.espi.common.domain.common.DateTimeInterval;
import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import jakarta.persistence.*;
import org.hibernate.proxy.HibernateProxy;
@@ -98,7 +99,7 @@ public class CustomerAgreementEntity extends IdentifiedObject {
@AttributeOverride(name = "radio", column = @Column(name = "doc_radio"))
@AttributeOverride(name = "userID", column = @Column(name = "doc_user_id"))
@AttributeOverride(name = "password", column = @Column(name = "doc_password"))
- private Organisation.ElectronicAddress electronicAddress;
+ private ElectronicAddress electronicAddress;
/**
* Subject of this document, intended for this document to be found by a search engine.
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java
index 5f7c6b03..6b7661d4 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java
@@ -58,27 +58,25 @@ public class CustomerEntity extends IdentifiedObject {
* Organisation having this role.
*/
@Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "organisationName", column = @Column(name = "customer_organisation_name")),
- @AttributeOverride(name = "streetAddress.streetDetail", column = @Column(name = "customer_street_detail")),
- @AttributeOverride(name = "streetAddress.townDetail", column = @Column(name = "customer_town_detail")),
- @AttributeOverride(name = "streetAddress.stateOrProvince", column = @Column(name = "customer_state_or_province")),
- @AttributeOverride(name = "streetAddress.postalCode", column = @Column(name = "customer_postal_code")),
- @AttributeOverride(name = "streetAddress.country", column = @Column(name = "customer_country")),
- @AttributeOverride(name = "postalAddress.streetDetail", column = @Column(name = "customer_postal_street_detail")),
- @AttributeOverride(name = "postalAddress.townDetail", column = @Column(name = "customer_postal_town_detail")),
- @AttributeOverride(name = "postalAddress.stateOrProvince", column = @Column(name = "customer_postal_state_or_province")),
- @AttributeOverride(name = "postalAddress.postalCode", column = @Column(name = "customer_postal_postal_code")),
- @AttributeOverride(name = "postalAddress.country", column = @Column(name = "customer_postal_country")),
- @AttributeOverride(name = "electronicAddress.lan", column = @Column(name = "customer_lan")),
- @AttributeOverride(name = "electronicAddress.mac", column = @Column(name = "customer_mac")),
- @AttributeOverride(name = "electronicAddress.email1", column = @Column(name = "customer_email1")),
- @AttributeOverride(name = "electronicAddress.email2", column = @Column(name = "customer_email2")),
- @AttributeOverride(name = "electronicAddress.web", column = @Column(name = "customer_web")),
- @AttributeOverride(name = "electronicAddress.radio", column = @Column(name = "customer_radio")),
- @AttributeOverride(name = "electronicAddress.userID", column = @Column(name = "customer_user_id")),
- @AttributeOverride(name = "electronicAddress.password", column = @Column(name = "customer_password"))
- })
+ @AttributeOverride(name = "organisationName", column = @Column(name = "customer_organisation_name"))
+ @AttributeOverride(name = "streetAddress.streetDetail", column = @Column(name = "customer_street_detail"))
+ @AttributeOverride(name = "streetAddress.townDetail", column = @Column(name = "customer_town_detail"))
+ @AttributeOverride(name = "streetAddress.stateOrProvince", column = @Column(name = "customer_state_or_province"))
+ @AttributeOverride(name = "streetAddress.postalCode", column = @Column(name = "customer_postal_code"))
+ @AttributeOverride(name = "streetAddress.country", column = @Column(name = "customer_country"))
+ @AttributeOverride(name = "postalAddress.streetDetail", column = @Column(name = "customer_postal_street_detail"))
+ @AttributeOverride(name = "postalAddress.townDetail", column = @Column(name = "customer_postal_town_detail"))
+ @AttributeOverride(name = "postalAddress.stateOrProvince", column = @Column(name = "customer_postal_state_or_province"))
+ @AttributeOverride(name = "postalAddress.postalCode", column = @Column(name = "customer_postal_postal_code"))
+ @AttributeOverride(name = "postalAddress.country", column = @Column(name = "customer_postal_country"))
+ @AttributeOverride(name = "electronicAddress.lan", column = @Column(name = "customer_lan"))
+ @AttributeOverride(name = "electronicAddress.mac", column = @Column(name = "customer_mac"))
+ @AttributeOverride(name = "electronicAddress.email1", column = @Column(name = "customer_email1"))
+ @AttributeOverride(name = "electronicAddress.email2", column = @Column(name = "customer_email2"))
+ @AttributeOverride(name = "electronicAddress.web", column = @Column(name = "customer_web"))
+ @AttributeOverride(name = "electronicAddress.radio", column = @Column(name = "customer_radio"))
+ @AttributeOverride(name = "electronicAddress.userID", column = @Column(name = "customer_user_id"))
+ @AttributeOverride(name = "electronicAddress.password", column = @Column(name = "customer_password"))
private Organisation organisation;
/**
@@ -111,22 +109,18 @@ public class CustomerEntity extends IdentifiedObject {
* Status of this customer.
*/
@Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "value", column = @Column(name = "status_value")),
- @AttributeOverride(name = "dateTime", column = @Column(name = "status_date_time")),
- @AttributeOverride(name = "reason", column = @Column(name = "status_reason"))
- })
+ @AttributeOverride(name = "value", column = @Column(name = "status_value"))
+ @AttributeOverride(name = "dateTime", column = @Column(name = "status_date_time"))
+ @AttributeOverride(name = "reason", column = @Column(name = "status_reason"))
private Status status;
/**
* Priority of the customer.
*/
@Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "value", column = @Column(name = "priority_value")),
- @AttributeOverride(name = "rank", column = @Column(name = "priority_rank")),
- @AttributeOverride(name = "type", column = @Column(name = "priority_type"))
- })
+ @AttributeOverride(name = "value", column = @Column(name = "priority_value"))
+ @AttributeOverride(name = "rank", column = @Column(name = "priority_rank"))
+ @AttributeOverride(name = "type", column = @Column(name = "priority_type"))
private Priority priority;
/**
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceEntity.java
index aa6a2220..1c872823 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceEntity.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceEntity.java
@@ -24,6 +24,7 @@
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import java.math.BigDecimal;
@@ -103,7 +104,7 @@ public class EndDeviceEntity extends IdentifiedObject {
@AttributeOverride(name = "userID", column = @Column(name = "end_device_user_id")),
@AttributeOverride(name = "password", column = @Column(name = "end_device_password"))
})
- private Organisation.ElectronicAddress electronicAddress;
+ private ElectronicAddress electronicAddress;
/**
* Lifecycle dates for this asset.
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Location.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Location.java
index acb31aab..16f66f38 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Location.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Location.java
@@ -23,6 +23,8 @@
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import jakarta.persistence.*;
import java.io.Serializable;
@@ -58,27 +60,23 @@ public abstract class Location implements Serializable {
* Main address of the location.
*/
@Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "streetDetail", column = @Column(name = "location_main_street_detail")),
- @AttributeOverride(name = "townDetail", column = @Column(name = "location_main_town_detail")),
- @AttributeOverride(name = "stateOrProvince", column = @Column(name = "location_main_state_or_province")),
- @AttributeOverride(name = "postalCode", column = @Column(name = "location_main_postal_code")),
- @AttributeOverride(name = "country", column = @Column(name = "location_main_country"))
- })
- private Organisation.StreetAddress mainAddress;
+ @AttributeOverride(name = "streetDetail", column = @Column(name = "location_main_street_detail"))
+ @AttributeOverride(name = "townDetail", column = @Column(name = "location_main_town_detail"))
+ @AttributeOverride(name = "stateOrProvince", column = @Column(name = "location_main_state_or_province"))
+ @AttributeOverride(name = "postalCode", column = @Column(name = "location_main_postal_code"))
+ @AttributeOverride(name = "country", column = @Column(name = "location_main_country"))
+ private StreetAddress mainAddress;
/**
* Secondary address of the location. For example, PO Box address may have different ZIP code than that in the 'mainAddress'.
*/
@Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "streetDetail", column = @Column(name = "location_secondary_street_detail")),
- @AttributeOverride(name = "townDetail", column = @Column(name = "location_secondary_town_detail")),
- @AttributeOverride(name = "stateOrProvince", column = @Column(name = "location_secondary_state_or_province")),
- @AttributeOverride(name = "postalCode", column = @Column(name = "location_secondary_postal_code")),
- @AttributeOverride(name = "country", column = @Column(name = "location_secondary_country"))
- })
- private Organisation.StreetAddress secondaryAddress;
+ @AttributeOverride(name = "streetDetail", column = @Column(name = "location_secondary_street_detail"))
+ @AttributeOverride(name = "townDetail", column = @Column(name = "location_secondary_town_detail"))
+ @AttributeOverride(name = "stateOrProvince", column = @Column(name = "location_secondary_state_or_province"))
+ @AttributeOverride(name = "postalCode", column = @Column(name = "location_secondary_postal_code"))
+ @AttributeOverride(name = "country", column = @Column(name = "location_secondary_country"))
+ private StreetAddress secondaryAddress;
// PhoneNumber fields removed - phone numbers are managed separately via PhoneNumberEntity
// to avoid JPA column mapping conflicts in embedded contexts
@@ -87,17 +85,15 @@ public abstract class Location implements Serializable {
* Electronic address.
*/
@Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "lan", column = @Column(name = "location_lan")),
- @AttributeOverride(name = "mac", column = @Column(name = "location_mac")),
- @AttributeOverride(name = "email1", column = @Column(name = "location_email1")),
- @AttributeOverride(name = "email2", column = @Column(name = "location_email2")),
- @AttributeOverride(name = "web", column = @Column(name = "location_web")),
- @AttributeOverride(name = "radio", column = @Column(name = "location_radio")),
- @AttributeOverride(name = "userID", column = @Column(name = "location_user_id")),
- @AttributeOverride(name = "password", column = @Column(name = "location_password"))
- })
- private Organisation.ElectronicAddress electronicAddress;
+ @AttributeOverride(name = "lan", column = @Column(name = "location_lan"))
+ @AttributeOverride(name = "mac", column = @Column(name = "location_mac"))
+ @AttributeOverride(name = "email1", column = @Column(name = "location_email1"))
+ @AttributeOverride(name = "email2", column = @Column(name = "location_email2"))
+ @AttributeOverride(name = "web", column = @Column(name = "location_web"))
+ @AttributeOverride(name = "radio", column = @Column(name = "location_radio"))
+ @AttributeOverride(name = "userID", column = @Column(name = "location_user_id"))
+ @AttributeOverride(name = "password", column = @Column(name = "location_password"))
+ private ElectronicAddress electronicAddress;
/**
* (if applicable) Reference to geographical information source, often external to the utility.
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/MeterEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/MeterEntity.java
index ced50a96..2edd1495 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/MeterEntity.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/MeterEntity.java
@@ -19,14 +19,14 @@
package org.greenbuttonalliance.espi.common.domain.customer.entity;
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Table;
+import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
+import org.greenbuttonalliance.espi.common.domain.customer.common.MeterMultiplier;
import org.hibernate.proxy.HibernateProxy;
+import java.util.List;
import java.util.Objects;
/**
@@ -52,10 +52,13 @@ public class MeterEntity extends EndDeviceEntity {
/**
* All multipliers applied at this meter.
- * TODO: Create MeterMultiplierEntity and enable this relationship
+ * Per customer.xsd Meter.MeterMultipliers (collection of MeterMultiplier embeddables).
*/
- // @OneToMany(mappedBy = "meter", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
- // private List meterMultipliers;
+ @ElementCollection
+ @CollectionTable(name = "meter_multipliers", joinColumns = @JoinColumn(name = "meter_id"))
+ @AttributeOverride(name = "kind", column = @Column(name = "multiplier_kind"))
+ @AttributeOverride(name = "value", column = @Column(name = "multiplier_value"))
+ private List meterMultipliers;
/**
* [extension] Current interval length specified in seconds.
@@ -66,9 +69,13 @@ public class MeterEntity extends EndDeviceEntity {
@Override
public String toString() {
return getClass().getSimpleName() + "(" +
+ // IdentifiedObject fields
"id = " + getId() + ", " +
- "formNumber = " + getFormNumber() + ", " +
- "intervalLength = " + getIntervalLength() + ", " +
+ "description = " + getDescription() + ", " +
+ "created = " + getCreated() + ", " +
+ "updated = " + getUpdated() + ", " +
+ "published = " + getPublished() + ", " +
+ // Asset fields (via EndDevice)
"type = " + getType() + ", " +
"utcNumber = " + getUtcNumber() + ", " +
"serialNumber = " + getSerialNumber() + ", " +
@@ -81,13 +88,14 @@ public String toString() {
"initialCondition = " + getInitialCondition() + ", " +
"initialLossOfLife = " + getInitialLossOfLife() + ", " +
"status = " + getStatus() + ", " +
+ // EndDevice fields
"isVirtual = " + getIsVirtual() + ", " +
"isPan = " + getIsPan() + ", " +
"installCode = " + getInstallCode() + ", " +
"amrSystem = " + getAmrSystem() + ", " +
- "description = " + getDescription() + ", " +
- "created = " + getCreated() + ", " +
- "updated = " + getUpdated() + ", " +
- "published = " + getPublished() + ")";
+ // Meter-specific fields (per customer.xsd Meter sequence)
+ "formNumber = " + getFormNumber() + ", " +
+ "meterMultipliers = " + getMeterMultipliers() + ", " +
+ "intervalLength = " + getIntervalLength() + ")";
}
}
\ No newline at end of file
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Organisation.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Organisation.java
index 7564c2d2..8e9c9bfe 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Organisation.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Organisation.java
@@ -23,6 +23,8 @@
import jakarta.persistence.Embeddable;
import jakarta.persistence.Embedded;
import lombok.*;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import java.io.Serializable;
@@ -65,130 +67,4 @@ public class Organisation implements Serializable {
*/
@Embedded
private ElectronicAddress electronicAddress;
-
- /**
- * Embeddable class for StreetAddress
- */
- @Embeddable
- @Data
- @NoArgsConstructor
- public static class StreetAddress implements Serializable {
- @Column(name = "street_detail", length = 256)
- private String streetDetail;
-
- @Column(name = "town_detail", length = 256)
- private String townDetail;
-
- @Column(name = "state_or_province", length = 256)
- private String stateOrProvince;
-
- @Column(name = "postal_code", length = 256)
- private String postalCode;
-
- @Column(name = "country", length = 256)
- private String country;
- }
-
- /**
- * Embeddable class for TelephoneNumber.
- * Per customer.xsd TelephoneNumber type (lines 1428-1478).
- * 8 fields per ESPI 4.0 specification.
- */
- @Embeddable
- @Getter
- @Setter
- @NoArgsConstructor
- @AllArgsConstructor
- public static class TelephoneNumber implements Serializable {
- @Column(name = "country_code", length = 256)
- private String countryCode;
-
- @Column(name = "area_code", length = 256)
- private String areaCode;
-
- @Column(name = "city_code", length = 256)
- private String cityCode;
-
- @Column(name = "local_number", length = 256)
- private String localNumber;
-
- @Column(name = "ext", length = 256)
- private String ext;
-
- @Column(name = "dial_out", length = 256)
- private String dialOut;
-
- @Column(name = "international_prefix", length = 256)
- private String internationalPrefix;
-
- @Column(name = "itu_phone", length = 256)
- private String ituPhone;
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TelephoneNumber that = (TelephoneNumber) o;
- return java.util.Objects.equals(countryCode, that.countryCode) &&
- java.util.Objects.equals(areaCode, that.areaCode) &&
- java.util.Objects.equals(cityCode, that.cityCode) &&
- java.util.Objects.equals(localNumber, that.localNumber) &&
- java.util.Objects.equals(ext, that.ext) &&
- java.util.Objects.equals(dialOut, that.dialOut) &&
- java.util.Objects.equals(internationalPrefix, that.internationalPrefix) &&
- java.util.Objects.equals(ituPhone, that.ituPhone);
- }
-
- @Override
- public int hashCode() {
- return java.util.Objects.hash(countryCode, areaCode, cityCode, localNumber, ext, dialOut, internationalPrefix, ituPhone);
- }
-
- @Override
- public String toString() {
- return "TelephoneNumber{" +
- "countryCode='" + countryCode + '\'' +
- ", areaCode='" + areaCode + '\'' +
- ", cityCode='" + cityCode + '\'' +
- ", localNumber='" + localNumber + '\'' +
- ", ext='" + ext + '\'' +
- ", dialOut='" + dialOut + '\'' +
- ", internationalPrefix='" + internationalPrefix + '\'' +
- ", ituPhone='" + ituPhone + '\'' +
- '}';
- }
- }
-
- /**
- * Embeddable class for ElectronicAddress.
- * Per customer.xsd ElectronicAddress type (lines 886-936).
- */
- @Embeddable
- @Data
- @NoArgsConstructor
- public static class ElectronicAddress implements Serializable {
- @Column(name = "lan", length = 256)
- private String lan;
-
- @Column(name = "mac", length = 256)
- private String mac;
-
- @Column(name = "email1", length = 256)
- private String email1;
-
- @Column(name = "email2", length = 256)
- private String email2;
-
- @Column(name = "web", length = 256)
- private String web;
-
- @Column(name = "radio", length = 256)
- private String radio;
-
- @Column(name = "user_id", length = 256)
- private String userID;
-
- @Column(name = "password", length = 256)
- private String password;
- }
}
\ No newline at end of file
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/OrganisationRole.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/OrganisationRole.java
index 681d09ef..b232a7a3 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/OrganisationRole.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/OrganisationRole.java
@@ -44,22 +44,20 @@ public class OrganisationRole {
* Organisation having this role.
*/
@Embedded
- @AttributeOverrides({
- @AttributeOverride(name = "organisationName", column = @Column(name = "role_organisation_name")),
- @AttributeOverride(name = "streetAddress.streetDetail", column = @Column(name = "role_street_detail")),
- @AttributeOverride(name = "streetAddress.townDetail", column = @Column(name = "role_town_detail")),
- @AttributeOverride(name = "streetAddress.stateOrProvince", column = @Column(name = "role_state_or_province")),
- @AttributeOverride(name = "streetAddress.postalCode", column = @Column(name = "role_postal_code")),
- @AttributeOverride(name = "streetAddress.country", column = @Column(name = "role_country")),
- @AttributeOverride(name = "postalAddress.streetDetail", column = @Column(name = "role_postal_street_detail")),
- @AttributeOverride(name = "postalAddress.townDetail", column = @Column(name = "role_postal_town_detail")),
- @AttributeOverride(name = "postalAddress.stateOrProvince", column = @Column(name = "role_postal_state_or_province")),
- @AttributeOverride(name = "postalAddress.postalCode", column = @Column(name = "role_postal_postal_code")),
- @AttributeOverride(name = "postalAddress.country", column = @Column(name = "role_postal_country")),
- @AttributeOverride(name = "electronicAddress.email1", column = @Column(name = "role_email1")),
- @AttributeOverride(name = "electronicAddress.email2", column = @Column(name = "role_email2")),
- @AttributeOverride(name = "electronicAddress.web", column = @Column(name = "role_web")),
- @AttributeOverride(name = "electronicAddress.radio", column = @Column(name = "role_radio"))
- })
+ @AttributeOverride(name = "organisationName", column = @Column(name = "role_organisation_name"))
+ @AttributeOverride(name = "streetAddress.streetDetail", column = @Column(name = "role_street_detail"))
+ @AttributeOverride(name = "streetAddress.townDetail", column = @Column(name = "role_town_detail"))
+ @AttributeOverride(name = "streetAddress.stateOrProvince", column = @Column(name = "role_state_or_province"))
+ @AttributeOverride(name = "streetAddress.postalCode", column = @Column(name = "role_postal_code"))
+ @AttributeOverride(name = "streetAddress.country", column = @Column(name = "role_country"))
+ @AttributeOverride(name = "postalAddress.streetDetail", column = @Column(name = "role_postal_street_detail"))
+ @AttributeOverride(name = "postalAddress.townDetail", column = @Column(name = "role_postal_town_detail"))
+ @AttributeOverride(name = "postalAddress.stateOrProvince", column = @Column(name = "role_postal_state_or_province"))
+ @AttributeOverride(name = "postalAddress.postalCode", column = @Column(name = "role_postal_postal_code"))
+ @AttributeOverride(name = "postalAddress.country", column = @Column(name = "role_postal_country"))
+ @AttributeOverride(name = "electronicAddress.email1", column = @Column(name = "role_email1"))
+ @AttributeOverride(name = "electronicAddress.email2", column = @Column(name = "role_email2"))
+ @AttributeOverride(name = "electronicAddress.web", column = @Column(name = "role_web"))
+ @AttributeOverride(name = "electronicAddress.radio", column = @Column(name = "role_radio"))
private Organisation organisation;
}
\ No newline at end of file
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java
index ba7f4a19..dd7d1a27 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java
@@ -24,6 +24,9 @@
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.hibernate.proxy.HibernateProxy;
import java.util.List;
@@ -62,7 +65,7 @@
@AttributeOverride(name = "stateOrProvince", column = @Column(name = "main_state_or_province"))
@AttributeOverride(name = "postalCode", column = @Column(name = "main_postal_code"))
@AttributeOverride(name = "country", column = @Column(name = "main_country"))
- private Organisation.StreetAddress mainAddress;
+ private StreetAddress mainAddress;
/**
* Secondary address of the location. For example, PO Box address may have different ZIP code than that in the 'mainAddress'.
@@ -73,7 +76,7 @@
@AttributeOverride(name = "stateOrProvince", column = @Column(name = "secondary_state_or_province"))
@AttributeOverride(name = "postalCode", column = @Column(name = "secondary_postal_code"))
@AttributeOverride(name = "country", column = @Column(name = "secondary_country"))
- private Organisation.StreetAddress secondaryAddress;
+ private StreetAddress secondaryAddress;
/**
* Primary phone number for this service location.
@@ -88,7 +91,7 @@
@AttributeOverride(name = "dialOut", column = @Column(name = "phone1_dial_out"))
@AttributeOverride(name = "internationalPrefix", column = @Column(name = "phone1_international_prefix"))
@AttributeOverride(name = "ituPhone", column = @Column(name = "phone1_itu_phone"))
- private Organisation.TelephoneNumber phone1;
+ private TelephoneNumber phone1;
/**
* Secondary phone number for this service location.
@@ -103,7 +106,7 @@
@AttributeOverride(name = "dialOut", column = @Column(name = "phone2_dial_out"))
@AttributeOverride(name = "internationalPrefix", column = @Column(name = "phone2_international_prefix"))
@AttributeOverride(name = "ituPhone", column = @Column(name = "phone2_itu_phone"))
- private Organisation.TelephoneNumber phone2;
+ private TelephoneNumber phone2;
/**
* Electronic address.
@@ -117,7 +120,7 @@
@AttributeOverride(name = "radio", column = @Column(name = "electronic_radio"))
@AttributeOverride(name = "userID", column = @Column(name = "electronic_user_id"))
@AttributeOverride(name = "password", column = @Column(name = "electronic_password"))
- private Organisation.ElectronicAddress electronicAddress;
+ private ElectronicAddress electronicAddress;
/**
* (if applicable) Reference to geographical information source, often external to the utility.
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/MeterMultiplierKind.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/MeterMultiplierKind.java
new file mode 100644
index 00000000..72722b43
--- /dev/null
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/MeterMultiplierKind.java
@@ -0,0 +1,100 @@
+/*
+ *
+ * Copyright (c) 2025 Green Button Alliance, Inc.
+ *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.greenbuttonalliance.espi.common.domain.customer.enums;
+
+import jakarta.xml.bind.annotation.XmlEnum;
+import jakarta.xml.bind.annotation.XmlEnumValue;
+import jakarta.xml.bind.annotation.XmlType;
+
+/**
+ * Enumeration for MeterMultiplierKind values.
+ *
+ * Kind of meter multiplier applied to meter readings.
+ * Per ESPI 4.0 customer.xsd lines 1920-1960.
+ */
+@XmlType(name = "MeterMultiplierKind", namespace = "http://naesb.org/espi/customer")
+@XmlEnum
+public enum MeterMultiplierKind {
+
+ /**
+ * Meter kh (watthour) constant.
+ * The number of watthours that must be applied to the meter to cause one disk revolution
+ * for an electromechanical meter or the number of watthours represented by one increment
+ * pulse for an electronic meter.
+ * XSD value: "kH" (line 1927)
+ */
+ @XmlEnumValue("kH")
+ KH("kH"),
+
+ /**
+ * The ratio of the transformer's primary and secondary windings (turns) with respect to each other.
+ * XSD value: "transformerRatio" (line 1932)
+ */
+ @XmlEnumValue("transformerRatio")
+ TRANSFORMER_RATIO("transformerRatio"),
+
+ /**
+ * Register multiplier.
+ * The number to multiply the register reading by in order to get kWh.
+ * XSD value: "kR" (line 1937)
+ */
+ @XmlEnumValue("kR")
+ KR("kR"),
+
+ /**
+ * Test constant.
+ * XSD value: "kE" (line 1942)
+ */
+ @XmlEnumValue("kE")
+ KE("kE"),
+
+ /**
+ * Current transformer ratio used to convert associated quantities to real measurements.
+ * XSD value: "ctRatio" (line 1947)
+ */
+ @XmlEnumValue("ctRatio")
+ CT_RATIO("ctRatio"),
+
+ /**
+ * Potential transformer ratio used to convert associated quantities to real measurements.
+ * XSD value: "ptRatio" (line 1952)
+ */
+ @XmlEnumValue("ptRatio")
+ PT_RATIO("ptRatio");
+
+ private final String value;
+
+ MeterMultiplierKind(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public static MeterMultiplierKind fromValue(String value) {
+ for (MeterMultiplierKind kind : MeterMultiplierKind.values()) {
+ if (kind.value.equals(value)) {
+ return kind;
+ }
+ }
+ throw new IllegalArgumentException("Invalid MeterMultiplierKind value: " + value);
+ }
+}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/MeterDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/MeterDto.java
index 10ee2cfe..6bafe492 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/MeterDto.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/MeterDto.java
@@ -19,30 +19,40 @@
package org.greenbuttonalliance.espi.common.dto.customer;
-import org.greenbuttonalliance.espi.common.dto.atom.LinkDto;
-
-import jakarta.xml.bind.annotation.*;
+import jakarta.xml.bind.annotation.XmlAccessType;
+import jakarta.xml.bind.annotation.XmlAccessorType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlType;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
-import java.time.OffsetDateTime;
import java.util.List;
/**
- * Meter DTO class for JAXB XML marshalling/unmarshalling.
- *
- * Represents a meter device extending EndDevice with meter-specific functionality.
- * Supports Atom protocol XML wrapping.
+ * Meter DTO for ESPI 4.0 XSD compliance.
+ *
+ * Represents a physical metering device extending EndDevice.
+ * Contains all fields from Asset (12) + EndDevice (4) + Meter (3) = 19 fields total.
+ *
+ * XSD Inheritance Chain: Object → IdentifiedObject → Asset → AssetContainer → EndDevice → Meter
+ *
+ * Field sequence MUST match customer.xsd definition exactly.
+ * ONLY contains XSD-defined fields - NO Atom metadata (handled by AtomEntryDto wrapper).
*/
@XmlRootElement(name = "Meter", namespace = "http://naesb.org/espi/customer")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Meter", namespace = "http://naesb.org/espi/customer", propOrder = {
- "published", "updated", "selfLink", "upLink", "relatedLinks",
- "description", "amrSystem", "installCode", "isPan", "installDate",
- "removedDate", "serialNumber", "formNumber", "kh", "meterMultiplier",
- "serviceLocation"
+ // Asset fields (12) - from customer.xsd lines 650-709
+ "type", "utcNumber", "serialNumber", "lotNumber", "purchasePrice", "critical",
+ "electronicAddress", "lifecycle", "acceptanceTest", "initialCondition",
+ "initialLossOfLife", "status",
+ // EndDevice fields (4) - from customer.xsd lines 219-238
+ "isVirtual", "isPan", "installCode", "amrSystem",
+ // Meter fields (3) - from customer.xsd lines 250-264
+ "formNumber", "meterMultipliers", "intervalLength"
})
@Getter
@Setter
@@ -50,86 +60,150 @@
@AllArgsConstructor
public class MeterDto {
- @XmlTransient
- private Long id;
+ // ========================================
+ // Asset Fields (12 fields from customer.xsd lines 650-709)
+ // ========================================
- @XmlAttribute(name = "mRID")
- private String uuid;
+ /**
+ * Utility supplied name for the type of meter.
+ * XSD: String256, minOccurs="0"
+ */
+ @XmlElement(name = "type", namespace = "http://naesb.org/espi/customer")
+ private String type;
- @XmlElement(name = "published")
- private OffsetDateTime published;
+ /**
+ * Uniquely identifies the meter within utility.
+ * XSD: String256, minOccurs="0"
+ */
+ @XmlElement(name = "utcNumber", namespace = "http://naesb.org/espi/customer")
+ private String utcNumber;
- @XmlElement(name = "updated")
- private OffsetDateTime updated;
+ /**
+ * Serial number of the physical meter.
+ * Used for UUID v5 generation in ESPI 4.0.
+ * XSD: String256, minOccurs="0"
+ */
+ @XmlElement(name = "serialNumber", namespace = "http://naesb.org/espi/customer")
+ private String serialNumber;
- @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom")
- @XmlElementWrapper(name = "links", namespace = "http://www.w3.org/2005/Atom")
- private List relatedLinks;
+ /**
+ * Lot number for the meter.
+ * XSD: String256, minOccurs="0"
+ */
+ @XmlElement(name = "lotNumber", namespace = "http://naesb.org/espi/customer")
+ private String lotNumber;
- @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom")
- private LinkDto selfLink;
+ /**
+ * Purchase price of the meter in currency minor units (cents).
+ * XSD: Int48, minOccurs="0"
+ */
+ @XmlElement(name = "purchasePrice", namespace = "http://naesb.org/espi/customer")
+ private Long purchasePrice;
- @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom")
- private LinkDto upLink;
+ /**
+ * True if asset is considered critical for some reason (e.g., staffing, safety).
+ * XSD: xs:boolean, minOccurs="0"
+ */
+ @XmlElement(name = "critical", namespace = "http://naesb.org/espi/customer")
+ private Boolean critical;
- @XmlElement(name = "description")
- private String description;
+ /**
+ * Electronic address (email, URL, radio, etc.) for this asset.
+ * XSD: ElectronicAddress (complex type), minOccurs="0"
+ * Note: Singular, not a collection.
+ */
+ @XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer")
+ private ElectronicAddressDto electronicAddress;
- // EndDevice fields
- @XmlElement(name = "amrSystem")
- private String amrSystem;
+ /**
+ * Lifecycle dates for the asset.
+ * XSD: LifecycleDate (complex type), minOccurs="0"
+ */
+ @XmlElement(name = "lifecycle", namespace = "http://naesb.org/espi/customer")
+ private LifecycleDateDto lifecycle;
- @XmlElement(name = "installCode")
- private String installCode;
+ /**
+ * Acceptance test information.
+ * XSD: AcceptanceTest (complex type), minOccurs="0"
+ */
+ @XmlElement(name = "acceptanceTest", namespace = "http://naesb.org/espi/customer")
+ private AcceptanceTestDto acceptanceTest;
- @XmlElement(name = "isPan")
- private Boolean isPan;
+ /**
+ * Condition of the asset when it was initially received.
+ * XSD: String256, minOccurs="0"
+ */
+ @XmlElement(name = "initialCondition", namespace = "http://naesb.org/espi/customer")
+ private String initialCondition;
- @XmlElement(name = "installDate")
- private OffsetDateTime installDate;
+ /**
+ * Initial loss of life as a percentage.
+ * XSD: PerCent (UInt16), minOccurs="0"
+ */
+ @XmlElement(name = "initialLossOfLife", namespace = "http://naesb.org/espi/customer")
+ private Integer initialLossOfLife;
- @XmlElement(name = "removedDate")
- private OffsetDateTime removedDate;
+ /**
+ * Status information for this asset.
+ * XSD: Status (complex type), minOccurs="0"
+ */
+ @XmlElement(name = "status", namespace = "http://naesb.org/espi/customer")
+ private StatusDto status;
- @XmlElement(name = "serialNumber")
- private String serialNumber;
+ // ========================================
+ // EndDevice Fields (4 fields from customer.xsd lines 219-238)
+ // ========================================
- // Meter-specific fields
- @XmlElement(name = "formNumber")
- private String formNumber;
+ /**
+ * If true, this is a virtual device (not a physical device).
+ * XSD: xs:boolean, minOccurs="0"
+ */
+ @XmlElement(name = "isVirtual", namespace = "http://naesb.org/espi/customer")
+ private Boolean isVirtual;
- @XmlElement(name = "kh")
- private Double kh;
+ /**
+ * If true, this is a personal area network (PAN) device.
+ * XSD: xs:boolean, minOccurs="0"
+ */
+ @XmlElement(name = "isPan", namespace = "http://naesb.org/espi/customer")
+ private Boolean isPan;
- @XmlElement(name = "meterMultiplier")
- private Double meterMultiplier;
+ /**
+ * Installation code for the device.
+ * XSD: String256, minOccurs="0"
+ */
+ @XmlElement(name = "installCode", namespace = "http://naesb.org/espi/customer")
+ private String installCode;
- @XmlElement(name = "ServiceLocation")
- private ServiceLocationDto serviceLocation;
+ /**
+ * Automated meter reading (AMR) system identifier.
+ * XSD: String256, minOccurs="0"
+ */
+ @XmlElement(name = "amrSystem", namespace = "http://naesb.org/espi/customer")
+ private String amrSystem;
+
+ // ========================================
+ // Meter Fields (3 fields from customer.xsd lines 250-264)
+ // ========================================
/**
- * Minimal constructor for basic meter data.
+ * Utility meter form designation per ANSI C12.10 or other regional standards.
+ * XSD: String256, minOccurs="0"
*/
- public MeterDto(String uuid, String serialNumber, String formNumber) {
- this(null, uuid, null, null, null, null, null, null,
- null, null, null, null, null, serialNumber, formNumber, null, null, null);
- }
+ @XmlElement(name = "formNumber", namespace = "http://naesb.org/espi/customer")
+ private String formNumber;
/**
- * Gets the self href for this meter.
- *
- * @return self href string
+ * Collection of meter multipliers applied to the meter readings.
+ * XSD: MeterMultiplier (complex type), minOccurs="0", maxOccurs="unbounded"
*/
- public String getSelfHref() {
- return selfLink != null ? selfLink.getHref() : null;
- }
+ @XmlElement(name = "MeterMultipliers", namespace = "http://naesb.org/espi/customer")
+ private List meterMultipliers;
/**
- * Gets the up href for this meter.
- *
- * @return up href string
+ * Default interval length (in seconds) for interval readings.
+ * XSD: UInt32, minOccurs="0"
*/
- public String getUpHref() {
- return upLink != null ? upLink.getHref() : null;
- }
-}
\ No newline at end of file
+ @XmlElement(name = "intervalLength", namespace = "http://naesb.org/espi/customer")
+ private Long intervalLength;
+}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/MeterMultiplierDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/MeterMultiplierDto.java
new file mode 100644
index 00000000..c3fc7afe
--- /dev/null
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/MeterMultiplierDto.java
@@ -0,0 +1,67 @@
+/*
+ *
+ * Copyright (c) 2025 Green Button Alliance, Inc.
+ *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.greenbuttonalliance.espi.common.dto.customer;
+
+import jakarta.xml.bind.annotation.XmlAccessType;
+import jakarta.xml.bind.annotation.XmlAccessorType;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlType;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.greenbuttonalliance.espi.common.domain.customer.enums.MeterMultiplierKind;
+
+/**
+ * MeterMultiplier DTO for ESPI 4.0 XSD compliance.
+ *
+ * Represents a multiplier applied at the meter level.
+ *
+ * XSD Definition: customer.xsd lines 1056-1076
+ * Extends: Object (NOT IdentifiedObject - no mRID or description)
+ *
+ * Field sequence MUST match customer.xsd definition exactly.
+ */
+@XmlRootElement(name = "MeterMultiplier", namespace = "http://naesb.org/espi/customer")
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlType(name = "MeterMultiplier", namespace = "http://naesb.org/espi/customer", propOrder = {
+ "kind", "value"
+})
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class MeterMultiplierDto {
+
+ /**
+ * Kind of multiplier (e.g., kH, transformerRatio, ctRatio, ptRatio).
+ * XSD: MeterMultiplierKind, minOccurs="0"
+ */
+ @XmlElement(name = "kind", namespace = "http://naesb.org/espi/customer")
+ private MeterMultiplierKind kind;
+
+ /**
+ * Numeric value of the multiplier.
+ * XSD: xs:float, minOccurs="0"
+ */
+ @XmlElement(name = "value", namespace = "http://naesb.org/espi/customer")
+ private Float value;
+}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerMapper.java
index 10c63f10..ff1a2e57 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerMapper.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerMapper.java
@@ -20,8 +20,10 @@
package org.greenbuttonalliance.espi.common.mapper.customer;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
import org.greenbuttonalliance.espi.common.domain.customer.entity.PhoneNumberEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.dto.customer.CustomerDto;
import org.greenbuttonalliance.espi.common.dto.customer.ElectronicAddressDto;
import org.greenbuttonalliance.espi.common.mapper.BaseIdentifiedObjectMapper;
@@ -139,7 +141,7 @@ default Organisation mapOrganisationFromDto(CustomerDto.OrganisationDto orgDto)
}
// Helper methods for address mapping
- default CustomerDto.StreetAddressDto mapStreetAddress(Organisation.StreetAddress address) {
+ default CustomerDto.StreetAddressDto mapStreetAddress(StreetAddress address) {
if (address == null) return null;
return new CustomerDto.StreetAddressDto(
address.getStreetDetail(),
@@ -150,9 +152,9 @@ default CustomerDto.StreetAddressDto mapStreetAddress(Organisation.StreetAddress
);
}
- default Organisation.StreetAddress mapStreetAddressFromDto(CustomerDto.StreetAddressDto dto) {
+ default StreetAddress mapStreetAddressFromDto(CustomerDto.StreetAddressDto dto) {
if (dto == null) return null;
- Organisation.StreetAddress address = new Organisation.StreetAddress();
+ StreetAddress address = new StreetAddress();
address.setStreetDetail(dto.getStreetDetail());
address.setTownDetail(dto.getTownDetail());
address.setStateOrProvince(dto.getStateOrProvince());
@@ -166,7 +168,7 @@ default Organisation.StreetAddress mapStreetAddressFromDto(CustomerDto.StreetAdd
* Delegates to simple field-to-field copy since ElectronicAddressMapper
* is not directly accessible from default interface methods.
*/
- default ElectronicAddressDto mapElectronicAddress(Organisation.ElectronicAddress address) {
+ default ElectronicAddressDto mapElectronicAddress(ElectronicAddress address) {
if (address == null) return null;
return new ElectronicAddressDto(
address.getLan(),
@@ -185,9 +187,9 @@ default ElectronicAddressDto mapElectronicAddress(Organisation.ElectronicAddress
* Delegates to simple field-to-field copy since ElectronicAddressMapper
* is not directly accessible from default interface methods.
*/
- default Organisation.ElectronicAddress mapElectronicAddressFromDto(ElectronicAddressDto dto) {
+ default ElectronicAddress mapElectronicAddressFromDto(ElectronicAddressDto dto) {
if (dto == null) return null;
- Organisation.ElectronicAddress address = new Organisation.ElectronicAddress();
+ ElectronicAddress address = new ElectronicAddress();
address.setLan(dto.getLan());
address.setMac(dto.getMac());
address.setEmail1(dto.getEmail1());
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ElectronicAddressMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ElectronicAddressMapper.java
index 4d1a6ca2..f6824bab 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ElectronicAddressMapper.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ElectronicAddressMapper.java
@@ -19,7 +19,7 @@
package org.greenbuttonalliance.espi.common.mapper.customer;
-import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.dto.customer.ElectronicAddressDto;
import org.mapstruct.Mapper;
@@ -35,7 +35,7 @@ public interface ElectronicAddressMapper {
* @param entity the electronic address entity
* @return the electronic address DTO
*/
- ElectronicAddressDto toDto(Organisation.ElectronicAddress entity);
+ ElectronicAddressDto toDto(ElectronicAddress entity);
/**
* Converts an ElectronicAddress DTO to an entity.
@@ -43,5 +43,5 @@ public interface ElectronicAddressMapper {
* @param dto the electronic address DTO
* @return the electronic address entity
*/
- Organisation.ElectronicAddress toEntity(ElectronicAddressDto dto);
+ ElectronicAddress toEntity(ElectronicAddressDto dto);
}
\ No newline at end of file
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/MeterMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/MeterMapper.java
new file mode 100644
index 00000000..9623cfb0
--- /dev/null
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/MeterMapper.java
@@ -0,0 +1,123 @@
+/*
+ *
+ * Copyright (c) 2025 Green Button Alliance, Inc.
+ *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.greenbuttonalliance.espi.common.mapper.customer;
+
+import org.greenbuttonalliance.espi.common.domain.customer.entity.MeterEntity;
+import org.greenbuttonalliance.espi.common.dto.customer.MeterDto;
+import org.greenbuttonalliance.espi.common.mapper.BaseMapperUtils;
+import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+/**
+ * MapStruct mapper for converting between MeterEntity and MeterDto.
+ *
+ * Handles the conversion between the JPA entity used for persistence and the DTO
+ * used for JAXB XML marshalling in the Green Button API.
+ *
+ * Maps customer.xsd Meter fields (lines 243-268) including:
+ * - Asset fields (12) - lines 650-709
+ * - EndDevice fields (4) - lines 219-238
+ * - Meter fields (3) - lines 250-264
+ *
+ * Note: meterMultipliers collection is deferred (commented out in entity).
+ */
+@Mapper(componentModel = "spring", uses = {
+ DateTimeMapper.class,
+ BaseMapperUtils.class,
+ ElectronicAddressMapper.class,
+ LifecycleDateMapper.class,
+ AcceptanceTestMapper.class,
+ StatusMapper.class
+})
+public interface MeterMapper {
+
+ /**
+ * Converts a MeterEntity to a MeterDto.
+ * Maps all Asset fields (12), EndDevice fields (4), and Meter fields (3) per customer.xsd.
+ *
+ * @param entity the meter entity
+ * @return the meter DTO
+ */
+ // Asset fields (12) - customer.xsd lines 650-709
+ @Mapping(target = "type", source = "type")
+ @Mapping(target = "utcNumber", source = "utcNumber")
+ @Mapping(target = "serialNumber", source = "serialNumber")
+ @Mapping(target = "lotNumber", source = "lotNumber")
+ @Mapping(target = "purchasePrice", source = "purchasePrice")
+ @Mapping(target = "critical", source = "critical")
+ @Mapping(target = "electronicAddress", source = "electronicAddress")
+ @Mapping(target = "lifecycle", source = "lifecycle")
+ @Mapping(target = "acceptanceTest", source = "acceptanceTest")
+ @Mapping(target = "initialCondition", source = "initialCondition")
+ @Mapping(target = "initialLossOfLife", source = "initialLossOfLife")
+ @Mapping(target = "status", source = "status")
+ // EndDevice fields (4) - customer.xsd lines 219-238
+ @Mapping(target = "isVirtual", source = "isVirtual")
+ @Mapping(target = "isPan", source = "isPan")
+ @Mapping(target = "installCode", source = "installCode")
+ @Mapping(target = "amrSystem", source = "amrSystem")
+ // Meter fields (3) - customer.xsd lines 250-264
+ @Mapping(target = "formNumber", source = "formNumber")
+ @Mapping(target = "meterMultipliers", ignore = true) // TODO: Implement when MeterMultiplierEntity exists
+ @Mapping(target = "intervalLength", source = "intervalLength")
+ MeterDto toDto(MeterEntity entity);
+
+ /**
+ * Converts a MeterDto to a MeterEntity.
+ * Maps all Asset fields (12), EndDevice fields (4), and Meter fields (3) per customer.xsd.
+ *
+ * @param dto the meter DTO
+ * @return the meter entity
+ */
+ // Asset fields (12) - customer.xsd lines 650-709
+ @Mapping(target = "type", source = "type")
+ @Mapping(target = "utcNumber", source = "utcNumber")
+ @Mapping(target = "serialNumber", source = "serialNumber")
+ @Mapping(target = "lotNumber", source = "lotNumber")
+ @Mapping(target = "purchasePrice", source = "purchasePrice")
+ @Mapping(target = "critical", source = "critical")
+ @Mapping(target = "electronicAddress", source = "electronicAddress")
+ @Mapping(target = "lifecycle", source = "lifecycle")
+ @Mapping(target = "acceptanceTest", source = "acceptanceTest")
+ @Mapping(target = "initialCondition", source = "initialCondition")
+ @Mapping(target = "initialLossOfLife", source = "initialLossOfLife")
+ @Mapping(target = "status", source = "status")
+ // EndDevice fields (4) - customer.xsd lines 219-238
+ @Mapping(target = "isVirtual", source = "isVirtual")
+ @Mapping(target = "isPan", source = "isPan")
+ @Mapping(target = "installCode", source = "installCode")
+ @Mapping(target = "amrSystem", source = "amrSystem")
+ // Meter fields (3) - customer.xsd lines 250-264
+ @Mapping(target = "formNumber", source = "formNumber")
+ // meterMultipliers not mapped - collection commented out in entity (TODO)
+ @Mapping(target = "intervalLength", source = "intervalLength")
+ // IdentifiedObject fields (inherited) - handled by Atom layer, ignore during mapping
+ @Mapping(target = "description", ignore = true)
+ @Mapping(target = "created", ignore = true)
+ @Mapping(target = "updated", ignore = true)
+ @Mapping(target = "published", ignore = true)
+ @Mapping(target = "selfLink", ignore = true)
+ @Mapping(target = "upLink", ignore = true)
+ // JPA entity-only fields
+ @Mapping(target = "relatedLinks", ignore = true)
+ @Mapping(target = "relatedLinkHrefs", ignore = true)
+ MeterEntity toEntity(MeterDto dto);
+}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java
index 897ec740..50c9df6e 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ServiceLocationMapper.java
@@ -19,9 +19,11 @@
package org.greenbuttonalliance.espi.common.mapper.customer;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
import org.greenbuttonalliance.espi.common.domain.customer.entity.ServiceLocationEntity;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.dto.customer.CustomerDto;
import org.greenbuttonalliance.espi.common.dto.customer.ServiceLocationDto;
import org.greenbuttonalliance.espi.common.dto.customer.StatusDto;
@@ -103,9 +105,9 @@ public interface ServiceLocationMapper {
// Helper methods for embedded type conversions
/**
- * Maps Organisation.StreetAddress entity to CustomerDto.StreetAddressDto.
+ * Maps StreetAddress entity to CustomerDto.StreetAddressDto.
*/
- default CustomerDto.StreetAddressDto map(Organisation.StreetAddress address) {
+ default CustomerDto.StreetAddressDto map(StreetAddress address) {
if (address == null) return null;
return new CustomerDto.StreetAddressDto(
address.getStreetDetail(),
@@ -117,11 +119,11 @@ default CustomerDto.StreetAddressDto map(Organisation.StreetAddress address) {
}
/**
- * Maps CustomerDto.StreetAddressDto to Organisation.StreetAddress entity.
+ * Maps CustomerDto.StreetAddressDto to StreetAddress entity.
*/
- default Organisation.StreetAddress map(CustomerDto.StreetAddressDto dto) {
+ default StreetAddress map(CustomerDto.StreetAddressDto dto) {
if (dto == null) return null;
- Organisation.StreetAddress address = new Organisation.StreetAddress();
+ StreetAddress address = new StreetAddress();
address.setStreetDetail(dto.getStreetDetail());
address.setTownDetail(dto.getTownDetail());
address.setStateOrProvince(dto.getStateOrProvince());
@@ -131,9 +133,9 @@ default Organisation.StreetAddress map(CustomerDto.StreetAddressDto dto) {
}
/**
- * Maps Organisation.TelephoneNumber entity to CustomerDto.TelephoneNumberDto.
+ * Maps TelephoneNumber entity to CustomerDto.TelephoneNumberDto.
*/
- default CustomerDto.TelephoneNumberDto mapTelephone(Organisation.TelephoneNumber phone) {
+ default CustomerDto.TelephoneNumberDto mapTelephone(TelephoneNumber phone) {
if (phone == null) return null;
return new CustomerDto.TelephoneNumberDto(
phone.getCountryCode(),
@@ -148,11 +150,11 @@ default CustomerDto.TelephoneNumberDto mapTelephone(Organisation.TelephoneNumber
}
/**
- * Maps CustomerDto.TelephoneNumberDto to Organisation.TelephoneNumber entity.
+ * Maps CustomerDto.TelephoneNumberDto to TelephoneNumber entity.
*/
- default Organisation.TelephoneNumber mapTelephone(CustomerDto.TelephoneNumberDto dto) {
+ default TelephoneNumber mapTelephone(CustomerDto.TelephoneNumberDto dto) {
if (dto == null) return null;
- return new Organisation.TelephoneNumber(
+ return new TelephoneNumber(
dto.getCountryCode(),
dto.getAreaCode(),
dto.getCityCode(),
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/StreetAddressMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/StreetAddressMapper.java
index f2e3e629..0b2757c9 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/StreetAddressMapper.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/StreetAddressMapper.java
@@ -19,7 +19,7 @@
package org.greenbuttonalliance.espi.common.mapper.customer;
-import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.dto.customer.CustomerDto;
import org.mapstruct.Mapper;
@@ -35,7 +35,7 @@ public interface StreetAddressMapper {
* @param entity the street address entity
* @return the street address DTO
*/
- CustomerDto.StreetAddressDto toDto(Organisation.StreetAddress entity);
+ CustomerDto.StreetAddressDto toDto(StreetAddress entity);
/**
* Converts a StreetAddress DTO to an entity.
@@ -43,5 +43,5 @@ public interface StreetAddressMapper {
* @param dto the street address DTO
* @return the street address entity
*/
- Organisation.StreetAddress toEntity(CustomerDto.StreetAddressDto dto);
+ StreetAddress toEntity(CustomerDto.StreetAddressDto dto);
}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/MeterRepository.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/MeterRepository.java
index 8f85cee5..49ac334a 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/MeterRepository.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/MeterRepository.java
@@ -21,79 +21,17 @@
import org.greenbuttonalliance.espi.common.domain.customer.entity.MeterEntity;
import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
-import java.util.List;
-import java.util.Optional;
import java.util.UUID;
/**
* Spring Data JPA repository for Meter entities.
*
- * Manages physical metering device data including serial numbers, form numbers, and installation details.
+ * Manages physical metering device data.
+ * Per ESPI 4.0 compliance: ONLY inherited JpaRepository methods (no custom queries).
*/
@Repository
public interface MeterRepository extends JpaRepository {
-
- /**
- * Find meter by serial number.
- */
- @Query("SELECT m FROM MeterEntity m WHERE m.serialNumber = :serialNumber")
- Optional findBySerialNumber(@Param("serialNumber") String serialNumber);
-
- /**
- * Find meters by form number.
- */
- @Query("SELECT m FROM MeterEntity m WHERE m.formNumber = :formNumber")
- List findByFormNumber(@Param("formNumber") String formNumber);
-
- /**
- * Find meters by UTC number.
- */
- @Query("SELECT m FROM MeterEntity m WHERE m.utcNumber = :utcNumber")
- List findByUtcNumber(@Param("utcNumber") String utcNumber);
-
- /**
- * Find virtual meters.
- */
- @Query("SELECT m FROM MeterEntity m WHERE m.isVirtual = true")
- List findVirtualMeters();
-
- /**
- * Find physical meters.
- */
- @Query("SELECT m FROM MeterEntity m WHERE m.isVirtual = false OR m.isVirtual IS NULL")
- List findPhysicalMeters();
-
- /**
- * Find PAN devices.
- */
- @Query("SELECT m FROM MeterEntity m WHERE m.isPan = true")
- List findPanDevices();
-
- /**
- * Find meters by AMR system.
- */
- @Query("SELECT m FROM MeterEntity m WHERE m.amrSystem = :amrSystem")
- List findByAmrSystem(@Param("amrSystem") String amrSystem);
-
- /**
- * Find meters by install code.
- */
- @Query("SELECT m FROM MeterEntity m WHERE m.installCode = :installCode")
- List findByInstallCode(@Param("installCode") String installCode);
-
- /**
- * Find meters with interval length greater than specified seconds.
- */
- @Query("SELECT m FROM MeterEntity m WHERE m.intervalLength > :seconds")
- List findByIntervalLengthGreaterThan(@Param("seconds") Long seconds);
-
- /**
- * Find critical meters.
- */
- @Query("SELECT m FROM MeterEntity m WHERE m.critical = true")
- List findCriticalMeters();
+ // ONLY inherited methods - NO custom queries
}
\ No newline at end of file
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/MeterService.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/MeterService.java
index 186b08b7..79ed286d 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/MeterService.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/MeterService.java
@@ -27,104 +27,56 @@
/**
* Service interface for Meter management.
- *
- * Handles physical metering device operations including serial numbers,
- * form numbers, AMR systems, and device characteristics.
+ *
+ * Per ESPI 4.0 compliance: ONLY 6 CRUD methods (no custom queries).
+ * Handles physical metering device operations.
*/
public interface MeterService {
/**
* Find all meters.
+ *
+ * @return list of all meters
*/
List findAll();
/**
* Find meter by ID.
+ *
+ * @param id meter UUID
+ * @return optional meter entity
*/
Optional findById(UUID id);
/**
- * Find meter by UUID.
- */
- Optional findByUuid(String uuid);
-
- /**
- * Find meter by serial number.
- */
- Optional findBySerialNumber(String serialNumber);
-
- /**
- * Find meters by form number.
- */
- List findByFormNumber(String formNumber);
-
- /**
- * Find meters by UTC number.
- */
- List findByUtcNumber(String utcNumber);
-
- /**
- * Find virtual meters.
- */
- List findVirtualMeters();
-
- /**
- * Find physical meters.
- */
- List findPhysicalMeters();
-
- /**
- * Find PAN devices.
- */
- List findPanDevices();
-
- /**
- * Find meters by AMR system.
- */
- List findByAmrSystem(String amrSystem);
-
- /**
- * Find meters by install code.
- */
- List findByInstallCode(String installCode);
-
- /**
- * Find meters with interval length greater than specified seconds.
- */
- List findByIntervalLengthGreaterThan(Long seconds);
-
- /**
- * Find critical meters.
- */
- List findCriticalMeters();
-
- /**
- * Save meter.
+ * Save meter with UUID v5 generation.
+ * If meter.id is null, generates deterministic UUID v5 from serialNumber.
+ *
+ * @param meter meter entity to save
+ * @return saved meter entity
+ * @throws IllegalArgumentException if serialNumber is null when ID generation is needed
*/
MeterEntity save(MeterEntity meter);
/**
* Delete meter by ID.
+ *
+ * @param id meter UUID to delete
*/
void deleteById(UUID id);
/**
- * Check if meter exists by serial number.
+ * Check if meter exists by ID.
+ *
+ * @param id meter UUID
+ * @return true if meter exists
*/
- boolean existsBySerialNumber(String serialNumber);
+ boolean existsById(UUID id);
/**
* Count total meters.
+ *
+ * @return total count of meters
*/
- long countMeters();
-
- /**
- * Count virtual meters.
- */
- long countVirtualMeters();
-
- /**
- * Count physical meters.
- */
- long countPhysicalMeters();
+ long count();
}
\ No newline at end of file
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/MeterServiceImpl.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/MeterServiceImpl.java
index 6244afba..b2b36a99 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/MeterServiceImpl.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/MeterServiceImpl.java
@@ -20,8 +20,10 @@
package org.greenbuttonalliance.espi.common.service.customer.impl;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.greenbuttonalliance.espi.common.domain.customer.entity.MeterEntity;
import org.greenbuttonalliance.espi.common.repositories.customer.MeterRepository;
+import org.greenbuttonalliance.espi.common.service.EspiIdGeneratorService;
import org.greenbuttonalliance.espi.common.service.customer.MeterService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -32,129 +34,62 @@
/**
* Service implementation for Meter management.
- *
- * Provides business logic for physical metering device operations.
+ *
+ * Per ESPI 4.0 compliance: Uses UUID v5 generation (NO random fallback).
+ * Provides ONLY 6 CRUD methods.
*/
@Service
-@Transactional
+@Slf4j
@RequiredArgsConstructor
public class MeterServiceImpl implements MeterService {
- private final MeterRepository meterRepository;
-
- @Override
- @Transactional(readOnly = true)
- public List findAll() {
- return meterRepository.findAll();
- }
-
- @Override
- @Transactional(readOnly = true)
- public Optional findById(UUID id) {
- return meterRepository.findById(id);
- }
-
- @Override
- @Transactional(readOnly = true)
- public Optional findByUuid(String uuid) {
- return meterRepository.findById(UUID.fromString(uuid));
- }
-
- @Override
- @Transactional(readOnly = true)
- public Optional findBySerialNumber(String serialNumber) {
- return meterRepository.findBySerialNumber(serialNumber);
- }
-
- @Override
- @Transactional(readOnly = true)
- public List findByFormNumber(String formNumber) {
- return meterRepository.findByFormNumber(formNumber);
- }
-
- @Override
- @Transactional(readOnly = true)
- public List findByUtcNumber(String utcNumber) {
- return meterRepository.findByUtcNumber(utcNumber);
- }
-
- @Override
- @Transactional(readOnly = true)
- public List findVirtualMeters() {
- return meterRepository.findVirtualMeters();
- }
-
- @Override
- @Transactional(readOnly = true)
- public List findPhysicalMeters() {
- return meterRepository.findPhysicalMeters();
- }
-
- @Override
- @Transactional(readOnly = true)
- public List findPanDevices() {
- return meterRepository.findPanDevices();
- }
-
- @Override
- @Transactional(readOnly = true)
- public List findByAmrSystem(String amrSystem) {
- return meterRepository.findByAmrSystem(amrSystem);
- }
-
- @Override
- @Transactional(readOnly = true)
- public List findByInstallCode(String installCode) {
- return meterRepository.findByInstallCode(installCode);
- }
-
- @Override
- @Transactional(readOnly = true)
- public List findByIntervalLengthGreaterThan(Long seconds) {
- return meterRepository.findByIntervalLengthGreaterThan(seconds);
- }
-
- @Override
- @Transactional(readOnly = true)
- public List findCriticalMeters() {
- return meterRepository.findCriticalMeters();
- }
+ private final MeterRepository repository;
+ private final EspiIdGeneratorService idGenerator;
@Override
+ @Transactional
public MeterEntity save(MeterEntity meter) {
- // Generate UUID if not present
if (meter.getId() == null) {
- meter.setId(UUID.randomUUID());
+ // ❌ 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.generateEntityId(
+ "Meter", meter.getSerialNumber());
+ meter.setId(deterministicId);
+ log.debug("Generated UUID v5 for Meter: {}", deterministicId);
}
- return meterRepository.save(meter);
+ return repository.save(meter);
}
@Override
- public void deleteById(UUID id) {
- meterRepository.deleteById(id);
+ @Transactional(readOnly = true)
+ public List findAll() {
+ return repository.findAll();
}
@Override
@Transactional(readOnly = true)
- public boolean existsBySerialNumber(String serialNumber) {
- return meterRepository.findBySerialNumber(serialNumber).isPresent();
+ public Optional findById(UUID id) {
+ return repository.findById(id);
}
@Override
- @Transactional(readOnly = true)
- public long countMeters() {
- return meterRepository.count();
+ @Transactional
+ public void deleteById(UUID id) {
+ repository.deleteById(id);
}
@Override
@Transactional(readOnly = true)
- public long countVirtualMeters() {
- return meterRepository.findVirtualMeters().size();
+ public boolean existsById(UUID id) {
+ return repository.existsById(id);
}
@Override
@Transactional(readOnly = true)
- public long countPhysicalMeters() {
- return meterRepository.findPhysicalMeters().size();
+ public long count() {
+ return repository.count();
}
-}
\ No newline at end of file
+}
diff --git a/openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql b/openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql
index d4b06bde..e334859c 100644
--- a/openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql
+++ b/openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql
@@ -523,6 +523,18 @@ CREATE TABLE meters
CREATE INDEX idx_meters_form_number ON meters (form_number);
+-- Meter Multipliers Collection Table (@ElementCollection for MeterEntity.meterMultipliers)
+-- Per customer.xsd MeterMultiplier type (embedded collection)
+CREATE TABLE meter_multipliers
+(
+ meter_id CHAR(36) NOT NULL,
+ multiplier_kind VARCHAR(256),
+ multiplier_value DECIMAL(19, 4),
+ FOREIGN KEY (meter_id) REFERENCES meters (id) ON DELETE CASCADE
+);
+
+CREATE INDEX idx_meter_multipliers_meter_id ON meter_multipliers (meter_id);
+
-- Related Links Table for Meters
CREATE TABLE meter_related_links
(
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/MigrationVerificationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/MigrationVerificationTest.java
index ab290340..8ff0e864 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/MigrationVerificationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/MigrationVerificationTest.java
@@ -22,10 +22,20 @@
import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject;
import org.greenbuttonalliance.espi.common.domain.usage.UsagePointEntity;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.MeterEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.ServiceLocationEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.enums.CustomerKind;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.dto.atom.UsageAtomEntryDto;
import org.greenbuttonalliance.espi.common.dto.usage.UsagePointDto;
import org.greenbuttonalliance.espi.common.dto.SummaryMeasurementDto;
@@ -232,7 +242,7 @@ void customerWithAllEmbeddedObjectsShouldWork() {
org.setOrganisationName("Test Organisation");
// Test StreetAddress nested embedded object
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail("123 Test Street");
streetAddress.setTownDetail("Test City");
streetAddress.setStateOrProvince("CA");
@@ -241,7 +251,7 @@ void customerWithAllEmbeddedObjectsShouldWork() {
org.setStreetAddress(streetAddress);
// Test ElectronicAddress nested embedded object
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("test@example.com");
electronicAddress.setWeb("https://example.com");
org.setElectronicAddress(electronicAddress);
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/MeterDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/MeterDtoTest.java
new file mode 100644
index 00000000..32b78b97
--- /dev/null
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/MeterDtoTest.java
@@ -0,0 +1,418 @@
+/*
+ *
+ * Copyright (c) 2025 Green Button Alliance, Inc.
+ *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.greenbuttonalliance.espi.common.dto.customer;
+
+import org.greenbuttonalliance.espi.common.domain.customer.enums.MeterMultiplierKind;
+import org.greenbuttonalliance.espi.common.dto.atom.CustomerAtomEntryDto;
+import org.greenbuttonalliance.espi.common.dto.atom.AtomFeedDto;
+import org.greenbuttonalliance.espi.common.service.impl.DtoExportServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * XML marshalling/unmarshalling tests for MeterDto.
+ * Verifies Jakarta JAXB Marshaller processes JAXB annotations correctly for ESPI 4.0 customer.xsd compliance.
+ * Meter extends EndDevice which extends Asset - tests verify all 19 fields (12 Asset + 4 EndDevice + 3 Meter).
+ */
+@DisplayName("MeterDto XML Marshalling Tests")
+class MeterDtoTest {
+
+ private DtoExportServiceImpl dtoExportService;
+
+ @BeforeEach
+ void setUp() {
+ // Initialize DtoExportService with null repository/mapper (not needed for DTO-only tests)
+ org.greenbuttonalliance.espi.common.service.EspiIdGeneratorService espiIdGeneratorService =
+ new org.greenbuttonalliance.espi.common.service.EspiIdGeneratorService();
+ dtoExportService = new DtoExportServiceImpl(null, null, espiIdGeneratorService);
+ }
+
+ @Test
+ @DisplayName("Should export Meter with complete realistic data")
+ void shouldExportMeterWithRealisticData() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ MeterDto meter = createFullMeterDto();
+ CustomerAtomEntryDto entry = new CustomerAtomEntryDto(
+ "urn:uuid:770e8400-e29b-51d4-a716-446655440000",
+ "Electric Meter Device",
+ now, now, null, meter
+ );
+
+ AtomFeedDto feed = new AtomFeedDto(
+ "urn:uuid:feed-id", "Meter Feed", now, now, null,
+ List.of(entry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(feed, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Debug output
+ System.out.println("========== Meter XML Output ==========");
+ System.out.println(xml);
+ System.out.println("======================================");
+
+ // Assert - Basic structure and namespaces
+ assertThat(xml)
+ .startsWith("")
+ .contains("");
+
+ // Assert - Asset fields present (12)
+ assertThat(xml)
+ .contains("ELECTRIC_METER")
+ .contains("EM-2025-12345")
+ .contains("UTC-67890")
+ .contains("LOT-2025-BATCH-A")
+ .contains("35000")
+ .contains("true");
+
+ // Assert - EndDevice fields present (4)
+ assertThat(xml)
+ .contains("false")
+ .contains("false")
+ .contains("INST-METER-9876")
+ .contains("OpenWay CENTRON");
+
+ // Assert - Meter specific fields present (3)
+ assertThat(xml)
+ .contains("16S")
+ .contains("900");
+ }
+
+ @Test
+ @DisplayName("Should verify Meter field order matches customer.xsd")
+ void shouldVerifyMeterFieldOrder() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ MeterDto meter = createFullMeterDto();
+ CustomerAtomEntryDto entry = new CustomerAtomEntryDto(
+ "urn:uuid:770e8400-e29b-51d4-a716-446655440001",
+ "Test Meter",
+ now, now, null, meter
+ );
+
+ AtomFeedDto feed = new AtomFeedDto(
+ "urn:uuid:feed-id", "Test Feed", now, now, null,
+ List.of(entry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(feed, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - Verify XSD element order per customer.xsd
+ // Asset fields (12): type, utcNumber, serialNumber, lotNumber, purchasePrice, critical,
+ // electronicAddress, lifecycle, acceptanceTest, initialCondition, initialLossOfLife, status
+ // EndDevice fields (4): isVirtual, isPan, installCode, amrSystem
+ // Meter fields (3): formNumber, meterMultipliers, intervalLength
+ int typePos = xml.indexOf("");
+ int utcNumberPos = xml.indexOf("");
+ int serialNumberPos = xml.indexOf("");
+ int lotNumberPos = xml.indexOf("");
+ int purchasePricePos = xml.indexOf("");
+ int criticalPos = xml.indexOf("");
+ int electronicAddressPos = xml.indexOf("");
+ int lifecyclePos = xml.indexOf("");
+ int acceptanceTestPos = xml.indexOf("");
+ int initialConditionPos = xml.indexOf("");
+ int initialLossOfLifePos = xml.indexOf("");
+ int statusPos = xml.indexOf("");
+ int isVirtualPos = xml.indexOf("");
+ int isPanPos = xml.indexOf("");
+ int installCodePos = xml.indexOf("");
+ int amrSystemPos = xml.indexOf("");
+ int formNumberPos = xml.indexOf("");
+ int intervalLengthPos = xml.indexOf("");
+
+ // Assert - Field ordering with chained assertions
+ assertThat(typePos)
+ .isGreaterThan(0)
+ .isLessThan(utcNumberPos);
+ assertThat(utcNumberPos).isLessThan(serialNumberPos);
+ assertThat(serialNumberPos).isLessThan(lotNumberPos);
+ assertThat(lotNumberPos).isLessThan(purchasePricePos);
+ assertThat(purchasePricePos).isLessThan(criticalPos);
+ assertThat(criticalPos).isLessThan(electronicAddressPos);
+ assertThat(electronicAddressPos).isLessThan(lifecyclePos);
+ assertThat(lifecyclePos).isLessThan(acceptanceTestPos);
+ assertThat(acceptanceTestPos).isLessThan(initialConditionPos);
+ assertThat(initialConditionPos).isLessThan(initialLossOfLifePos);
+ assertThat(initialLossOfLifePos).isLessThan(statusPos);
+ assertThat(statusPos).isLessThan(isVirtualPos);
+ assertThat(isVirtualPos).isLessThan(isPanPos);
+ assertThat(isPanPos).isLessThan(installCodePos);
+ assertThat(installCodePos).isLessThan(amrSystemPos);
+ assertThat(amrSystemPos).isLessThan(formNumberPos);
+ // meterMultipliers would be between formNumber and intervalLength (if present)
+ assertThat(formNumberPos).isLessThan(intervalLengthPos);
+ }
+
+ @Test
+ @DisplayName("Should export minimal Meter with only required fields")
+ void shouldExportMinimalMeter() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ MeterDto meter = new MeterDto();
+ // No required fields per XSD - all fields are optional
+ meter.setSerialNumber("MINIMAL-METER-001");
+ meter.setFormNumber("1S");
+
+ CustomerAtomEntryDto entry = new CustomerAtomEntryDto(
+ "urn:uuid:770e8400-e29b-51d4-a716-446655440002",
+ "Minimal Meter",
+ now, now, null, meter
+ );
+
+ AtomFeedDto feed = new AtomFeedDto(
+ "urn:uuid:feed-id", "Minimal Feed", now, now, null,
+ List.of(entry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(feed, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - Basic structure present even with minimal data
+ assertThat(xml)
+ .contains("")
+ .contains("MINIMAL-METER-001")
+ .contains("1S");
+ }
+
+ @Test
+ @DisplayName("Should export Meter with lifecycle dates")
+ void shouldExportMeterWithLifecycleDates() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ LifecycleDateDto lifecycle = new LifecycleDateDto(
+ now.minusDays(120), // manufacturedDate
+ now.minusDays(45) // installationDate
+ );
+
+ MeterDto meter = new MeterDto();
+ meter.setSerialNumber("LC-METER-001");
+ meter.setLifecycle(lifecycle);
+
+ CustomerAtomEntryDto entry = new CustomerAtomEntryDto(
+ "urn:uuid:770e8400-e29b-51d4-a716-446655440003",
+ "Lifecycle Meter",
+ now, now, null, meter
+ );
+
+ AtomFeedDto feed = new AtomFeedDto(
+ "urn:uuid:feed-id", "Lifecycle Feed", now, now, null,
+ List.of(entry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(feed, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - Lifecycle dates present
+ assertThat(xml)
+ .contains("")
+ .contains("")
+ .contains("")
+ .contains("");
+ }
+
+ @Test
+ @DisplayName("Should export Meter with acceptance test")
+ void shouldExportMeterWithAcceptanceTest() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ AcceptanceTestDto acceptanceTest = new AcceptanceTestDto(
+ now.minusDays(10), // dateTime
+ true, // success
+ "FIELD_CALIBRATION", // type
+ "Meter accuracy verified" // remark
+ );
+
+ MeterDto meter = new MeterDto();
+ meter.setSerialNumber("AT-METER-001");
+ meter.setAcceptanceTest(acceptanceTest);
+
+ CustomerAtomEntryDto entry = new CustomerAtomEntryDto(
+ "urn:uuid:770e8400-e29b-51d4-a716-446655440004",
+ "AcceptanceTest Meter",
+ now, now, null, meter
+ );
+
+ AtomFeedDto feed = new AtomFeedDto(
+ "urn:uuid:feed-id", "AcceptanceTest Feed", now, now, null,
+ List.of(entry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(feed, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - Acceptance test present
+ assertThat(xml)
+ .contains("")
+ .contains("")
+ .contains("true")
+ .contains("FIELD_CALIBRATION")
+ .contains("Meter accuracy verified")
+ .contains("");
+ }
+
+ @Test
+ @DisplayName("Should export Meter with meterMultipliers collection")
+ void shouldExportMeterWithMeterMultipliers() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ MeterMultiplierDto ctRatio = new MeterMultiplierDto(MeterMultiplierKind.CT_RATIO, 120.0f);
+ MeterMultiplierDto ptRatio = new MeterMultiplierDto(MeterMultiplierKind.PT_RATIO, 60.0f);
+
+ MeterDto meter = new MeterDto();
+ meter.setSerialNumber("MM-METER-001");
+ meter.setFormNumber("16S");
+ meter.setMeterMultipliers(List.of(ctRatio, ptRatio));
+
+ CustomerAtomEntryDto entry = new CustomerAtomEntryDto(
+ "urn:uuid:770e8400-e29b-51d4-a716-446655440005",
+ "MeterMultipliers Meter",
+ now, now, null, meter
+ );
+
+ AtomFeedDto feed = new AtomFeedDto(
+ "urn:uuid:feed-id", "MeterMultipliers Feed", now, now, null,
+ List.of(entry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(feed, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - MeterMultipliers collection present
+ assertThat(xml)
+ .contains("")
+ .contains("ctRatio")
+ .contains("120.0")
+ .contains("")
+ .contains("")
+ .contains("ptRatio")
+ .contains("60.0");
+ }
+
+ /**
+ * Helper method to create a fully populated MeterDto for testing.
+ * Creates all 19 fields: 12 Asset + 4 EndDevice + 3 Meter.
+ */
+ private MeterDto createFullMeterDto() {
+ ElectronicAddressDto electronicAddress = new ElectronicAddressDto(
+ "192.168.50.100", // lan
+ "00:1B:44:11:3A:B7", // mac
+ "meter@pge.com", // email1
+ null, // email2
+ "https://meter-portal.pge.com", // web
+ null, // radio
+ "meter_admin", // userID
+ null // password
+ );
+
+ LifecycleDateDto lifecycle = new LifecycleDateDto(
+ OffsetDateTime.now().minusDays(120), // manufacturedDate
+ OffsetDateTime.now().minusDays(45) // installationDate
+ );
+
+ AcceptanceTestDto acceptanceTest = new AcceptanceTestDto(
+ OffsetDateTime.now().minusDays(40), // dateTime
+ true, // success
+ "FIELD_CALIBRATION", // type
+ "All tests passed" // remark
+ );
+
+ StatusDto status = new StatusDto(
+ "COMMISSIONED", // value
+ OffsetDateTime.now().minusDays(39), // dateTime
+ "Meter is operational", // remark
+ "Installation and testing complete" // reason
+ );
+
+ MeterMultiplierDto ctRatio = new MeterMultiplierDto(MeterMultiplierKind.CT_RATIO, 200.0f);
+ MeterMultiplierDto ptRatio = new MeterMultiplierDto(MeterMultiplierKind.PT_RATIO, 120.0f);
+
+ // Create MeterDto with all 19 fields (12 Asset + 4 EndDevice + 3 Meter)
+ return new MeterDto(
+ // Asset fields (12)
+ "ELECTRIC_METER", // type
+ "UTC-67890", // utcNumber
+ "EM-2025-12345", // serialNumber
+ "LOT-2025-BATCH-A", // lotNumber
+ 35000L, // purchasePrice (in cents)
+ true, // critical
+ electronicAddress,
+ lifecycle,
+ acceptanceTest,
+ "NEW", // initialCondition
+ 0, // initialLossOfLife (0%)
+ status,
+ // EndDevice fields (4)
+ false, // isVirtual
+ false, // isPan
+ "INST-METER-9876", // installCode
+ "OpenWay CENTRON", // amrSystem
+ // Meter fields (3)
+ "16S", // formNumber (16S = 3-wire delta, 3-phase)
+ List.of(ctRatio, ptRatio), // meterMultipliers
+ 900L // intervalLength (15 minutes in seconds)
+ );
+ }
+}
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAccountRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAccountRepositoryTest.java
index 32d388ce..b966a8d2 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAccountRepositoryTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAccountRepositoryTest.java
@@ -19,9 +19,17 @@
package org.greenbuttonalliance.espi.common.repositories.customer;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAccountEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.test.BaseRepositoryTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@@ -81,7 +89,7 @@ private CustomerAccountEntity createValidCustomerAccount() {
account.setRevisionNumber("1.0");
// Document.electronicAddress
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1(faker.internet().emailAddress());
electronicAddress.setEmail2(faker.internet().emailAddress());
electronicAddress.setWeb(faker.internet().url());
@@ -105,7 +113,7 @@ private CustomerAccountEntity createValidCustomerAccount() {
// contactInfo Organisation embedded object
Organisation contactInfo = new Organisation();
contactInfo.setOrganisationName(faker.company().name());
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail(faker.address().streetAddress());
streetAddress.setTownDetail(faker.address().city());
streetAddress.setStateOrProvince(faker.address().stateAbbr());
@@ -305,7 +313,7 @@ void shouldPersistAllDocumentBaseFieldsCorrectly() {
void shouldPersistDocumentElectronicAddressEmbeddedObject() {
// Arrange
CustomerAccountEntity account = createCompleteTestSetup();
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("primary@example.com");
electronicAddress.setEmail2("secondary@example.com");
electronicAddress.setWeb("https://www.example.com");
@@ -393,7 +401,7 @@ void shouldPersistContactInfoOrganisationEmbeddedObject() {
Organisation contactInfo = new Organisation();
contactInfo.setOrganisationName("ACME Corporation");
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail("123 Main Street");
streetAddress.setTownDetail("Springfield");
streetAddress.setStateOrProvince("IL");
@@ -401,7 +409,7 @@ void shouldPersistContactInfoOrganisationEmbeddedObject() {
streetAddress.setCountry("USA");
contactInfo.setStreetAddress(streetAddress);
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("contact@acme.com");
electronicAddress.setWeb("https://www.acme.com");
contactInfo.setElectronicAddress(electronicAddress);
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepositoryTest.java
index 964429d4..d89dc8e9 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepositoryTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepositoryTest.java
@@ -22,6 +22,9 @@
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAgreementEntity;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.test.BaseRepositoryTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@@ -66,7 +69,7 @@ private CustomerAgreementEntity createValidCustomerAgreement() {
agreement.setRevisionNumber("1.0");
// Document.electronicAddress (customer.xsd lines 886-936)
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setLan("192.168." + faker.number().numberBetween(1, 255) + "." + faker.number().numberBetween(1, 255));
electronicAddress.setMac(faker.internet().macAddress());
electronicAddress.setEmail1(faker.internet().emailAddress());
@@ -266,7 +269,7 @@ void shouldPersistElectronicAddressWithAllFields() {
// Assert - ElectronicAddress fields (customer.xsd lines 886-936)
assertThat(retrieved).isPresent();
- Organisation.ElectronicAddress result = retrieved.get().getElectronicAddress();
+ ElectronicAddress result = retrieved.get().getElectronicAddress();
assertThat(result).isNotNull();
assertThat(result.getLan()).isEqualTo(agreement.getElectronicAddress().getLan());
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerRepositoryTest.java
index e79611b3..0d90a8e7 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerRepositoryTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerRepositoryTest.java
@@ -20,8 +20,17 @@
import jakarta.validation.ConstraintViolation;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.domain.customer.enums.CustomerKind;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.test.BaseRepositoryTest;
import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
import org.junit.jupiter.api.DisplayName;
@@ -204,7 +213,7 @@ void shouldPersistAndRetrieveOrganisation() {
Organisation org = new Organisation();
org.setOrganisationName("ACME Energy Services");
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail("123 Main Street");
streetAddress.setTownDetail("San Francisco");
streetAddress.setStateOrProvince("CA");
@@ -212,7 +221,7 @@ void shouldPersistAndRetrieveOrganisation() {
streetAddress.setCountry("USA");
org.setStreetAddress(streetAddress);
- Organisation.StreetAddress postalAddress = new Organisation.StreetAddress();
+ StreetAddress postalAddress = new StreetAddress();
postalAddress.setStreetDetail("PO Box 789");
postalAddress.setTownDetail("San Francisco");
postalAddress.setStateOrProvince("CA");
@@ -220,7 +229,7 @@ void shouldPersistAndRetrieveOrganisation() {
postalAddress.setCountry("USA");
org.setPostalAddress(postalAddress);
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("contact@acme.com");
electronicAddress.setEmail2("support@acme.com");
electronicAddress.setWeb("https://www.acme.com");
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/EndDeviceRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/EndDeviceRepositoryTest.java
index df52b1d9..edbf09b5 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/EndDeviceRepositoryTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/EndDeviceRepositoryTest.java
@@ -21,6 +21,7 @@
import org.greenbuttonalliance.espi.common.domain.customer.entity.Asset;
import org.greenbuttonalliance.espi.common.domain.customer.entity.EndDeviceEntity;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
import org.greenbuttonalliance.espi.common.test.BaseRepositoryTest;
import org.junit.jupiter.api.DisplayName;
@@ -295,7 +296,7 @@ void shouldPersistStatusCorrectly() {
@DisplayName("Should persist electronic address correctly")
void shouldPersistElectronicAddressCorrectly() {
// Arrange
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setLan("192.168.1.100");
electronicAddress.setMac("00:1A:2B:3C:4D:5E");
electronicAddress.setEmail1("meter@utility.com");
@@ -315,10 +316,10 @@ void shouldPersistElectronicAddressCorrectly() {
.hasValueSatisfying(device -> assertThat(device.getElectronicAddress())
.isNotNull()
.extracting(
- Organisation.ElectronicAddress::getLan,
- Organisation.ElectronicAddress::getMac,
- Organisation.ElectronicAddress::getEmail1,
- Organisation.ElectronicAddress::getWeb
+ ElectronicAddress::getLan,
+ ElectronicAddress::getMac,
+ ElectronicAddress::getEmail1,
+ ElectronicAddress::getWeb
)
.containsExactly("192.168.1.100", "00:1A:2B:3C:4D:5E",
"meter@utility.com", "https://meter.utility.com"));
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/MeterRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/MeterRepositoryTest.java
index c7c06b75..10127ff6 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/MeterRepositoryTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/MeterRepositoryTest.java
@@ -18,7 +18,11 @@
package org.greenbuttonalliance.espi.common.repositories.customer;
+import org.greenbuttonalliance.espi.common.domain.customer.entity.Asset;
import org.greenbuttonalliance.espi.common.domain.customer.entity.MeterEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.MeterMultiplier;
import org.greenbuttonalliance.espi.common.test.BaseRepositoryTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@@ -26,17 +30,18 @@
import org.springframework.beans.factory.annotation.Autowired;
import java.math.BigDecimal;
+import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
-import static org.assertj.core.api.Assertions.*;
+import static org.assertj.core.api.Assertions.assertThat;
/**
* Comprehensive test suite for MeterRepository.
- *
- * Tests all CRUD operations, 10 custom query methods, meter device field testing,
- * EndDeviceEntity inheritance testing, and IdentifiedObject base functionality.
+ *
+ * Tests all CRUD operations and validation constraints for Meter entities.
+ * Per ESPI 4.0 API specification, only default JpaRepository methods are supported.
*/
@DisplayName("Meter Repository Tests")
class MeterRepositoryTest extends BaseRepositoryTest {
@@ -44,66 +49,6 @@ class MeterRepositoryTest extends BaseRepositoryTest {
@Autowired
private MeterRepository meterRepository;
- /**
- * Creates a valid MeterEntity for testing.
- */
- private MeterEntity createValidMeter() {
- MeterEntity meter = new MeterEntity();
- meter.setDescription("Test Meter - " + faker.lorem().sentence(3));
-
- // MeterEntity specific fields
- meter.setFormNumber("FORM-" + faker.number().digits(4));
- meter.setIntervalLength(faker.number().numberBetween(300L, 3600L)); // 5 minutes to 1 hour
-
- // EndDeviceEntity inherited fields
- meter.setType("ELECTRIC_METER");
- meter.setUtcNumber("UTC-" + faker.number().digits(8));
- meter.setSerialNumber("SN-" + faker.number().digits(10));
- meter.setLotNumber("LOT-" + faker.number().digits(6));
- meter.setPurchasePrice(faker.number().numberBetween(50000L, 500000L)); // $500-$5000
- meter.setCritical(faker.bool().bool());
- meter.setInitialCondition("NEW");
- meter.setInitialLossOfLife(BigDecimal.ZERO);
- meter.setIsVirtual(false);
- meter.setIsPan(faker.bool().bool());
- meter.setInstallCode("INSTALL-" + faker.number().digits(8));
- meter.setAmrSystem("AMR-" + faker.company().name());
-
- return meter;
- }
-
- /**
- * Creates a virtual meter for testing.
- */
- private MeterEntity createVirtualMeter() {
- MeterEntity meter = createValidMeter();
- meter.setIsVirtual(true);
- meter.setType("VIRTUAL_METER");
- meter.setSerialNumber("VIRTUAL-" + faker.number().digits(8));
- return meter;
- }
-
- /**
- * Creates a PAN device meter for testing.
- */
- private MeterEntity createPanMeter() {
- MeterEntity meter = createValidMeter();
- meter.setIsPan(true);
- meter.setType("PAN_DEVICE");
- meter.setInstallCode("PAN-" + faker.number().digits(8));
- return meter;
- }
-
- /**
- * Creates a critical meter for testing.
- */
- private MeterEntity createCriticalMeter() {
- MeterEntity meter = createValidMeter();
- meter.setCritical(true);
- meter.setType("CRITICAL_METER");
- return meter;
- }
-
@Nested
@DisplayName("CRUD Operations")
class CrudOperationsTest {
@@ -113,8 +58,8 @@ class CrudOperationsTest {
void shouldSaveAndRetrieveMeterSuccessfully() {
// Arrange
MeterEntity meter = createValidMeter();
- meter.setDescription("Test Meter for CRUD");
- meter.setSerialNumber("CRUD-TEST-12345");
+ meter.setDescription("Test Electric Meter");
+ meter.setSerialNumber("SM-12345");
// Act
MeterEntity saved = meterRepository.save(meter);
@@ -122,33 +67,40 @@ void shouldSaveAndRetrieveMeterSuccessfully() {
Optional retrieved = meterRepository.findById(saved.getId());
// Assert
- assertThat(saved).isNotNull();
assertThat(saved.getId()).isNotNull();
- assertThat(retrieved).isPresent();
- assertThat(retrieved.get().getDescription()).isEqualTo("Test Meter for CRUD");
- assertThat(retrieved.get().getSerialNumber()).isEqualTo("CRUD-TEST-12345");
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> assertThat(m)
+ .extracting(
+ MeterEntity::getDescription,
+ MeterEntity::getSerialNumber,
+ MeterEntity::getIsVirtual
+ )
+ .containsExactly("Test Electric Meter", "SM-12345", false));
}
@Test
- @DisplayName("Should update meter successfully")
- void shouldUpdateMeterSuccessfully() {
+ @DisplayName("Should find all meters")
+ void shouldFindAllMeters() {
// Arrange
- MeterEntity meter = createValidMeter();
- MeterEntity saved = persistAndFlush(meter);
+ MeterEntity meter1 = createValidMeter();
+ meter1.setSerialNumber("METER-001");
+ MeterEntity meter2 = createValidMeter();
+ meter2.setSerialNumber("METER-002");
+ MeterEntity meter3 = createValidMeter();
+ meter3.setSerialNumber("METER-003");
- // Act
- saved.setDescription("Updated Meter Description");
- saved.setFormNumber("UPDATED-FORM-9999");
- saved.setIntervalLength(1800L); // 30 minutes
- MeterEntity updated = meterRepository.save(saved);
+ meterRepository.saveAll(List.of(meter1, meter2, meter3));
flushAndClear();
+ // Act
+ List allMeters = meterRepository.findAll();
+
// Assert
- Optional retrieved = meterRepository.findById(updated.getId());
- assertThat(retrieved).isPresent();
- assertThat(retrieved.get().getDescription()).isEqualTo("Updated Meter Description");
- assertThat(retrieved.get().getFormNumber()).isEqualTo("UPDATED-FORM-9999");
- assertThat(retrieved.get().getIntervalLength()).isEqualTo(1800L);
+ assertThat(allMeters)
+ .hasSizeGreaterThanOrEqualTo(3)
+ .extracting(MeterEntity::getSerialNumber)
+ .contains("METER-001", "METER-002", "METER-003");
}
@Test
@@ -156,523 +108,376 @@ void shouldUpdateMeterSuccessfully() {
void shouldDeleteMeterSuccessfully() {
// Arrange
MeterEntity meter = createValidMeter();
- MeterEntity saved = persistAndFlush(meter);
- UUID savedId = saved.getId();
+ meter.setSerialNumber("METER-DELETE");
+ MeterEntity saved = meterRepository.save(meter);
+ UUID meterId = saved.getId();
+ flushAndClear();
// Act
- meterRepository.deleteById(savedId);
+ meterRepository.deleteById(meterId);
flushAndClear();
+ Optional retrieved = meterRepository.findById(meterId);
// Assert
- Optional retrieved = meterRepository.findById(savedId);
assertThat(retrieved).isEmpty();
}
@Test
- @DisplayName("Should find all meters")
- void shouldFindAllMeters() {
+ @DisplayName("Should check if meter exists")
+ void shouldCheckIfMeterExists() {
// Arrange
- MeterEntity meter1 = createValidMeter();
- meter1.setSerialNumber("METER-001");
- MeterEntity meter2 = createValidMeter();
- meter2.setSerialNumber("METER-002");
-
- persistAndFlush(meter1);
- persistAndFlush(meter2);
-
- // Act
- List allMeters = meterRepository.findAll();
+ MeterEntity meter = createValidMeter();
+ meter.setSerialNumber("EXIST-CHECK");
+ MeterEntity saved = meterRepository.save(meter);
+ flushAndClear();
- // Assert
- assertThat(allMeters).hasSizeGreaterThanOrEqualTo(2);
- assertThat(allMeters)
- .extracting(MeterEntity::getSerialNumber)
- .contains("METER-001", "METER-002");
+ // Act & Assert
+ assertThat(meterRepository.existsById(saved.getId())).isTrue();
+ assertThat(meterRepository.existsById(UUID.randomUUID())).isFalse();
}
@Test
- @DisplayName("Should count meters correctly")
- void shouldCountMetersCorrectly() {
+ @DisplayName("Should count meters")
+ void shouldCountMeters() {
// Arrange
long initialCount = meterRepository.count();
- MeterEntity meter1 = createValidMeter();
- MeterEntity meter2 = createValidMeter();
-
- persistAndFlush(meter1);
- persistAndFlush(meter2);
-
- // Act
- long finalCount = meterRepository.count();
+ meterRepository.saveAll(List.of(
+ createValidMeter(),
+ createValidMeter(),
+ createValidMeter()
+ ));
+ flushAndClear();
- // Assert
- assertThat(finalCount).isEqualTo(initialCount + 2);
+ // Act & Assert
+ assertThat(meterRepository.count()).isEqualTo(initialCount + 3);
}
}
@Nested
- @DisplayName("Custom Query Methods")
- class CustomQueryMethodsTest {
+ @DisplayName("Meter Specific Field Persistence")
+ class MeterSpecificFieldPersistenceTest {
@Test
- @DisplayName("Should find meter by serial number")
- void shouldFindMeterBySerialNumber() {
+ @DisplayName("Should persist all Meter-specific fields correctly")
+ void shouldPersistAllMeterSpecificFields() {
// Arrange
MeterEntity meter = createValidMeter();
- meter.setSerialNumber("UNIQUE-SERIAL-12345");
- MeterEntity saved = persistAndFlush(meter);
-
- // Act
- Optional found = meterRepository.findBySerialNumber("UNIQUE-SERIAL-12345");
-
- // Assert
- assertThat(found).isPresent();
- assertThat(found.get().getId()).isEqualTo(saved.getId());
- assertThat(found.get().getSerialNumber()).isEqualTo("UNIQUE-SERIAL-12345");
- }
-
- @Test
- @DisplayName("Should find meters by form number")
- void shouldFindMetersByFormNumber() {
- // Arrange
- MeterEntity meter1 = createValidMeter();
- meter1.setFormNumber("FORM-A123");
- MeterEntity meter2 = createValidMeter();
- meter2.setFormNumber("FORM-A123");
- MeterEntity meter3 = createValidMeter();
- meter3.setFormNumber("FORM-B456");
-
- persistAndFlush(meter1);
- persistAndFlush(meter2);
- persistAndFlush(meter3);
+ meter.setFormNumber("FORM-2A");
+ meter.setIntervalLength(900L); // 15 minutes
// Act
- List formAMeters = meterRepository.findByFormNumber("FORM-A123");
+ MeterEntity saved = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(saved.getId());
// Assert
- assertThat(formAMeters).hasSize(2);
- assertThat(formAMeters).extracting(MeterEntity::getFormNumber)
- .allMatch(form -> form.equals("FORM-A123"));
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> assertThat(m)
+ .extracting(
+ MeterEntity::getFormNumber,
+ MeterEntity::getIntervalLength
+ )
+ .containsExactly("FORM-2A", 900L));
}
@Test
- @DisplayName("Should find meters by UTC number")
- void shouldFindMetersByUtcNumber() {
+ @DisplayName("Should persist MeterMultiplier collection")
+ void shouldPersistMeterMultiplierCollection() {
// Arrange
- MeterEntity meter1 = createValidMeter();
- meter1.setUtcNumber("UTC-999888");
- MeterEntity meter2 = createValidMeter();
- meter2.setUtcNumber("UTC-999888");
- MeterEntity meter3 = createValidMeter();
- meter3.setUtcNumber("UTC-777666");
-
- persistAndFlush(meter1);
- persistAndFlush(meter2);
- persistAndFlush(meter3);
+ MeterMultiplier multiplier1 = new MeterMultiplier("voltage", new BigDecimal("120.5"));
+ MeterMultiplier multiplier2 = new MeterMultiplier("kH", new BigDecimal("7.2"));
- // Act
- List utcMeters = meterRepository.findByUtcNumber("UTC-999888");
-
- // Assert
- assertThat(utcMeters).hasSize(2);
- assertThat(utcMeters).extracting(MeterEntity::getUtcNumber)
- .allMatch(utc -> utc.equals("UTC-999888"));
- }
-
- @Test
- @DisplayName("Should find virtual meters")
- void shouldFindVirtualMeters() {
- // Arrange
- MeterEntity virtualMeter1 = createVirtualMeter();
- MeterEntity virtualMeter2 = createVirtualMeter();
- MeterEntity physicalMeter = createValidMeter();
- physicalMeter.setIsVirtual(false);
-
- persistAndFlush(virtualMeter1);
- persistAndFlush(virtualMeter2);
- persistAndFlush(physicalMeter);
+ MeterEntity meter = createValidMeter();
+ meter.setMeterMultipliers(List.of(multiplier1, multiplier2));
// Act
- List virtualMeters = meterRepository.findVirtualMeters();
+ MeterEntity saved = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(saved.getId());
// Assert
- assertThat(virtualMeters).hasSize(2);
- assertThat(virtualMeters).extracting(MeterEntity::getIsVirtual)
- .allMatch(isVirtual -> isVirtual.equals(true));
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> {
+ assertThat(m.getMeterMultipliers()).hasSize(2);
+ // BigDecimal assertions use isEqualByComparingTo() for cross-platform precision tolerance
+ assertThat(m.getMeterMultipliers().get(0).getKind()).isEqualTo("voltage");
+ assertThat(m.getMeterMultipliers().get(0).getValue())
+ .isEqualByComparingTo(new BigDecimal("120.5"));
+ assertThat(m.getMeterMultipliers().get(1).getKind()).isEqualTo("kH");
+ assertThat(m.getMeterMultipliers().get(1).getValue())
+ .isEqualByComparingTo(new BigDecimal("7.2"));
+ });
}
@Test
- @DisplayName("Should find physical meters")
- void shouldFindPhysicalMeters() {
+ @DisplayName("Should handle empty MeterMultipliers collection")
+ void shouldHandleEmptyMeterMultipliersCollection() {
// Arrange
- MeterEntity physicalMeter1 = createValidMeter();
- physicalMeter1.setIsVirtual(false);
- MeterEntity physicalMeter2 = createValidMeter();
- physicalMeter2.setIsVirtual(null); // Should be considered physical
- MeterEntity virtualMeter = createVirtualMeter();
-
- persistAndFlush(physicalMeter1);
- persistAndFlush(physicalMeter2);
- persistAndFlush(virtualMeter);
+ MeterEntity meter = createValidMeter();
+ meter.setMeterMultipliers(List.of());
// Act
- List physicalMeters = meterRepository.findPhysicalMeters();
+ MeterEntity saved = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(saved.getId());
// Assert
- assertThat(physicalMeters).hasSize(2);
- assertThat(physicalMeters).extracting(MeterEntity::getIsVirtual)
- .allMatch(isVirtual -> isVirtual == null || isVirtual.equals(false));
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> assertThat(m.getMeterMultipliers()).isEmpty());
}
+ }
- @Test
- @DisplayName("Should find PAN devices")
- void shouldFindPanDevices() {
- // Arrange
- MeterEntity panMeter1 = createPanMeter();
- MeterEntity panMeter2 = createPanMeter();
- MeterEntity regularMeter = createValidMeter();
- regularMeter.setIsPan(false);
-
- persistAndFlush(panMeter1);
- persistAndFlush(panMeter2);
- persistAndFlush(regularMeter);
-
- // Act
- List panDevices = meterRepository.findPanDevices();
-
- // Assert
- assertThat(panDevices).hasSize(2);
- assertThat(panDevices).extracting(MeterEntity::getIsPan)
- .allMatch(isPan -> isPan.equals(true));
- }
+ @Nested
+ @DisplayName("EndDevice Inherited Field Persistence")
+ class EndDeviceInheritedFieldPersistenceTest {
@Test
- @DisplayName("Should find meters by AMR system")
- void shouldFindMetersByAmrSystem() {
+ @DisplayName("Should persist all Asset fields inherited through EndDevice")
+ void shouldPersistAllAssetFields() {
// Arrange
- MeterEntity meter1 = createValidMeter();
- meter1.setAmrSystem("AMR-SYSTEM-ALPHA");
- MeterEntity meter2 = createValidMeter();
- meter2.setAmrSystem("AMR-SYSTEM-ALPHA");
- MeterEntity meter3 = createValidMeter();
- meter3.setAmrSystem("AMR-SYSTEM-BETA");
-
- persistAndFlush(meter1);
- persistAndFlush(meter2);
- persistAndFlush(meter3);
+ MeterEntity meter = createValidMeter();
+ meter.setType("ElectricMeter");
+ meter.setUtcNumber("UTC-54321");
+ meter.setSerialNumber("SN-ASSET-001");
+ meter.setLotNumber("LOT-2025-Q1");
+ meter.setPurchasePrice(25000L);
+ meter.setCritical(true);
+ meter.setInitialCondition("NEW");
+ meter.setInitialLossOfLife(BigDecimal.ZERO);
// Act
- List alphaMeters = meterRepository.findByAmrSystem("AMR-SYSTEM-ALPHA");
+ MeterEntity saved = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(saved.getId());
// Assert
- assertThat(alphaMeters).hasSize(2);
- assertThat(alphaMeters).extracting(MeterEntity::getAmrSystem)
- .allMatch(amr -> amr.equals("AMR-SYSTEM-ALPHA"));
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> assertThat(m)
+ .extracting(
+ MeterEntity::getType,
+ MeterEntity::getUtcNumber,
+ MeterEntity::getSerialNumber,
+ MeterEntity::getLotNumber,
+ MeterEntity::getPurchasePrice,
+ MeterEntity::getCritical,
+ MeterEntity::getInitialCondition
+ )
+ .containsExactly("ElectricMeter", "UTC-54321", "SN-ASSET-001",
+ "LOT-2025-Q1", 25000L, true, "NEW"))
+ .hasValueSatisfying(m ->
+ assertThat(m.getInitialLossOfLife()).isEqualByComparingTo(BigDecimal.ZERO));
}
@Test
- @DisplayName("Should find meters by install code")
- void shouldFindMetersByInstallCode() {
+ @DisplayName("Should persist lifecycle dates correctly")
+ void shouldPersistLifecycleDatesCorrectly() {
// Arrange
- MeterEntity meter1 = createValidMeter();
- meter1.setInstallCode("INSTALL-CODE-XYZ");
- MeterEntity meter2 = createValidMeter();
- meter2.setInstallCode("INSTALL-CODE-XYZ");
- MeterEntity meter3 = createValidMeter();
- meter3.setInstallCode("INSTALL-CODE-ABC");
-
- persistAndFlush(meter1);
- persistAndFlush(meter2);
- persistAndFlush(meter3);
-
- // Act
- List xyzMeters = meterRepository.findByInstallCode("INSTALL-CODE-XYZ");
-
- // Assert
- assertThat(xyzMeters).hasSize(2);
- assertThat(xyzMeters).extracting(MeterEntity::getInstallCode)
- .allMatch(code -> code.equals("INSTALL-CODE-XYZ"));
- }
+ OffsetDateTime now = OffsetDateTime.now();
+ Asset.LifecycleDate lifecycle = new Asset.LifecycleDate();
+ lifecycle.setInstallationDate(now.minusDays(30));
+ lifecycle.setManufacturedDate(now.minusDays(90));
+ lifecycle.setPurchaseDate(now.minusDays(60));
+ lifecycle.setReceivedDate(now.minusDays(45));
- @Test
- @DisplayName("Should find meters by interval length greater than")
- void shouldFindMetersByIntervalLengthGreaterThan() {
- // Arrange
- MeterEntity meter1 = createValidMeter();
- meter1.setIntervalLength(300L); // 5 minutes
- MeterEntity meter2 = createValidMeter();
- meter2.setIntervalLength(1800L); // 30 minutes
- MeterEntity meter3 = createValidMeter();
- meter3.setIntervalLength(3600L); // 60 minutes
-
- persistAndFlush(meter1);
- persistAndFlush(meter2);
- persistAndFlush(meter3);
+ MeterEntity meter = createValidMeter();
+ meter.setLifecycle(lifecycle);
// Act
- List longIntervalMeters = meterRepository.findByIntervalLengthGreaterThan(900L); // > 15 minutes
+ MeterEntity saved = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(saved.getId());
// Assert
- assertThat(longIntervalMeters).hasSize(2);
- assertThat(longIntervalMeters).extracting(MeterEntity::getIntervalLength)
- .allMatch(interval -> interval > 900L);
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> assertThat(m.getLifecycle())
+ .isNotNull()
+ .extracting(
+ Asset.LifecycleDate::getInstallationDate,
+ Asset.LifecycleDate::getManufacturedDate,
+ Asset.LifecycleDate::getPurchaseDate,
+ Asset.LifecycleDate::getReceivedDate
+ )
+ .doesNotContainNull());
}
@Test
- @DisplayName("Should find critical meters")
- void shouldFindCriticalMeters() {
+ @DisplayName("Should persist acceptance test correctly")
+ void shouldPersistAcceptanceTestCorrectly() {
// Arrange
- MeterEntity criticalMeter1 = createCriticalMeter();
- MeterEntity criticalMeter2 = createCriticalMeter();
- MeterEntity regularMeter = createValidMeter();
- regularMeter.setCritical(false);
-
- persistAndFlush(criticalMeter1);
- persistAndFlush(criticalMeter2);
- persistAndFlush(regularMeter);
-
- // Act
- List criticalMeters = meterRepository.findCriticalMeters();
-
- // Assert
- assertThat(criticalMeters).hasSize(2);
- assertThat(criticalMeters).extracting(MeterEntity::getCritical)
- .allMatch(critical -> critical.equals(true));
- }
+ Asset.AcceptanceTest acceptanceTest = new Asset.AcceptanceTest();
+ acceptanceTest.setDateTime(OffsetDateTime.now().minusDays(7));
+ acceptanceTest.setSuccess(true);
+ acceptanceTest.setType("FIELD_TEST");
- @Test
- @DisplayName("Should return empty results when no matches found")
- void shouldReturnEmptyResultsWhenNoMatchesFound() {
- // Arrange
MeterEntity meter = createValidMeter();
- meter.setSerialNumber("EXISTING-METER");
- persistAndFlush(meter);
+ meter.setAcceptanceTest(acceptanceTest);
// Act
- Optional notFound = meterRepository.findBySerialNumber("NON-EXISTENT");
- List emptyList = meterRepository.findByFormNumber("NON-EXISTENT-FORM");
+ MeterEntity saved = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(saved.getId());
// Assert
- assertThat(notFound).isEmpty();
- assertThat(emptyList).isEmpty();
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> assertThat(m.getAcceptanceTest())
+ .isNotNull()
+ .extracting(
+ Asset.AcceptanceTest::getSuccess,
+ Asset.AcceptanceTest::getType
+ )
+ .containsExactly(true, "FIELD_TEST"))
+ .hasValueSatisfying(m ->
+ assertThat(m.getAcceptanceTest().getDateTime()).isNotNull());
}
- }
-
- @Nested
- @DisplayName("Meter Device Field Testing")
- class MeterDeviceFieldTest {
@Test
- @DisplayName("Should persist all meter specific fields correctly")
- void shouldPersistAllMeterSpecificFieldsCorrectly() {
+ @DisplayName("Should persist status correctly")
+ void shouldPersistStatusCorrectly() {
// Arrange
+ Status status = new Status();
+ status.setValue("ACTIVE");
+ status.setDateTime(OffsetDateTime.now());
+ status.setRemark("Meter operational");
+ status.setReason("Installation complete");
+
MeterEntity meter = createValidMeter();
- meter.setFormNumber("SPECIAL-FORM-12345");
- meter.setIntervalLength(2700L); // 45 minutes
+ meter.setStatus(status);
// Act
- MeterEntity saved = persistAndFlush(meter);
+ MeterEntity saved = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(saved.getId());
// Assert
- Optional retrieved = meterRepository.findById(saved.getId());
- assertThat(retrieved).isPresent();
- MeterEntity entity = retrieved.get();
- assertThat(entity.getFormNumber()).isEqualTo("SPECIAL-FORM-12345");
- assertThat(entity.getIntervalLength()).isEqualTo(2700L);
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> assertThat(m.getStatus())
+ .isNotNull()
+ .extracting(
+ Status::getValue,
+ Status::getRemark,
+ Status::getReason
+ )
+ .containsExactly("ACTIVE", "Meter operational", "Installation complete"))
+ .hasValueSatisfying(m ->
+ assertThat(m.getStatus().getDateTime()).isNotNull());
}
@Test
- @DisplayName("Should persist all inherited EndDevice fields correctly")
- void shouldPersistAllInheritedEndDeviceFieldsCorrectly() {
+ @DisplayName("Should persist electronic address correctly")
+ void shouldPersistElectronicAddressCorrectly() {
// Arrange
+ ElectronicAddress electronicAddress = new ElectronicAddress();
+ electronicAddress.setLan("192.168.1.100");
+ electronicAddress.setMac("00:1A:2B:3C:4D:5E");
+ electronicAddress.setEmail1("meter@utility.com");
+ electronicAddress.setWeb("https://meter.utility.com");
+
MeterEntity meter = createValidMeter();
- meter.setType("SMART_METER");
- meter.setUtcNumber("UTC-SPECIAL-999");
- meter.setSerialNumber("SN-SPECIAL-888777");
- meter.setLotNumber("LOT-SPECIAL-666");
- meter.setPurchasePrice(125000L); // $1250.00
- meter.setCritical(true);
- meter.setInitialCondition("REFURBISHED");
- meter.setInitialLossOfLife(new BigDecimal("0.15"));
- meter.setIsVirtual(false);
- meter.setIsPan(true);
- meter.setInstallCode("SPECIAL-INSTALL-CODE");
- meter.setAmrSystem("SPECIAL-AMR-SYSTEM");
+ meter.setElectronicAddress(electronicAddress);
// Act
- MeterEntity saved = persistAndFlush(meter);
-
- // Assert
+ MeterEntity saved = meterRepository.save(meter);
+ flushAndClear();
Optional retrieved = meterRepository.findById(saved.getId());
- assertThat(retrieved).isPresent();
- MeterEntity entity = retrieved.get();
- assertThat(entity.getType()).isEqualTo("SMART_METER");
- assertThat(entity.getUtcNumber()).isEqualTo("UTC-SPECIAL-999");
- assertThat(entity.getSerialNumber()).isEqualTo("SN-SPECIAL-888777");
- assertThat(entity.getLotNumber()).isEqualTo("LOT-SPECIAL-666");
- assertThat(entity.getPurchasePrice()).isEqualTo(125000L);
- assertThat(entity.getCritical()).isTrue();
- assertThat(entity.getInitialCondition()).isEqualTo("REFURBISHED");
- assertThat(entity.getInitialLossOfLife()).isEqualTo(new BigDecimal("0.15"));
- assertThat(entity.getIsVirtual()).isFalse();
- assertThat(entity.getIsPan()).isTrue();
- assertThat(entity.getInstallCode()).isEqualTo("SPECIAL-INSTALL-CODE");
- assertThat(entity.getAmrSystem()).isEqualTo("SPECIAL-AMR-SYSTEM");
- }
-
- @Test
- @DisplayName("Should handle null optional fields correctly")
- void shouldHandleNullOptionalFieldsCorrectly() {
- // Arrange
- MeterEntity meter = new MeterEntity();
- meter.setDescription("Minimal Meter");
- meter.setFormNumber(null);
- meter.setIntervalLength(null);
- meter.setType(null);
- meter.setUtcNumber(null);
- meter.setSerialNumber(null);
- meter.setLotNumber(null);
- meter.setPurchasePrice(null);
- meter.setCritical(null);
- meter.setInitialCondition(null);
- meter.setInitialLossOfLife(null);
- meter.setIsVirtual(null);
- meter.setIsPan(null);
- meter.setInstallCode(null);
- meter.setAmrSystem(null);
-
- // Act
- MeterEntity saved = persistAndFlush(meter);
// Assert
- Optional retrieved = meterRepository.findById(saved.getId());
- assertThat(retrieved).isPresent();
- MeterEntity entity = retrieved.get();
- assertThat(entity.getFormNumber()).isNull();
- assertThat(entity.getIntervalLength()).isNull();
- assertThat(entity.getType()).isNull();
- assertThat(entity.getUtcNumber()).isNull();
- assertThat(entity.getSerialNumber()).isNull();
- assertThat(entity.getLotNumber()).isNull();
- assertThat(entity.getPurchasePrice()).isNull();
- assertThat(entity.getCritical()).isNull();
- assertThat(entity.getInitialCondition()).isNull();
- assertThat(entity.getInitialLossOfLife()).isNull();
- assertThat(entity.getIsVirtual()).isNull();
- assertThat(entity.getIsPan()).isNull();
- assertThat(entity.getInstallCode()).isNull();
- assertThat(entity.getAmrSystem()).isNull();
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> assertThat(m.getElectronicAddress())
+ .isNotNull()
+ .extracting(
+ ElectronicAddress::getLan,
+ ElectronicAddress::getMac,
+ ElectronicAddress::getEmail1,
+ ElectronicAddress::getWeb
+ )
+ .containsExactly("192.168.1.100", "00:1A:2B:3C:4D:5E",
+ "meter@utility.com", "https://meter.utility.com"));
}
}
@Nested
- @DisplayName("Inheritance Testing")
- class InheritanceTest {
+ @DisplayName("EndDevice Specific Fields")
+ class EndDeviceSpecificFieldsTest {
@Test
- @DisplayName("Should inherit IdentifiedObject functionality through EndDeviceEntity")
- void shouldInheritIdentifiedObjectFunctionalityThroughEndDeviceEntity() {
+ @DisplayName("Should persist virtual device flag")
+ void shouldPersistVirtualDeviceFlag() {
// Arrange
- MeterEntity meter = createValidMeter();
+ MeterEntity virtualMeter = createValidMeter();
+ virtualMeter.setIsVirtual(true);
// Act
- MeterEntity saved = meterRepository.save(meter);
+ MeterEntity saved = meterRepository.save(virtualMeter);
flushAndClear();
-
- // Assert
Optional retrieved = meterRepository.findById(saved.getId());
- assertThat(retrieved).isPresent();
-
- MeterEntity entity = retrieved.get();
- // IdentifiedObject fields
- assertThat(entity.getId()).isNotNull();
- assertThat(entity.getCreated()).isNotNull();
- assertThat(entity.getUpdated()).isNotNull();
- assertThat(entity.getDescription()).isNotNull();
- }
-
- @Test
- @DisplayName("Should update timestamps on modification")
- void shouldUpdateTimestampsOnModification() {
- // Arrange
- MeterEntity meter = createValidMeter();
- MeterEntity saved = persistAndFlush(meter);
-
- // Wait a moment to ensure timestamp difference
- try {
- Thread.sleep(10);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
-
- // Act
- saved.setDescription("Updated Description");
- MeterEntity updated = meterRepository.save(saved);
- flushAndClear();
// Assert
- Optional retrieved = meterRepository.findById(updated.getId());
- assertThat(retrieved).isPresent();
- assertThat(retrieved.get().getUpdated()).isAfter(retrieved.get().getCreated());
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> assertThat(m.getIsVirtual()).isTrue());
}
@Test
- @DisplayName("Should generate unique IDs for different entities")
- void shouldGenerateUniqueIdsForDifferentEntities() {
+ @DisplayName("Should persist PAN device flag")
+ void shouldPersistPanDeviceFlag() {
// Arrange
- MeterEntity meter1 = createValidMeter();
- MeterEntity meter2 = createValidMeter();
+ MeterEntity panMeter = createValidMeter();
+ panMeter.setIsPan(true);
// Act
- MeterEntity saved1 = meterRepository.save(meter1);
- MeterEntity saved2 = meterRepository.save(meter2);
+ MeterEntity saved = meterRepository.save(panMeter);
flushAndClear();
+ Optional retrieved = meterRepository.findById(saved.getId());
// Assert
- assertThat(saved1.getId()).isNotEqualTo(saved2.getId());
- assertThat(saved1.getId()).isNotNull();
- assertThat(saved2.getId()).isNotNull();
- }
-
- @Test
- @DisplayName("Should handle equals and hashCode correctly")
- void shouldHandleEqualsAndHashCodeCorrectly() {
- // Arrange
- MeterEntity meter1 = createValidMeter();
- MeterEntity meter2 = createValidMeter();
-
- MeterEntity saved1 = persistAndFlush(meter1);
- MeterEntity saved2 = persistAndFlush(meter2);
-
- // Act & Assert
- assertThat(saved1).isNotEqualTo(saved2);
- // Note: Hibernate proxy-aware hashCode implementation returns class hashCode for different entities
- // This is expected behavior for entities with different IDs
-
- // Same entity should be equal to itself
- assertThat(saved1).isEqualTo(saved1);
- assertThat(saved1.hashCode()).isEqualTo(saved1.hashCode());
-
- // Different entities with different IDs should not be equal
- assertThat(saved1.getId()).isNotEqualTo(saved2.getId());
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> assertThat(m.getIsPan()).isTrue());
}
@Test
- @DisplayName("Should generate meaningful toString representation")
- void shouldGenerateMeaningfulToStringRepresentation() {
+ @DisplayName("Should persist install code and AMR system")
+ void shouldPersistInstallCodeAndAmrSystem() {
// Arrange
MeterEntity meter = createValidMeter();
- meter.setSerialNumber("TEST-SERIAL-12345");
- meter.setFormNumber("TEST-FORM-67890");
- MeterEntity saved = persistAndFlush(meter);
+ meter.setInstallCode("INSTALL-CODE-12345");
+ meter.setAmrSystem("ZigBee Smart Energy 2.0");
// Act
- String toString = saved.toString();
+ MeterEntity saved = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(saved.getId());
// Assert
- assertThat(toString).contains("MeterEntity");
- assertThat(toString).contains("id = " + saved.getId());
- assertThat(toString).contains("serialNumber = TEST-SERIAL-12345");
- assertThat(toString).contains("formNumber = TEST-FORM-67890");
+ assertThat(retrieved)
+ .isPresent()
+ .hasValueSatisfying(m -> assertThat(m)
+ .extracting(
+ MeterEntity::getInstallCode,
+ MeterEntity::getAmrSystem
+ )
+ .containsExactly("INSTALL-CODE-12345", "ZigBee Smart Energy 2.0"));
}
}
-}
\ No newline at end of file
+
+ /**
+ * Helper method to create a valid MeterEntity for testing.
+ */
+ private MeterEntity createValidMeter() {
+ MeterEntity meter = new MeterEntity();
+ meter.setSerialNumber("DEFAULT-SN-" + UUID.randomUUID().toString().substring(0, 8));
+ meter.setFormNumber("FORM-1");
+ meter.setIntervalLength(900L); // 15 minutes default
+ meter.setIsVirtual(false);
+ meter.setIsPan(false);
+ return meter;
+ }
+}
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepositoryTest.java
index 30d698d7..58ef86ed 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepositoryTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ServiceLocationRepositoryTest.java
@@ -19,6 +19,9 @@
package org.greenbuttonalliance.espi.common.repositories.customer;
import org.greenbuttonalliance.espi.common.domain.customer.entity.*;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.test.BaseRepositoryTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@@ -56,7 +59,7 @@ private ServiceLocationEntity createValidServiceLocation() {
serviceLocation.setNeedsInspection(false);
// Create main address
- Organisation.StreetAddress mainAddress = new Organisation.StreetAddress();
+ StreetAddress mainAddress = new StreetAddress();
mainAddress.setStreetDetail(faker.address().streetAddress());
mainAddress.setTownDetail(faker.address().city());
mainAddress.setStateOrProvince(faker.address().state());
@@ -110,7 +113,7 @@ void shouldSaveServiceLocationWithEmbeddedAddresses() {
ServiceLocationEntity serviceLocation = createValidServiceLocation();
// Add secondary address
- Organisation.StreetAddress secondaryAddress = new Organisation.StreetAddress();
+ StreetAddress secondaryAddress = new StreetAddress();
secondaryAddress.setStreetDetail("PO Box 123");
secondaryAddress.setTownDetail(faker.address().city());
secondaryAddress.setStateOrProvince(faker.address().state());
@@ -376,7 +379,7 @@ void shouldHandleServiceLocationWithElectronicAddress() {
// Arrange
ServiceLocationEntity serviceLocation = createValidServiceLocation();
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("test@example.com");
electronicAddress.setEmail2("backup@example.com");
electronicAddress.setWeb("https://example.com");
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountMySQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountMySQLIntegrationTest.java
index 3b7858fb..efda41d4 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountMySQLIntegrationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountMySQLIntegrationTest.java
@@ -19,10 +19,20 @@
package org.greenbuttonalliance.espi.common.repositories.integration;
import org.greenbuttonalliance.espi.common.domain.customer.entity.AccountNotification;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAccountEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.enums.NotificationMethodKind;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.repositories.customer.CustomerAccountRepository;
import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
@@ -92,7 +102,7 @@ void shouldSaveAndRetrieveCustomerAccountWithAllFields() {
account.setIsPrePay(true);
// Electronic address for document
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("document@mysql.test");
electronicAddress.setWeb("https://mysql-account.test");
account.setElectronicAddress(electronicAddress);
@@ -108,7 +118,7 @@ void shouldSaveAndRetrieveCustomerAccountWithAllFields() {
Organisation contactInfo = new Organisation();
contactInfo.setOrganisationName("MySQL Contact Corp");
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail("123 MySQL Contact Street");
streetAddress.setTownDetail("Contact City");
streetAddress.setStateOrProvince("CA");
@@ -116,7 +126,7 @@ void shouldSaveAndRetrieveCustomerAccountWithAllFields() {
streetAddress.setCountry("USA");
contactInfo.setStreetAddress(streetAddress);
- Organisation.ElectronicAddress contactElectronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress contactElectronicAddress = new ElectronicAddress();
contactElectronicAddress.setEmail1("contact@mysql.test");
contactElectronicAddress.setWeb("https://contact.mysql.test");
contactInfo.setElectronicAddress(contactElectronicAddress);
@@ -260,7 +270,7 @@ void shouldPersistOrganisationWithAllTypes() {
Organisation contactInfo = new Organisation();
contactInfo.setOrganisationName("Complete MySQL Organization");
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail("456 MySQL Contact Avenue");
streetAddress.setTownDetail("MySQL Town");
streetAddress.setStateOrProvince("NY");
@@ -268,7 +278,7 @@ void shouldPersistOrganisationWithAllTypes() {
streetAddress.setCountry("USA");
contactInfo.setStreetAddress(streetAddress);
- Organisation.StreetAddress postalAddress = new Organisation.StreetAddress();
+ StreetAddress postalAddress = new StreetAddress();
postalAddress.setStreetDetail("PO Box 888");
postalAddress.setTownDetail("MySQL Town");
postalAddress.setStateOrProvince("NY");
@@ -276,7 +286,7 @@ void shouldPersistOrganisationWithAllTypes() {
postalAddress.setCountry("USA");
contactInfo.setPostalAddress(postalAddress);
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("primary@mysql.test");
electronicAddress.setEmail2("secondary@mysql.test");
electronicAddress.setWeb("https://mysql.org.test");
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountPostgreSQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountPostgreSQLIntegrationTest.java
index 49bfc67b..3e4d0483 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountPostgreSQLIntegrationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountPostgreSQLIntegrationTest.java
@@ -19,10 +19,20 @@
package org.greenbuttonalliance.espi.common.repositories.integration;
import org.greenbuttonalliance.espi.common.domain.customer.entity.AccountNotification;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAccountEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.domain.customer.enums.NotificationMethodKind;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
import org.greenbuttonalliance.espi.common.repositories.customer.CustomerAccountRepository;
import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
@@ -92,7 +102,7 @@ void shouldSaveAndRetrieveCustomerAccountWithAllFields() {
account.setIsPrePay(false);
// Electronic address for document
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("document@postgres.test");
electronicAddress.setWeb("https://postgres-account.test");
account.setElectronicAddress(electronicAddress);
@@ -108,7 +118,7 @@ void shouldSaveAndRetrieveCustomerAccountWithAllFields() {
Organisation contactInfo = new Organisation();
contactInfo.setOrganisationName("PostgreSQL Contact Services");
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail("789 PostgreSQL Contact Boulevard");
streetAddress.setTownDetail("Postgres City");
streetAddress.setStateOrProvince("WA");
@@ -116,7 +126,7 @@ void shouldSaveAndRetrieveCustomerAccountWithAllFields() {
streetAddress.setCountry("USA");
contactInfo.setStreetAddress(streetAddress);
- Organisation.ElectronicAddress contactElectronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress contactElectronicAddress = new ElectronicAddress();
contactElectronicAddress.setEmail1("contact@postgres.test");
contactElectronicAddress.setWeb("https://contact.postgres.test");
contactInfo.setElectronicAddress(contactElectronicAddress);
@@ -260,7 +270,7 @@ void shouldPersistOrganisationWithAllTypes() {
Organisation contactInfo = new Organisation();
contactInfo.setOrganisationName("Complete PostgreSQL Organization");
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail("321 PostgreSQL Contact Drive");
streetAddress.setTownDetail("Postgres Town");
streetAddress.setStateOrProvince("OR");
@@ -268,7 +278,7 @@ void shouldPersistOrganisationWithAllTypes() {
streetAddress.setCountry("USA");
contactInfo.setStreetAddress(streetAddress);
- Organisation.StreetAddress postalAddress = new Organisation.StreetAddress();
+ StreetAddress postalAddress = new StreetAddress();
postalAddress.setStreetDetail("PO Box 666");
postalAddress.setTownDetail("Postgres Town");
postalAddress.setStateOrProvince("OR");
@@ -276,7 +286,7 @@ void shouldPersistOrganisationWithAllTypes() {
postalAddress.setCountry("USA");
contactInfo.setPostalAddress(postalAddress);
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("primary@postgres.test");
electronicAddress.setEmail2("secondary@postgres.test");
electronicAddress.setWeb("https://postgres.org.test");
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAgreementMySQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAgreementMySQLIntegrationTest.java
index 199e419a..8659a587 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAgreementMySQLIntegrationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAgreementMySQLIntegrationTest.java
@@ -20,8 +20,11 @@
import org.greenbuttonalliance.espi.common.domain.common.DateTimeInterval;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAgreementEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.repositories.customer.CustomerAgreementRepository;
import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
@@ -93,7 +96,7 @@ void shouldSaveAndRetrieveCustomerAgreementWithAllFields() {
agreement.setAgreementId("MYSQL-AGR-12345");
// Electronic address for document
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("agreement@mysql.test");
electronicAddress.setWeb("https://mysql-agreement.test");
agreement.setElectronicAddress(electronicAddress);
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAgreementPostgreSQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAgreementPostgreSQLIntegrationTest.java
index f45f3999..216aaefb 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAgreementPostgreSQLIntegrationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAgreementPostgreSQLIntegrationTest.java
@@ -20,8 +20,11 @@
import org.greenbuttonalliance.espi.common.domain.common.DateTimeInterval;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAgreementEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.repositories.customer.CustomerAgreementRepository;
import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
@@ -93,7 +96,7 @@ void shouldSaveAndRetrieveCustomerAgreementWithAllFields() {
agreement.setAgreementId("POSTGRES-AGR-67890");
// Electronic address for document
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("agreement@postgres.test");
electronicAddress.setWeb("https://postgres-agreement.test");
agreement.setElectronicAddress(electronicAddress);
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerMySQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerMySQLIntegrationTest.java
index ce9026b5..b8114b54 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerMySQLIntegrationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerMySQLIntegrationTest.java
@@ -19,8 +19,17 @@
package org.greenbuttonalliance.espi.common.repositories.integration;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.domain.customer.enums.CustomerKind;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.repositories.customer.CustomerRepository;
import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
@@ -86,7 +95,7 @@ void shouldSaveAndRetrieveCustomerWithAllFields() {
Organisation org = new Organisation();
org.setOrganisationName("MySQL Test Corporation");
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail("123 MySQL Street");
streetAddress.setTownDetail("Database City");
streetAddress.setStateOrProvince("CA");
@@ -94,7 +103,7 @@ void shouldSaveAndRetrieveCustomerWithAllFields() {
streetAddress.setCountry("USA");
org.setStreetAddress(streetAddress);
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("mysql@test.com");
electronicAddress.setWeb("https://mysql.test.com");
org.setElectronicAddress(electronicAddress);
@@ -248,7 +257,7 @@ void shouldPersistOrganisationWithAllTypes() {
Organisation org = new Organisation();
org.setOrganisationName("Complete MySQL Corporation");
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail("456 MySQL Avenue");
streetAddress.setTownDetail("Database Town");
streetAddress.setStateOrProvince("NY");
@@ -256,7 +265,7 @@ void shouldPersistOrganisationWithAllTypes() {
streetAddress.setCountry("USA");
org.setStreetAddress(streetAddress);
- Organisation.StreetAddress postalAddress = new Organisation.StreetAddress();
+ StreetAddress postalAddress = new StreetAddress();
postalAddress.setStreetDetail("PO Box 999");
postalAddress.setTownDetail("Database Town");
postalAddress.setStateOrProvince("NY");
@@ -264,7 +273,7 @@ void shouldPersistOrganisationWithAllTypes() {
postalAddress.setCountry("USA");
org.setPostalAddress(postalAddress);
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("contact@mysql.test");
electronicAddress.setEmail2("support@mysql.test");
electronicAddress.setWeb("https://mysql.test");
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerPostgreSQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerPostgreSQLIntegrationTest.java
index d3c23d30..8eb33a7c 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerPostgreSQLIntegrationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerPostgreSQLIntegrationTest.java
@@ -19,8 +19,17 @@
package org.greenbuttonalliance.espi.common.repositories.integration;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.domain.customer.enums.CustomerKind;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.repositories.customer.CustomerRepository;
import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
@@ -86,7 +95,7 @@ void shouldSaveAndRetrieveCustomerWithAllFields() {
Organisation org = new Organisation();
org.setOrganisationName("PostgreSQL Test Services");
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail("789 PostgreSQL Boulevard");
streetAddress.setTownDetail("Postgres City");
streetAddress.setStateOrProvince("WA");
@@ -94,7 +103,7 @@ void shouldSaveAndRetrieveCustomerWithAllFields() {
streetAddress.setCountry("USA");
org.setStreetAddress(streetAddress);
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("postgres@test.com");
electronicAddress.setWeb("https://postgres.test.com");
org.setElectronicAddress(electronicAddress);
@@ -248,7 +257,7 @@ void shouldPersistOrganisationWithAllTypes() {
Organisation org = new Organisation();
org.setOrganisationName("Complete PostgreSQL Corporation");
- Organisation.StreetAddress streetAddress = new Organisation.StreetAddress();
+ StreetAddress streetAddress = new StreetAddress();
streetAddress.setStreetDetail("321 PostgreSQL Drive");
streetAddress.setTownDetail("Postgres Town");
streetAddress.setStateOrProvince("OR");
@@ -256,7 +265,7 @@ void shouldPersistOrganisationWithAllTypes() {
streetAddress.setCountry("USA");
org.setStreetAddress(streetAddress);
- Organisation.StreetAddress postalAddress = new Organisation.StreetAddress();
+ StreetAddress postalAddress = new StreetAddress();
postalAddress.setStreetDetail("PO Box 777");
postalAddress.setTownDetail("Postgres Town");
postalAddress.setStateOrProvince("OR");
@@ -264,7 +273,7 @@ void shouldPersistOrganisationWithAllTypes() {
postalAddress.setCountry("USA");
org.setPostalAddress(postalAddress);
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("contact@postgres.test");
electronicAddress.setEmail2("support@postgres.test");
electronicAddress.setWeb("https://postgres.test");
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/EndDeviceMySQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/EndDeviceMySQLIntegrationTest.java
index cd8755be..7c2b33e4 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/EndDeviceMySQLIntegrationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/EndDeviceMySQLIntegrationTest.java
@@ -19,9 +19,13 @@
package org.greenbuttonalliance.espi.common.repositories.integration;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Asset;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.EndDeviceEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.repositories.customer.EndDeviceRepository;
import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
@@ -91,7 +95,7 @@ void shouldSaveAndRetrieveEndDeviceWithAllFields() {
endDevice.setAmrSystem("AMR-MYSQL-SYSTEM");
// Electronic address
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("device@mysql.test");
electronicAddress.setMac("AA:BB:CC:DD:EE:FF");
electronicAddress.setWeb("https://mysql-device.test");
@@ -350,7 +354,7 @@ void shouldPersistElectronicAddressWithAllFields() {
EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice();
endDevice.setSerialNumber("MYSQL-ELECTRONIC-TEST");
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setLan("192.168.100.50");
electronicAddress.setMac("11:22:33:44:55:66");
electronicAddress.setEmail1("device1@mysql.test");
@@ -368,7 +372,7 @@ void shouldPersistElectronicAddressWithAllFields() {
// Assert
assertThat(retrieved).isPresent();
- Organisation.ElectronicAddress retrievedAddress = retrieved.get().getElectronicAddress();
+ ElectronicAddress retrievedAddress = retrieved.get().getElectronicAddress();
assertThat(retrievedAddress).isNotNull();
assertThat(retrievedAddress.getLan()).isEqualTo("192.168.100.50");
assertThat(retrievedAddress.getMac()).isEqualTo("11:22:33:44:55:66");
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/EndDevicePostgreSQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/EndDevicePostgreSQLIntegrationTest.java
index 5ddd88cf..d6a836d2 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/EndDevicePostgreSQLIntegrationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/EndDevicePostgreSQLIntegrationTest.java
@@ -19,9 +19,13 @@
package org.greenbuttonalliance.espi.common.repositories.integration;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Asset;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.EndDeviceEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
import org.greenbuttonalliance.espi.common.repositories.customer.EndDeviceRepository;
import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
@@ -91,7 +95,7 @@ void shouldSaveAndRetrieveEndDeviceWithAllFields() {
endDevice.setAmrSystem("AMR-POSTGRES-SYSTEM");
// Electronic address
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("device@postgres.test");
electronicAddress.setMac("00:11:22:33:44:55");
electronicAddress.setWeb("https://postgres-device.test");
@@ -350,7 +354,7 @@ void shouldPersistElectronicAddressWithAllFields() {
EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice();
endDevice.setSerialNumber("POSTGRES-ELECTRONIC-TEST");
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setLan("10.10.10.100");
electronicAddress.setMac("FF:EE:DD:CC:BB:AA");
electronicAddress.setEmail1("device1@postgres.test");
@@ -368,7 +372,7 @@ void shouldPersistElectronicAddressWithAllFields() {
// Assert
assertThat(retrieved).isPresent();
- Organisation.ElectronicAddress retrievedAddress = retrieved.get().getElectronicAddress();
+ ElectronicAddress retrievedAddress = retrieved.get().getElectronicAddress();
assertThat(retrievedAddress).isNotNull();
assertThat(retrievedAddress.getLan()).isEqualTo("10.10.10.100");
assertThat(retrievedAddress.getMac()).isEqualTo("FF:EE:DD:CC:BB:AA");
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/MeterMySQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/MeterMySQLIntegrationTest.java
new file mode 100644
index 00000000..b98f76a7
--- /dev/null
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/MeterMySQLIntegrationTest.java
@@ -0,0 +1,369 @@
+/*
+ *
+ * Copyright (c) 2025 Green Button Alliance, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.greenbuttonalliance.espi.common.repositories.integration;
+
+import org.greenbuttonalliance.espi.common.domain.customer.entity.Asset;
+import org.greenbuttonalliance.espi.common.domain.customer.entity.MeterEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.MeterMultiplier;
+import org.greenbuttonalliance.espi.common.repositories.customer.MeterRepository;
+import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
+import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.junit.jupiter.Container;
+
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Meter entity integration tests using MySQL TestContainer.
+ *
+ * Tests full CRUD operations and relationship persistence with a real MySQL database.
+ */
+@DisplayName("Meter Integration Tests - MySQL")
+@ActiveProfiles({"test", "test-mysql"})
+class MeterMySQLIntegrationTest extends BaseTestContainersTest {
+
+ @Container
+ private static final org.testcontainers.containers.MySQLContainer> mysql = mysqlContainer;
+
+ static {
+ mysql.start();
+ }
+
+ @DynamicPropertySource
+ static void configureMySQLProperties(DynamicPropertyRegistry registry) {
+ registry.add("spring.datasource.url", mysql::getJdbcUrl);
+ registry.add("spring.datasource.username", mysql::getUsername);
+ registry.add("spring.datasource.password", mysql::getPassword);
+ registry.add("spring.datasource.driver-class-name", () -> "com.mysql.cj.jdbc.Driver");
+ }
+
+ @Autowired
+ private MeterRepository meterRepository;
+
+ @Nested
+ @DisplayName("CRUD Operations")
+ class CrudOperationsTest {
+
+ @Test
+ @DisplayName("Should save and retrieve meter with all fields")
+ void shouldSaveAndRetrieveMeterWithAllFields() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setType("Electric Meter");
+ meter.setSerialNumber("MYSQL-METER-SN-123456789");
+ meter.setUtcNumber("MYSQL-UTC-987654321");
+ meter.setLotNumber("LOT-MYSQL-M001");
+ meter.setPurchasePrice(25000L);
+ meter.setCritical(true);
+ meter.setInitialCondition("New");
+ meter.setInitialLossOfLife(BigDecimal.ZERO);
+ meter.setIsVirtual(false);
+ meter.setIsPan(true);
+ meter.setInstallCode("INSTALL-MYSQL-METER-XYZ");
+ meter.setAmrSystem("AMR-MYSQL-METER-SYSTEM");
+
+ // Meter-specific fields
+ meter.setFormNumber("FORM-2A");
+ meter.setIntervalLength(900L); // 15 minutes
+
+ // MeterMultipliers collection
+ MeterMultiplier multiplier1 = new MeterMultiplier("voltage", new BigDecimal("120.0"));
+ MeterMultiplier multiplier2 = new MeterMultiplier("kH", new BigDecimal("7.2"));
+ meter.setMeterMultipliers(List.of(multiplier1, multiplier2));
+
+ // Electronic address
+ ElectronicAddress electronicAddress = new ElectronicAddress();
+ electronicAddress.setEmail1("meter@mysql.test");
+ electronicAddress.setMac("11:22:33:44:55:66");
+ electronicAddress.setWeb("https://mysql-meter.test");
+ meter.setElectronicAddress(electronicAddress);
+
+ // Lifecycle dates
+ Asset.LifecycleDate lifecycle = new Asset.LifecycleDate();
+ lifecycle.setManufacturedDate(OffsetDateTime.now().minusYears(2));
+ lifecycle.setPurchaseDate(OffsetDateTime.now().minusYears(1));
+ lifecycle.setInstallationDate(OffsetDateTime.now().minusMonths(6));
+ meter.setLifecycle(lifecycle);
+
+ // Acceptance test
+ Asset.AcceptanceTest acceptanceTest = new Asset.AcceptanceTest();
+ acceptanceTest.setSuccess(true);
+ acceptanceTest.setDateTime(OffsetDateTime.now().minusMonths(6));
+ acceptanceTest.setType("Field Test");
+ meter.setAcceptanceTest(acceptanceTest);
+
+ // Status
+ Status status = new Status();
+ status.setValue("operational");
+ status.setDateTime(OffsetDateTime.now());
+ status.setReason("MySQL meter test");
+ meter.setStatus(status);
+
+ // Act
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(savedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ MeterEntity result = retrieved.get();
+
+ // EndDevice inherited fields
+ assertThat(result.getType()).isEqualTo("Electric Meter");
+ assertThat(result.getSerialNumber()).isEqualTo("MYSQL-METER-SN-123456789");
+ assertThat(result.getUtcNumber()).isEqualTo("MYSQL-UTC-987654321");
+ assertThat(result.getLotNumber()).isEqualTo("LOT-MYSQL-M001");
+ assertThat(result.getPurchasePrice()).isEqualTo(25000L);
+ assertThat(result.getCritical()).isTrue();
+ assertThat(result.getInitialCondition()).isEqualTo("New");
+ assertThat(result.getIsVirtual()).isFalse();
+ assertThat(result.getIsPan()).isTrue();
+ assertThat(result.getInstallCode()).isEqualTo("INSTALL-MYSQL-METER-XYZ");
+ assertThat(result.getAmrSystem()).isEqualTo("AMR-MYSQL-METER-SYSTEM");
+
+ // Meter-specific fields
+ assertThat(result.getFormNumber()).isEqualTo("FORM-2A");
+ assertThat(result.getIntervalLength()).isEqualTo(900L);
+
+ // MeterMultipliers collection
+ assertThat(result.getMeterMultipliers()).hasSize(2);
+ assertThat(result.getMeterMultipliers().get(0).getKind()).isEqualTo("voltage");
+ assertThat(result.getMeterMultipliers().get(0).getValue()).isEqualByComparingTo(new BigDecimal("120.0"));
+ assertThat(result.getMeterMultipliers().get(1).getKind()).isEqualTo("kH");
+ assertThat(result.getMeterMultipliers().get(1).getValue()).isEqualByComparingTo(new BigDecimal("7.2"));
+
+ // Embedded objects
+ assertThat(result.getElectronicAddress()).isNotNull();
+ assertThat(result.getElectronicAddress().getEmail1()).isEqualTo("meter@mysql.test");
+ assertThat(result.getElectronicAddress().getMac()).isEqualTo("11:22:33:44:55:66");
+
+ assertThat(result.getLifecycle()).isNotNull();
+ assertThat(result.getLifecycle().getManufacturedDate()).isNotNull();
+ assertThat(result.getLifecycle().getInstallationDate()).isNotNull();
+
+ assertThat(result.getAcceptanceTest()).isNotNull();
+ assertThat(result.getAcceptanceTest().getSuccess()).isTrue();
+ assertThat(result.getAcceptanceTest().getType()).isEqualTo("Field Test");
+
+ assertThat(result.getStatus()).isNotNull();
+ assertThat(result.getStatus().getValue()).isEqualTo("operational");
+ }
+
+ @Test
+ @DisplayName("Should update meter fields")
+ void shouldUpdateMeterFields() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setSerialNumber("ORIGINAL-METER-SN-001");
+ meter.setFormNumber("FORM-1");
+ meter.setIntervalLength(600L);
+ meter.setIsVirtual(false);
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+
+ // Act
+ savedMeter.setSerialNumber("UPDATED-METER-SN-002");
+ savedMeter.setFormNumber("FORM-3B");
+ savedMeter.setIntervalLength(1800L);
+ savedMeter.setIsVirtual(true);
+ savedMeter.setInstallCode("UPDATED-INSTALL-CODE");
+ MeterEntity updatedMeter = meterRepository.save(savedMeter);
+ flushAndClear();
+
+ Optional retrieved = meterRepository.findById(updatedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getSerialNumber()).isEqualTo("UPDATED-METER-SN-002");
+ assertThat(retrieved.get().getFormNumber()).isEqualTo("FORM-3B");
+ assertThat(retrieved.get().getIntervalLength()).isEqualTo(1800L);
+ assertThat(retrieved.get().getIsVirtual()).isTrue();
+ assertThat(retrieved.get().getInstallCode()).isEqualTo("UPDATED-INSTALL-CODE");
+ }
+
+ @Test
+ @DisplayName("Should delete meter")
+ void shouldDeleteMeter() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setSerialNumber("DELETE-ME-METER-SN");
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+
+ // Act
+ meterRepository.deleteById(savedMeter.getId());
+ flushAndClear();
+
+ Optional retrieved = meterRepository.findById(savedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isEmpty();
+ }
+ }
+
+ @Nested
+ @DisplayName("Bulk Operations")
+ class BulkOperationsTest {
+
+ @Test
+ @DisplayName("Should handle bulk save operations")
+ void shouldHandleBulkSaveOperations() {
+ // Arrange
+ List meters = TestDataBuilders.createValidEntities(5,
+ TestDataBuilders::createValidMeter);
+
+ for (int i = 0; i < meters.size(); i++) {
+ meters.get(i).setSerialNumber("MYSQL-BULK-METER-SN-" + i);
+ meters.get(i).setFormNumber("FORM-BULK-" + i);
+ }
+
+ // Act
+ List savedMeters = meterRepository.saveAll(meters);
+ flushAndClear();
+
+ // Assert
+ assertThat(savedMeters).hasSize(5);
+ assertThat(savedMeters).allMatch(meter -> meter.getId() != null);
+
+ long count = meterRepository.count();
+ assertThat(count).isGreaterThanOrEqualTo(5);
+ }
+
+ @Test
+ @DisplayName("Should handle bulk delete operations")
+ void shouldHandleBulkDeleteOperations() {
+ // Arrange
+ List meters = TestDataBuilders.createValidEntities(3,
+ TestDataBuilders::createValidMeter);
+
+ List savedMeters = meterRepository.saveAll(meters);
+ long initialCount = meterRepository.count();
+ flushAndClear();
+
+ // Act
+ meterRepository.deleteAll(savedMeters);
+ flushAndClear();
+
+ // Assert
+ long finalCount = meterRepository.count();
+ assertThat(finalCount).isEqualTo(initialCount - 3);
+ }
+ }
+
+ @Nested
+ @DisplayName("MeterMultiplier Collection Persistence")
+ class MeterMultiplierPersistenceTest {
+
+ @Test
+ @DisplayName("Should persist meter with multiple multipliers")
+ void shouldPersistMeterWithMultipleMultipliers() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setSerialNumber("MYSQL-MULT-SN-001");
+
+ MeterMultiplier mult1 = new MeterMultiplier("voltage", new BigDecimal("240.0"));
+ MeterMultiplier mult2 = new MeterMultiplier("current", new BigDecimal("5.0"));
+ MeterMultiplier mult3 = new MeterMultiplier("kH", new BigDecimal("1.8"));
+ meter.setMeterMultipliers(List.of(mult1, mult2, mult3));
+
+ // Act
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(savedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getMeterMultipliers()).hasSize(3);
+ assertThat(retrieved.get().getMeterMultipliers())
+ .extracting(MeterMultiplier::getKind)
+ .containsExactly("voltage", "current", "kH");
+ // BigDecimal assertions use isEqualByComparingTo() for cross-platform precision tolerance
+ assertThat(retrieved.get().getMeterMultipliers().get(0).getValue())
+ .isEqualByComparingTo(new BigDecimal("240.0"));
+ assertThat(retrieved.get().getMeterMultipliers().get(1).getValue())
+ .isEqualByComparingTo(new BigDecimal("5.0"));
+ assertThat(retrieved.get().getMeterMultipliers().get(2).getValue())
+ .isEqualByComparingTo(new BigDecimal("1.8"));
+ }
+
+ @Test
+ @DisplayName("Should update meter multipliers collection")
+ void shouldUpdateMeterMultipliersCollection() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setSerialNumber("MYSQL-MULT-UPDATE-001");
+ meter.setMeterMultipliers(List.of(
+ new MeterMultiplier("voltage", new BigDecimal("120.0"))
+ ));
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+
+ // Act
+ savedMeter.setMeterMultipliers(List.of(
+ new MeterMultiplier("voltage", new BigDecimal("240.0")),
+ new MeterMultiplier("current", new BigDecimal("10.0"))
+ ));
+ meterRepository.save(savedMeter);
+ flushAndClear();
+
+ Optional retrieved = meterRepository.findById(savedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getMeterMultipliers()).hasSize(2);
+ assertThat(retrieved.get().getMeterMultipliers().get(0).getKind()).isEqualTo("voltage");
+ assertThat(retrieved.get().getMeterMultipliers().get(0).getValue())
+ .isEqualByComparingTo(new BigDecimal("240.0"));
+ assertThat(retrieved.get().getMeterMultipliers().get(1).getKind()).isEqualTo("current");
+ assertThat(retrieved.get().getMeterMultipliers().get(1).getValue())
+ .isEqualByComparingTo(new BigDecimal("10.0"));
+ }
+
+ @Test
+ @DisplayName("Should handle empty meter multipliers collection")
+ void shouldHandleEmptyMeterMultipliersCollection() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setSerialNumber("MYSQL-MULT-EMPTY-001");
+ meter.setMeterMultipliers(List.of());
+
+ // Act
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(savedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getMeterMultipliers()).isEmpty();
+ }
+ }
+}
\ No newline at end of file
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/MeterPostgreSQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/MeterPostgreSQLIntegrationTest.java
new file mode 100644
index 00000000..d9ecd131
--- /dev/null
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/MeterPostgreSQLIntegrationTest.java
@@ -0,0 +1,369 @@
+/*
+ *
+ * Copyright (c) 2025 Green Button Alliance, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.greenbuttonalliance.espi.common.repositories.integration;
+
+import org.greenbuttonalliance.espi.common.domain.customer.entity.Asset;
+import org.greenbuttonalliance.espi.common.domain.customer.entity.MeterEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.MeterMultiplier;
+import org.greenbuttonalliance.espi.common.repositories.customer.MeterRepository;
+import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
+import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.junit.jupiter.Container;
+
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Meter entity integration tests using PostgreSQL TestContainer.
+ *
+ * Tests full CRUD operations and relationship persistence with a real PostgreSQL database.
+ */
+@DisplayName("Meter Integration Tests - PostgreSQL")
+@ActiveProfiles({"test", "test-postgresql"})
+class MeterPostgreSQLIntegrationTest extends BaseTestContainersTest {
+
+ @Container
+ private static final org.testcontainers.containers.PostgreSQLContainer> postgres = postgresqlContainer;
+
+ static {
+ postgres.start();
+ }
+
+ @DynamicPropertySource
+ static void configurePostgreSQLProperties(DynamicPropertyRegistry registry) {
+ registry.add("spring.datasource.url", postgres::getJdbcUrl);
+ registry.add("spring.datasource.username", postgres::getUsername);
+ registry.add("spring.datasource.password", postgres::getPassword);
+ registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
+ }
+
+ @Autowired
+ private MeterRepository meterRepository;
+
+ @Nested
+ @DisplayName("CRUD Operations")
+ class CrudOperationsTest {
+
+ @Test
+ @DisplayName("Should save and retrieve meter with all fields")
+ void shouldSaveAndRetrieveMeterWithAllFields() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setType("Electric Meter");
+ meter.setSerialNumber("PGSQL-METER-SN-123456789");
+ meter.setUtcNumber("PGSQL-UTC-987654321");
+ meter.setLotNumber("LOT-PGSQL-M001");
+ meter.setPurchasePrice(25000L);
+ meter.setCritical(true);
+ meter.setInitialCondition("New");
+ meter.setInitialLossOfLife(BigDecimal.ZERO);
+ meter.setIsVirtual(false);
+ meter.setIsPan(true);
+ meter.setInstallCode("INSTALL-PGSQL-METER-XYZ");
+ meter.setAmrSystem("AMR-PGSQL-METER-SYSTEM");
+
+ // Meter-specific fields
+ meter.setFormNumber("FORM-2A");
+ meter.setIntervalLength(900L); // 15 minutes
+
+ // MeterMultipliers collection
+ MeterMultiplier multiplier1 = new MeterMultiplier("voltage", new BigDecimal("120.0"));
+ MeterMultiplier multiplier2 = new MeterMultiplier("kH", new BigDecimal("7.2"));
+ meter.setMeterMultipliers(List.of(multiplier1, multiplier2));
+
+ // Electronic address
+ ElectronicAddress electronicAddress = new ElectronicAddress();
+ electronicAddress.setEmail1("meter@pgsql.test");
+ electronicAddress.setMac("11:22:33:44:55:66");
+ electronicAddress.setWeb("https://pgsql-meter.test");
+ meter.setElectronicAddress(electronicAddress);
+
+ // Lifecycle dates
+ Asset.LifecycleDate lifecycle = new Asset.LifecycleDate();
+ lifecycle.setManufacturedDate(OffsetDateTime.now().minusYears(2));
+ lifecycle.setPurchaseDate(OffsetDateTime.now().minusYears(1));
+ lifecycle.setInstallationDate(OffsetDateTime.now().minusMonths(6));
+ meter.setLifecycle(lifecycle);
+
+ // Acceptance test
+ Asset.AcceptanceTest acceptanceTest = new Asset.AcceptanceTest();
+ acceptanceTest.setSuccess(true);
+ acceptanceTest.setDateTime(OffsetDateTime.now().minusMonths(6));
+ acceptanceTest.setType("Field Test");
+ meter.setAcceptanceTest(acceptanceTest);
+
+ // Status
+ Status status = new Status();
+ status.setValue("operational");
+ status.setDateTime(OffsetDateTime.now());
+ status.setReason("PostgreSQL meter test");
+ meter.setStatus(status);
+
+ // Act
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(savedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ MeterEntity result = retrieved.get();
+
+ // EndDevice inherited fields
+ assertThat(result.getType()).isEqualTo("Electric Meter");
+ assertThat(result.getSerialNumber()).isEqualTo("PGSQL-METER-SN-123456789");
+ assertThat(result.getUtcNumber()).isEqualTo("PGSQL-UTC-987654321");
+ assertThat(result.getLotNumber()).isEqualTo("LOT-PGSQL-M001");
+ assertThat(result.getPurchasePrice()).isEqualTo(25000L);
+ assertThat(result.getCritical()).isTrue();
+ assertThat(result.getInitialCondition()).isEqualTo("New");
+ assertThat(result.getIsVirtual()).isFalse();
+ assertThat(result.getIsPan()).isTrue();
+ assertThat(result.getInstallCode()).isEqualTo("INSTALL-PGSQL-METER-XYZ");
+ assertThat(result.getAmrSystem()).isEqualTo("AMR-PGSQL-METER-SYSTEM");
+
+ // Meter-specific fields
+ assertThat(result.getFormNumber()).isEqualTo("FORM-2A");
+ assertThat(result.getIntervalLength()).isEqualTo(900L);
+
+ // MeterMultipliers collection
+ assertThat(result.getMeterMultipliers()).hasSize(2);
+ assertThat(result.getMeterMultipliers().get(0).getKind()).isEqualTo("voltage");
+ assertThat(result.getMeterMultipliers().get(0).getValue()).isEqualByComparingTo(new BigDecimal("120.0"));
+ assertThat(result.getMeterMultipliers().get(1).getKind()).isEqualTo("kH");
+ assertThat(result.getMeterMultipliers().get(1).getValue()).isEqualByComparingTo(new BigDecimal("7.2"));
+
+ // Embedded objects
+ assertThat(result.getElectronicAddress()).isNotNull();
+ assertThat(result.getElectronicAddress().getEmail1()).isEqualTo("meter@pgsql.test");
+ assertThat(result.getElectronicAddress().getMac()).isEqualTo("11:22:33:44:55:66");
+
+ assertThat(result.getLifecycle()).isNotNull();
+ assertThat(result.getLifecycle().getManufacturedDate()).isNotNull();
+ assertThat(result.getLifecycle().getInstallationDate()).isNotNull();
+
+ assertThat(result.getAcceptanceTest()).isNotNull();
+ assertThat(result.getAcceptanceTest().getSuccess()).isTrue();
+ assertThat(result.getAcceptanceTest().getType()).isEqualTo("Field Test");
+
+ assertThat(result.getStatus()).isNotNull();
+ assertThat(result.getStatus().getValue()).isEqualTo("operational");
+ }
+
+ @Test
+ @DisplayName("Should update meter fields")
+ void shouldUpdateMeterFields() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setSerialNumber("ORIGINAL-METER-SN-001");
+ meter.setFormNumber("FORM-1");
+ meter.setIntervalLength(600L);
+ meter.setIsVirtual(false);
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+
+ // Act
+ savedMeter.setSerialNumber("UPDATED-METER-SN-002");
+ savedMeter.setFormNumber("FORM-3B");
+ savedMeter.setIntervalLength(1800L);
+ savedMeter.setIsVirtual(true);
+ savedMeter.setInstallCode("UPDATED-INSTALL-CODE");
+ MeterEntity updatedMeter = meterRepository.save(savedMeter);
+ flushAndClear();
+
+ Optional retrieved = meterRepository.findById(updatedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getSerialNumber()).isEqualTo("UPDATED-METER-SN-002");
+ assertThat(retrieved.get().getFormNumber()).isEqualTo("FORM-3B");
+ assertThat(retrieved.get().getIntervalLength()).isEqualTo(1800L);
+ assertThat(retrieved.get().getIsVirtual()).isTrue();
+ assertThat(retrieved.get().getInstallCode()).isEqualTo("UPDATED-INSTALL-CODE");
+ }
+
+ @Test
+ @DisplayName("Should delete meter")
+ void shouldDeleteMeter() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setSerialNumber("DELETE-ME-METER-SN");
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+
+ // Act
+ meterRepository.deleteById(savedMeter.getId());
+ flushAndClear();
+
+ Optional retrieved = meterRepository.findById(savedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isEmpty();
+ }
+ }
+
+ @Nested
+ @DisplayName("Bulk Operations")
+ class BulkOperationsTest {
+
+ @Test
+ @DisplayName("Should handle bulk save operations")
+ void shouldHandleBulkSaveOperations() {
+ // Arrange
+ List meters = TestDataBuilders.createValidEntities(5,
+ TestDataBuilders::createValidMeter);
+
+ for (int i = 0; i < meters.size(); i++) {
+ meters.get(i).setSerialNumber("PGSQL-BULK-METER-SN-" + i);
+ meters.get(i).setFormNumber("FORM-BULK-" + i);
+ }
+
+ // Act
+ List savedMeters = meterRepository.saveAll(meters);
+ flushAndClear();
+
+ // Assert
+ assertThat(savedMeters).hasSize(5);
+ assertThat(savedMeters).allMatch(meter -> meter.getId() != null);
+
+ long count = meterRepository.count();
+ assertThat(count).isGreaterThanOrEqualTo(5);
+ }
+
+ @Test
+ @DisplayName("Should handle bulk delete operations")
+ void shouldHandleBulkDeleteOperations() {
+ // Arrange
+ List meters = TestDataBuilders.createValidEntities(3,
+ TestDataBuilders::createValidMeter);
+
+ List savedMeters = meterRepository.saveAll(meters);
+ long initialCount = meterRepository.count();
+ flushAndClear();
+
+ // Act
+ meterRepository.deleteAll(savedMeters);
+ flushAndClear();
+
+ // Assert
+ long finalCount = meterRepository.count();
+ assertThat(finalCount).isEqualTo(initialCount - 3);
+ }
+ }
+
+ @Nested
+ @DisplayName("MeterMultiplier Collection Persistence")
+ class MeterMultiplierPersistenceTest {
+
+ @Test
+ @DisplayName("Should persist meter with multiple multipliers")
+ void shouldPersistMeterWithMultipleMultipliers() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setSerialNumber("PGSQL-MULT-SN-001");
+
+ MeterMultiplier mult1 = new MeterMultiplier("voltage", new BigDecimal("240.0"));
+ MeterMultiplier mult2 = new MeterMultiplier("current", new BigDecimal("5.0"));
+ MeterMultiplier mult3 = new MeterMultiplier("kH", new BigDecimal("1.8"));
+ meter.setMeterMultipliers(List.of(mult1, mult2, mult3));
+
+ // Act
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(savedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getMeterMultipliers()).hasSize(3);
+ assertThat(retrieved.get().getMeterMultipliers())
+ .extracting(MeterMultiplier::getKind)
+ .containsExactly("voltage", "current", "kH");
+ // BigDecimal assertions use isEqualByComparingTo() for cross-platform precision tolerance
+ assertThat(retrieved.get().getMeterMultipliers().get(0).getValue())
+ .isEqualByComparingTo(new BigDecimal("240.0"));
+ assertThat(retrieved.get().getMeterMultipliers().get(1).getValue())
+ .isEqualByComparingTo(new BigDecimal("5.0"));
+ assertThat(retrieved.get().getMeterMultipliers().get(2).getValue())
+ .isEqualByComparingTo(new BigDecimal("1.8"));
+ }
+
+ @Test
+ @DisplayName("Should update meter multipliers collection")
+ void shouldUpdateMeterMultipliersCollection() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setSerialNumber("PGSQL-MULT-UPDATE-001");
+ meter.setMeterMultipliers(List.of(
+ new MeterMultiplier("voltage", new BigDecimal("120.0"))
+ ));
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+
+ // Act
+ savedMeter.setMeterMultipliers(List.of(
+ new MeterMultiplier("voltage", new BigDecimal("240.0")),
+ new MeterMultiplier("current", new BigDecimal("10.0"))
+ ));
+ meterRepository.save(savedMeter);
+ flushAndClear();
+
+ Optional retrieved = meterRepository.findById(savedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getMeterMultipliers()).hasSize(2);
+ assertThat(retrieved.get().getMeterMultipliers().get(0).getKind()).isEqualTo("voltage");
+ assertThat(retrieved.get().getMeterMultipliers().get(0).getValue())
+ .isEqualByComparingTo(new BigDecimal("240.0"));
+ assertThat(retrieved.get().getMeterMultipliers().get(1).getKind()).isEqualTo("current");
+ assertThat(retrieved.get().getMeterMultipliers().get(1).getValue())
+ .isEqualByComparingTo(new BigDecimal("10.0"));
+ }
+
+ @Test
+ @DisplayName("Should handle empty meter multipliers collection")
+ void shouldHandleEmptyMeterMultipliersCollection() {
+ // Arrange
+ MeterEntity meter = TestDataBuilders.createValidMeter();
+ meter.setSerialNumber("PGSQL-MULT-EMPTY-001");
+ meter.setMeterMultipliers(List.of());
+
+ // Act
+ MeterEntity savedMeter = meterRepository.save(meter);
+ flushAndClear();
+ Optional retrieved = meterRepository.findById(savedMeter.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getMeterMultipliers()).isEmpty();
+ }
+ }
+}
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ServiceLocationMySQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ServiceLocationMySQLIntegrationTest.java
index d3ba6e26..fcbe4e11 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ServiceLocationMySQLIntegrationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ServiceLocationMySQLIntegrationTest.java
@@ -19,8 +19,17 @@
package org.greenbuttonalliance.espi.common.repositories.integration;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.domain.customer.entity.ServiceLocationEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.repositories.customer.ServiceLocationRepository;
import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
@@ -85,7 +94,7 @@ void shouldSaveAndRetrieveServiceLocationWithAllFields() {
location.setOutageBlock("MYSQL-BLOCK-789");
// Main address
- Organisation.StreetAddress mainAddress = new Organisation.StreetAddress();
+ StreetAddress mainAddress = new StreetAddress();
mainAddress.setStreetDetail("100 MySQL Main Street");
mainAddress.setTownDetail("MySQL City");
mainAddress.setStateOrProvince("CA");
@@ -94,7 +103,7 @@ void shouldSaveAndRetrieveServiceLocationWithAllFields() {
location.setMainAddress(mainAddress);
// Secondary address
- Organisation.StreetAddress secondaryAddress = new Organisation.StreetAddress();
+ StreetAddress secondaryAddress = new StreetAddress();
secondaryAddress.setStreetDetail("PO Box 12345");
secondaryAddress.setTownDetail("MySQL City");
secondaryAddress.setStateOrProvince("CA");
@@ -103,13 +112,13 @@ void shouldSaveAndRetrieveServiceLocationWithAllFields() {
location.setSecondaryAddress(secondaryAddress);
// Phone numbers
- Organisation.TelephoneNumber phone1 = new Organisation.TelephoneNumber();
+ TelephoneNumber phone1 = new TelephoneNumber();
phone1.setCountryCode("1");
phone1.setAreaCode("555");
phone1.setLocalNumber("1234567");
location.setPhone1(phone1);
- Organisation.TelephoneNumber phone2 = new Organisation.TelephoneNumber();
+ TelephoneNumber phone2 = new TelephoneNumber();
phone2.setCountryCode("1");
phone2.setAreaCode("555");
phone2.setLocalNumber("7654321");
@@ -117,7 +126,7 @@ void shouldSaveAndRetrieveServiceLocationWithAllFields() {
location.setPhone2(phone2);
// Electronic address
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("location@mysql.test");
electronicAddress.setWeb("https://location.mysql.test");
location.setElectronicAddress(electronicAddress);
@@ -281,7 +290,7 @@ void shouldPersistAddressesWithAllFields() {
ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation();
location.setType("MySQL Address Test");
- Organisation.StreetAddress mainAddress = new Organisation.StreetAddress();
+ StreetAddress mainAddress = new StreetAddress();
mainAddress.setStreetDetail("500 MySQL Complete Street");
mainAddress.setTownDetail("Complete City");
mainAddress.setStateOrProvince("TX");
@@ -289,7 +298,7 @@ void shouldPersistAddressesWithAllFields() {
mainAddress.setCountry("USA");
location.setMainAddress(mainAddress);
- Organisation.StreetAddress secondaryAddress = new Organisation.StreetAddress();
+ StreetAddress secondaryAddress = new StreetAddress();
secondaryAddress.setStreetDetail("PO Box 999");
secondaryAddress.setTownDetail("Complete City");
secondaryAddress.setStateOrProvince("TX");
@@ -348,7 +357,7 @@ void shouldPersistElectronicAddressWithAllFields() {
ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation();
location.setType("MySQL Electronic Address Test");
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setLan("192.168.1.100");
electronicAddress.setMac("00:1A:2B:3C:4D:5E");
electronicAddress.setEmail1("primary@mysql-loc.test");
@@ -366,7 +375,7 @@ void shouldPersistElectronicAddressWithAllFields() {
// Assert
assertThat(retrieved).isPresent();
- Organisation.ElectronicAddress retrievedAddress = retrieved.get().getElectronicAddress();
+ ElectronicAddress retrievedAddress = retrieved.get().getElectronicAddress();
assertThat(retrievedAddress).isNotNull();
assertThat(retrievedAddress.getLan()).isEqualTo("192.168.1.100");
assertThat(retrievedAddress.getMac()).isEqualTo("00:1A:2B:3C:4D:5E");
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ServiceLocationPostgreSQLIntegrationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ServiceLocationPostgreSQLIntegrationTest.java
index 4c38e72f..c46977ea 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ServiceLocationPostgreSQLIntegrationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ServiceLocationPostgreSQLIntegrationTest.java
@@ -19,8 +19,17 @@
package org.greenbuttonalliance.espi.common.repositories.integration;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.domain.customer.entity.ServiceLocationEntity;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.domain.customer.entity.Status;
+import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.StreetAddress;
+import org.greenbuttonalliance.espi.common.domain.customer.common.TelephoneNumber;
import org.greenbuttonalliance.espi.common.repositories.customer.ServiceLocationRepository;
import org.greenbuttonalliance.espi.common.test.BaseTestContainersTest;
import org.greenbuttonalliance.espi.common.test.TestDataBuilders;
@@ -85,7 +94,7 @@ void shouldSaveAndRetrieveServiceLocationWithAllFields() {
location.setOutageBlock("POSTGRES-BLOCK-456");
// Main address
- Organisation.StreetAddress mainAddress = new Organisation.StreetAddress();
+ StreetAddress mainAddress = new StreetAddress();
mainAddress.setStreetDetail("200 PostgreSQL Avenue");
mainAddress.setTownDetail("Postgres Town");
mainAddress.setStateOrProvince("WA");
@@ -94,7 +103,7 @@ void shouldSaveAndRetrieveServiceLocationWithAllFields() {
location.setMainAddress(mainAddress);
// Secondary address
- Organisation.StreetAddress secondaryAddress = new Organisation.StreetAddress();
+ StreetAddress secondaryAddress = new StreetAddress();
secondaryAddress.setStreetDetail("PO Box 54321");
secondaryAddress.setTownDetail("Postgres Town");
secondaryAddress.setStateOrProvince("WA");
@@ -103,13 +112,13 @@ void shouldSaveAndRetrieveServiceLocationWithAllFields() {
location.setSecondaryAddress(secondaryAddress);
// Phone numbers
- Organisation.TelephoneNumber phone1 = new Organisation.TelephoneNumber();
+ TelephoneNumber phone1 = new TelephoneNumber();
phone1.setCountryCode("1");
phone1.setAreaCode("206");
phone1.setLocalNumber("9876543");
location.setPhone1(phone1);
- Organisation.TelephoneNumber phone2 = new Organisation.TelephoneNumber();
+ TelephoneNumber phone2 = new TelephoneNumber();
phone2.setCountryCode("1");
phone2.setAreaCode("206");
phone2.setLocalNumber("3456789");
@@ -117,7 +126,7 @@ void shouldSaveAndRetrieveServiceLocationWithAllFields() {
location.setPhone2(phone2);
// Electronic address
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setEmail1("location@postgres.test");
electronicAddress.setWeb("https://location.postgres.test");
location.setElectronicAddress(electronicAddress);
@@ -281,7 +290,7 @@ void shouldPersistAddressesWithAllFields() {
ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation();
location.setType("PostgreSQL Address Test");
- Organisation.StreetAddress mainAddress = new Organisation.StreetAddress();
+ StreetAddress mainAddress = new StreetAddress();
mainAddress.setStreetDetail("600 PostgreSQL Complete Boulevard");
mainAddress.setTownDetail("Complete Town");
mainAddress.setStateOrProvince("OR");
@@ -289,7 +298,7 @@ void shouldPersistAddressesWithAllFields() {
mainAddress.setCountry("USA");
location.setMainAddress(mainAddress);
- Organisation.StreetAddress secondaryAddress = new Organisation.StreetAddress();
+ StreetAddress secondaryAddress = new StreetAddress();
secondaryAddress.setStreetDetail("PO Box 111");
secondaryAddress.setTownDetail("Complete Town");
secondaryAddress.setStateOrProvince("OR");
@@ -348,7 +357,7 @@ void shouldPersistElectronicAddressWithAllFields() {
ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation();
location.setType("PostgreSQL Electronic Address Test");
- Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ ElectronicAddress electronicAddress = new ElectronicAddress();
electronicAddress.setLan("10.0.0.100");
electronicAddress.setMac("AA:BB:CC:DD:EE:FF");
electronicAddress.setEmail1("primary@postgres-loc.test");
@@ -366,7 +375,7 @@ void shouldPersistElectronicAddressWithAllFields() {
// Assert
assertThat(retrieved).isPresent();
- Organisation.ElectronicAddress retrievedAddress = retrieved.get().getElectronicAddress();
+ ElectronicAddress retrievedAddress = retrieved.get().getElectronicAddress();
assertThat(retrievedAddress).isNotNull();
assertThat(retrievedAddress.getLan()).isEqualTo("10.0.0.100");
assertThat(retrievedAddress.getMac()).isEqualTo("AA:BB:CC:DD:EE:FF");
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/test/TestDataBuilders.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/test/TestDataBuilders.java
index 9ec91170..d4f9a505 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/test/TestDataBuilders.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/test/TestDataBuilders.java
@@ -379,4 +379,21 @@ public static EndDeviceEntity createValidEndDevice() {
endDevice.setAmrSystem("AMR-" + faker.lorem().word());
return endDevice;
}
+
+ /**
+ * Creates a valid MeterEntity for testing.
+ */
+ public static MeterEntity createValidMeter() {
+ MeterEntity meter = new MeterEntity();
+ meter.setDescription(faker.lorem().sentence(4, 8));
+ meter.setType("Electric Meter");
+ meter.setSerialNumber("SN-" + faker.number().digits(12));
+ meter.setUtcNumber("UTC-" + faker.number().digits(8));
+ meter.setFormNumber("FORM-" + faker.number().numberBetween(1, 10));
+ meter.setIntervalLength(900L); // 15 minutes default
+ meter.setIsVirtual(false);
+ meter.setIsPan(false);
+ meter.setAmrSystem("AMR-" + faker.lorem().word());
+ return meter;
+ }
}
\ No newline at end of file