diff --git a/CLAUDE.md b/CLAUDE.md index 34b430cb..db7980d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -237,6 +237,40 @@ ESPI uses Atom XML feeds for data exchange. Key patterns: ### Common Development Patterns +#### File Modification Best Practices +**CRITICAL: Always use the Edit tool with visible diffs instead of sed/awk for file modifications** + +- **NEVER use sed, awk, or bash text manipulation** for modifying source code or test files +- **ALWAYS use the Edit tool** which shows explicit diffs for review +- **Benefits of Edit tool**: + - Changes are visible and reviewable before execution + - Prevents cascading errors from bulk updates + - Easy to verify correctness with explicit old_string → new_string diff + - Catches errors immediately rather than discovering them during compilation + +- **When multiple files need the same change**: + - Use multiple Edit tool calls (one per file) in a single message + - Show exactly what's changing in each file + - Do NOT use sed/awk loops even if it seems more efficient + +- **Exception**: Only use bash text tools (grep, find) for **searching/analysis**, never for modifications + +**Example - WRONG approach:** +```bash +# DON'T DO THIS - sed makes changes invisibly +sed -i '' 's/OldClass/NewClass/g' src/**/*.java +``` + +**Example - CORRECT approach:** +``` +# DO THIS - Use Edit tool with explicit diffs +Edit tool call showing: +old_string: "import com.example.OldClass;" +new_string: "import com.example.NewClass;" +``` + +This practice prevents issues like missing imports, broken references, and compilation errors that are difficult to track down. + #### Adding New ESPI Entity 1. Create entity in `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/` (or `/customer/`) 2. Extend `IdentifiedObject` base class diff --git a/PHASE_26_METER_IMPLEMENTATION_PLAN.md b/PHASE_26_METER_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..68080451 --- /dev/null +++ b/PHASE_26_METER_IMPLEMENTATION_PLAN.md @@ -0,0 +1,651 @@ +# Phase 26: Meter - ESPI 4.0 Schema Compliance Implementation Plan + +## Overview +Implement complete ESPI 4.0 customer.xsd schema compliance for Meter following Phase 25 (EndDevice) patterns. Meter extends EndDevice and adds meter-specific fields. + +**Branch**: `feature/schema-compliance-phase-26-meter` +**Issue**: #28 Phase 26 + +**Scope**: +1. **Phase 26 (Meter)**: Complete implementation with unit and integration tests +2. **Remove non-ID queries** from MeterRepository and MeterService +3. **Rewrite MeterDto** for strict XSD compliance +4. **Create MeterMapper** for entity-to-DTO conversion + +## Current State +**Existing**: +- ✅ MeterEntity.java (extends EndDeviceEntity, has 3 Meter fields) +- ✅ MeterRepository.java (HAS NON-ID QUERIES - NEEDS CLEANUP) +- ✅ MeterService.java (HAS NON-ID QUERIES - NEEDS CLEANUP) +- ✅ MeterDto.java (has Atom fields - NEEDS REWRITE) + +**Missing**: +- ❌ MeterMapper.java +- ❌ MeterServiceImpl.java (implementation may exist but needs UUID v5 pattern) +- ❌ MeterDtoTest.java +- ❌ MeterRepositoryTest.java +- ❌ MeterMySQLIntegrationTest.java +- ❌ MeterPostgreSQLIntegrationTest.java + +## Critical Issues + +### MeterRepository.java ⚠️ +**Has 11 non-ID custom query methods (MUST REMOVE ALL)**: +- ❌ findBySerialNumber() +- ❌ findByFormNumber() +- ❌ findByUtcNumber() +- ❌ findVirtualMeters() +- ❌ findPhysicalMeters() +- ❌ findPanDevices() +- ❌ findByAmrSystem() +- ❌ findByInstallCode() +- ❌ findByIntervalLengthGreaterThan() +- ❌ findCriticalMeters() +- ❌ All other non-ID queries + +**Target**: ONLY inherited JpaRepository methods (findById, findAll, save, delete) + +### MeterService.java ⚠️ +**Has 18 non-ID service methods (MUST REMOVE ALL)**: +- ❌ findByUuid() +- ❌ findBySerialNumber() +- ❌ findByFormNumber() +- ❌ findByUtcNumber() +- ❌ findVirtualMeters() +- ❌ findPhysicalMeters() +- ❌ findPanDevices() +- ❌ findByAmrSystem() +- ❌ findByInstallCode() +- ❌ findByIntervalLengthGreaterThan() +- ❌ findCriticalMeters() +- ❌ existsBySerialNumber() +- ❌ countMeters() +- ❌ countVirtualMeters() +- ❌ countPhysicalMeters() +- ❌ All other non-ID methods + +**Target**: ONLY 6 CRUD methods (findAll, findById, save, deleteById, existsById, count) + +### MeterDto.java ❌ +**Current (WRONG)**: +- Has Atom fields: id, uuid, published, updated, selfLink, upLink, relatedLinks +- Has description field (goes to AtomEntryDto.title) +- Has serviceLocation embedded DTO (use Atom link) +- Has wrong fields: installDate, removedDate, kh, meterMultiplier (not in XSD) +- Missing ALL 12 Asset fields +- Missing ALL 4 EndDevice fields + +**Target (CORRECT)**: +- ONLY 19 XSD fields: 12 Asset + 4 EndDevice + 3 Meter +- NO Atom fields +- NO embedded relationships + +## XSD Structure + +**Meter extends EndDevice** (customer.xsd lines 243-268): + +### Inheritance Chain +``` +Object → IdentifiedObject → Asset → EndDevice → Meter +``` + +### Meter Fields (3 fields) +```xml + + + + + + + + + +``` + +### Field Count Summary +- **Asset fields**: 12 (aliasName, initialCondition, initialLossOfLife, lifecycleDate, serialNumber, type, utcNumber, lotNumber, electronicAddress, acceptanceTest, status, category) +- **EndDevice fields**: 4 (amrSystem, installCode, isPan, timeZoneOffset) +- **Meter fields**: 3 (formNumber, meterMultipliers, intervalLength) +- **Total**: 19 fields + +## Implementation Tasks + +### Task 1: Remove Non-ID Queries from MeterRepository + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/MeterRepository.java` + +**REMOVE ALL custom queries (lines 40-99)**: +```java +// DELETE THESE METHODS: +Optional findBySerialNumber(String serialNumber); +List findByFormNumber(String formNumber); +List findByUtcNumber(String utcNumber); +List findVirtualMeters(); +List findPhysicalMeters(); +List findPanDevices(); +List findByAmrSystem(String amrSystem); +List findByInstallCode(String installCode); +List findByIntervalLengthGreaterThan(Long seconds); +List findCriticalMeters(); +``` + +**KEEP ONLY**: +```java +@Repository +public interface MeterRepository extends JpaRepository { + // ONLY inherited methods - NO custom queries +} +``` + +### Task 2: Simplify MeterService to 6 CRUD Methods + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/MeterService.java` + +**REMOVE ALL non-CRUD methods (lines 48-130)**: +```java +// DELETE THESE METHODS: +Optional findByUuid(String uuid); +Optional findBySerialNumber(String serialNumber); +List findByFormNumber(String formNumber); +List findByUtcNumber(String utcNumber); +List findVirtualMeters(); +List findPhysicalMeters(); +List findPanDevices(); +List findByAmrSystem(String amrSystem); +List findByInstallCode(String installCode); +List findByIntervalLengthGreaterThan(Long seconds); +List findCriticalMeters(); +boolean existsBySerialNumber(String serialNumber); +long countMeters(); +long countVirtualMeters(); +long countPhysicalMeters(); +``` + +**KEEP ONLY 6 CRUD methods**: +```java +public interface MeterService { + List findAll(); + Optional findById(UUID id); + MeterEntity save(MeterEntity meter); + void deleteById(UUID id); + boolean existsById(UUID id); + long count(); +} +``` + +### Task 3: Update MeterEntity.java + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/MeterEntity.java` + +**Verify Structure**: +- ✅ Extends EndDeviceEntity +- ✅ Has formNumber field +- ✅ Has intervalLength field +- ⚠️ Has meterMultipliers TODO comment (line 57-58) + +**Action**: Keep as-is for now. MeterMultiplier collection can be added in future phase. + +**Update equals/hashCode**: Verify uses pattern matching for HibernateProxy + +### Task 4: Rewrite MeterDto.java + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/MeterDto.java` + +**REMOVE**: +- ❌ id, uuid, published, updated, relatedLinks, selfLink, upLink (Atom fields) +- ❌ description (goes to AtomEntryDto.title) +- ❌ serviceLocation (use Atom link) +- ❌ installDate, removedDate (not in XSD) +- ❌ kh, meterMultiplier (not in XSD - wrong names, XSD has MeterMultipliers collection) +- ❌ getSelfHref(), getUpHref() methods + +**ADD**: +- ✅ All 12 Asset fields (from EndDeviceDto) +- ✅ All 4 EndDevice fields (from EndDeviceDto) +- ✅ All 3 Meter fields (formNumber, meterMultipliers, intervalLength) + +**propOrder**: +```java +@XmlType(name = "Meter", namespace = "http://naesb.org/espi/customer", propOrder = { + // Asset fields (12) - inherited from EndDevice + "aliasName", "initialCondition", "initialLossOfLife", "lifecycleDate", + "serialNumber", "type", "utcNumber", "lotNumber", + "electronicAddress", "acceptanceTest", "status", "category", + // EndDevice fields (4) - inherited from EndDevice + "amrSystem", "installCode", "isPan", "timeZoneOffset", + // Meter fields (3) + "formNumber", "meterMultipliers", "intervalLength" +}) +``` + +**Structure**: +```java +@XmlRootElement(name = "Meter", namespace = "http://naesb.org/espi/customer") +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "Meter", namespace = "http://naesb.org/espi/customer", propOrder = {...}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class MeterDto { + + // Asset fields (12) - from customer.xsd Asset type + @XmlElement(name = "aliasName", namespace = "http://naesb.org/espi/customer") + private String aliasName; + + @XmlElement(name = "initialCondition", namespace = "http://naesb.org/espi/customer") + private String initialCondition; + + @XmlElement(name = "initialLossOfLife", namespace = "http://naesb.org/espi/customer") + private String initialLossOfLife; + + @XmlElement(name = "lifecycleDate", namespace = "http://naesb.org/espi/customer") + private LifecycleDateDto lifecycleDate; + + @XmlElement(name = "serialNumber", namespace = "http://naesb.org/espi/customer") + private String serialNumber; + + @XmlElement(name = "type", namespace = "http://naesb.org/espi/customer") + private String type; + + @XmlElement(name = "utcNumber", namespace = "http://naesb.org/espi/customer") + private String utcNumber; + + @XmlElement(name = "lotNumber", namespace = "http://naesb.org/espi/customer") + private String lotNumber; + + @XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") + private List electronicAddress; + + @XmlElement(name = "acceptanceTest", namespace = "http://naesb.org/espi/customer") + private AcceptanceTestDto acceptanceTest; + + @XmlElement(name = "status", namespace = "http://naesb.org/espi/customer") + private StatusDto status; + + @XmlElement(name = "category", namespace = "http://naesb.org/espi/customer") + private String category; + + // EndDevice fields (4) - from customer.xsd EndDevice type + @XmlElement(name = "amrSystem", namespace = "http://naesb.org/espi/customer") + private String amrSystem; + + @XmlElement(name = "installCode", namespace = "http://naesb.org/espi/customer") + private String installCode; + + @XmlElement(name = "isPan", namespace = "http://naesb.org/espi/customer") + private Boolean isPan; + + @XmlElement(name = "timeZoneOffset", namespace = "http://naesb.org/espi/customer") + private Long timeZoneOffset; + + // Meter fields (3) - from customer.xsd Meter type + @XmlElement(name = "formNumber", namespace = "http://naesb.org/espi/customer") + private String formNumber; + + @XmlElement(name = "MeterMultipliers", namespace = "http://naesb.org/espi/customer") + private List meterMultipliers; + + @XmlElement(name = "intervalLength", namespace = "http://naesb.org/espi/customer") + private Long intervalLength; +} +``` + +**Note**: MeterMultiplierDto is a simple embedded object with 2 fields: +```java +public class MeterMultiplierDto { + private String kind; // MeterMultiplierKind enum as String + private Float value; +} +``` + +### Task 5: Create MeterMapper.java + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/MeterMapper.java` + +```java +@Mapper(componentModel = "spring", uses = { + LifecycleDateMapper.class, + AcceptanceTestMapper.class, + ElectronicAddressMapper.class, + StatusMapper.class +}) +public interface MeterMapper { + + @Mapping(target = "id", ignore = true) + MeterEntity toEntity(MeterDto dto); + + MeterDto toDto(MeterEntity entity); + + // MeterMultiplier mappings (if implementing collection) + MeterMultiplierDto toDto(MeterMultiplier entity); + MeterMultiplier toEntity(MeterMultiplierDto dto); +} +``` + +### Task 6: Rewrite MeterServiceImpl.java + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/MeterServiceImpl.java` + +**Pattern**: Follow EndDeviceServiceImpl pattern with UUID v5 generation + +```java +@Service +@Slf4j +@RequiredArgsConstructor +public class MeterServiceImpl implements MeterService { + + private static final String NAMESPACE = "ESPI-METER"; + private final MeterRepository repository; + private final EspiIdGeneratorService idGenerator; + + @Override + @Transactional + public MeterEntity save(MeterEntity meter) { + if (meter.getId() == null) { + // ❌ NO random UUID fallback - ESPI requires UUID v5 + if (meter.getSerialNumber() == null) { + throw new IllegalArgumentException( + "SerialNumber is required for Meter UUID generation"); + } + UUID deterministicId = idGenerator.generateV5UUID( + NAMESPACE, meter.getSerialNumber()); + meter.setId(deterministicId); + log.debug("Generated UUID v5 for Meter: {}", deterministicId); + } + return repository.save(meter); + } + + @Override + @Transactional(readOnly = true) + public List findAll() { + return repository.findAll(); + } + + @Override + @Transactional(readOnly = true) + public Optional 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