diff --git a/CLAUDE.md b/CLAUDE.md index 129612af..34b430cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -230,6 +230,10 @@ ESPI uses Atom XML feeds for data exchange. Key patterns: - All tests must pass before committing: `mvn test` - Run tests for affected module and its dependents: `mvn test -pl -am` - Integration tests should pass before PR merge: `mvn verify -Pfull-integration` +- **IMPORTANT**: Use AssertJ chained assertions - combine all assertions for a single object into one assertion chain + - Good: `assertThat(retrieved).isPresent().hasValueSatisfying(device -> assertThat(device).extracting(...).containsExactly(...))` + - Bad: Multiple separate `assertThat()` statements for the same object + - This improves readability and provides better failure messages ### Common Development Patterns diff --git a/PHASE_25_ENDDEVICE_IMPLEMENTATION_PLAN.md b/PHASE_25_ENDDEVICE_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..95d8c4f9 --- /dev/null +++ b/PHASE_25_ENDDEVICE_IMPLEMENTATION_PLAN.md @@ -0,0 +1,523 @@ +# Phase 25: EndDevice - ESPI 4.0 Schema Compliance Implementation Plan + +## Overview +Implement complete ESPI 4.0 customer.xsd schema compliance for EndDevice following Phase 18/23/24 patterns. This phase also includes backfilling missing integration tests for Phases 18, 23, and 24. + +**Branch**: `feature/schema-compliance-phase-25-end-device` +**Issue**: #28 Phase 25 + +**Scope**: +1. **Phase 25 (EndDevice)**: Complete implementation with unit and integration tests +2. **Phase 18 Backfill**: Add missing CustomerAccount integration tests +3. **Phase 23 Backfill**: Add missing ServiceLocation integration tests +4. **Phase 24 Backfill**: Add missing CustomerAgreement integration tests + +## Current State +**Existing**: +- ✅ EndDeviceEntity.java (extends IdentifiedObject, has Asset fields inline, has EndDevice fields) +- ✅ EndDeviceDto.java (has Atom fields - NEEDS REWRITE) + +**Missing**: +- ❌ EndDeviceMapper.java +- ❌ EndDeviceRepository.java +- ❌ EndDeviceService.java + EndDeviceServiceImpl.java +- ❌ EndDeviceDtoTest.java +- ❌ EndDeviceRepositoryTest.java + +## Critical Issues + +### EndDeviceEntity.java ⚠️ +- ❌ **Status field type**: Uses `CustomerEntity.Status` → must use shared `Status` +- ⚠️ **Field order**: Verify matches XSD (Asset fields, then EndDevice fields) + +### EndDeviceDto.java ❌ +**Current (WRONG)**: +- Has Atom fields: published, updated, selfLink, upLink, relatedLinks +- Has description field +- Has serviceLocation embedded DTO +- Missing ALL 12 Asset fields + +**Target (CORRECT)**: +- ONLY 16 XSD fields: 12 Asset + 4 EndDevice +- NO Atom fields +- NO embedded relationships + +## Implementation Tasks + +### Task 1: Verify No Non-ID Queries + +✅ **Verified**: No EndDeviceRepository or EndDeviceService exists yet +**Result**: No queries to remove + +### Task 2: Update EndDeviceEntity.java + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceEntity.java` + +**Fix Status Type** (line 147): +```java +// BEFORE: +@Embedded +private CustomerEntity.Status status; + +// AFTER (use repeatable @AttributeOverride, no @AttributeOverrides wrapper): +@Embedded +@AttributeOverride(name = "value", column = @Column(name = "status_value")) +@AttributeOverride(name = "dateTime", column = @Column(name = "status_date_time")) +@AttributeOverride(name = "remark", column = @Column(name = "status_remark")) +@AttributeOverride(name = "reason", column = @Column(name = "status_reason")) +private Status status; +``` + +**Verify Field Order**: +1. Asset fields (12): type, utcNumber, serialNumber, lotNumber, purchasePrice, critical, electronicAddress, lifecycle, acceptanceTest, initialCondition, initialLossOfLife, status +2. EndDevice fields (4): isVirtual, isPan, installCode, amrSystem + +**Update equals/hashCode**: Use pattern matching for HibernateProxy + +### Task 3: Rewrite EndDeviceDto.java + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/EndDeviceDto.java` + +**REMOVE**: +- ❌ id, published, updated, relatedLinks, selfLink, upLink (Atom fields) +- ❌ description (goes to AtomEntryDto.title) +- ❌ serviceLocation (use Atom link) +- ❌ getSelfHref(), getUpHref() methods + +**ADD**: +- ✅ All 12 Asset fields +- ✅ All 4 EndDevice fields +- ✅ Nested LifecycleDateDto (2 fields) +- ✅ Nested AcceptanceTestDto (4 fields) + +**propOrder**: +```java +@XmlType(name = "EndDevice", namespace = "http://naesb.org/espi/customer", propOrder = { + // Asset fields (12) + "type", "utcNumber", "serialNumber", "lotNumber", "purchasePrice", "critical", + "electronicAddress", "lifecycle", "acceptanceTest", "initialCondition", + "initialLossOfLife", "status", + // EndDevice fields (4) + "isVirtual", "isPan", "installCode", "amrSystem" +}) +``` + +### Task 4: Create EndDeviceMapper.java + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/EndDeviceMapper.java` + +```java +@Mapper(componentModel = "spring", uses = {CustomerMapper.class}) +public interface EndDeviceMapper { + + @Mapping(target = "uuid", source = "id") + // Asset fields (12 mappings) + // EndDevice fields (4 mappings) + EndDeviceDto toDto(EndDeviceEntity entity); + + @InheritInverseConfiguration + @Mapping(target = "id", source = "uuid") + EndDeviceEntity toEntity(EndDeviceDto dto); + + // LifecycleDate and AcceptanceTest mappings +} +``` + +### Task 5: Create EndDeviceRepository.java + +**File**: `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/EndDeviceRepository.java` + +```java +@Repository +public interface EndDeviceRepository extends JpaRepository { + // ONLY inherited methods - NO custom queries +} +``` + +### Task 6: Create EndDeviceService.java + EndDeviceServiceImpl.java + +**Service Interface**: 6 CRUD methods + +**Service Implementation**: +```java +@Service +@RequiredArgsConstructor +public class EndDeviceServiceImpl implements EndDeviceService { + + private static final String NAMESPACE = "ESPI-END-DEVICE"; + private final EndDeviceRepository repository; + private final EspiIdGeneratorService idGenerator; + + @Override + @Transactional + public EndDeviceEntity save(EndDeviceEntity endDevice) { + if (endDevice.getId() == null) { + // ❌ NO random UUID fallback - ESPI requires UUID v5 + if (endDevice.getSerialNumber() == null) { + throw new IllegalArgumentException( + "SerialNumber is required for EndDevice UUID generation"); + } + UUID deterministicId = idGenerator.generateV5UUID( + NAMESPACE, endDevice.getSerialNumber()); + endDevice.setId(deterministicId); + log.debug("Generated UUID v5 for EndDevice: {}", deterministicId); + } + return repository.save(endDevice); + } + + // ... other CRUD methods ... +} +``` + +**CRITICAL**: NO random UUID fallback - ESPI standard requires UUID v5 + +### Task 7: Register EndDeviceDto in DtoExportServiceImpl + +Add to JAXBContext initialization (line ~264): +```java +org.greenbuttonalliance.espi.common.dto.customer.EndDeviceDto.class, +``` + +### Task 8: Verify Flyway Migration + +Verify end_devices table has all columns including `status_remark`. + +### Task 9: Create Unit Tests + +**EndDeviceDtoTest.java** (6+ tests): +- shouldExportEndDeviceWithRealisticData +- shouldVerifyEndDeviceFieldOrder +- shouldVerifyStatus4FieldCompliance +- shouldVerifyElectronicAddress8FieldCompliance +- shouldExportEndDeviceWithMinimalData +- shouldUseCorrectCustomerNamespace + +**EndDeviceRepositoryTest.java** (21+ tests): +- CRUD Operations (7) +- Asset Field Persistence (5) +- EndDevice Field Persistence (3) +- Base Class Functionality (5) + +### Task 10: Create Missing Integration Tests for Phase 18 (CustomerAccount) + +**Files to Create**: +- `CustomerAccountMySQLIntegrationTest.java` +- `CustomerAccountPostgreSQLIntegrationTest.java` + +**Pattern**: Follow `CustomerMySQLIntegrationTest.java` pattern + +**Location**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/integration/customer/` + +**CustomerAccountMySQLIntegrationTest.java**: +```java +@DisplayName("CustomerAccount Integration Tests - MySQL") +@ActiveProfiles({"test", "test-mysql"}) +class CustomerAccountMySQLIntegrationTest extends BaseTestContainersTest { + + @Container + private static final MySQLContainer mysql = mysqlContainer; + + @Autowired + private CustomerAccountRepository customerAccountRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + // 7+ tests: save, retrieve, update, delete, findAll, exists, count + } + + @Nested + @DisplayName("Field Persistence") + class FieldPersistenceTest { + // 3+ tests: billingAddress, currency, budgetBill, accountType + } + + @Nested + @DisplayName("Relationship Persistence") + class RelationshipPersistenceTest { + // 2+ tests: Customer relationship via Atom links + } +} +``` + +**CustomerAccountPostgreSQLIntegrationTest.java**: Same structure with PostgreSQL container + +**Expected**: 12+ tests per database (24+ total integration tests for CustomerAccount) + +### Task 11: Create Missing Integration Tests for Phase 23 (ServiceLocation) + +**Files to Create**: +- `ServiceLocationMySQLIntegrationTest.java` +- `ServiceLocationPostgreSQLIntegrationTest.java` + +**Pattern**: Follow `CustomerMySQLIntegrationTest.java` pattern + +**Location**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/integration/customer/` + +**ServiceLocationMySQLIntegrationTest.java**: +```java +@DisplayName("ServiceLocation Integration Tests - MySQL") +@ActiveProfiles({"test", "test-mysql"}) +class ServiceLocationMySQLIntegrationTest extends BaseTestContainersTest { + + @Container + private static final MySQLContainer mysql = mysqlContainer; + + @Autowired + private ServiceLocationRepository serviceLocationRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + // 7+ tests: save, retrieve, update, delete, findAll, exists, count + } + + @Nested + @DisplayName("Location Field Persistence") + class LocationFieldPersistenceTest { + // 4+ tests: mainAddress, phone1, electronicAddress, status, positionPoints + } + + @Nested + @DisplayName("ServiceLocation Field Persistence") + class ServiceLocationFieldPersistenceTest { + // 2+ tests: accessMethod, siteAccessProblem, needsInspection, outageBlock + } + + @Nested + @DisplayName("Relationship Persistence") + class RelationshipPersistenceTest { + // 2+ tests: UsagePoint cross-stream references via hrefs + } +} +``` + +**ServiceLocationPostgreSQLIntegrationTest.java**: Same structure with PostgreSQL container + +**Expected**: 12+ tests per database (24+ total integration tests for ServiceLocation) + +### Task 12: Create Missing Integration Tests for Phase 24 (CustomerAgreement) + +**Files to Create**: +- `CustomerAgreementMySQLIntegrationTest.java` +- `CustomerAgreementPostgreSQLIntegrationTest.java` + +**Pattern**: Follow `CustomerMySQLIntegrationTest.java` pattern + +**Location**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/integration/customer/` + +**CustomerAgreementMySQLIntegrationTest.java**: +```java +@DisplayName("CustomerAgreement Integration Tests - MySQL") +@ActiveProfiles({"test", "test-mysql"}) +class CustomerAgreementMySQLIntegrationTest extends BaseTestContainersTest { + + @Container + private static final MySQLContainer mysql = mysqlContainer; + + @Autowired + private CustomerAgreementRepository customerAgreementRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + // 7+ tests: save, retrieve, update, delete, findAll, exists, count + } + + @Nested + @DisplayName("Field Persistence") + class FieldPersistenceTest { + // 3+ tests: signDate, loadMgmt, validityInterval, budgetBill + } + + @Nested + @DisplayName("Relationship Persistence") + class RelationshipPersistenceTest { + // 2+ tests: CustomerAccount, ServiceLocation relationships + } +} +``` + +**CustomerAgreementPostgreSQLIntegrationTest.java**: Same structure with PostgreSQL container + +**Expected**: 12+ tests per database (24+ total integration tests for CustomerAgreement) + +### Task 13: Create Integration Tests for Phase 25 (EndDevice) + +**Files to Create**: +- `EndDeviceMySQLIntegrationTest.java` +- `EndDevicePostgreSQLIntegrationTest.java` + +**Pattern**: Follow `CustomerMySQLIntegrationTest.java` pattern + +**Location**: `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/integration/customer/` + +**EndDeviceMySQLIntegrationTest.java**: +```java +@DisplayName("EndDevice Integration Tests - MySQL") +@ActiveProfiles({"test", "test-mysql"}) +class EndDeviceMySQLIntegrationTest extends BaseTestContainersTest { + + @Container + private static final org.testcontainers.containers.MySQLContainer mysql = mysqlContainer; + + @Autowired + private EndDeviceRepository endDeviceRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + // 7+ tests for save, retrieve, update, delete, findAll, exists, count + } + + @Nested + @DisplayName("Asset Field Persistence") + class AssetFieldPersistenceTest { + // 3+ tests for Asset fields, ElectronicAddress, Status + } + + @Nested + @DisplayName("EndDevice Field Persistence") + class EndDeviceFieldPersistenceTest { + // 2+ tests for isVirtual, isPan, installCode, amrSystem + } + + @Nested + @DisplayName("Relationship Persistence") + class RelationshipPersistenceTest { + // 2+ tests for ServiceLocation relationship via Atom links + } +} +``` + +**EndDevicePostgreSQLIntegrationTest.java**: Same structure with PostgreSQL container + +**Expected**: 12+ tests per database (24+ total integration tests for EndDevice) + +### Task 14: Run All Tests + +```bash +cd openespi-common + +# Run unit tests +mvn test + +# Run integration tests +mvn verify -DskipUnitTests +``` + +**Expected Results**: +- Unit tests: 660+ pass (636 existing + 24 new EndDevice) +- Integration tests: 96+ new integration tests + - 24 CustomerAccount (12 MySQL + 12 PostgreSQL) + - 24 ServiceLocation (12 MySQL + 12 PostgreSQL) + - 24 CustomerAgreement (12 MySQL + 12 PostgreSQL) + - 24 EndDevice (12 MySQL + 12 PostgreSQL) +- **Total**: 756+ tests pass + +### Task 15: Run SonarQube Analysis + +```bash +cd openespi-common + +# Run SonarQube analysis with test coverage +mvn clean verify sonar:sonar \ + -Dsonar.projectKey=openespi-greenbutton-java \ + -Dsonar.host.url=http://localhost:9000 \ + -Dsonar.login= +``` + +**Verify in SonarQube Dashboard**: +- ✅ Zero code smells +- ✅ Zero bugs +- ✅ Zero vulnerabilities +- ✅ Zero security hotspots +- ✅ Coverage metrics acceptable +- ✅ Quality Gate: PASSED + +**Fix any issues before proceeding to commit.** + +### Task 16: Commit, Push, PR + +Follow Phase 18/23/24 git workflow. Update Issue #28 (do NOT close). + +## Expected File Changes + +| File | Type | Description | +|------|------|-------------| +| **Phase 25: EndDevice Files** | | | +| EndDeviceEntity.java | MODIFY | Fix Status type | +| EndDeviceDto.java | REWRITE | Remove Atom, add 16 XSD fields | +| EndDeviceMapper.java | CREATE | MapStruct mapper | +| EndDeviceRepository.java | CREATE | JpaRepository | +| EndDeviceService.java | CREATE | Service interface | +| EndDeviceServiceImpl.java | CREATE | Service with UUID v5 (NO fallback) | +| DtoExportServiceImpl.java | MODIFY | Add to JAXBContext | +| EndDeviceDtoTest.java | CREATE | 6+ unit tests | +| EndDeviceRepositoryTest.java | CREATE | 21+ unit tests | +| EndDeviceMySQLIntegrationTest.java | CREATE | 12+ integration tests | +| EndDevicePostgreSQLIntegrationTest.java | CREATE | 12+ integration tests | +| **Phase 18: CustomerAccount Missing Tests** | | | +| CustomerAccountMySQLIntegrationTest.java | CREATE | 12+ integration tests | +| CustomerAccountPostgreSQLIntegrationTest.java | CREATE | 12+ integration tests | +| **Phase 23: ServiceLocation Missing Tests** | | | +| ServiceLocationMySQLIntegrationTest.java | CREATE | 12+ integration tests | +| ServiceLocationPostgreSQLIntegrationTest.java | CREATE | 12+ integration tests | +| **Phase 24: CustomerAgreement Missing Tests** | | | +| CustomerAgreementMySQLIntegrationTest.java | CREATE | 12+ integration tests | +| CustomerAgreementPostgreSQLIntegrationTest.java | CREATE | 12+ integration tests | + +**Total**: 17 files (2 modified, 15 created) + +## Success Criteria + +**Phase 25: EndDevice** +- ✅ Status: Uses shared `Status` +- ✅ DTO: NO Atom fields +- ✅ DTO: All 16 XSD fields +- ✅ Repository: NO non-ID queries +- ✅ UUID v5: NO random fallback +- ✅ @AttributeOverride: No wrapper + +**Testing** +- ✅ Unit Tests: 660+ pass (636 existing + 24 new EndDevice) +- ✅ Integration Tests: 96+ pass + - 24 CustomerAccount (Phase 18 backfill) + - 24 ServiceLocation (Phase 23 backfill) + - 24 CustomerAgreement (Phase 24 backfill) + - 24 EndDevice (Phase 25 new) +- ✅ Total Tests: 756+ 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. **@AttributeOverride**: Apply directly (no wrapper) + +3. **Asset Embedded**: Fields inline in EndDevice + +4. **Repository**: NO non-ID custom queries + +5. **Integration Test Backfill**: + - Phases 18, 23, 24 merged without integration tests + - This phase adds 96 missing integration tests (24 per phase × 3 phases + 24 for EndDevice) + - All tests follow CustomerMySQLIntegrationTest.java pattern + - Each phase gets MySQL and PostgreSQL coverage + +--- + +**Version**: 2.0 +**Created**: 2026-01-27 +**Updated**: 2026-01-27 +**Status**: ✅ Ready for Implementation + +**Change Log**: +- v1.0: Initial EndDevice implementation plan +- v2.0: Added missing integration tests for Phases 18, 23, 24 (96 additional tests) 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 1e49e763..aa6a2220 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 @@ -139,12 +139,11 @@ public class EndDeviceEntity extends IdentifiedObject { * Status of this asset. */ @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")) - }) - private CustomerEntity.Status status; + @AttributeOverride(name = "value", column = @Column(name = "status_value")) + @AttributeOverride(name = "dateTime", column = @Column(name = "status_date_time")) + @AttributeOverride(name = "remark", column = @Column(name = "status_remark")) + @AttributeOverride(name = "reason", column = @Column(name = "status_reason")) + private Status status; // AssetContainer fields (AssetContainer is simply an Asset that can contain other assets - no additional fields) @@ -174,4 +173,20 @@ public class EndDeviceEntity extends IdentifiedObject { */ @Column(name = "amr_system", length = 256) private String amrSystem; + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = o instanceof org.hibernate.proxy.HibernateProxy ? ((org.hibernate.proxy.HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); + Class thisEffectiveClass = this instanceof org.hibernate.proxy.HibernateProxy ? ((org.hibernate.proxy.HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + EndDeviceEntity that = (EndDeviceEntity) o; + return getId() != null && java.util.Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + return this instanceof org.hibernate.proxy.HibernateProxy ? ((org.hibernate.proxy.HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); + } } \ No newline at end of file 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 6a17038c..ced50a96 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 @@ -63,22 +63,6 @@ public class MeterEntity extends EndDeviceEntity { @Column(name = "interval_length") private Long intervalLength; - @Override - public final boolean equals(Object o) { - if (this == o) return true; - if (o == null) return false; - Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); - Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); - if (thisEffectiveClass != oEffectiveClass) return false; - MeterEntity that = (MeterEntity) o; - return getId() != null && Objects.equals(getId(), that.getId()); - } - - @Override - public final int hashCode() { - return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); - } - @Override public String toString() { return getClass().getSimpleName() + "(" + diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/NotificationMethodKind.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/NotificationMethodKind.java index eb9a54de..133ed2f6 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/NotificationMethodKind.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/NotificationMethodKind.java @@ -21,37 +21,33 @@ /** * Enumeration for NotificationMethodKind values. - * + * Per ESPI 4.0 customer.xsd lines 1961-1996. + * * Method by which the customer was notified. */ public enum NotificationMethodKind { /** - * Email notification + * Contacted by phone by customer service representative. */ - EMAIL, - - /** - * Phone call notification - */ - PHONE, - + CALL, + /** - * SMS/Text message notification + * Trouble reported by email. */ - SMS, - + EMAIL, + /** - * Letter/Mail notification + * Trouble reported by letter. */ LETTER, - + /** - * In-person notification + * Trouble reported by other means. */ - IN_PERSON, - + OTHER, + /** - * Other notification method + * Trouble reported through interactive voice response system. */ - OTHER + IVR } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/AcceptanceTestDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/AcceptanceTestDto.java new file mode 100644 index 00000000..275cd21f --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/AcceptanceTestDto.java @@ -0,0 +1,70 @@ +/* + * + * 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.XmlType; +import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.greenbuttonalliance.espi.common.utils.OffsetDateTimeAdapter; + +import java.io.Serializable; +import java.time.OffsetDateTime; + +/** + * Shared DTO for AcceptanceTest information. + * Per ESPI 4.0 customer.xsd lines 539-569. + * + * Acceptance test information for asset with 4 fields: + * - dateTime: Date and time of test + * - success: Whether test was successful + * - type: Type of acceptance test + * - remark: Additional information about the test + * + * Used by Asset-containing entities (EndDevice, Meter). + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "AcceptanceTest", namespace = "http://naesb.org/espi/customer", propOrder = { + "dateTime", "success", "type", "remark" +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class AcceptanceTestDto implements Serializable { + + @XmlElement(name = "dateTime", namespace = "http://naesb.org/espi/customer") + @XmlJavaTypeAdapter(OffsetDateTimeAdapter.class) + private OffsetDateTime dateTime; + + @XmlElement(name = "success", namespace = "http://naesb.org/espi/customer") + private Boolean success; + + @XmlElement(name = "type", namespace = "http://naesb.org/espi/customer") + private String type; + + @XmlElement(name = "remark", namespace = "http://naesb.org/espi/customer") + private String remark; +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java index ab4b5be5..3a660ab6 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDto.java @@ -53,9 +53,6 @@ @AllArgsConstructor public class CustomerAccountDto { - @XmlTransient - private String uuid; - // ========== Document fields (customer.xsd lines 819-872) ========== /** @@ -92,7 +89,7 @@ public class CustomerAccountDto { * Electronic address for the document. */ @XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") - private CustomerDto.ElectronicAddressDto electronicAddress; + private ElectronicAddressDto 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/dto/customer/CustomerAgreementDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDto.java index 19fead50..4e84887c 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDto.java @@ -60,10 +60,6 @@ @AllArgsConstructor public class CustomerAgreementDto { - // UUID identifier (mRID attribute) - from IdentifiedObject - @XmlAttribute(name = "mRID") - private String uuid; - // ==================== Document fields (customer.xsd lines 819-885) ==================== /** @@ -100,7 +96,7 @@ public class CustomerAgreementDto { * Electronic address for the document. */ @XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") - private CustomerDto.ElectronicAddressDto electronicAddress; + private ElectronicAddressDto 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/dto/customer/CustomerDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDto.java index 6286ab2c..53be1f10 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDto.java @@ -47,9 +47,6 @@ @AllArgsConstructor public class CustomerDto { - @XmlTransient - private String uuid; - @XmlElement(name = "Organisation", namespace = "http://naesb.org/espi/customer") private OrganisationDto organisation; @@ -199,42 +196,4 @@ public static class TelephoneNumberDto implements Serializable { @XmlElement(name = "ituPhone", namespace = "http://naesb.org/espi/customer") private String ituPhone; } - - /** - * Embeddable DTO for ElectronicAddress. - * Per customer.xsd ElectronicAddress type (lines 886-936). - */ - @XmlAccessorType(XmlAccessType.FIELD) - @XmlType(name = "ElectronicAddress", namespace = "http://naesb.org/espi/customer", propOrder = { - "lan", "mac", "email1", "email2", "web", "radio", "userID", "password" - }) - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - public static class ElectronicAddressDto { - @XmlElement(name = "lan", namespace = "http://naesb.org/espi/customer") - private String lan; - - @XmlElement(name = "mac", namespace = "http://naesb.org/espi/customer") - private String mac; - - @XmlElement(name = "email1", namespace = "http://naesb.org/espi/customer") - private String email1; - - @XmlElement(name = "email2", namespace = "http://naesb.org/espi/customer") - private String email2; - - @XmlElement(name = "web", namespace = "http://naesb.org/espi/customer") - private String web; - - @XmlElement(name = "radio", namespace = "http://naesb.org/espi/customer") - private String radio; - - @XmlElement(name = "userID", namespace = "http://naesb.org/espi/customer") - private String userID; - - @XmlElement(name = "password", namespace = "http://naesb.org/espi/customer") - private String password; - } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ElectronicAddressDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ElectronicAddressDto.java new file mode 100644 index 00000000..3edb1366 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ElectronicAddressDto.java @@ -0,0 +1,82 @@ +/* + * + * 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.XmlType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +/** + * Shared DTO for ElectronicAddress information. + * Per ESPI 4.0 customer.xsd lines 886-936. + * + * Electronic address information with 8 fields: + * - lan: Local area network address + * - mac: MAC address + * - email1: Primary email address + * - email2: Alternate email address + * - web: Web address + * - radio: Radio address + * - userID: User ID + * - password: Password + * + * Used by Customer (via Organisation) and EndDevice (Asset field). + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ElectronicAddress", namespace = "http://naesb.org/espi/customer", propOrder = { + "lan", "mac", "email1", "email2", "web", "radio", "userID", "password" +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ElectronicAddressDto implements Serializable { + + @XmlElement(name = "lan", namespace = "http://naesb.org/espi/customer") + private String lan; + + @XmlElement(name = "mac", namespace = "http://naesb.org/espi/customer") + private String mac; + + @XmlElement(name = "email1", namespace = "http://naesb.org/espi/customer") + private String email1; + + @XmlElement(name = "email2", namespace = "http://naesb.org/espi/customer") + private String email2; + + @XmlElement(name = "web", namespace = "http://naesb.org/espi/customer") + private String web; + + @XmlElement(name = "radio", namespace = "http://naesb.org/espi/customer") + private String radio; + + @XmlElement(name = "userID", namespace = "http://naesb.org/espi/customer") + private String userID; + + @XmlElement(name = "password", namespace = "http://naesb.org/espi/customer") + private String password; +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/EndDeviceDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/EndDeviceDto.java index 2155254d..5b34ef95 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/EndDeviceDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/EndDeviceDto.java @@ -19,129 +19,156 @@ package org.greenbuttonalliance.espi.common.dto.customer; -import org.greenbuttonalliance.espi.common.dto.atom.LinkDto; - import jakarta.xml.bind.annotation.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.io.Serializable; +import java.math.BigDecimal; import java.time.OffsetDateTime; -import java.util.List; /** - * EndDevice DTO class for JAXB XML marshalling/unmarshalling. + * EndDevice DTO for ESPI 4.0 customer.xsd schema compliance. + * Per customer.xsd lines 210-242. + * + * EndDevice is an ESPI Resource that inherits from IdentifiedObject. + * Asset fields are embedded inline (Asset inherits from Object in ESPI standard). + * + * Asset that performs one or more end device functions. One type of end device is a meter + * which can perform metering, load management, connect/disconnect, accounting functions, etc. + * Some end devices, such as ones monitoring and controlling air conditioners, refrigerators, pool pumps + * may be connected to a meter. All end devices may have communication capability defined by the associated + * communication function(s). An end device may be owned by a consumer, a service provider, utility or otherwise. + * + * This DTO contains ONLY the 16 XSD elements from customer.xsd: + * - 12 Asset elements: type, utcNumber, serialNumber, lotNumber, purchasePrice, critical, + * electronicAddress (8 fields), lifecycle (2 fields), acceptanceTest (4 fields), + * initialCondition, initialLossOfLife, status (4 fields) + * - 4 EndDevice elements: isVirtual, isPan, installCode, amrSystem + * + * Complex types with nested fields: + * - electronicAddress: ElectronicAddressDto (8 fields: lan, mac, email1, email2, web, radio, userID, password) + * - lifecycle: LifecycleDateDto (2 fields: manufacturedDate, installationDate) + * - acceptanceTest: AcceptanceTestDto (4 fields: dateTime, success, type, remark) + * - status: StatusDto (4 fields: value, dateTime, remark, reason) * - * Represents an end device such as a meter or other measurement equipment. - * Supports Atom protocol XML wrapping. + * Atom protocol fields (id, published, updated, links) are handled by CustomerAtomEntryDto wrapper. */ @XmlRootElement(name = "EndDevice", namespace = "http://naesb.org/espi/customer") @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "EndDevice", namespace = "http://naesb.org/espi/customer", propOrder = { - "published", "updated", "selfLink", "upLink", "relatedLinks", - "description", "amrSystem", "installCode", "isPan", "installDate", - "removedDate", "serialNumber", "serviceLocation" + // Asset fields (12) + "type", "utcNumber", "serialNumber", "lotNumber", "purchasePrice", "critical", + "electronicAddress", "lifecycle", "acceptanceTest", "initialCondition", + "initialLossOfLife", "status", + // EndDevice fields (4) + "isVirtual", "isPan", "installCode", "amrSystem" }) @Getter @Setter @NoArgsConstructor @AllArgsConstructor -public class EndDeviceDto { +public class EndDeviceDto implements Serializable { - @XmlTransient - private Long id; + // ==================== Asset fields (12) ==================== - @XmlAttribute(name = "mRID") - private String uuid; - - @XmlElement(name = "published") - private OffsetDateTime published; - - @XmlElement(name = "updated") - private OffsetDateTime updated; - - @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom") - @XmlElementWrapper(name = "links", namespace = "http://www.w3.org/2005/Atom") - private List relatedLinks; + /** + * Utility-specific classification of Asset and its subtypes. + */ + @XmlElement(name = "type", namespace = "http://naesb.org/espi/customer") + private String type; - @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom") - private LinkDto selfLink; + /** + * Uniquely tracked commodity (UTC) number. + */ + @XmlElement(name = "utcNumber", namespace = "http://naesb.org/espi/customer") + private String utcNumber; - @XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom") - private LinkDto upLink; + /** + * Serial number of this asset. + */ + @XmlElement(name = "serialNumber", namespace = "http://naesb.org/espi/customer") + private String serialNumber; - @XmlElement(name = "description") - private String description; + /** + * Lot number for this asset. + */ + @XmlElement(name = "lotNumber", namespace = "http://naesb.org/espi/customer") + private String lotNumber; - @XmlElement(name = "amrSystem") - private String amrSystem; + /** + * Purchase price of asset. + */ + @XmlElement(name = "purchasePrice", namespace = "http://naesb.org/espi/customer") + private Long purchasePrice; - @XmlElement(name = "installCode") - private String installCode; + /** + * True if asset is considered critical for some reason. + */ + @XmlElement(name = "critical", namespace = "http://naesb.org/espi/customer") + private Boolean critical; - @XmlElement(name = "isPan") - private Boolean isPan; + /** + * Electronic address (complex type with 8 fields). + */ + @XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") + private ElectronicAddressDto electronicAddress; - @XmlElement(name = "installDate") - private OffsetDateTime installDate; + /** + * Lifecycle dates for this asset (complex type with 2 fields). + */ + @XmlElement(name = "lifecycle", namespace = "http://naesb.org/espi/customer") + private LifecycleDateDto lifecycle; - @XmlElement(name = "removedDate") - private OffsetDateTime removedDate; + /** + * Information on acceptance test (complex type with 4 fields). + */ + @XmlElement(name = "acceptanceTest", namespace = "http://naesb.org/espi/customer") + private AcceptanceTestDto acceptanceTest; - @XmlElement(name = "serialNumber") - private String serialNumber; + /** + * Condition of asset in inventory or at time of installation. + */ + @XmlElement(name = "initialCondition", namespace = "http://naesb.org/espi/customer") + private String initialCondition; - @XmlElement(name = "ServiceLocation") - private ServiceLocationDto serviceLocation; + /** + * Percentage of expected life for the asset when it was new; zero for new devices. + */ + @XmlElement(name = "initialLossOfLife", namespace = "http://naesb.org/espi/customer") + private BigDecimal initialLossOfLife; /** - * Minimal constructor for basic device data. + * Status of this asset (complex type with 4 fields). */ - public EndDeviceDto(String uuid, String serialNumber) { - this(null, uuid, null, null, null, null, null, null, - null, null, null, null, null, serialNumber, null); - } + @XmlElement(name = "status", namespace = "http://naesb.org/espi/customer") + private StatusDto status; + + // ==================== EndDevice fields (4) ==================== /** - * Gets the self href for this end device. - * - * @return self href string + * If true, there is no physical device (e.g., virtual meter for aggregation). */ - public String getSelfHref() { - return selfLink != null ? selfLink.getHref() : null; - } + @XmlElement(name = "isVirtual", namespace = "http://naesb.org/espi/customer") + private Boolean isVirtual; /** - * Gets the up href for this end device. - * - * @return up href string + * If true, this is a premises area network (PAN) device. */ - public String getUpHref() { - return upLink != null ? upLink.getHref() : null; - } + @XmlElement(name = "isPan", namespace = "http://naesb.org/espi/customer") + private Boolean isPan; /** - * Generates the default self href for an end device. - * - * @return default self href + * Installation code. */ - public String generateSelfHref() { - if (uuid != null && serviceLocation != null && serviceLocation.getUuid() != null) { - return "/espi/1_1/resource/ServiceLocation/" + serviceLocation.getUuid() + "/EndDevice/" + uuid; - } - return uuid != null ? "/espi/1_1/resource/EndDevice/" + uuid : null; - } + @XmlElement(name = "installCode", namespace = "http://naesb.org/espi/customer") + private String installCode; /** - * Generates the default up href for an end device. - * - * @return default up href + * Automated meter reading (AMR) or other communication system. */ - public String generateUpHref() { - if (serviceLocation != null && serviceLocation.getUuid() != null) { - return "/espi/1_1/resource/ServiceLocation/" + serviceLocation.getUuid() + "/EndDevice"; - } - return "/espi/1_1/resource/EndDevice"; - } -} \ No newline at end of file + @XmlElement(name = "amrSystem", namespace = "http://naesb.org/espi/customer") + private String amrSystem; +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/LifecycleDateDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/LifecycleDateDto.java new file mode 100644 index 00000000..dcbb7e95 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/LifecycleDateDto.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.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.XmlType; +import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.greenbuttonalliance.espi.common.utils.OffsetDateTimeAdapter; + +import java.io.Serializable; +import java.time.OffsetDateTime; + +/** + * Shared DTO for LifecycleDate information. + * Per ESPI 4.0 customer.xsd lines 991-1006. + * + * Lifecycle dates for asset with 2 fields: + * - manufacturedDate: Date asset was manufactured + * - installationDate: Date asset was installed + * + * Used by Asset-containing entities (EndDevice, Meter). + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "LifecycleDate", namespace = "http://naesb.org/espi/customer", propOrder = { + "manufacturedDate", "installationDate" +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class LifecycleDateDto implements Serializable { + + @XmlElement(name = "manufacturedDate", namespace = "http://naesb.org/espi/customer") + @XmlJavaTypeAdapter(OffsetDateTimeAdapter.class) + private OffsetDateTime manufacturedDate; + + @XmlElement(name = "installationDate", namespace = "http://naesb.org/espi/customer") + @XmlJavaTypeAdapter(OffsetDateTimeAdapter.class) + private OffsetDateTime installationDate; +} 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 b8237967..10ee2cfe 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 @@ -132,28 +132,4 @@ public String getSelfHref() { public String getUpHref() { return upLink != null ? upLink.getHref() : null; } - - /** - * Generates the default self href for a meter. - * - * @return default self href - */ - public String generateSelfHref() { - if (uuid != null && serviceLocation != null && serviceLocation.getUuid() != null) { - return "/espi/1_1/resource/ServiceLocation/" + serviceLocation.getUuid() + "/Meter/" + uuid; - } - return uuid != null ? "/espi/1_1/resource/Meter/" + uuid : null; - } - - /** - * Generates the default up href for a meter. - * - * @return default up href - */ - public String generateUpHref() { - if (serviceLocation != null && serviceLocation.getUuid() != null) { - return "/espi/1_1/resource/ServiceLocation/" + serviceLocation.getUuid() + "/Meter"; - } - return "/espi/1_1/resource/Meter"; - } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingsDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingsDto.java index 64a3cafa..61b323ae 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingsDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingsDto.java @@ -117,28 +117,4 @@ public String getSelfHref() { public String getUpHref() { return upLink != null ? upLink.getHref() : null; } - - /** - * Generates the default self href for a program date mapping. - * - * @return default self href - */ - public String generateSelfHref() { - if (uuid != null && customer != null && customer.getUuid() != null) { - return "/espi/1_1/resource/Customer/" + customer.getUuid() + "/ProgramDateIdMappings/" + uuid; - } - return uuid != null ? "/espi/1_1/resource/ProgramDateIdMappings/" + uuid : null; - } - - /** - * Generates the default up href for a program date mapping. - * - * @return default up href - */ - public String generateUpHref() { - if (customer != null && customer.getUuid() != null) { - return "/espi/1_1/resource/Customer/" + customer.getUuid() + "/ProgramDateIdMappings"; - } - return "/espi/1_1/resource/ProgramDateIdMappings"; - } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java index 5e17674a..65006060 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDto.java @@ -50,12 +50,6 @@ @AllArgsConstructor public class ServiceLocationDto implements Serializable { - @XmlTransient - private String id; - - @XmlAttribute(name = "mRID") - private String uuid; - // Location fields (inherited from Location → WorkLocation → ServiceLocation) /** @@ -92,7 +86,7 @@ public class ServiceLocationDto implements Serializable { * Electronic address (email, web, etc.). */ @XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer") - private CustomerDto.ElectronicAddressDto electronicAddress; + private ElectronicAddressDto electronicAddress; /** * Reference to geographical information source, often external to the utility. diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementDto.java index a04c70b3..525a9276 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementDto.java @@ -124,28 +124,4 @@ public String getSelfHref() { public String getUpHref() { return upLink != null ? upLink.getHref() : null; } - - /** - * Generates the default self href for a statement. - * - * @return default self href - */ - public String generateSelfHref() { - if (uuid != null && customerAgreement != null && customerAgreement.getUuid() != null) { - return "/espi/1_1/resource/CustomerAgreement/" + customerAgreement.getUuid() + "/Statement/" + uuid; - } - return uuid != null ? "/espi/1_1/resource/Statement/" + uuid : null; - } - - /** - * Generates the default up href for a statement. - * - * @return default up href - */ - public String generateUpHref() { - if (customerAgreement != null && customerAgreement.getUuid() != null) { - return "/espi/1_1/resource/CustomerAgreement/" + customerAgreement.getUuid() + "/Statement"; - } - return "/espi/1_1/resource/Statement"; - } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ApplicationInformationDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ApplicationInformationDto.java index a6188196..0e057856 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ApplicationInformationDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ApplicationInformationDto.java @@ -96,10 +96,6 @@ }) public class ApplicationInformationDto { - // Internal UUID (not in XSD) - @XmlTransient - private String uuid; - // 1. dataCustodianId - Required private String dataCustodianId; diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/AuthorizationDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/AuthorizationDto.java index 8cef323e..391ffe80 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/AuthorizationDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/AuthorizationDto.java @@ -59,10 +59,6 @@ }) public class AuthorizationDto { - // UUID (not in XSD - internal use only) - @XmlTransient - private String uuid; - // XSD-compliant fields (in order) @Schema(description = "Period during which this authorization is valid", @@ -164,19 +160,10 @@ public class AuthorizationDto { example = "660e8400-e29b-41d4-a716-446655440000") private String retailCustomerId; - /** - * Constructor for basic authorization (XSD-compliant fields only). - */ - public AuthorizationDto(String scope, Short status, String resourceURI, String authorizationUri) { - this(null, null, null, status, null, null, scope, null, null, null, null, - resourceURI, authorizationUri, null, null, null, null, null, null, null, null, null); - } - // JAXB property accessors for XSD-compliant fields (in propOrder sequence) // OAuth2 implementation field accessors (marked @XmlTransient, not in XML output) - public String getUuid() { return uuid; } public String getAccessToken() { return accessToken; } public String getRefreshToken() { return refreshToken; } public String getAuthorizationCode() { return authorizationCode; } diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ElectricPowerQualitySummaryDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ElectricPowerQualitySummaryDto.java index 8ab63b1c..52a063b3 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ElectricPowerQualitySummaryDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ElectricPowerQualitySummaryDto.java @@ -49,12 +49,6 @@ @AllArgsConstructor public class ElectricPowerQualitySummaryDto { - @XmlTransient - private Long id; - - @XmlAttribute(name = "mRID") - private String uuid; - /** * Flicker PLT (Long-term) measurement. * Represents long-term flicker severity as per IEC 61000-4-15. @@ -180,16 +174,6 @@ public class ElectricPowerQualitySummaryDto { @XmlTransient private Long usagePointId; - /** - * Constructor with basic identification. - * - * @param id the database identifier - * @param uuid the unique resource identifier - */ - public ElectricPowerQualitySummaryDto(Long id, String uuid) { - this(id, uuid, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null); - } /** * Checks if this summary contains voltage quality measurements. diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/IntervalBlockDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/IntervalBlockDto.java index 271beec9..65e4dfc6 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/IntervalBlockDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/IntervalBlockDto.java @@ -51,46 +51,12 @@ @AllArgsConstructor public class IntervalBlockDto { - @XmlTransient - private Long id; - - @XmlTransient - private String uuid; - @XmlElement(name = "interval", namespace = "http://naesb.org/espi") private DateTimeIntervalDto interval; @XmlElement(name = "IntervalReading", namespace = "http://naesb.org/espi") private List intervalReadings; - /** - * Convenience constructor for creating interval block with uuid, interval, and readings. - * - * @param uuid the resource identifier - * @param interval the time interval - * @param intervalReadings the list of readings - */ - public IntervalBlockDto(String uuid, DateTimeIntervalDto interval, List intervalReadings) { - this(null, uuid, interval, intervalReadings); - } - - /** - * Generates the default self href for an interval block. - * - * @return default self href - */ - public String generateSelfHref() { - return uuid != null ? "/espi/1_1/resource/IntervalBlock/" + uuid : null; - } - - /** - * Generates the default up href for an interval block. - * - * @return default up href - */ - public String generateUpHref() { - return "/espi/1_1/resource/IntervalBlock"; - } /** * Gets the total number of interval readings. diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/MeterReadingDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/MeterReadingDto.java index 1238f992..7301f777 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/MeterReadingDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/MeterReadingDto.java @@ -43,19 +43,5 @@ @Getter @Setter @NoArgsConstructor -@AllArgsConstructor public class MeterReadingDto { - - @XmlTransient - private Long id; - - @XmlTransient - private String uuid; - - /** - * Minimal constructor for basic meter reading data. - */ - public MeterReadingDto(String uuid) { - this(null, uuid); - } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ReadingTypeDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ReadingTypeDto.java index 54d265cb..6df68a3d 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ReadingTypeDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ReadingTypeDto.java @@ -51,17 +51,7 @@ "measuringPeriod", "argument" }) public class ReadingTypeDto { - - @XmlTransient - private Long id; - - @XmlTransient - // @XmlAttribute(name = "mRID") - private String uuid; - @XmlTransient - private String description; - /** * Accumulation behavior describing how readings accumulate over time. * @@ -312,33 +302,6 @@ public class ReadingTypeDto { @XmlElement(name = "argument") private RationalNumberDto argument; - /** - * Constructor with basic identification. - * - * @param id the database identifier - * @param uuid the unique resource identifier - * @param description human-readable description - */ - public ReadingTypeDto(Long id, String uuid, String description) { - this(id, uuid, description, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null); - } - - /** - * Constructor with essential measurement characteristics. - * - * @param id the database identifier - * @param uuid the unique resource identifier - * @param description human-readable description - * @param commodity the commodity being measured - * @param kind the kind of measurement - * @param uom the unit of measure - */ - public ReadingTypeDto(Long id, String uuid, String description, String commodity, String kind, String uom) { - this(id, uuid, description, null, commodity, null, null, null, null, null, - null, kind, null, null, null, uom, null, null, null, null, null); - } - /** * Checks if this reading type represents energy measurements. * diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDto.java index ed3d7a00..e0282c48 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDto.java @@ -53,18 +53,6 @@ @AllArgsConstructor public class TimeConfigurationDto { - /** - * Internal DTO identifier (not serialized to XML). - */ - @XmlTransient - private Long id; - - /** - * Resource identifier (mRID). - */ - @XmlTransient - private String uuid; - /** * Rule to calculate end of daylight savings time in the current year. * Result of dstEndRule must be greater than result of dstStartRule. @@ -103,17 +91,7 @@ public class TimeConfigurationDto { * @param tzOffset the timezone offset in seconds from UTC */ public TimeConfigurationDto(Long tzOffset) { - this(null, null, null, null, null, tzOffset); - } - - /** - * Constructor with UUID and timezone offset. - * - * @param uuid the resource identifier - * @param tzOffset the timezone offset in seconds from UTC - */ - public TimeConfigurationDto(String uuid, Long tzOffset) { - this(null, uuid, null, null, null, tzOffset); + this(null, null, null, tzOffset); } // Custom getters for defensive copying of byte arrays diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsagePointDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsagePointDto.java index 73b296d1..19a2f407 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsagePointDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsagePointDto.java @@ -56,9 +56,6 @@ @AllArgsConstructor public class UsagePointDto { - @XmlTransient - private String uuid; - @XmlElement(name = "roleFlags", type = String.class) @XmlJavaTypeAdapter(HexBinaryAdapter.class) private byte[] roleFlags; @@ -208,37 +205,6 @@ public class UsagePointDto { @XmlTransient private Object electricPowerQualitySummaries; // List - temporarily Object for compilation - /** - * Minimal constructor for basic usage point data. - * - * @param uuid the resource identifier - * @param serviceCategory the service category - */ - public UsagePointDto(String uuid, ServiceCategory serviceCategory) { - this(uuid, null, serviceCategory, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null); - } - - /** - * Constructor with core ESPI elements. - * - * @param uuid the resource identifier - * @param serviceCategory the service category - * @param estimatedLoad estimated load measurement - * @param nominalServiceVoltage nominal voltage measurement - * @param ratedCurrent rated current measurement - * @param ratedPower rated power measurement - * @param serviceDeliveryPoint service delivery point details - */ - public UsagePointDto(String uuid, ServiceCategory serviceCategory, - SummaryMeasurementDto estimatedLoad, SummaryMeasurementDto nominalServiceVoltage, - SummaryMeasurementDto ratedCurrent, SummaryMeasurementDto ratedPower, - ServiceDeliveryPointDto serviceDeliveryPoint) { - this(uuid, null, serviceCategory, null, serviceDeliveryPoint, null, null, null, estimatedLoad, - null, null, null, null, nominalServiceVoltage, null, null, ratedCurrent, ratedPower, - null, null, null, null, null, null, null, null, null); - } - /** * Override getRoleFlags to return cloned array for defensive copying. * Lombok @Getter will be overridden by this explicit method. @@ -251,24 +217,6 @@ public byte[] getRoleFlags() { // Utility methods (no @XmlTransient needed with FIELD access) - /** - * Generates the default self href for a usage point. - * - * @return default self href - */ - public String generateSelfHref() { - return uuid != null ? "/espi/1_1/resource/UsagePoint/" + uuid : null; - } - - /** - * Generates the default up href for a usage point. - * - * @return default up href - */ - public String generateUpHref() { - return "/espi/1_1/resource/UsagePoint"; - } - /** * Gets the total number of meter readings. * diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsageSummaryDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsageSummaryDto.java index 0a1a3a23..14b4b787 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsageSummaryDto.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsageSummaryDto.java @@ -60,12 +60,6 @@ }) public class UsageSummaryDto { - @XmlTransient - private Long id; - - @XmlAttribute(name = "mRID") - private String uuid; - @XmlElement(name = "billingPeriod") private DateTimeIntervalDto billingPeriod; @@ -144,22 +138,4 @@ public class UsageSummaryDto { * @param uuid the resource identifier (mRID) * @param statusTimeStamp the status timestamp (required) */ - public UsageSummaryDto(String uuid, Long statusTimeStamp) { - this(null, uuid, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - null, null, null, null, statusTimeStamp, null, null, null, null, null); - } - - /** - * Constructor with billing period and status timestamp. - * - * @param uuid the resource identifier (mRID) - * @param billingPeriod the billing period - * @param statusTimeStamp the status timestamp (required) - */ - public UsageSummaryDto(String uuid, DateTimeIntervalDto billingPeriod, Long statusTimeStamp) { - this(null, uuid, billingPeriod, null, null, null, null, null, - null, null, null, null, null, null, null, null, - null, null, null, null, statusTimeStamp, null, null, null, null, null); - } } diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/AcceptanceTestMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/AcceptanceTestMapper.java new file mode 100644 index 00000000..7a5b6582 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/AcceptanceTestMapper.java @@ -0,0 +1,62 @@ +/* + * + * 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.Asset; +import org.greenbuttonalliance.espi.common.dto.customer.AcceptanceTestDto; +import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * MapStruct mapper for converting between Asset.AcceptanceTest and AcceptanceTestDto. + *

+ * Maps acceptance test fields per customer.xsd AcceptanceTest type (lines 618-657). + * Note: customer.xsd defines 4 fields (dateTime, success, type, remark) but Asset.AcceptanceTest + * entity only has 3 fields (dateTime, success, type). The remark field is ignored during toEntity. + */ +@Mapper(componentModel = "spring", uses = {DateTimeMapper.class}) +public interface AcceptanceTestMapper { + + /** + * Converts Asset.AcceptanceTest entity to AcceptanceTestDto. + * Maps the 3 fields from entity. DTO's remark field will be null. + * + * @param entity the acceptance test entity + * @return the acceptance test DTO + */ + @Mapping(target = "dateTime", source = "dateTime") + @Mapping(target = "success", source = "success") + @Mapping(target = "type", source = "type") + @Mapping(target = "remark", ignore = true) + AcceptanceTestDto toDto(Asset.AcceptanceTest entity); + + /** + * Converts AcceptanceTestDto to Asset.AcceptanceTest entity. + * Maps the 3 fields that exist in entity. DTO's remark field is ignored. + * + * @param dto the acceptance test DTO + * @return the acceptance test entity + */ + @Mapping(target = "dateTime", source = "dateTime") + @Mapping(target = "success", source = "success") + @Mapping(target = "type", source = "type") + Asset.AcceptanceTest toEntity(AcceptanceTestDto dto); +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAccountMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAccountMapper.java index fe6c2be4..270e0536 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAccountMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAccountMapper.java @@ -52,7 +52,6 @@ public interface CustomerAccountMapper { * @param entity the customer account entity * @return the customer account DTO */ - @Mapping(target = "uuid", source = "id") // Document fields @Mapping(target = "type", source = "type") @Mapping(target = "authorName", source = "authorName") @@ -79,7 +78,6 @@ public interface CustomerAccountMapper { * @param dto the customer account DTO * @return the customer account entity */ - @Mapping(target = "id", source = "uuid") // Document fields @Mapping(target = "type", source = "type") @Mapping(target = "authorName", source = "authorName") diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAgreementMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAgreementMapper.java index 409a2a43..57bf02b7 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAgreementMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAgreementMapper.java @@ -52,7 +52,6 @@ public interface CustomerAgreementMapper { * @param entity the customer agreement entity * @return the customer agreement DTO */ - @Mapping(target = "uuid", source = "id") // Document fields (11) @Mapping(target = "type", source = "type") @Mapping(target = "authorName", source = "authorName") @@ -84,7 +83,6 @@ public interface CustomerAgreementMapper { * @param dto the customer agreement DTO * @return the customer agreement entity */ - @Mapping(target = "id", source = "uuid") // Document fields (11) @Mapping(target = "type", source = "type") @Mapping(target = "authorName", source = "authorName") 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 bb9170f3..10c63f10 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 @@ -23,6 +23,7 @@ import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation; import org.greenbuttonalliance.espi.common.domain.customer.entity.PhoneNumberEntity; import org.greenbuttonalliance.espi.common.dto.customer.CustomerDto; +import org.greenbuttonalliance.espi.common.dto.customer.ElectronicAddressDto; import org.greenbuttonalliance.espi.common.mapper.BaseIdentifiedObjectMapper; import org.greenbuttonalliance.espi.common.mapper.BaseMapperUtils; import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper; @@ -43,7 +44,8 @@ * used for JAXB XML marshalling in the Green Button API. */ @Mapper(componentModel = "spring", uses = { - DateTimeMapper.class + DateTimeMapper.class, + ElectronicAddressMapper.class }) public interface CustomerMapper extends BaseMapperUtils { @@ -159,9 +161,14 @@ default Organisation.StreetAddress mapStreetAddressFromDto(CustomerDto.StreetAdd return address; } - default CustomerDto.ElectronicAddressDto mapElectronicAddress(Organisation.ElectronicAddress address) { + /** + * Helper method for mapping electronic address in custom Organisation mapping. + * Delegates to simple field-to-field copy since ElectronicAddressMapper + * is not directly accessible from default interface methods. + */ + default ElectronicAddressDto mapElectronicAddress(Organisation.ElectronicAddress address) { if (address == null) return null; - return new CustomerDto.ElectronicAddressDto( + return new ElectronicAddressDto( address.getLan(), address.getMac(), address.getEmail1(), @@ -173,7 +180,12 @@ default CustomerDto.ElectronicAddressDto mapElectronicAddress(Organisation.Elect ); } - default Organisation.ElectronicAddress mapElectronicAddressFromDto(CustomerDto.ElectronicAddressDto dto) { + /** + * Helper method for mapping electronic address from DTO in custom Organisation mapping. + * Delegates to simple field-to-field copy since ElectronicAddressMapper + * is not directly accessible from default interface methods. + */ + default Organisation.ElectronicAddress mapElectronicAddressFromDto(ElectronicAddressDto dto) { if (dto == null) return null; Organisation.ElectronicAddress address = new Organisation.ElectronicAddress(); address.setLan(dto.getLan()); 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 8367e075..4d1a6ca2 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 @@ -20,7 +20,7 @@ package org.greenbuttonalliance.espi.common.mapper.customer; import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation; -import org.greenbuttonalliance.espi.common.dto.customer.CustomerDto; +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 */ - CustomerDto.ElectronicAddressDto toDto(Organisation.ElectronicAddress entity); + ElectronicAddressDto toDto(Organisation.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(CustomerDto.ElectronicAddressDto dto); + Organisation.ElectronicAddress toEntity(ElectronicAddressDto dto); } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/EndDeviceMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/EndDeviceMapper.java new file mode 100644 index 00000000..1b2522bd --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/EndDeviceMapper.java @@ -0,0 +1,112 @@ +/* + * + * 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.EndDeviceEntity; +import org.greenbuttonalliance.espi.common.dto.customer.EndDeviceDto; +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 EndDeviceEntity and EndDeviceDto. + *

+ * 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 EndDevice fields (lines 888-958) including Asset base fields (lines 651-797). + * EndDevice extends Asset (which extends Object per ESPI standard, NOT IdentifiedObject). + * Asset fields are embedded inline in EndDeviceEntity. + */ +@Mapper(componentModel = "spring", uses = { + DateTimeMapper.class, + BaseMapperUtils.class, + ElectronicAddressMapper.class, + LifecycleDateMapper.class, + AcceptanceTestMapper.class, + StatusMapper.class +}) +public interface EndDeviceMapper { + + /** + * Converts an EndDeviceEntity to an EndDeviceDto. + * Maps all Asset fields (12) and EndDevice fields (4) per customer.xsd. + * + * @param entity the end device entity + * @return the end device DTO + */ + // Asset fields (12) + @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) + @Mapping(target = "isVirtual", source = "isVirtual") + @Mapping(target = "isPan", source = "isPan") + @Mapping(target = "installCode", source = "installCode") + @Mapping(target = "amrSystem", source = "amrSystem") + EndDeviceDto toDto(EndDeviceEntity entity); + + /** + * Converts an EndDeviceDto to an EndDeviceEntity. + * Maps all Asset fields (12) and EndDevice fields (4) per customer.xsd. + * + * @param dto the end device DTO + * @return the end device entity + */ + // Asset fields (12) + @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) + @Mapping(target = "isVirtual", source = "isVirtual") + @Mapping(target = "isPan", source = "isPan") + @Mapping(target = "installCode", source = "installCode") + @Mapping(target = "amrSystem", source = "amrSystem") + // 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) + EndDeviceEntity toEntity(EndDeviceDto dto); +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/LifecycleDateMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/LifecycleDateMapper.java new file mode 100644 index 00000000..e798f1bb --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/LifecycleDateMapper.java @@ -0,0 +1,62 @@ +/* + * + * 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.Asset; +import org.greenbuttonalliance.espi.common.dto.customer.LifecycleDateDto; +import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * MapStruct mapper for converting between Asset.LifecycleDate and LifecycleDateDto. + *

+ * Maps lifecycle date fields per customer.xsd LifecycleDate type (lines 923-947). + * Only manufacturedDate and installationDate are included in ESPI 4.0 customer.xsd. + */ +@Mapper(componentModel = "spring", uses = {DateTimeMapper.class}) +public interface LifecycleDateMapper { + + /** + * Converts Asset.LifecycleDate entity to LifecycleDateDto. + * Only maps the 2 fields defined in customer.xsd: manufacturedDate and installationDate. + * + * @param entity the lifecycle date entity + * @return the lifecycle date DTO + */ + @Mapping(target = "manufacturedDate", source = "manufacturedDate") + @Mapping(target = "installationDate", source = "installationDate") + LifecycleDateDto toDto(Asset.LifecycleDate entity); + + /** + * Converts LifecycleDateDto to Asset.LifecycleDate entity. + * Only maps the 2 fields from customer.xsd. Other entity-specific fields default to null. + * + * @param dto the lifecycle date DTO + * @return the lifecycle date entity + */ + @Mapping(target = "manufacturedDate", source = "manufacturedDate") + @Mapping(target = "installationDate", source = "installationDate") + @Mapping(target = "purchaseDate", ignore = true) + @Mapping(target = "receivedDate", ignore = true) + @Mapping(target = "retirementDate", ignore = true) + @Mapping(target = "removalDate", ignore = true) + Asset.LifecycleDate toEntity(LifecycleDateDto 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 51ea43a7..897ec740 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 @@ -42,7 +42,8 @@ * used for JAXB XML marshalling in the Green Button API. */ @Mapper(componentModel = "spring", uses = { - DateTimeMapper.class + DateTimeMapper.class, + ElectronicAddressMapper.class }) public interface ServiceLocationMapper { @@ -53,8 +54,6 @@ public interface ServiceLocationMapper { * @param entity the service location entity * @return the service location DTO */ - @Mapping(target = "id", ignore = true) - @Mapping(target = "uuid", source = "id") @Mapping(target = "type", source = "type") @Mapping(target = "mainAddress", source = "mainAddress") @Mapping(target = "secondaryAddress", source = "secondaryAddress") @@ -79,7 +78,6 @@ public interface ServiceLocationMapper { * @param dto the service location DTO * @return the service location entity */ - @Mapping(target = "id", source = "uuid") @Mapping(target = "type", source = "type") @Mapping(target = "mainAddress", source = "mainAddress") @Mapping(target = "secondaryAddress", source = "secondaryAddress") @@ -166,40 +164,6 @@ default Organisation.TelephoneNumber mapTelephone(CustomerDto.TelephoneNumberDto ); } - /** - * Maps Organisation.ElectronicAddress entity to CustomerDto.ElectronicAddressDto. - */ - default CustomerDto.ElectronicAddressDto mapElectronic(Organisation.ElectronicAddress address) { - if (address == null) return null; - return new CustomerDto.ElectronicAddressDto( - address.getLan(), - address.getMac(), - address.getEmail1(), - address.getEmail2(), - address.getWeb(), - address.getRadio(), - address.getUserID(), - address.getPassword() - ); - } - - /** - * Maps CustomerDto.ElectronicAddressDto to Organisation.ElectronicAddress entity. - */ - default Organisation.ElectronicAddress mapElectronic(CustomerDto.ElectronicAddressDto dto) { - if (dto == null) return null; - Organisation.ElectronicAddress address = new Organisation.ElectronicAddress(); - address.setLan(dto.getLan()); - address.setMac(dto.getMac()); - address.setEmail1(dto.getEmail1()); - address.setEmail2(dto.getEmail2()); - address.setWeb(dto.getWeb()); - address.setRadio(dto.getRadio()); - address.setUserID(dto.getUserID()); - address.setPassword(dto.getPassword()); - return address; - } - /** * Maps Status entity to StatusDto. */ diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ElectricPowerQualitySummaryMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ElectricPowerQualitySummaryMapper.java index 7c6798f1..73f6452b 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ElectricPowerQualitySummaryMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ElectricPowerQualitySummaryMapper.java @@ -51,7 +51,6 @@ public interface ElectricPowerQualitySummaryMapper { * @param entity the electric power quality summary entity * @return the electric power quality summary DTO */ - @Mapping(target = "id", ignore = true) // IdentifiedObject field handled by Atom layer @Mapping(target = "usagePointId", source = "usagePoint.id", qualifiedByName = "uuidToLong") ElectricPowerQualitySummaryDto toDto(ElectricPowerQualitySummaryEntity entity); diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/IntervalBlockMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/IntervalBlockMapper.java index 43a541a7..a3730fb9 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/IntervalBlockMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/IntervalBlockMapper.java @@ -51,7 +51,6 @@ public interface IntervalBlockMapper { * @param entity the interval block entity * @return the interval block DTO */ - @Mapping(target = "id", ignore = true) // IdentifiedObject field handled by Atom layer @Mapping(target = "interval", source = "interval") @Mapping(target = "intervalReadings", source = "intervalReadings") IntervalBlockDto toDto(IntervalBlockEntity entity); diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/MeterReadingMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/MeterReadingMapper.java index 6ed6cbce..da57ada3 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/MeterReadingMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/MeterReadingMapper.java @@ -48,7 +48,6 @@ public interface MeterReadingMapper { * @param entity the meter reading entity * @return the meter reading DTO */ - @Mapping(target = "id", ignore = true) // IdentifiedObject field handled by Atom layer MeterReadingDto toDto(MeterReadingEntity entity); /** diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ReadingTypeMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ReadingTypeMapper.java index f927e5d5..1f4cbbb0 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ReadingTypeMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ReadingTypeMapper.java @@ -49,7 +49,6 @@ public interface ReadingTypeMapper { * @param entity the reading type entity * @return the reading type DTO */ - @Mapping(target = "id", ignore = true) // IdentifiedObject field handled by Atom layer @Mapping(target = "argument", source = "argument") ReadingTypeDto toDto(ReadingTypeEntity entity); diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/TimeConfigurationMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/TimeConfigurationMapper.java index 7540ddc3..1ef4ccb9 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/TimeConfigurationMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/TimeConfigurationMapper.java @@ -49,7 +49,6 @@ public interface TimeConfigurationMapper extends BaseIdentifiedObjectMapper { * @param entity the time configuration entity * @return the time configuration DTO */ - @Mapping(target = "id", ignore = true) // IdentifiedObject field handled by Atom layer TimeConfigurationDto toDto(TimeConfigurationEntity entity); /** diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/UsageSummaryMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/UsageSummaryMapper.java index 96c8e1eb..9e7b8a95 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/UsageSummaryMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/UsageSummaryMapper.java @@ -55,7 +55,6 @@ public interface UsageSummaryMapper { * @param entity the usage summary entity * @return the usage summary DTO */ - @Mapping(target = "id", ignore = true) // IdentifiedObject field handled by Atom layer @Mapping(target = "billingPeriod", source = "billingPeriod") @Mapping(target = "billLastPeriod", source = "billLastPeriod") @Mapping(target = "billToDate", source = "billToDate") diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/EndDeviceRepository.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/EndDeviceRepository.java new file mode 100644 index 00000000..f8a33e17 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/EndDeviceRepository.java @@ -0,0 +1,40 @@ +/* + * + * 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.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.EndDeviceEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +/** + * Spring Data JPA repository for EndDevice entities. + *

+ * Provides EndDevice schema specific query methods for end device data access. + * EndDevice data includes meter devices, sensors, and other assets that perform + * metering and monitoring functions. + *

+ * Per ESPI 4.0 API specification, only findById is supported (provided by JpaRepository). + */ +@Repository +public interface EndDeviceRepository extends JpaRepository { + // Only default JpaRepository methods are supported (findById, findAll, save, delete, etc.) +} \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/EndDeviceService.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/EndDeviceService.java new file mode 100644 index 00000000..5e76f448 --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/EndDeviceService.java @@ -0,0 +1,69 @@ +/* + * + * 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.service.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.EndDeviceEntity; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Service interface for EndDevice data management. + * + * Handles EndDevice schema operations for metering devices, sensors, and other assets + * that perform metering and monitoring functions. EndDevice represents physical or virtual + * devices that perform one or more end device functions such as metering, load management, + * connect/disconnect, and monitoring. + *

+ * Per ESPI 4.0 API specification, only basic CRUD operations are supported. + */ +public interface EndDeviceService { + + /** + * Find all end devices. + */ + List findAll(); + + /** + * Find end device by ID. + */ + Optional findById(UUID id); + + /** + * Save end device. + */ + EndDeviceEntity save(EndDeviceEntity endDevice); + + /** + * Delete end device by ID. + */ + void deleteById(UUID id); + + /** + * Check if end device exists by ID. + */ + boolean existsById(UUID id); + + /** + * Count total end devices. + */ + long countEndDevices(); +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/EndDeviceServiceImpl.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/EndDeviceServiceImpl.java new file mode 100644 index 00000000..dbeb05fa --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/EndDeviceServiceImpl.java @@ -0,0 +1,84 @@ +/* + * + * 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.service.customer.impl; + +import lombok.RequiredArgsConstructor; +import org.greenbuttonalliance.espi.common.domain.customer.entity.EndDeviceEntity; +import org.greenbuttonalliance.espi.common.repositories.customer.EndDeviceRepository; +import org.greenbuttonalliance.espi.common.service.customer.EndDeviceService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Service implementation for EndDevice data management. + * + * Provides business logic for EndDevice schema operations including metering devices, + * sensors, and other monitoring assets. Per ESPI 4.0 API specification, only basic + * CRUD operations are supported. + */ +@Service +@Transactional +@RequiredArgsConstructor +public class EndDeviceServiceImpl implements EndDeviceService { + + private final EndDeviceRepository endDeviceRepository; + + @Override + @Transactional(readOnly = true) + public List findAll() { + return endDeviceRepository.findAll(); + } + + @Override + @Transactional(readOnly = true) + public Optional findById(UUID id) { + return endDeviceRepository.findById(id); + } + + @Override + public EndDeviceEntity save(EndDeviceEntity endDevice) { + // Generate UUID if not present + if (endDevice.getId() == null) { + endDevice.setId(UUID.randomUUID()); + } + return endDeviceRepository.save(endDevice); + } + + @Override + public void deleteById(UUID id) { + endDeviceRepository.deleteById(id); + } + + @Override + @Transactional(readOnly = true) + public boolean existsById(UUID id) { + return endDeviceRepository.existsById(id); + } + + @Override + @Transactional(readOnly = true) + public long countEndDevices() { + return endDeviceRepository.count(); + } +} 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 9b0c2490..d4b06bde 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 @@ -480,6 +480,7 @@ CREATE TABLE end_devices initial_loss_of_life DECIMAL(5, 2), status_value VARCHAR(256), status_date_time TIMESTAMP, + status_remark VARCHAR(256), status_reason VARCHAR(256), is_virtual BOOLEAN DEFAULT FALSE, is_pan BOOLEAN DEFAULT FALSE, diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/CustomerXmlDebugTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/CustomerXmlDebugTest.java index ce8878c7..fbf2be04 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/CustomerXmlDebugTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/CustomerXmlDebugTest.java @@ -103,7 +103,6 @@ private Marshaller createMarshallerForCustomerDomain() throws JAXBException { void shouldDeclareCustomerNamespaceOnly() throws Exception { // Arrange CustomerDto customer = new CustomerDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440001", // uuid null, // organisation null, // kind "Wheelchair access required", // specialNeed - add actual value to force namespace usage @@ -154,7 +153,6 @@ void shouldDeclareCustomerNamespaceOnly() throws Exception { void shouldUseCustPrefixForCustomer() throws Exception { // Arrange CustomerDto customer = new CustomerDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440003", // uuid null, // organisation null, // kind null, // specialNeed @@ -183,7 +181,6 @@ void shouldUseCustPrefixForCustomer() throws Exception { void shouldUseAtomAsDefaultNamespace() throws Exception { // Arrange CustomerDto customer = new CustomerDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440005", // uuid null, // organisation null, // kind null, // specialNeed @@ -220,7 +217,6 @@ void shouldUseAtomAsDefaultNamespace() throws Exception { void debugCompleteCustomerDomainXml() throws Exception { // Arrange CustomerDto customer = new CustomerDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440007", // uuid null, // organisation null, // kind "Hearing impaired", // specialNeed diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/JaxbXmlMarshallingTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/JaxbXmlMarshallingTest.java index e4b5f7da..ca65a142 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/JaxbXmlMarshallingTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/JaxbXmlMarshallingTest.java @@ -63,7 +63,6 @@ void setUp() throws JAXBException { void shouldMarshalUsagePointWithRealisticData() throws Exception { // Create a UsagePointDto with realistic ESPI data UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:test-usage-point", new byte[]{0x01, 0x04}, // Electricity consumer role flags null, // serviceCategory (short) 1, // Active status @@ -95,7 +94,6 @@ void shouldMarshalUsagePointWithRealisticData() throws Exception { void shouldPerformRoundTripMarshallingForUsagePoint() throws Exception { // Create original UsagePoint with comprehensive data UsagePointDto originalUsagePoint = new UsagePointDto( - "urn:uuid:commercial-gas-point", new byte[]{0x02, 0x08}, // Gas consumer role flags null, // serviceCategory (short) 1, // Active status @@ -129,7 +127,6 @@ void shouldPerformRoundTripMarshallingForUsagePoint() throws Exception { void shouldHandleEmptyUsagePointWithoutErrors() throws Exception { // Create empty UsagePoint UsagePointDto empty = new UsagePointDto( - null, // uuid null, // roleFlags null, // serviceCategory null, // status @@ -166,7 +163,6 @@ void shouldHandleEmptyUsagePointWithoutErrors() throws Exception { void shouldHandleNullValuesGracefully() throws Exception { // Create UsagePoint with some null values UsagePointDto withNulls = new UsagePointDto( - "urn:uuid:test-nulls", null, // Null role flags null, // serviceCategory (short) 1, // Non-null status @@ -200,7 +196,6 @@ void shouldHandleNullValuesGracefully() throws Exception { void shouldIncludeProperXmlNamespaces() throws Exception { // Create UsagePoint UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:test-namespaces", null, // roleFlags null, // serviceCategory null, // status @@ -234,7 +229,6 @@ void shouldIncludeProperXmlNamespaces() throws Exception { void shouldMarshalSpecialCharactersCorrectly() throws Exception { // Create UsagePoint with special characters in description (will be in Atom title) UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:test-special-chars", null, // roleFlags null, // serviceCategory null, // status @@ -273,7 +267,6 @@ void shouldMarshalSpecialCharactersCorrectly() throws Exception { void shouldNotThrowExceptionsDuringMarshalling() { // Create UsagePoint UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:test-no-exceptions", null, // roleFlags null, // serviceCategory null, // status 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 3ff80313..ab290340 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 @@ -74,7 +74,6 @@ void jaxbXmlWithJaxbAnnotationsShouldWork() throws Exception { // Create a simple DTO with all nulls UsagePointDto dto = new UsagePointDto( - null, // uuid null, // roleFlags null, // serviceCategory null, // status diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/UsageXmlDebugTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/UsageXmlDebugTest.java index c72d9ac6..58bd8fba 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/UsageXmlDebugTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/UsageXmlDebugTest.java @@ -109,7 +109,6 @@ private Marshaller createMarshallerForUsageDomain() throws JAXBException { void shouldDeclareEspiNamespaceOnly() throws Exception { // Arrange UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440010", new byte[]{0x01}, null, (short) 1, null, null, null, null, null, @@ -158,7 +157,6 @@ void shouldDeclareEspiNamespaceOnly() throws Exception { void shouldUseEspiPrefixForUsagePoint() throws Exception { // Arrange UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440012", new byte[]{0x02}, null, (short) 1, null, null, null, null, null, @@ -187,7 +185,6 @@ void shouldUseEspiPrefixForUsagePoint() throws Exception { void shouldUseAtomAsDefaultNamespace() throws Exception { // Arrange UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440014", new byte[]{0x03}, null, (short) 1, null, null, null, null, null, @@ -222,7 +219,6 @@ void shouldUseAtomAsDefaultNamespace() throws Exception { void debugCompleteUsageDomainXml() throws Exception { // Arrange UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:debug-usage", new byte[]{0x01, 0x02}, // roleFlags null, // serviceCategory (short) 1, // status diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDtoTest.java index 0ea8d2ff..5297cf2a 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDtoTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAccountDtoTest.java @@ -22,6 +22,7 @@ import org.greenbuttonalliance.espi.common.dto.atom.AtomFeedDto; import org.greenbuttonalliance.espi.common.dto.atom.CustomerAtomEntryDto; import org.greenbuttonalliance.espi.common.service.impl.DtoExportServiceImpl; +import org.greenbuttonalliance.espi.common.dto.customer.ElectronicAddressDto; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -205,7 +206,7 @@ void shouldUseCorrectCustomerNamespace() { // Helper methods private CustomerAccountDto createFullCustomerAccountDto() { - CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto( + ElectronicAddressDto electronicAddress = new ElectronicAddressDto( null, null, "billing@example.com", "support@example.com", "https://www.example.com", null, null, null ); @@ -219,12 +220,11 @@ private CustomerAccountDto createFullCustomerAccountDto() { CustomerDto.OrganisationDto contactInfo = new CustomerDto.OrganisationDto( new CustomerDto.StreetAddressDto("123 Main St", "Springfield", "IL", "62701", "USA"), null, null, null, - new CustomerDto.ElectronicAddressDto(null, null, "contact@acme.com", null, "https://acme.com", null, null, null), + new ElectronicAddressDto(null, null, "contact@acme.com", null, "https://acme.com", null, null, null), "ACME Corporation" ); return new CustomerAccountDto( - "550e8400-e29b-51d4-a716-446655440000", "BILLING", "Billing System", OffsetDateTime.of(2025, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC), @@ -245,7 +245,6 @@ private CustomerAccountDto createFullCustomerAccountDto() { private CustomerAccountDto createMinimalCustomerAccountDto() { return new CustomerAccountDto( - "test-uuid", null, null, null, null, null, null, null, null, null, null, null, null, null, null, "ACCT-MIN" ); diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDtoTest.java index 2d62a2ce..77d130c5 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDtoTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDtoTest.java @@ -23,6 +23,7 @@ import org.greenbuttonalliance.espi.common.dto.atom.CustomerAtomEntryDto; import org.greenbuttonalliance.espi.common.dto.common.DateTimeIntervalDto; import org.greenbuttonalliance.espi.common.service.impl.DtoExportServiceImpl; +import org.greenbuttonalliance.espi.common.dto.customer.ElectronicAddressDto; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -228,7 +229,7 @@ void shouldExportElectronicAddressWithAllFields() { // Arrange OffsetDateTime now = OffsetDateTime.of(2025, 1, 26, 10, 30, 0, 0, ZoneOffset.UTC); - CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto( + ElectronicAddressDto electronicAddress = new ElectronicAddressDto( "192.168.1.1", "00:11:22:33:44:55", "primary@utility.com", @@ -240,7 +241,6 @@ void shouldExportElectronicAddressWithAllFields() { ); CustomerAgreementDto customerAgreement = new CustomerAgreementDto( - "test-uuid", "SERVICE", "Author", now, now, @@ -288,7 +288,7 @@ void shouldExportElectronicAddressWithAllFields() { // Helper methods private CustomerAgreementDto createFullCustomerAgreementDto() { - CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto( + ElectronicAddressDto electronicAddress = new ElectronicAddressDto( "10.0.0.1", "AA:BB:CC:DD:EE:FF", "service@utility.com", @@ -320,7 +320,6 @@ private CustomerAgreementDto createFullCustomerAgreementDto() { ); return new CustomerAgreementDto( - "650e8400-e29b-51d4-a716-446655440000", "SERVICE_AGREEMENT", "Utility Service Dept", OffsetDateTime.of(2024, 12, 15, 0, 0, 0, 0, ZoneOffset.UTC), @@ -345,7 +344,6 @@ private CustomerAgreementDto createFullCustomerAgreementDto() { private CustomerAgreementDto createMinimalCustomerAgreementDto() { return new CustomerAgreementDto( - "test-uuid", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "AGR-MIN" ); diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoMarshallingTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoMarshallingTest.java index 52cc4582..7de9fc0a 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoMarshallingTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoMarshallingTest.java @@ -22,6 +22,7 @@ import org.greenbuttonalliance.espi.common.domain.customer.enums.CustomerKind; import org.greenbuttonalliance.espi.common.dto.atom.CustomerAtomEntryDto; import org.greenbuttonalliance.espi.common.service.impl.CustomerExportService; +import org.greenbuttonalliance.espi.common.dto.customer.ElectronicAddressDto; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -90,7 +91,7 @@ void shouldMarshalCustomerWithAllFields() { null ); - CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto( + ElectronicAddressDto electronicAddress = new ElectronicAddressDto( null, // lan null, // mac "customer@example.com", // email1 @@ -127,7 +128,6 @@ void shouldMarshalCustomerWithAllFields() { ); CustomerDto customer = new CustomerDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440000", // uuid organisation, CustomerKind.RESIDENTIAL, "Wheelchair access required", // specialNeed @@ -248,7 +248,6 @@ void shouldMarshalCustomerWithAllFields() { void shouldMarshalCustomerWithMinimalFields() { // Arrange - Create minimal CustomerDto (only customerName required) CustomerDto customer = new CustomerDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440010", null, // organisation null, // kind null, // specialNeed @@ -282,7 +281,6 @@ void shouldMarshalCustomerWithMinimalFields() { void shouldUseCustPrefixForAllElements() { // Arrange CustomerDto customer = new CustomerDto( - "urn:uuid:test-prefix", null, CustomerKind.COMMERCIAL, "Test special need", diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoTest.java index 511c1999..2f661b02 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerDtoTest.java @@ -23,6 +23,7 @@ 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.greenbuttonalliance.espi.common.dto.customer.ElectronicAddressDto; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -205,7 +206,6 @@ void shouldExportCustomerWithMinimalData() throws IOException { ); CustomerDto customer = new CustomerDto( - "550e8400-e29b-51d4-a716-446655440002", organisation, CustomerKind.ENTERPRISE, null, null, null, null, null, null, null @@ -281,7 +281,7 @@ private CustomerDto createFullCustomerDto() { "1", "217", null, "555-5678", "101", null, null, null ); - CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto( + ElectronicAddressDto electronicAddress = new ElectronicAddressDto( null, null, "customer@example.com", "support@example.com", "https://www.example.com", null, null, null ); @@ -299,7 +299,6 @@ private CustomerDto createFullCustomerDto() { CustomerDto.PriorityDto priority = new CustomerDto.PriorityDto(5, 1, "STANDARD"); return new CustomerDto( - "550e8400-e29b-51d4-a716-446655440000", organisation, CustomerKind.RESIDENTIAL, "Life support required", @@ -318,7 +317,6 @@ private CustomerDto createMinimalCustomerDto() { ); return new CustomerDto( - "test-uuid", organisation, CustomerKind.RESIDENTIAL, null, null, null, null, null, null, "Test Name" diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/EndDeviceDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/EndDeviceDtoTest.java new file mode 100644 index 00000000..1cc21918 --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/EndDeviceDtoTest.java @@ -0,0 +1,354 @@ +/* + * + * 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.dto.atom.CustomerAtomEntryDto; +import org.greenbuttonalliance.espi.common.dto.atom.AtomFeedDto; +import org.greenbuttonalliance.espi.common.service.impl.DtoExportServiceImpl; +import org.greenbuttonalliance.espi.common.dto.customer.ElectronicAddressDto; +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 EndDeviceDto. + * Verifies Jakarta JAXB Marshaller processes JAXB annotations correctly for ESPI 4.0 customer.xsd compliance. + * Follows the same pattern as CustomerDtoTest for customer domain resources. + */ +@DisplayName("EndDeviceDto XML Marshalling Tests") +class EndDeviceDtoTest { + + 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 EndDevice with complete realistic data") + void shouldExportEndDeviceWithRealisticData() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + EndDeviceDto endDevice = createFullEndDeviceDto(); + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:660e8400-e29b-51d4-a716-446655440000", + "Smart Meter Device", + now, now, null, endDevice + ); + + AtomFeedDto feed = new AtomFeedDto( + "urn:uuid:feed-id", "EndDevice 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("========== EndDevice XML Output =========="); + System.out.println(xml); + System.out.println("=========================================="); + + // Assert - Basic structure and namespaces + assertThat(xml) + .startsWith("") + .contains(""); + + // Assert - Asset fields present + assertThat(xml) + .contains("SMART_METER") + .contains("SM-2025-001") + .contains("UTC-12345") + .contains("LOT-2025-Q1") + .contains("25000") + .contains("true"); + + // Assert - EndDevice specific fields + assertThat(xml) + .contains("false") + .contains("false") + .contains("INST-CODE-12345") + .contains("ZigBee Smart Energy 2.0"); + } + + @Test + @DisplayName("Should verify EndDevice field order matches customer.xsd") + void shouldVerifyEndDeviceFieldOrder() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + EndDeviceDto endDevice = createFullEndDeviceDto(); + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:660e8400-e29b-51d4-a716-446655440001", + "Test EndDevice", + now, now, null, endDevice + ); + + 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 lines 577-662 + // Asset fields (12): type, utcNumber, serialNumber, lotNumber, purchasePrice, critical, + // electronicAddress, lifecycle, acceptanceTest, initialCondition, initialLossOfLife, status + // EndDevice fields (4): isVirtual, isPan, installCode, amrSystem + 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(""); + + // 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); + } + + @Test + @DisplayName("Should export minimal EndDevice with only required fields") + void shouldExportMinimalEndDevice() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + EndDeviceDto endDevice = new EndDeviceDto(); + // No required fields per XSD - all fields are optional + endDevice.setSerialNumber("MINIMAL-001"); + + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:660e8400-e29b-51d4-a716-446655440002", + "Minimal EndDevice", + now, now, null, endDevice + ); + + 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-001"); + } + + @Test + @DisplayName("Should export EndDevice with lifecycle dates") + void shouldExportEndDeviceWithLifecycleDates() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + LifecycleDateDto lifecycle = new LifecycleDateDto( + now.minusDays(90), // manufacturedDate + now.minusDays(30) // installationDate + ); + + EndDeviceDto endDevice = new EndDeviceDto(); + endDevice.setSerialNumber("LC-001"); + endDevice.setLifecycle(lifecycle); + + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:660e8400-e29b-51d4-a716-446655440003", + "Lifecycle EndDevice", + now, now, null, endDevice + ); + + 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 EndDevice with acceptance test") + void shouldExportEndDeviceWithAcceptanceTest() throws IOException { + // Arrange + LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime(); + + AcceptanceTestDto acceptanceTest = new AcceptanceTestDto( + now.minusDays(7), // dateTime + true, // success + "FIELD_INSTALLATION", // type + null // remark + ); + + EndDeviceDto endDevice = new EndDeviceDto(); + endDevice.setSerialNumber("AT-001"); + endDevice.setAcceptanceTest(acceptanceTest); + + CustomerAtomEntryDto entry = new CustomerAtomEntryDto( + "urn:uuid:660e8400-e29b-51d4-a716-446655440004", + "AcceptanceTest EndDevice", + now, now, null, endDevice + ); + + 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_INSTALLATION") + .contains(""); + } + + /** + * Helper method to create a fully populated EndDeviceDto for testing. + */ + private EndDeviceDto createFullEndDeviceDto() { + ElectronicAddressDto electronicAddress = new ElectronicAddressDto( + "192.168.1.100", // lan + "00:1A:2B:3C:4D:5E", // mac + "meter@utility.com", // email1 + null, // email2 + "https://meter.utility.com", // web + null, // radio + "meter_user", // userID + null // password + ); + + LifecycleDateDto lifecycle = new LifecycleDateDto( + OffsetDateTime.now().minusDays(90), // manufacturedDate + OffsetDateTime.now().minusDays(30) // installationDate + ); + + AcceptanceTestDto acceptanceTest = new AcceptanceTestDto( + OffsetDateTime.now().minusDays(25), // dateTime + true, // success + "FIELD_INSTALLATION", // type + null // remark + ); + + StatusDto status = new StatusDto( + "ACTIVE", // value + OffsetDateTime.now().minusDays(24), // dateTime + "Device operational", // remark + "Installation complete" // reason + ); + + // Create EndDeviceDto with all 16 fields (12 Asset + 4 EndDevice) + return new EndDeviceDto( + // Asset fields (12) + "SMART_METER", // type + "UTC-12345", // utcNumber + "SM-2025-001", // serialNumber + "LOT-2025-Q1", // lotNumber + 25000L, // purchasePrice + true, // critical + electronicAddress, + lifecycle, + acceptanceTest, + "NEW", // initialCondition + BigDecimal.ZERO, // initialLossOfLife + status, + // EndDevice fields (4) + false, // isVirtual + false, // isPan + "INST-CODE-12345", // installCode + "ZigBee Smart Energy 2.0" // amrSystem + ); + } +} \ No newline at end of file diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java index b3eb619e..8e34f20f 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ServiceLocationDtoTest.java @@ -22,6 +22,7 @@ import org.greenbuttonalliance.espi.common.dto.atom.AtomFeedDto; import org.greenbuttonalliance.espi.common.dto.atom.CustomerAtomEntryDto; import org.greenbuttonalliance.espi.common.service.impl.DtoExportServiceImpl; +import org.greenbuttonalliance.espi.common.dto.customer.ElectronicAddressDto; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -311,7 +312,7 @@ private ServiceLocationDto createFullServiceLocationDto() { "1", "312", "773", "555-2000", "200", "9", "011", "+1-312-555-2000" ); - CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto( + ElectronicAddressDto electronicAddress = new ElectronicAddressDto( "192.168.1.100", "00:11:22:33:44:55", "meter@example.com", "support@example.com", "https://meter.example.com", "VHF-123", "meter_user", null ); @@ -340,8 +341,6 @@ private ServiceLocationDto createFullServiceLocationDto() { ); return new ServiceLocationDto( - null, // id - "650e8400-e29b-51d4-a716-446655440000", // uuid "COMMERCIAL", // type mainAddress, secondaryAddress, @@ -365,8 +364,6 @@ private ServiceLocationDto createFullServiceLocationDto() { */ private ServiceLocationDto createMinimalServiceLocationDto() { return new ServiceLocationDto( - null, // id - "650e8400-e29b-51d4-a716-446655440099", // uuid null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ); diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/usage/SubscriptionDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/usage/SubscriptionDtoTest.java index fab248d2..633790ea 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/usage/SubscriptionDtoTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/usage/SubscriptionDtoTest.java @@ -133,7 +133,7 @@ void shouldAddEntriesUsingFluentApi() { UsageAtomEntryDto usagePointEntry = new UsageAtomEntryDto( "urn:uuid:test-usage-point", "Test Usage Point", - new UsagePointDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null) + new UsagePointDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null) ); UsageAtomEntryDto meterReadingEntry = new UsageAtomEntryDto( @@ -165,7 +165,7 @@ void shouldAddMultipleEntriesAtOnce() { SubscriptionDto.SchemaType.ENERGY, now, now - ).withEntry(new UsageAtomEntryDto("urn:uuid:entry1", "Entry 1", new UsagePointDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null))) + ).withEntry(new UsageAtomEntryDto("urn:uuid:entry1", "Entry 1", new UsagePointDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null))) .withEntry(new UsageAtomEntryDto("urn:uuid:entry2", "Entry 2", new MeterReadingDto())); assertThat(dto.getEntries()).hasSize(2); @@ -214,7 +214,7 @@ void shouldConvertToAtomFeedDtoWithCustomerTitle() { void shouldIncludeAllEntriesInAtomFeedDto() { OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC); - UsageAtomEntryDto entry1 = new UsageAtomEntryDto("urn:uuid:entry1", "Entry 1", new UsagePointDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)); + UsageAtomEntryDto entry1 = new UsageAtomEntryDto("urn:uuid:entry1", "Entry 1", new UsagePointDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)); UsageAtomEntryDto entry2 = new UsageAtomEntryDto("urn:uuid:entry2", "Entry 2", new MeterReadingDto()); SubscriptionDto dto = new SubscriptionDto( diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDtoTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDtoTest.java index d1dca9a2..9b9c2b0f 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDtoTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDtoTest.java @@ -60,8 +60,6 @@ void setUp() throws JAXBException { void shouldMarshalTimeConfigurationWithRealisticData() throws Exception { // Create TimeConfigurationDto with Pacific Time (UTC-8) TimeConfigurationDto timeConfig = new TimeConfigurationDto( - null, // id (transient) - "urn:uuid:550e8400-e29b-51d4-a716-446655440000", // uuid (Version-5) new byte[]{0x01, 0x0B, 0x05, 0x00, 0x02, 0x00}, // dstEndRule (Nov 1st, 2am) 3600L, // dstOffset (1 hour in seconds) new byte[]{0x01, 0x03, 0x02, 0x00, 0x02, 0x00}, // dstStartRule (Mar 2nd, 2am) @@ -88,8 +86,6 @@ void shouldMarshalTimeConfigurationWithRealisticData() throws Exception { void shouldPerformRoundTripMarshallingForTimeConfiguration() throws Exception { // Create original TimeConfiguration with Eastern Time (UTC-5) TimeConfigurationDto original = new TimeConfigurationDto( - null, // id - "urn:uuid:550e8400-e29b-51d4-a716-446655440001", // uuid (Version-5) new byte[]{0x01, 0x0B, 0x01, 0x00, 0x02, 0x00}, // dstEndRule 3600L, // dstOffset new byte[]{0x01, 0x03, 0x08, 0x00, 0x02, 0x00}, // dstStartRule @@ -117,8 +113,6 @@ void shouldPerformRoundTripMarshallingForTimeConfiguration() throws Exception { void shouldHandleTimeConfigurationWithOnlyTimezoneOffset() throws Exception { // Create TimeConfiguration with only timezone offset (no DST) TimeConfigurationDto simple = new TimeConfigurationDto( - null, // id - "urn:uuid:550e8400-e29b-51d4-a716-446655440002", // uuid (Version-5) null, // dstEndRule null, // dstOffset null, // dstStartRule @@ -175,8 +169,6 @@ void shouldHandleEmptyTimeConfigurationWithoutErrors() throws Exception { void shouldIncludeProperXmlNamespacesAndElementOrder() throws Exception { // Create TimeConfiguration with all fields TimeConfigurationDto timeConfig = new TimeConfigurationDto( - null, - "urn:uuid:550e8400-e29b-51d4-a716-446655440003", // uuid (Version-5) new byte[]{0x01}, // dstEndRule 3600L, // dstOffset new byte[]{0x01}, // dstStartRule @@ -212,7 +204,6 @@ void shouldHandleByteArrayCloningForDstRules() { // Create TimeConfiguration TimeConfigurationDto timeConfig = new TimeConfigurationDto( - null, "urn:uuid:clone-test", originalEndRule, 3600L, originalStartRule, -18000L ); @@ -245,7 +236,7 @@ void shouldCalculateTimezoneOffsetInHoursCorrectly() { TimeConfigurationDto utc = new TimeConfigurationDto(0L); // UTC assertEquals(0.0, utc.getTzOffsetInHours(), 0.01, "UTC should be 0 hours"); - TimeConfigurationDto noOffset = new TimeConfigurationDto(null, null, null, null, null, null); + TimeConfigurationDto noOffset = new TimeConfigurationDto(null, null, null, null); assertNull(noOffset.getTzOffsetInHours(), "Null offset should return null hours"); } @@ -253,12 +244,12 @@ void shouldCalculateTimezoneOffsetInHoursCorrectly() { @DisplayName("Should calculate DST offset in hours correctly") void shouldCalculateDstOffsetInHoursCorrectly() { TimeConfigurationDto oneHourDst = new TimeConfigurationDto( - null, null, null, 3600L, null, -18000L + null, 3600L, null, -18000L ); assertEquals(1.0, oneHourDst.getDstOffsetInHours(), 0.01, "3600 seconds should be 1 hour"); TimeConfigurationDto halfHourDst = new TimeConfigurationDto( - null, null, null, 1800L, null, -18000L + null, 1800L, null, -18000L ); assertEquals(0.5, halfHourDst.getDstOffsetInHours(), 0.01, "1800 seconds should be 0.5 hours"); @@ -271,7 +262,7 @@ void shouldCalculateDstOffsetInHoursCorrectly() { void shouldCalculateEffectiveOffsetIncludingDst() { // Pacific Time: UTC-8 + DST 1 hour = UTC-7 TimeConfigurationDto pacificDst = new TimeConfigurationDto( - null, null, null, 3600L, null, -28800L + null, 3600L, null, -28800L ); assertEquals(-25200L, pacificDst.getEffectiveOffset(), "UTC-8 + 1hr DST should be -25200 seconds"); assertEquals(-7.0, pacificDst.getEffectiveOffsetInHours(), 0.01, "Effective offset should be -7 hours"); @@ -287,7 +278,6 @@ void shouldCalculateEffectiveOffsetIncludingDst() { void shouldDetectDstRulesPresenceCorrectly() { // With DST rules TimeConfigurationDto withDstRules = new TimeConfigurationDto( - null, null, new byte[]{0x01}, 3600L, new byte[]{0x01}, -28800L ); assertTrue(withDstRules.hasDstRules(), "Should detect DST rules when present"); @@ -298,7 +288,6 @@ void shouldDetectDstRulesPresenceCorrectly() { // Empty DST rules TimeConfigurationDto emptyDstRules = new TimeConfigurationDto( - null, null, new byte[]{}, 3600L, new byte[]{}, -28800L ); assertFalse(emptyDstRules.hasDstRules(), "Should not detect empty DST rules"); @@ -309,13 +298,13 @@ void shouldDetectDstRulesPresenceCorrectly() { void shouldDetectDstActiveStatusCorrectly() { // DST active TimeConfigurationDto dstActive = new TimeConfigurationDto( - null, null, null, 3600L, null, -28800L + null, 3600L, null, -28800L ); assertTrue(dstActive.isDstActive(), "Should detect active DST when offset is non-zero"); // DST inactive TimeConfigurationDto dstInactive = new TimeConfigurationDto( - null, null, null, 0L, null, -28800L + null, 0L, null, -28800L ); assertFalse(dstInactive.isDstActive(), "Should not detect active DST when offset is zero"); diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/mapper/usage/SubscriptionMapperTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/mapper/usage/SubscriptionMapperTest.java index 0ba56670..fe9ac014 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/mapper/usage/SubscriptionMapperTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/mapper/usage/SubscriptionMapperTest.java @@ -120,7 +120,7 @@ void shouldCreateAtomFeedDtoDirectlyWithEntries() { // Using Arrays.asList for polymorphic list creation List entries = java.util.Arrays.asList( - new UsageAtomEntryDto("urn:uuid:entry1", "Usage Point", new UsagePointDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)), + new UsageAtomEntryDto("urn:uuid:entry1", "Usage Point", new UsagePointDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null)), new UsageAtomEntryDto("urn:uuid:entry2", "Meter Reading", new MeterReadingDto()) ); 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 new file mode 100644 index 00000000..df52b1d9 --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/EndDeviceRepositoryTest.java @@ -0,0 +1,403 @@ +/* + * + * 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.customer; + +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.entity.Status; +import org.greenbuttonalliance.espi.common.test.BaseRepositoryTest; +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 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.assertThat; + +/** + * Comprehensive test suite for EndDeviceRepository. + * + * Tests all CRUD operations and validation constraints for EndDevice entities. + * Per ESPI 4.0 API specification, only default JpaRepository methods are supported. + */ +@DisplayName("EndDevice Repository Tests") +class EndDeviceRepositoryTest extends BaseRepositoryTest { + + @Autowired + private EndDeviceRepository endDeviceRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve end device successfully") + void shouldSaveAndRetrieveEndDeviceSuccessfully() { + // Arrange + EndDeviceEntity endDevice = createValidEndDevice(); + endDevice.setDescription("Test Smart Meter"); + endDevice.setSerialNumber("SM-12345"); + + // Act + EndDeviceEntity saved = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(saved.getId()); + + // Assert + assertThat(saved.getId()).isNotNull(); + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(device -> assertThat(device) + .extracting( + EndDeviceEntity::getDescription, + EndDeviceEntity::getSerialNumber, + EndDeviceEntity::getIsVirtual + ) + .containsExactly("Test Smart Meter", "SM-12345", false)); + } + + @Test + @DisplayName("Should find all end devices") + void shouldFindAllEndDevices() { + // Arrange + EndDeviceEntity device1 = createValidEndDevice(); + device1.setSerialNumber("METER-001"); + EndDeviceEntity device2 = createValidEndDevice(); + device2.setSerialNumber("METER-002"); + EndDeviceEntity device3 = createValidEndDevice(); + device3.setSerialNumber("METER-003"); + + endDeviceRepository.saveAll(List.of(device1, device2, device3)); + flushAndClear(); + + // Act + List allDevices = endDeviceRepository.findAll(); + + // Assert + assertThat(allDevices) + .hasSizeGreaterThanOrEqualTo(3) + .extracting(EndDeviceEntity::getSerialNumber) + .contains("METER-001", "METER-002", "METER-003"); + } + + @Test + @DisplayName("Should delete end device successfully") + void shouldDeleteEndDeviceSuccessfully() { + // Arrange + EndDeviceEntity endDevice = createValidEndDevice(); + endDevice.setSerialNumber("DEVICE-DELETE"); + EndDeviceEntity saved = endDeviceRepository.save(endDevice); + UUID deviceId = saved.getId(); + flushAndClear(); + + // Act + endDeviceRepository.deleteById(deviceId); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(deviceId); + + // Assert + assertThat(retrieved).isEmpty(); + } + + @Test + @DisplayName("Should check if end device exists") + void shouldCheckIfEndDeviceExists() { + // Arrange + EndDeviceEntity endDevice = createValidEndDevice(); + endDevice.setSerialNumber("EXIST-CHECK"); + EndDeviceEntity saved = endDeviceRepository.save(endDevice); + flushAndClear(); + + // Act & Assert + assertThat(endDeviceRepository.existsById(saved.getId())).isTrue(); + assertThat(endDeviceRepository.existsById(UUID.randomUUID())).isFalse(); + } + + @Test + @DisplayName("Should count end devices") + void shouldCountEndDevices() { + // Arrange + long initialCount = endDeviceRepository.count(); + endDeviceRepository.saveAll(List.of( + createValidEndDevice(), + createValidEndDevice(), + createValidEndDevice() + )); + flushAndClear(); + + // Act & Assert + assertThat(endDeviceRepository.count()).isEqualTo(initialCount + 3); + } + } + + @Nested + @DisplayName("Asset Field Persistence") + class AssetFieldPersistenceTest { + + @Test + @DisplayName("Should persist all Asset fields correctly") + void shouldPersistAllAssetFields() { + // Arrange + EndDeviceEntity endDevice = createValidEndDevice(); + endDevice.setType("SmartMeter"); + endDevice.setUtcNumber("UTC-98765"); + endDevice.setSerialNumber("SN-ASSET-001"); + endDevice.setLotNumber("LOT-2025-Q1"); + endDevice.setPurchasePrice(15000L); + endDevice.setCritical(true); + endDevice.setInitialCondition("NEW"); + endDevice.setInitialLossOfLife(BigDecimal.ZERO); + + // Act + EndDeviceEntity saved = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(device -> assertThat(device) + .extracting( + EndDeviceEntity::getType, + EndDeviceEntity::getUtcNumber, + EndDeviceEntity::getSerialNumber, + EndDeviceEntity::getLotNumber, + EndDeviceEntity::getPurchasePrice, + EndDeviceEntity::getCritical, + EndDeviceEntity::getInitialCondition + ) + .containsExactly("SmartMeter", "UTC-98765", "SN-ASSET-001", + "LOT-2025-Q1", 15000L, true, "NEW")) + .hasValueSatisfying(device -> + assertThat(device.getInitialLossOfLife()).isEqualByComparingTo(BigDecimal.ZERO)); + } + + @Test + @DisplayName("Should persist lifecycle dates correctly") + void shouldPersistLifecycleDatesCorrectly() { + // Arrange + 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)); + + EndDeviceEntity endDevice = createValidEndDevice(); + endDevice.setLifecycle(lifecycle); + + // Act + EndDeviceEntity saved = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(device -> assertThat(device.getLifecycle()) + .isNotNull() + .extracting( + Asset.LifecycleDate::getInstallationDate, + Asset.LifecycleDate::getManufacturedDate, + Asset.LifecycleDate::getPurchaseDate, + Asset.LifecycleDate::getReceivedDate + ) + .doesNotContainNull()); + } + + @Test + @DisplayName("Should persist acceptance test correctly") + void shouldPersistAcceptanceTestCorrectly() { + // Arrange + Asset.AcceptanceTest acceptanceTest = new Asset.AcceptanceTest(); + acceptanceTest.setDateTime(OffsetDateTime.now().minusDays(7)); + acceptanceTest.setSuccess(true); + acceptanceTest.setType("FIELD_TEST"); + + EndDeviceEntity endDevice = createValidEndDevice(); + endDevice.setAcceptanceTest(acceptanceTest); + + // Act + EndDeviceEntity saved = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(device -> assertThat(device.getAcceptanceTest()) + .isNotNull() + .extracting( + Asset.AcceptanceTest::getSuccess, + Asset.AcceptanceTest::getType + ) + .containsExactly(true, "FIELD_TEST")) + .hasValueSatisfying(device -> + assertThat(device.getAcceptanceTest().getDateTime()).isNotNull()); + } + + @Test + @DisplayName("Should persist status correctly") + void shouldPersistStatusCorrectly() { + // Arrange + Status status = new Status(); + status.setValue("ACTIVE"); + status.setDateTime(OffsetDateTime.now()); + status.setRemark("Device operational"); + status.setReason("Installation complete"); + + EndDeviceEntity endDevice = createValidEndDevice(); + endDevice.setStatus(status); + + // Act + EndDeviceEntity saved = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(device -> assertThat(device.getStatus()) + .isNotNull() + .extracting( + Status::getValue, + Status::getRemark, + Status::getReason + ) + .containsExactly("ACTIVE", "Device operational", "Installation complete")) + .hasValueSatisfying(device -> + assertThat(device.getStatus().getDateTime()).isNotNull()); + } + + @Test + @DisplayName("Should persist electronic address correctly") + void shouldPersistElectronicAddressCorrectly() { + // Arrange + Organisation.ElectronicAddress electronicAddress = new Organisation.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"); + + EndDeviceEntity endDevice = createValidEndDevice(); + endDevice.setElectronicAddress(electronicAddress); + + // Act + EndDeviceEntity saved = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(device -> assertThat(device.getElectronicAddress()) + .isNotNull() + .extracting( + Organisation.ElectronicAddress::getLan, + Organisation.ElectronicAddress::getMac, + Organisation.ElectronicAddress::getEmail1, + Organisation.ElectronicAddress::getWeb + ) + .containsExactly("192.168.1.100", "00:1A:2B:3C:4D:5E", + "meter@utility.com", "https://meter.utility.com")); + } + } + + @Nested + @DisplayName("EndDevice Specific Fields") + class EndDeviceSpecificFieldsTest { + + @Test + @DisplayName("Should persist virtual device flag") + void shouldPersistVirtualDeviceFlag() { + // Arrange + EndDeviceEntity virtualDevice = createValidEndDevice(); + virtualDevice.setIsVirtual(true); + + // Act + EndDeviceEntity saved = endDeviceRepository.save(virtualDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(device -> assertThat(device.getIsVirtual()).isTrue()); + } + + @Test + @DisplayName("Should persist PAN device flag") + void shouldPersistPanDeviceFlag() { + // Arrange + EndDeviceEntity panDevice = createValidEndDevice(); + panDevice.setIsPan(true); + + // Act + EndDeviceEntity saved = endDeviceRepository.save(panDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(device -> assertThat(device.getIsPan()).isTrue()); + } + + @Test + @DisplayName("Should persist install code and AMR system") + void shouldPersistInstallCodeAndAmrSystem() { + // Arrange + EndDeviceEntity endDevice = createValidEndDevice(); + endDevice.setInstallCode("INSTALL-CODE-12345"); + endDevice.setAmrSystem("ZigBee Smart Energy 2.0"); + + // Act + EndDeviceEntity saved = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(saved.getId()); + + // Assert + assertThat(retrieved) + .isPresent() + .hasValueSatisfying(device -> assertThat(device) + .extracting( + EndDeviceEntity::getInstallCode, + EndDeviceEntity::getAmrSystem + ) + .containsExactly("INSTALL-CODE-12345", "ZigBee Smart Energy 2.0")); + } + } + + /** + * Helper method to create a valid EndDeviceEntity for testing. + */ + private EndDeviceEntity createValidEndDevice() { + EndDeviceEntity endDevice = new EndDeviceEntity(); + endDevice.setSerialNumber("DEFAULT-SN-" + UUID.randomUUID().toString().substring(0, 8)); + endDevice.setIsVirtual(false); + endDevice.setIsPan(false); + return endDevice; + } +} 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 new file mode 100644 index 00000000..3b7858fb --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountMySQLIntegrationTest.java @@ -0,0 +1,374 @@ +/* + * + * 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.AccountNotification; +import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAccountEntity; +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.enums.NotificationMethodKind; +import org.greenbuttonalliance.espi.common.repositories.customer.CustomerAccountRepository; +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.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * CustomerAccount entity integration tests using MySQL TestContainer. + * + * Tests full CRUD operations and relationship persistence with a real MySQL database. + */ +@DisplayName("CustomerAccount Integration Tests - MySQL") +@ActiveProfiles({"test", "test-mysql"}) +class CustomerAccountMySQLIntegrationTest 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 CustomerAccountRepository customerAccountRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve customer account with all fields") + void shouldSaveAndRetrieveCustomerAccountWithAllFields() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setType("Commercial Account"); + account.setAuthorName("MySQL Test Author"); + account.setCreatedDateTime(OffsetDateTime.now().minusDays(10)); + account.setLastModifiedDateTime(OffsetDateTime.now()); + account.setRevisionNumber("Rev-1.0"); + account.setSubject("MySQL Account Subject"); + account.setTitle("MySQL Account Title"); + account.setBillingCycle("15"); + account.setBudgetBill("Budget Plan A"); + account.setLastBillAmount(150000L); + account.setAccountId("MYSQL-ACCT-12345"); + account.setIsPrePay(true); + + // Electronic address for document + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setEmail1("document@mysql.test"); + electronicAddress.setWeb("https://mysql-account.test"); + account.setElectronicAddress(electronicAddress); + + // Document status + Status docStatus = new Status(); + docStatus.setValue("approved"); + docStatus.setDateTime(OffsetDateTime.now()); + docStatus.setReason("MySQL document approval"); + account.setDocStatus(docStatus); + + // Contact info + Organisation contactInfo = new Organisation(); + contactInfo.setOrganisationName("MySQL Contact Corp"); + + Organisation.StreetAddress streetAddress = new Organisation.StreetAddress(); + streetAddress.setStreetDetail("123 MySQL Contact Street"); + streetAddress.setTownDetail("Contact City"); + streetAddress.setStateOrProvince("CA"); + streetAddress.setPostalCode("94001"); + streetAddress.setCountry("USA"); + contactInfo.setStreetAddress(streetAddress); + + Organisation.ElectronicAddress contactElectronicAddress = new Organisation.ElectronicAddress(); + contactElectronicAddress.setEmail1("contact@mysql.test"); + contactElectronicAddress.setWeb("https://contact.mysql.test"); + contactInfo.setElectronicAddress(contactElectronicAddress); + + account.setContactInfo(contactInfo); + + // Act + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + Optional retrieved = customerAccountRepository.findById(savedAccount.getId()); + + // Assert + assertThat(retrieved).isPresent(); + CustomerAccountEntity result = retrieved.get(); + + assertThat(result.getType()).isEqualTo("Commercial Account"); + assertThat(result.getAuthorName()).isEqualTo("MySQL Test Author"); + assertThat(result.getBillingCycle()).isEqualTo("15"); + assertThat(result.getBudgetBill()).isEqualTo("Budget Plan A"); + assertThat(result.getLastBillAmount()).isEqualTo(150000L); + assertThat(result.getAccountId()).isEqualTo("MYSQL-ACCT-12345"); + assertThat(result.getIsPrePay()).isTrue(); + + assertThat(result.getElectronicAddress()).isNotNull(); + assertThat(result.getElectronicAddress().getEmail1()).isEqualTo("document@mysql.test"); + + assertThat(result.getDocStatus()).isNotNull(); + assertThat(result.getDocStatus().getValue()).isEqualTo("approved"); + + assertThat(result.getContactInfo()).isNotNull(); + assertThat(result.getContactInfo().getOrganisationName()).isEqualTo("MySQL Contact Corp"); + assertThat(result.getContactInfo().getStreetAddress()).isNotNull(); + assertThat(result.getContactInfo().getStreetAddress().getStreetDetail()).isEqualTo("123 MySQL Contact Street"); + } + + @Test + @DisplayName("Should update customer account fields") + void shouldUpdateCustomerAccountFields() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setAccountId("ORIGINAL-ACCT-ID"); + account.setBillingCycle("1"); + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + + // Act + savedAccount.setAccountId("UPDATED-ACCT-ID"); + savedAccount.setBillingCycle("15"); + savedAccount.setIsPrePay(true); + CustomerAccountEntity updatedAccount = customerAccountRepository.save(savedAccount); + flushAndClear(); + + Optional retrieved = customerAccountRepository.findById(updatedAccount.getId()); + + // Assert + assertThat(retrieved).isPresent(); + assertThat(retrieved.get().getAccountId()).isEqualTo("UPDATED-ACCT-ID"); + assertThat(retrieved.get().getBillingCycle()).isEqualTo("15"); + assertThat(retrieved.get().getIsPrePay()).isTrue(); + } + + @Test + @DisplayName("Should delete customer account") + void shouldDeleteCustomerAccount() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setAccountId("DELETE-ME-ACCT"); + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + + // Act + customerAccountRepository.deleteById(savedAccount.getId()); + flushAndClear(); + + Optional retrieved = customerAccountRepository.findById(savedAccount.getId()); + + // Assert + assertThat(retrieved).isEmpty(); + } + } + + @Nested + @DisplayName("Bulk Operations") + class BulkOperationsTest { + + @Test + @DisplayName("Should handle bulk save operations") + void shouldHandleBulkSaveOperations() { + // Arrange + List accounts = TestDataBuilders.createValidEntities(5, + TestDataBuilders::createValidCustomerAccount); + + for (int i = 0; i < accounts.size(); i++) { + accounts.get(i).setAccountId("MYSQL-BULK-ACCT-" + i); + } + + // Act + List savedAccounts = customerAccountRepository.saveAll(accounts); + flushAndClear(); + + // Assert + assertThat(savedAccounts).hasSize(5); + assertThat(savedAccounts).allMatch(account -> account.getId() != null); + + long count = customerAccountRepository.count(); + assertThat(count).isGreaterThanOrEqualTo(5); + } + + @Test + @DisplayName("Should handle bulk delete operations") + void shouldHandleBulkDeleteOperations() { + // Arrange + List accounts = TestDataBuilders.createValidEntities(3, + TestDataBuilders::createValidCustomerAccount); + + List savedAccounts = customerAccountRepository.saveAll(accounts); + long initialCount = customerAccountRepository.count(); + flushAndClear(); + + // Act + customerAccountRepository.deleteAll(savedAccounts); + flushAndClear(); + + // Assert + long finalCount = customerAccountRepository.count(); + assertThat(finalCount).isEqualTo(initialCount - 3); + } + } + + @Nested + @DisplayName("Embedded Objects Persistence") + class EmbeddedObjectsTest { + + @Test + @DisplayName("Should persist and retrieve Organisation contact info with all nested types") + void shouldPersistOrganisationWithAllTypes() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setAccountId("MYSQL-ORG-TEST"); + + Organisation contactInfo = new Organisation(); + contactInfo.setOrganisationName("Complete MySQL Organization"); + + Organisation.StreetAddress streetAddress = new Organisation.StreetAddress(); + streetAddress.setStreetDetail("456 MySQL Contact Avenue"); + streetAddress.setTownDetail("MySQL Town"); + streetAddress.setStateOrProvince("NY"); + streetAddress.setPostalCode("10001"); + streetAddress.setCountry("USA"); + contactInfo.setStreetAddress(streetAddress); + + Organisation.StreetAddress postalAddress = new Organisation.StreetAddress(); + postalAddress.setStreetDetail("PO Box 888"); + postalAddress.setTownDetail("MySQL Town"); + postalAddress.setStateOrProvince("NY"); + postalAddress.setPostalCode("10002"); + postalAddress.setCountry("USA"); + contactInfo.setPostalAddress(postalAddress); + + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setEmail1("primary@mysql.test"); + electronicAddress.setEmail2("secondary@mysql.test"); + electronicAddress.setWeb("https://mysql.org.test"); + electronicAddress.setRadio("RADIO-MYSQL-123"); + contactInfo.setElectronicAddress(electronicAddress); + + account.setContactInfo(contactInfo); + + // Act + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + Optional retrieved = customerAccountRepository.findById(savedAccount.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Organisation retrievedContactInfo = retrieved.get().getContactInfo(); + assertThat(retrievedContactInfo).isNotNull(); + assertThat(retrievedContactInfo.getOrganisationName()).isEqualTo("Complete MySQL Organization"); + assertThat(retrievedContactInfo.getStreetAddress().getStreetDetail()).isEqualTo("456 MySQL Contact Avenue"); + assertThat(retrievedContactInfo.getPostalAddress().getStreetDetail()).isEqualTo("PO Box 888"); + assertThat(retrievedContactInfo.getElectronicAddress().getEmail1()).isEqualTo("primary@mysql.test"); + assertThat(retrievedContactInfo.getElectronicAddress().getRadio()).isEqualTo("RADIO-MYSQL-123"); + } + + @Test + @DisplayName("Should persist Status with all fields") + void shouldPersistStatusWithAllFields() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setAccountId("MYSQL-STATUS-TEST"); + + Status docStatus = new Status(); + docStatus.setValue("pending"); + OffsetDateTime testDateTime = OffsetDateTime.now(); + docStatus.setDateTime(testDateTime); + docStatus.setRemark("MySQL test remark"); + docStatus.setReason("MySQL test reason"); + account.setDocStatus(docStatus); + + // Act + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + Optional retrieved = customerAccountRepository.findById(savedAccount.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Status retrievedStatus = retrieved.get().getDocStatus(); + assertThat(retrievedStatus).isNotNull(); + assertThat(retrievedStatus.getValue()).isEqualTo("pending"); + assertThat(retrievedStatus.getDateTime()).isNotNull(); + assertThat(retrievedStatus.getRemark()).isEqualTo("MySQL test remark"); + assertThat(retrievedStatus.getReason()).isEqualTo("MySQL test reason"); + } + + @Test + @DisplayName("Should persist AccountNotification collection") + void shouldPersistAccountNotificationCollection() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setAccountId("MYSQL-NOTIFICATION-TEST"); + + List notifications = new ArrayList<>(); + + AccountNotification notification1 = new AccountNotification(); + notification1.setMethodKind(NotificationMethodKind.EMAIL); + notification1.setTime(OffsetDateTime.now().minusDays(5)); + notification1.setNote("First notification"); + notification1.setCustomerNotificationKind("DELINQUENCY"); + notifications.add(notification1); + + AccountNotification notification2 = new AccountNotification(); + notification2.setMethodKind(NotificationMethodKind.LETTER); + notification2.setTime(OffsetDateTime.now().minusDays(1)); + notification2.setNote("Second notification"); + notification2.setCustomerNotificationKind("MOVE_OUT"); + notifications.add(notification2); + + account.setNotifications(notifications); + + // Act + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + Optional retrieved = customerAccountRepository.findById(savedAccount.getId()); + + // Assert + assertThat(retrieved).isPresent(); + List retrievedNotifications = retrieved.get().getNotifications(); + assertThat(retrievedNotifications).isNotNull(); + assertThat(retrievedNotifications).hasSize(2); + assertThat(retrievedNotifications.get(0).getMethodKind()).isEqualTo(NotificationMethodKind.EMAIL); + assertThat(retrievedNotifications.get(0).getCustomerNotificationKind()).isEqualTo("DELINQUENCY"); + assertThat(retrievedNotifications.get(1).getMethodKind()).isEqualTo(NotificationMethodKind.LETTER); + } + } +} 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 new file mode 100644 index 00000000..49bfc67b --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAccountPostgreSQLIntegrationTest.java @@ -0,0 +1,374 @@ +/* + * + * 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.AccountNotification; +import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAccountEntity; +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.enums.NotificationMethodKind; +import org.greenbuttonalliance.espi.common.repositories.customer.CustomerAccountRepository; +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.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * CustomerAccount entity integration tests using PostgreSQL TestContainer. + * + * Tests full CRUD operations and relationship persistence with a real PostgreSQL database. + */ +@DisplayName("CustomerAccount Integration Tests - PostgreSQL") +@ActiveProfiles({"test", "test-postgresql"}) +class CustomerAccountPostgreSQLIntegrationTest 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 CustomerAccountRepository customerAccountRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve customer account with all fields") + void shouldSaveAndRetrieveCustomerAccountWithAllFields() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setType("Residential Account"); + account.setAuthorName("PostgreSQL Test Author"); + account.setCreatedDateTime(OffsetDateTime.now().minusDays(20)); + account.setLastModifiedDateTime(OffsetDateTime.now()); + account.setRevisionNumber("Rev-2.0"); + account.setSubject("PostgreSQL Account Subject"); + account.setTitle("PostgreSQL Account Title"); + account.setBillingCycle("30"); + account.setBudgetBill("Budget Plan B"); + account.setLastBillAmount(250000L); + account.setAccountId("POSTGRES-ACCT-67890"); + account.setIsPrePay(false); + + // Electronic address for document + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setEmail1("document@postgres.test"); + electronicAddress.setWeb("https://postgres-account.test"); + account.setElectronicAddress(electronicAddress); + + // Document status + Status docStatus = new Status(); + docStatus.setValue("active"); + docStatus.setDateTime(OffsetDateTime.now()); + docStatus.setReason("PostgreSQL document activation"); + account.setDocStatus(docStatus); + + // Contact info + Organisation contactInfo = new Organisation(); + contactInfo.setOrganisationName("PostgreSQL Contact Services"); + + Organisation.StreetAddress streetAddress = new Organisation.StreetAddress(); + streetAddress.setStreetDetail("789 PostgreSQL Contact Boulevard"); + streetAddress.setTownDetail("Postgres City"); + streetAddress.setStateOrProvince("WA"); + streetAddress.setPostalCode("98001"); + streetAddress.setCountry("USA"); + contactInfo.setStreetAddress(streetAddress); + + Organisation.ElectronicAddress contactElectronicAddress = new Organisation.ElectronicAddress(); + contactElectronicAddress.setEmail1("contact@postgres.test"); + contactElectronicAddress.setWeb("https://contact.postgres.test"); + contactInfo.setElectronicAddress(contactElectronicAddress); + + account.setContactInfo(contactInfo); + + // Act + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + Optional retrieved = customerAccountRepository.findById(savedAccount.getId()); + + // Assert + assertThat(retrieved).isPresent(); + CustomerAccountEntity result = retrieved.get(); + + assertThat(result.getType()).isEqualTo("Residential Account"); + assertThat(result.getAuthorName()).isEqualTo("PostgreSQL Test Author"); + assertThat(result.getBillingCycle()).isEqualTo("30"); + assertThat(result.getBudgetBill()).isEqualTo("Budget Plan B"); + assertThat(result.getLastBillAmount()).isEqualTo(250000L); + assertThat(result.getAccountId()).isEqualTo("POSTGRES-ACCT-67890"); + assertThat(result.getIsPrePay()).isFalse(); + + assertThat(result.getElectronicAddress()).isNotNull(); + assertThat(result.getElectronicAddress().getEmail1()).isEqualTo("document@postgres.test"); + + assertThat(result.getDocStatus()).isNotNull(); + assertThat(result.getDocStatus().getValue()).isEqualTo("active"); + + assertThat(result.getContactInfo()).isNotNull(); + assertThat(result.getContactInfo().getOrganisationName()).isEqualTo("PostgreSQL Contact Services"); + assertThat(result.getContactInfo().getStreetAddress()).isNotNull(); + assertThat(result.getContactInfo().getStreetAddress().getStreetDetail()).isEqualTo("789 PostgreSQL Contact Boulevard"); + } + + @Test + @DisplayName("Should update customer account fields") + void shouldUpdateCustomerAccountFields() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setAccountId("ORIGINAL-PG-ACCT-ID"); + account.setBillingCycle("5"); + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + + // Act + savedAccount.setAccountId("UPDATED-PG-ACCT-ID"); + savedAccount.setBillingCycle("20"); + savedAccount.setIsPrePay(true); + CustomerAccountEntity updatedAccount = customerAccountRepository.save(savedAccount); + flushAndClear(); + + Optional retrieved = customerAccountRepository.findById(updatedAccount.getId()); + + // Assert + assertThat(retrieved).isPresent(); + assertThat(retrieved.get().getAccountId()).isEqualTo("UPDATED-PG-ACCT-ID"); + assertThat(retrieved.get().getBillingCycle()).isEqualTo("20"); + assertThat(retrieved.get().getIsPrePay()).isTrue(); + } + + @Test + @DisplayName("Should delete customer account") + void shouldDeleteCustomerAccount() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setAccountId("DELETE-ME-PG-ACCT"); + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + + // Act + customerAccountRepository.deleteById(savedAccount.getId()); + flushAndClear(); + + Optional retrieved = customerAccountRepository.findById(savedAccount.getId()); + + // Assert + assertThat(retrieved).isEmpty(); + } + } + + @Nested + @DisplayName("Bulk Operations") + class BulkOperationsTest { + + @Test + @DisplayName("Should handle bulk save operations") + void shouldHandleBulkSaveOperations() { + // Arrange + List accounts = TestDataBuilders.createValidEntities(5, + TestDataBuilders::createValidCustomerAccount); + + for (int i = 0; i < accounts.size(); i++) { + accounts.get(i).setAccountId("POSTGRES-BULK-ACCT-" + i); + } + + // Act + List savedAccounts = customerAccountRepository.saveAll(accounts); + flushAndClear(); + + // Assert + assertThat(savedAccounts).hasSize(5); + assertThat(savedAccounts).allMatch(account -> account.getId() != null); + + long count = customerAccountRepository.count(); + assertThat(count).isGreaterThanOrEqualTo(5); + } + + @Test + @DisplayName("Should handle bulk delete operations") + void shouldHandleBulkDeleteOperations() { + // Arrange + List accounts = TestDataBuilders.createValidEntities(3, + TestDataBuilders::createValidCustomerAccount); + + List savedAccounts = customerAccountRepository.saveAll(accounts); + long initialCount = customerAccountRepository.count(); + flushAndClear(); + + // Act + customerAccountRepository.deleteAll(savedAccounts); + flushAndClear(); + + // Assert + long finalCount = customerAccountRepository.count(); + assertThat(finalCount).isEqualTo(initialCount - 3); + } + } + + @Nested + @DisplayName("Embedded Objects Persistence") + class EmbeddedObjectsTest { + + @Test + @DisplayName("Should persist and retrieve Organisation contact info with all nested types") + void shouldPersistOrganisationWithAllTypes() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setAccountId("POSTGRES-ORG-TEST"); + + Organisation contactInfo = new Organisation(); + contactInfo.setOrganisationName("Complete PostgreSQL Organization"); + + Organisation.StreetAddress streetAddress = new Organisation.StreetAddress(); + streetAddress.setStreetDetail("321 PostgreSQL Contact Drive"); + streetAddress.setTownDetail("Postgres Town"); + streetAddress.setStateOrProvince("OR"); + streetAddress.setPostalCode("97001"); + streetAddress.setCountry("USA"); + contactInfo.setStreetAddress(streetAddress); + + Organisation.StreetAddress postalAddress = new Organisation.StreetAddress(); + postalAddress.setStreetDetail("PO Box 666"); + postalAddress.setTownDetail("Postgres Town"); + postalAddress.setStateOrProvince("OR"); + postalAddress.setPostalCode("97002"); + postalAddress.setCountry("USA"); + contactInfo.setPostalAddress(postalAddress); + + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setEmail1("primary@postgres.test"); + electronicAddress.setEmail2("secondary@postgres.test"); + electronicAddress.setWeb("https://postgres.org.test"); + electronicAddress.setRadio("RADIO-PG-456"); + contactInfo.setElectronicAddress(electronicAddress); + + account.setContactInfo(contactInfo); + + // Act + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + Optional retrieved = customerAccountRepository.findById(savedAccount.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Organisation retrievedContactInfo = retrieved.get().getContactInfo(); + assertThat(retrievedContactInfo).isNotNull(); + assertThat(retrievedContactInfo.getOrganisationName()).isEqualTo("Complete PostgreSQL Organization"); + assertThat(retrievedContactInfo.getStreetAddress().getStreetDetail()).isEqualTo("321 PostgreSQL Contact Drive"); + assertThat(retrievedContactInfo.getPostalAddress().getStreetDetail()).isEqualTo("PO Box 666"); + assertThat(retrievedContactInfo.getElectronicAddress().getEmail1()).isEqualTo("primary@postgres.test"); + assertThat(retrievedContactInfo.getElectronicAddress().getRadio()).isEqualTo("RADIO-PG-456"); + } + + @Test + @DisplayName("Should persist Status with all fields") + void shouldPersistStatusWithAllFields() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setAccountId("POSTGRES-STATUS-TEST"); + + Status docStatus = new Status(); + docStatus.setValue("archived"); + OffsetDateTime testDateTime = OffsetDateTime.now(); + docStatus.setDateTime(testDateTime); + docStatus.setRemark("PostgreSQL test remark"); + docStatus.setReason("PostgreSQL test reason"); + account.setDocStatus(docStatus); + + // Act + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + Optional retrieved = customerAccountRepository.findById(savedAccount.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Status retrievedStatus = retrieved.get().getDocStatus(); + assertThat(retrievedStatus).isNotNull(); + assertThat(retrievedStatus.getValue()).isEqualTo("archived"); + assertThat(retrievedStatus.getDateTime()).isNotNull(); + assertThat(retrievedStatus.getRemark()).isEqualTo("PostgreSQL test remark"); + assertThat(retrievedStatus.getReason()).isEqualTo("PostgreSQL test reason"); + } + + @Test + @DisplayName("Should persist AccountNotification collection") + void shouldPersistAccountNotificationCollection() { + // Arrange + CustomerAccountEntity account = TestDataBuilders.createValidCustomerAccount(); + account.setAccountId("POSTGRES-NOTIFICATION-TEST"); + + List notifications = new ArrayList<>(); + + AccountNotification notification1 = new AccountNotification(); + notification1.setMethodKind(NotificationMethodKind.CALL); + notification1.setTime(OffsetDateTime.now().minusDays(3)); + notification1.setNote("PostgreSQL first notification"); + notification1.setCustomerNotificationKind("MOVE_IN"); + notifications.add(notification1); + + AccountNotification notification2 = new AccountNotification(); + notification2.setMethodKind(NotificationMethodKind.EMAIL); + notification2.setTime(OffsetDateTime.now().minusDays(1)); + notification2.setNote("PostgreSQL second notification"); + notification2.setCustomerNotificationKind("PAYMENT_DUE"); + notifications.add(notification2); + + account.setNotifications(notifications); + + // Act + CustomerAccountEntity savedAccount = customerAccountRepository.save(account); + flushAndClear(); + Optional retrieved = customerAccountRepository.findById(savedAccount.getId()); + + // Assert + assertThat(retrieved).isPresent(); + List retrievedNotifications = retrieved.get().getNotifications(); + assertThat(retrievedNotifications).isNotNull(); + assertThat(retrievedNotifications).hasSize(2); + assertThat(retrievedNotifications.get(0).getMethodKind()).isEqualTo(NotificationMethodKind.CALL); + assertThat(retrievedNotifications.get(0).getCustomerNotificationKind()).isEqualTo("MOVE_IN"); + assertThat(retrievedNotifications.get(1).getMethodKind()).isEqualTo(NotificationMethodKind.EMAIL); + } + } +} 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 new file mode 100644 index 00000000..199e419a --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAgreementMySQLIntegrationTest.java @@ -0,0 +1,350 @@ +/* + * + * 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.common.DateTimeInterval; +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.repositories.customer.CustomerAgreementRepository; +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.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * CustomerAgreement entity integration tests using MySQL TestContainer. + * + * Tests full CRUD operations and relationship persistence with a real MySQL database. + */ +@DisplayName("CustomerAgreement Integration Tests - MySQL") +@ActiveProfiles({"test", "test-mysql"}) +class CustomerAgreementMySQLIntegrationTest 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 CustomerAgreementRepository customerAgreementRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve customer agreement with all fields") + void shouldSaveAndRetrieveCustomerAgreementWithAllFields() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setType("Standard Service Agreement"); + agreement.setAuthorName("MySQL Agreement Author"); + agreement.setCreatedDateTime(OffsetDateTime.now().minusDays(30)); + agreement.setLastModifiedDateTime(OffsetDateTime.now()); + agreement.setRevisionNumber("Rev-1.2"); + agreement.setSubject("MySQL Agreement Subject"); + agreement.setTitle("MySQL Service Agreement"); + agreement.setComment("MySQL test agreement"); + agreement.setSignDate(OffsetDateTime.now().minusDays(15)); + agreement.setLoadMgmt("LOAD-MGMT-001"); + agreement.setIsPrePay(true); + agreement.setShutOffDateTime(OffsetDateTime.now().plusDays(365)); + agreement.setCurrency("USD"); + agreement.setAgreementId("MYSQL-AGR-12345"); + + // Electronic address for document + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setEmail1("agreement@mysql.test"); + electronicAddress.setWeb("https://mysql-agreement.test"); + agreement.setElectronicAddress(electronicAddress); + + // Document status + Status docStatus = new Status(); + docStatus.setValue("final"); + docStatus.setDateTime(OffsetDateTime.now()); + docStatus.setReason("MySQL document finalization"); + agreement.setDocStatus(docStatus); + + // Agreement status + Status status = new Status(); + status.setValue("active"); + status.setDateTime(OffsetDateTime.now()); + status.setReason("MySQL agreement activation"); + agreement.setStatus(status); + + // Validity interval + DateTimeInterval validityInterval = new DateTimeInterval(); + validityInterval.setStart(OffsetDateTime.now().minusDays(10).toEpochSecond()); + validityInterval.setDuration(31536000L); // 1 year in seconds + agreement.setValidityInterval(validityInterval); + + // Act + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + Optional retrieved = customerAgreementRepository.findById(savedAgreement.getId()); + + // Assert + assertThat(retrieved).isPresent(); + CustomerAgreementEntity result = retrieved.get(); + + assertThat(result.getType()).isEqualTo("Standard Service Agreement"); + assertThat(result.getAuthorName()).isEqualTo("MySQL Agreement Author"); + assertThat(result.getComment()).isEqualTo("MySQL test agreement"); + assertThat(result.getLoadMgmt()).isEqualTo("LOAD-MGMT-001"); + assertThat(result.getIsPrePay()).isTrue(); + assertThat(result.getCurrency()).isEqualTo("USD"); + assertThat(result.getAgreementId()).isEqualTo("MYSQL-AGR-12345"); + + assertThat(result.getElectronicAddress()).isNotNull(); + assertThat(result.getElectronicAddress().getEmail1()).isEqualTo("agreement@mysql.test"); + + assertThat(result.getDocStatus()).isNotNull(); + assertThat(result.getDocStatus().getValue()).isEqualTo("final"); + + assertThat(result.getStatus()).isNotNull(); + assertThat(result.getStatus().getValue()).isEqualTo("active"); + + assertThat(result.getValidityInterval()).isNotNull(); + assertThat(result.getValidityInterval().getDuration()).isEqualTo(31536000L); + } + + @Test + @DisplayName("Should update customer agreement fields") + void shouldUpdateCustomerAgreementFields() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setAgreementId("ORIGINAL-AGR-ID"); + agreement.setIsPrePay(false); + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + + // Act + savedAgreement.setAgreementId("UPDATED-AGR-ID"); + savedAgreement.setIsPrePay(true); + savedAgreement.setCurrency("EUR"); + CustomerAgreementEntity updatedAgreement = customerAgreementRepository.save(savedAgreement); + flushAndClear(); + + Optional retrieved = customerAgreementRepository.findById(updatedAgreement.getId()); + + // Assert + assertThat(retrieved).isPresent(); + assertThat(retrieved.get().getAgreementId()).isEqualTo("UPDATED-AGR-ID"); + assertThat(retrieved.get().getIsPrePay()).isTrue(); + assertThat(retrieved.get().getCurrency()).isEqualTo("EUR"); + } + + @Test + @DisplayName("Should delete customer agreement") + void shouldDeleteCustomerAgreement() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setAgreementId("DELETE-ME-AGR"); + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + + // Act + customerAgreementRepository.deleteById(savedAgreement.getId()); + flushAndClear(); + + Optional retrieved = customerAgreementRepository.findById(savedAgreement.getId()); + + // Assert + assertThat(retrieved).isEmpty(); + } + } + + @Nested + @DisplayName("Bulk Operations") + class BulkOperationsTest { + + @Test + @DisplayName("Should handle bulk save operations") + void shouldHandleBulkSaveOperations() { + // Arrange + List agreements = TestDataBuilders.createValidEntities(5, + TestDataBuilders::createValidCustomerAgreement); + + for (int i = 0; i < agreements.size(); i++) { + agreements.get(i).setAgreementId("MYSQL-BULK-AGR-" + i); + } + + // Act + List savedAgreements = customerAgreementRepository.saveAll(agreements); + flushAndClear(); + + // Assert + assertThat(savedAgreements).hasSize(5); + assertThat(savedAgreements).allMatch(agreement -> agreement.getId() != null); + + long count = customerAgreementRepository.count(); + assertThat(count).isGreaterThanOrEqualTo(5); + } + + @Test + @DisplayName("Should handle bulk delete operations") + void shouldHandleBulkDeleteOperations() { + // Arrange + List agreements = TestDataBuilders.createValidEntities(3, + TestDataBuilders::createValidCustomerAgreement); + + List savedAgreements = customerAgreementRepository.saveAll(agreements); + long initialCount = customerAgreementRepository.count(); + flushAndClear(); + + // Act + customerAgreementRepository.deleteAll(savedAgreements); + flushAndClear(); + + // Assert + long finalCount = customerAgreementRepository.count(); + assertThat(finalCount).isEqualTo(initialCount - 3); + } + } + + @Nested + @DisplayName("Embedded Objects Persistence") + class EmbeddedObjectsTest { + + @Test + @DisplayName("Should persist DateTimeInterval with all fields") + void shouldPersistDateTimeIntervalWithAllFields() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setAgreementId("MYSQL-INTERVAL-TEST"); + + DateTimeInterval validityInterval = new DateTimeInterval(); + validityInterval.setStart(OffsetDateTime.now().minusYears(1).toEpochSecond()); + validityInterval.setDuration(63072000L); // 2 years in seconds + agreement.setValidityInterval(validityInterval); + + // Act + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + Optional retrieved = customerAgreementRepository.findById(savedAgreement.getId()); + + // Assert + assertThat(retrieved).isPresent(); + DateTimeInterval retrievedInterval = retrieved.get().getValidityInterval(); + assertThat(retrievedInterval).isNotNull(); + assertThat(retrievedInterval.getDuration()).isEqualTo(63072000L); + assertThat(retrievedInterval.getStart()).isNotNull(); + } + + @Test + @DisplayName("Should persist both document and agreement status") + void shouldPersistBothStatuses() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setAgreementId("MYSQL-STATUS-TEST"); + + Status docStatus = new Status(); + docStatus.setValue("approved"); + docStatus.setDateTime(OffsetDateTime.now()); + docStatus.setRemark("MySQL doc remark"); + docStatus.setReason("MySQL doc reason"); + agreement.setDocStatus(docStatus); + + Status agreementStatus = new Status(); + agreementStatus.setValue("pending"); + agreementStatus.setDateTime(OffsetDateTime.now()); + agreementStatus.setRemark("MySQL agreement remark"); + agreementStatus.setReason("MySQL agreement reason"); + agreement.setStatus(agreementStatus); + + // Act + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + Optional retrieved = customerAgreementRepository.findById(savedAgreement.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Status retrievedDocStatus = retrieved.get().getDocStatus(); + assertThat(retrievedDocStatus).isNotNull(); + assertThat(retrievedDocStatus.getValue()).isEqualTo("approved"); + assertThat(retrievedDocStatus.getRemark()).isEqualTo("MySQL doc remark"); + + Status retrievedAgreementStatus = retrieved.get().getStatus(); + assertThat(retrievedAgreementStatus).isNotNull(); + assertThat(retrievedAgreementStatus.getValue()).isEqualTo("pending"); + assertThat(retrievedAgreementStatus.getRemark()).isEqualTo("MySQL agreement remark"); + } + + @Test + @DisplayName("Should persist future status collection") + void shouldPersistFutureStatusCollection() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setAgreementId("MYSQL-FUTURE-STATUS-TEST"); + + List futureStatuses = new ArrayList<>(); + + Status futureStatus1 = new Status(); + futureStatus1.setValue("scheduled_renewal"); + futureStatus1.setDateTime(OffsetDateTime.now().plusMonths(6)); + futureStatus1.setReason("Scheduled renewal"); + futureStatuses.add(futureStatus1); + + Status futureStatus2 = new Status(); + futureStatus2.setValue("scheduled_termination"); + futureStatus2.setDateTime(OffsetDateTime.now().plusYears(1)); + futureStatus2.setReason("End of contract period"); + futureStatuses.add(futureStatus2); + + agreement.setFutureStatus(futureStatuses); + + // Act + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + Optional retrieved = customerAgreementRepository.findById(savedAgreement.getId()); + + // Assert + assertThat(retrieved).isPresent(); + List retrievedFutureStatuses = retrieved.get().getFutureStatus(); + assertThat(retrievedFutureStatuses).isNotNull(); + assertThat(retrievedFutureStatuses).hasSize(2); + assertThat(retrievedFutureStatuses.get(0).getValue()).isEqualTo("scheduled_renewal"); + assertThat(retrievedFutureStatuses.get(1).getValue()).isEqualTo("scheduled_termination"); + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..f45f3999 --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/CustomerAgreementPostgreSQLIntegrationTest.java @@ -0,0 +1,350 @@ +/* + * + * 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.common.DateTimeInterval; +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.repositories.customer.CustomerAgreementRepository; +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.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * CustomerAgreement entity integration tests using PostgreSQL TestContainer. + * + * Tests full CRUD operations and relationship persistence with a real PostgreSQL database. + */ +@DisplayName("CustomerAgreement Integration Tests - PostgreSQL") +@ActiveProfiles({"test", "test-postgresql"}) +class CustomerAgreementPostgreSQLIntegrationTest 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 CustomerAgreementRepository customerAgreementRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve customer agreement with all fields") + void shouldSaveAndRetrieveCustomerAgreementWithAllFields() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setType("Premium Service Agreement"); + agreement.setAuthorName("PostgreSQL Agreement Author"); + agreement.setCreatedDateTime(OffsetDateTime.now().minusDays(45)); + agreement.setLastModifiedDateTime(OffsetDateTime.now()); + agreement.setRevisionNumber("Rev-2.1"); + agreement.setSubject("PostgreSQL Agreement Subject"); + agreement.setTitle("PostgreSQL Service Agreement"); + agreement.setComment("PostgreSQL test agreement"); + agreement.setSignDate(OffsetDateTime.now().minusDays(20)); + agreement.setLoadMgmt("LOAD-MGMT-002"); + agreement.setIsPrePay(false); + agreement.setShutOffDateTime(OffsetDateTime.now().plusDays(730)); + agreement.setCurrency("CAD"); + agreement.setAgreementId("POSTGRES-AGR-67890"); + + // Electronic address for document + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setEmail1("agreement@postgres.test"); + electronicAddress.setWeb("https://postgres-agreement.test"); + agreement.setElectronicAddress(electronicAddress); + + // Document status + Status docStatus = new Status(); + docStatus.setValue("draft"); + docStatus.setDateTime(OffsetDateTime.now()); + docStatus.setReason("PostgreSQL document drafting"); + agreement.setDocStatus(docStatus); + + // Agreement status + Status status = new Status(); + status.setValue("pending"); + status.setDateTime(OffsetDateTime.now()); + status.setReason("PostgreSQL agreement pending approval"); + agreement.setStatus(status); + + // Validity interval + DateTimeInterval validityInterval = new DateTimeInterval(); + validityInterval.setStart(OffsetDateTime.now().minusDays(5).toEpochSecond()); + validityInterval.setDuration(15768000L); // 6 months in seconds + agreement.setValidityInterval(validityInterval); + + // Act + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + Optional retrieved = customerAgreementRepository.findById(savedAgreement.getId()); + + // Assert + assertThat(retrieved).isPresent(); + CustomerAgreementEntity result = retrieved.get(); + + assertThat(result.getType()).isEqualTo("Premium Service Agreement"); + assertThat(result.getAuthorName()).isEqualTo("PostgreSQL Agreement Author"); + assertThat(result.getComment()).isEqualTo("PostgreSQL test agreement"); + assertThat(result.getLoadMgmt()).isEqualTo("LOAD-MGMT-002"); + assertThat(result.getIsPrePay()).isFalse(); + assertThat(result.getCurrency()).isEqualTo("CAD"); + assertThat(result.getAgreementId()).isEqualTo("POSTGRES-AGR-67890"); + + assertThat(result.getElectronicAddress()).isNotNull(); + assertThat(result.getElectronicAddress().getEmail1()).isEqualTo("agreement@postgres.test"); + + assertThat(result.getDocStatus()).isNotNull(); + assertThat(result.getDocStatus().getValue()).isEqualTo("draft"); + + assertThat(result.getStatus()).isNotNull(); + assertThat(result.getStatus().getValue()).isEqualTo("pending"); + + assertThat(result.getValidityInterval()).isNotNull(); + assertThat(result.getValidityInterval().getDuration()).isEqualTo(15768000L); + } + + @Test + @DisplayName("Should update customer agreement fields") + void shouldUpdateCustomerAgreementFields() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setAgreementId("ORIGINAL-PG-AGR-ID"); + agreement.setIsPrePay(true); + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + + // Act + savedAgreement.setAgreementId("UPDATED-PG-AGR-ID"); + savedAgreement.setIsPrePay(false); + savedAgreement.setCurrency("GBP"); + CustomerAgreementEntity updatedAgreement = customerAgreementRepository.save(savedAgreement); + flushAndClear(); + + Optional retrieved = customerAgreementRepository.findById(updatedAgreement.getId()); + + // Assert + assertThat(retrieved).isPresent(); + assertThat(retrieved.get().getAgreementId()).isEqualTo("UPDATED-PG-AGR-ID"); + assertThat(retrieved.get().getIsPrePay()).isFalse(); + assertThat(retrieved.get().getCurrency()).isEqualTo("GBP"); + } + + @Test + @DisplayName("Should delete customer agreement") + void shouldDeleteCustomerAgreement() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setAgreementId("DELETE-ME-PG-AGR"); + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + + // Act + customerAgreementRepository.deleteById(savedAgreement.getId()); + flushAndClear(); + + Optional retrieved = customerAgreementRepository.findById(savedAgreement.getId()); + + // Assert + assertThat(retrieved).isEmpty(); + } + } + + @Nested + @DisplayName("Bulk Operations") + class BulkOperationsTest { + + @Test + @DisplayName("Should handle bulk save operations") + void shouldHandleBulkSaveOperations() { + // Arrange + List agreements = TestDataBuilders.createValidEntities(5, + TestDataBuilders::createValidCustomerAgreement); + + for (int i = 0; i < agreements.size(); i++) { + agreements.get(i).setAgreementId("POSTGRES-BULK-AGR-" + i); + } + + // Act + List savedAgreements = customerAgreementRepository.saveAll(agreements); + flushAndClear(); + + // Assert + assertThat(savedAgreements).hasSize(5); + assertThat(savedAgreements).allMatch(agreement -> agreement.getId() != null); + + long count = customerAgreementRepository.count(); + assertThat(count).isGreaterThanOrEqualTo(5); + } + + @Test + @DisplayName("Should handle bulk delete operations") + void shouldHandleBulkDeleteOperations() { + // Arrange + List agreements = TestDataBuilders.createValidEntities(3, + TestDataBuilders::createValidCustomerAgreement); + + List savedAgreements = customerAgreementRepository.saveAll(agreements); + long initialCount = customerAgreementRepository.count(); + flushAndClear(); + + // Act + customerAgreementRepository.deleteAll(savedAgreements); + flushAndClear(); + + // Assert + long finalCount = customerAgreementRepository.count(); + assertThat(finalCount).isEqualTo(initialCount - 3); + } + } + + @Nested + @DisplayName("Embedded Objects Persistence") + class EmbeddedObjectsTest { + + @Test + @DisplayName("Should persist DateTimeInterval with all fields") + void shouldPersistDateTimeIntervalWithAllFields() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setAgreementId("POSTGRES-INTERVAL-TEST"); + + DateTimeInterval validityInterval = new DateTimeInterval(); + validityInterval.setStart(OffsetDateTime.now().minusMonths(3).toEpochSecond()); + validityInterval.setDuration(94608000L); // 3 years in seconds + agreement.setValidityInterval(validityInterval); + + // Act + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + Optional retrieved = customerAgreementRepository.findById(savedAgreement.getId()); + + // Assert + assertThat(retrieved).isPresent(); + DateTimeInterval retrievedInterval = retrieved.get().getValidityInterval(); + assertThat(retrievedInterval).isNotNull(); + assertThat(retrievedInterval.getDuration()).isEqualTo(94608000L); + assertThat(retrievedInterval.getStart()).isNotNull(); + } + + @Test + @DisplayName("Should persist both document and agreement status") + void shouldPersistBothStatuses() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setAgreementId("POSTGRES-STATUS-TEST"); + + Status docStatus = new Status(); + docStatus.setValue("published"); + docStatus.setDateTime(OffsetDateTime.now()); + docStatus.setRemark("PostgreSQL doc remark"); + docStatus.setReason("PostgreSQL doc reason"); + agreement.setDocStatus(docStatus); + + Status agreementStatus = new Status(); + agreementStatus.setValue("active"); + agreementStatus.setDateTime(OffsetDateTime.now()); + agreementStatus.setRemark("PostgreSQL agreement remark"); + agreementStatus.setReason("PostgreSQL agreement reason"); + agreement.setStatus(agreementStatus); + + // Act + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + Optional retrieved = customerAgreementRepository.findById(savedAgreement.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Status retrievedDocStatus = retrieved.get().getDocStatus(); + assertThat(retrievedDocStatus).isNotNull(); + assertThat(retrievedDocStatus.getValue()).isEqualTo("published"); + assertThat(retrievedDocStatus.getRemark()).isEqualTo("PostgreSQL doc remark"); + + Status retrievedAgreementStatus = retrieved.get().getStatus(); + assertThat(retrievedAgreementStatus).isNotNull(); + assertThat(retrievedAgreementStatus.getValue()).isEqualTo("active"); + assertThat(retrievedAgreementStatus.getRemark()).isEqualTo("PostgreSQL agreement remark"); + } + + @Test + @DisplayName("Should persist future status collection") + void shouldPersistFutureStatusCollection() { + // Arrange + CustomerAgreementEntity agreement = TestDataBuilders.createValidCustomerAgreement(); + agreement.setAgreementId("POSTGRES-FUTURE-STATUS-TEST"); + + List futureStatuses = new ArrayList<>(); + + Status futureStatus1 = new Status(); + futureStatus1.setValue("scheduled_update"); + futureStatus1.setDateTime(OffsetDateTime.now().plusMonths(3)); + futureStatus1.setReason("Quarterly review"); + futureStatuses.add(futureStatus1); + + Status futureStatus2 = new Status(); + futureStatus2.setValue("scheduled_expiration"); + futureStatus2.setDateTime(OffsetDateTime.now().plusMonths(18)); + futureStatus2.setReason("Contract expiration"); + futureStatuses.add(futureStatus2); + + agreement.setFutureStatus(futureStatuses); + + // Act + CustomerAgreementEntity savedAgreement = customerAgreementRepository.save(agreement); + flushAndClear(); + Optional retrieved = customerAgreementRepository.findById(savedAgreement.getId()); + + // Assert + assertThat(retrieved).isPresent(); + List retrievedFutureStatuses = retrieved.get().getFutureStatus(); + assertThat(retrievedFutureStatuses).isNotNull(); + assertThat(retrievedFutureStatuses).hasSize(2); + assertThat(retrievedFutureStatuses.get(0).getValue()).isEqualTo("scheduled_update"); + assertThat(retrievedFutureStatuses.get(1).getValue()).isEqualTo("scheduled_expiration"); + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..cd8755be --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/EndDeviceMySQLIntegrationTest.java @@ -0,0 +1,380 @@ +/* + * + * 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.EndDeviceEntity; +import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation; +import org.greenbuttonalliance.espi.common.domain.customer.entity.Status; +import org.greenbuttonalliance.espi.common.repositories.customer.EndDeviceRepository; +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; + +/** + * EndDevice entity integration tests using MySQL TestContainer. + * + * Tests full CRUD operations and relationship persistence with a real MySQL database. + */ +@DisplayName("EndDevice Integration Tests - MySQL") +@ActiveProfiles({"test", "test-mysql"}) +class EndDeviceMySQLIntegrationTest 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 EndDeviceRepository endDeviceRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve end device with all fields") + void shouldSaveAndRetrieveEndDeviceWithAllFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setType("Smart Meter"); + endDevice.setSerialNumber("MYSQL-SN-123456789"); + endDevice.setUtcNumber("MYSQL-UTC-987654321"); + endDevice.setLotNumber("LOT-MYSQL-001"); + endDevice.setPurchasePrice(50000L); + endDevice.setCritical(true); + endDevice.setInitialCondition("New"); + endDevice.setInitialLossOfLife(BigDecimal.ZERO); + endDevice.setIsVirtual(false); + endDevice.setIsPan(true); + endDevice.setInstallCode("INSTALL-MYSQL-XYZ"); + endDevice.setAmrSystem("AMR-MYSQL-SYSTEM"); + + // Electronic address + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setEmail1("device@mysql.test"); + electronicAddress.setMac("AA:BB:CC:DD:EE:FF"); + electronicAddress.setWeb("https://mysql-device.test"); + endDevice.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)); + endDevice.setLifecycle(lifecycle); + + // Acceptance test + Asset.AcceptanceTest acceptanceTest = new Asset.AcceptanceTest(); + acceptanceTest.setSuccess(true); + acceptanceTest.setDateTime(OffsetDateTime.now().minusMonths(6)); + acceptanceTest.setType("Field Test"); + endDevice.setAcceptanceTest(acceptanceTest); + + // Status + Status status = new Status(); + status.setValue("operational"); + status.setDateTime(OffsetDateTime.now()); + status.setReason("MySQL device test"); + endDevice.setStatus(status); + + // Act + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + EndDeviceEntity result = retrieved.get(); + + assertThat(result.getType()).isEqualTo("Smart Meter"); + assertThat(result.getSerialNumber()).isEqualTo("MYSQL-SN-123456789"); + assertThat(result.getUtcNumber()).isEqualTo("MYSQL-UTC-987654321"); + assertThat(result.getLotNumber()).isEqualTo("LOT-MYSQL-001"); + assertThat(result.getPurchasePrice()).isEqualTo(50000L); + assertThat(result.getCritical()).isTrue(); + assertThat(result.getInitialCondition()).isEqualTo("New"); + assertThat(result.getIsVirtual()).isFalse(); + assertThat(result.getIsPan()).isTrue(); + assertThat(result.getInstallCode()).isEqualTo("INSTALL-MYSQL-XYZ"); + assertThat(result.getAmrSystem()).isEqualTo("AMR-MYSQL-SYSTEM"); + + assertThat(result.getElectronicAddress()).isNotNull(); + assertThat(result.getElectronicAddress().getEmail1()).isEqualTo("device@mysql.test"); + assertThat(result.getElectronicAddress().getMac()).isEqualTo("AA:BB:CC:DD:EE:FF"); + + 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 end device fields") + void shouldUpdateEndDeviceFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("ORIGINAL-SN-001"); + endDevice.setIsVirtual(false); + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + + // Act + savedDevice.setSerialNumber("UPDATED-SN-002"); + savedDevice.setIsVirtual(true); + savedDevice.setInstallCode("UPDATED-INSTALL-CODE"); + EndDeviceEntity updatedDevice = endDeviceRepository.save(savedDevice); + flushAndClear(); + + Optional retrieved = endDeviceRepository.findById(updatedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + assertThat(retrieved.get().getSerialNumber()).isEqualTo("UPDATED-SN-002"); + assertThat(retrieved.get().getIsVirtual()).isTrue(); + assertThat(retrieved.get().getInstallCode()).isEqualTo("UPDATED-INSTALL-CODE"); + } + + @Test + @DisplayName("Should delete end device") + void shouldDeleteEndDevice() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("DELETE-ME-SN"); + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + + // Act + endDeviceRepository.deleteById(savedDevice.getId()); + flushAndClear(); + + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isEmpty(); + } + } + + @Nested + @DisplayName("Bulk Operations") + class BulkOperationsTest { + + @Test + @DisplayName("Should handle bulk save operations") + void shouldHandleBulkSaveOperations() { + // Arrange + List devices = TestDataBuilders.createValidEntities(5, + TestDataBuilders::createValidEndDevice); + + for (int i = 0; i < devices.size(); i++) { + devices.get(i).setSerialNumber("MYSQL-BULK-SN-" + i); + } + + // Act + List savedDevices = endDeviceRepository.saveAll(devices); + flushAndClear(); + + // Assert + assertThat(savedDevices).hasSize(5); + assertThat(savedDevices).allMatch(device -> device.getId() != null); + + long count = endDeviceRepository.count(); + assertThat(count).isGreaterThanOrEqualTo(5); + } + + @Test + @DisplayName("Should handle bulk delete operations") + void shouldHandleBulkDeleteOperations() { + // Arrange + List devices = TestDataBuilders.createValidEntities(3, + TestDataBuilders::createValidEndDevice); + + List savedDevices = endDeviceRepository.saveAll(devices); + long initialCount = endDeviceRepository.count(); + flushAndClear(); + + // Act + endDeviceRepository.deleteAll(savedDevices); + flushAndClear(); + + // Assert + long finalCount = endDeviceRepository.count(); + assertThat(finalCount).isEqualTo(initialCount - 3); + } + } + + @Nested + @DisplayName("Embedded Objects Persistence") + class EmbeddedObjectsTest { + + @Test + @DisplayName("Should persist LifecycleDate with all fields") + void shouldPersistLifecycleDateWithAllFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("MYSQL-LIFECYCLE-TEST"); + + Asset.LifecycleDate lifecycle = new Asset.LifecycleDate(); + lifecycle.setManufacturedDate(OffsetDateTime.now().minusYears(5)); + lifecycle.setPurchaseDate(OffsetDateTime.now().minusYears(4)); + lifecycle.setReceivedDate(OffsetDateTime.now().minusYears(3)); + lifecycle.setInstallationDate(OffsetDateTime.now().minusYears(2)); + lifecycle.setRemovalDate(OffsetDateTime.now().minusYears(1)); + lifecycle.setRetirementDate(OffsetDateTime.now().minusMonths(6)); + endDevice.setLifecycle(lifecycle); + + // Act + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Asset.LifecycleDate retrievedLifecycle = retrieved.get().getLifecycle(); + assertThat(retrievedLifecycle).isNotNull(); + assertThat(retrievedLifecycle.getManufacturedDate()).isNotNull(); + assertThat(retrievedLifecycle.getPurchaseDate()).isNotNull(); + assertThat(retrievedLifecycle.getReceivedDate()).isNotNull(); + assertThat(retrievedLifecycle.getInstallationDate()).isNotNull(); + assertThat(retrievedLifecycle.getRemovalDate()).isNotNull(); + assertThat(retrievedLifecycle.getRetirementDate()).isNotNull(); + } + + @Test + @DisplayName("Should persist AcceptanceTest with all fields") + void shouldPersistAcceptanceTestWithAllFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("MYSQL-ACCEPTANCE-TEST"); + + Asset.AcceptanceTest acceptanceTest = new Asset.AcceptanceTest(); + acceptanceTest.setSuccess(false); + acceptanceTest.setDateTime(OffsetDateTime.now().minusMonths(1)); + acceptanceTest.setType("Factory Test"); + endDevice.setAcceptanceTest(acceptanceTest); + + // Act + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Asset.AcceptanceTest retrievedTest = retrieved.get().getAcceptanceTest(); + assertThat(retrievedTest).isNotNull(); + assertThat(retrievedTest.getSuccess()).isFalse(); + assertThat(retrievedTest.getDateTime()).isNotNull(); + assertThat(retrievedTest.getType()).isEqualTo("Factory Test"); + } + + @Test + @DisplayName("Should persist Status with all fields") + void shouldPersistStatusWithAllFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("MYSQL-STATUS-TEST"); + + Status status = new Status(); + status.setValue("maintenance"); + OffsetDateTime testDateTime = OffsetDateTime.now(); + status.setDateTime(testDateTime); + status.setRemark("MySQL device maintenance"); + status.setReason("Scheduled maintenance"); + endDevice.setStatus(status); + + // Act + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Status retrievedStatus = retrieved.get().getStatus(); + assertThat(retrievedStatus).isNotNull(); + assertThat(retrievedStatus.getValue()).isEqualTo("maintenance"); + assertThat(retrievedStatus.getDateTime()).isNotNull(); + assertThat(retrievedStatus.getRemark()).isEqualTo("MySQL device maintenance"); + assertThat(retrievedStatus.getReason()).isEqualTo("Scheduled maintenance"); + } + + @Test + @DisplayName("Should persist ElectronicAddress with all fields") + void shouldPersistElectronicAddressWithAllFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("MYSQL-ELECTRONIC-TEST"); + + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setLan("192.168.100.50"); + electronicAddress.setMac("11:22:33:44:55:66"); + electronicAddress.setEmail1("device1@mysql.test"); + electronicAddress.setEmail2("device2@mysql.test"); + electronicAddress.setWeb("https://device.mysql.test"); + electronicAddress.setRadio("RADIO-DEVICE-789"); + electronicAddress.setUserID("device-user"); + electronicAddress.setPassword("device-pass"); + endDevice.setElectronicAddress(electronicAddress); + + // Act + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Organisation.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"); + assertThat(retrievedAddress.getEmail1()).isEqualTo("device1@mysql.test"); + assertThat(retrievedAddress.getEmail2()).isEqualTo("device2@mysql.test"); + assertThat(retrievedAddress.getRadio()).isEqualTo("RADIO-DEVICE-789"); + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..5ddd88cf --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/EndDevicePostgreSQLIntegrationTest.java @@ -0,0 +1,380 @@ +/* + * + * 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.EndDeviceEntity; +import org.greenbuttonalliance.espi.common.domain.customer.entity.Organisation; +import org.greenbuttonalliance.espi.common.domain.customer.entity.Status; +import org.greenbuttonalliance.espi.common.repositories.customer.EndDeviceRepository; +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; + +/** + * EndDevice entity integration tests using PostgreSQL TestContainer. + * + * Tests full CRUD operations and relationship persistence with a real PostgreSQL database. + */ +@DisplayName("EndDevice Integration Tests - PostgreSQL") +@ActiveProfiles({"test", "test-postgresql"}) +class EndDevicePostgreSQLIntegrationTest 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 EndDeviceRepository endDeviceRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve end device with all fields") + void shouldSaveAndRetrieveEndDeviceWithAllFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setType("Advanced Metering Infrastructure"); + endDevice.setSerialNumber("POSTGRES-SN-987654321"); + endDevice.setUtcNumber("POSTGRES-UTC-123456789"); + endDevice.setLotNumber("LOT-POSTGRES-002"); + endDevice.setPurchasePrice(75000L); + endDevice.setCritical(false); + endDevice.setInitialCondition("Refurbished"); + endDevice.setInitialLossOfLife(BigDecimal.valueOf(0.15)); + endDevice.setIsVirtual(true); + endDevice.setIsPan(false); + endDevice.setInstallCode("INSTALL-PG-ABC"); + endDevice.setAmrSystem("AMR-POSTGRES-SYSTEM"); + + // Electronic address + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setEmail1("device@postgres.test"); + electronicAddress.setMac("00:11:22:33:44:55"); + electronicAddress.setWeb("https://postgres-device.test"); + endDevice.setElectronicAddress(electronicAddress); + + // Lifecycle dates + Asset.LifecycleDate lifecycle = new Asset.LifecycleDate(); + lifecycle.setManufacturedDate(OffsetDateTime.now().minusYears(3)); + lifecycle.setPurchaseDate(OffsetDateTime.now().minusYears(2)); + lifecycle.setInstallationDate(OffsetDateTime.now().minusMonths(3)); + endDevice.setLifecycle(lifecycle); + + // Acceptance test + Asset.AcceptanceTest acceptanceTest = new Asset.AcceptanceTest(); + acceptanceTest.setSuccess(true); + acceptanceTest.setDateTime(OffsetDateTime.now().minusMonths(3)); + acceptanceTest.setType("Integration Test"); + endDevice.setAcceptanceTest(acceptanceTest); + + // Status + Status status = new Status(); + status.setValue("active"); + status.setDateTime(OffsetDateTime.now()); + status.setReason("PostgreSQL device test"); + endDevice.setStatus(status); + + // Act + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + EndDeviceEntity result = retrieved.get(); + + assertThat(result.getType()).isEqualTo("Advanced Metering Infrastructure"); + assertThat(result.getSerialNumber()).isEqualTo("POSTGRES-SN-987654321"); + assertThat(result.getUtcNumber()).isEqualTo("POSTGRES-UTC-123456789"); + assertThat(result.getLotNumber()).isEqualTo("LOT-POSTGRES-002"); + assertThat(result.getPurchasePrice()).isEqualTo(75000L); + assertThat(result.getCritical()).isFalse(); + assertThat(result.getInitialCondition()).isEqualTo("Refurbished"); + assertThat(result.getIsVirtual()).isTrue(); + assertThat(result.getIsPan()).isFalse(); + assertThat(result.getInstallCode()).isEqualTo("INSTALL-PG-ABC"); + assertThat(result.getAmrSystem()).isEqualTo("AMR-POSTGRES-SYSTEM"); + + assertThat(result.getElectronicAddress()).isNotNull(); + assertThat(result.getElectronicAddress().getEmail1()).isEqualTo("device@postgres.test"); + assertThat(result.getElectronicAddress().getMac()).isEqualTo("00:11:22:33:44:55"); + + 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("Integration Test"); + + assertThat(result.getStatus()).isNotNull(); + assertThat(result.getStatus().getValue()).isEqualTo("active"); + } + + @Test + @DisplayName("Should update end device fields") + void shouldUpdateEndDeviceFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("ORIGINAL-PG-SN-001"); + endDevice.setIsPan(false); + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + + // Act + savedDevice.setSerialNumber("UPDATED-PG-SN-002"); + savedDevice.setIsPan(true); + savedDevice.setInstallCode("UPDATED-PG-INSTALL-CODE"); + EndDeviceEntity updatedDevice = endDeviceRepository.save(savedDevice); + flushAndClear(); + + Optional retrieved = endDeviceRepository.findById(updatedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + assertThat(retrieved.get().getSerialNumber()).isEqualTo("UPDATED-PG-SN-002"); + assertThat(retrieved.get().getIsPan()).isTrue(); + assertThat(retrieved.get().getInstallCode()).isEqualTo("UPDATED-PG-INSTALL-CODE"); + } + + @Test + @DisplayName("Should delete end device") + void shouldDeleteEndDevice() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("DELETE-ME-PG-SN"); + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + + // Act + endDeviceRepository.deleteById(savedDevice.getId()); + flushAndClear(); + + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isEmpty(); + } + } + + @Nested + @DisplayName("Bulk Operations") + class BulkOperationsTest { + + @Test + @DisplayName("Should handle bulk save operations") + void shouldHandleBulkSaveOperations() { + // Arrange + List devices = TestDataBuilders.createValidEntities(5, + TestDataBuilders::createValidEndDevice); + + for (int i = 0; i < devices.size(); i++) { + devices.get(i).setSerialNumber("POSTGRES-BULK-SN-" + i); + } + + // Act + List savedDevices = endDeviceRepository.saveAll(devices); + flushAndClear(); + + // Assert + assertThat(savedDevices).hasSize(5); + assertThat(savedDevices).allMatch(device -> device.getId() != null); + + long count = endDeviceRepository.count(); + assertThat(count).isGreaterThanOrEqualTo(5); + } + + @Test + @DisplayName("Should handle bulk delete operations") + void shouldHandleBulkDeleteOperations() { + // Arrange + List devices = TestDataBuilders.createValidEntities(3, + TestDataBuilders::createValidEndDevice); + + List savedDevices = endDeviceRepository.saveAll(devices); + long initialCount = endDeviceRepository.count(); + flushAndClear(); + + // Act + endDeviceRepository.deleteAll(savedDevices); + flushAndClear(); + + // Assert + long finalCount = endDeviceRepository.count(); + assertThat(finalCount).isEqualTo(initialCount - 3); + } + } + + @Nested + @DisplayName("Embedded Objects Persistence") + class EmbeddedObjectsTest { + + @Test + @DisplayName("Should persist LifecycleDate with all fields") + void shouldPersistLifecycleDateWithAllFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("POSTGRES-LIFECYCLE-TEST"); + + Asset.LifecycleDate lifecycle = new Asset.LifecycleDate(); + lifecycle.setManufacturedDate(OffsetDateTime.now().minusYears(6)); + lifecycle.setPurchaseDate(OffsetDateTime.now().minusYears(5)); + lifecycle.setReceivedDate(OffsetDateTime.now().minusYears(4)); + lifecycle.setInstallationDate(OffsetDateTime.now().minusYears(3)); + lifecycle.setRemovalDate(OffsetDateTime.now().minusYears(2)); + lifecycle.setRetirementDate(OffsetDateTime.now().minusYears(1)); + endDevice.setLifecycle(lifecycle); + + // Act + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Asset.LifecycleDate retrievedLifecycle = retrieved.get().getLifecycle(); + assertThat(retrievedLifecycle).isNotNull(); + assertThat(retrievedLifecycle.getManufacturedDate()).isNotNull(); + assertThat(retrievedLifecycle.getPurchaseDate()).isNotNull(); + assertThat(retrievedLifecycle.getReceivedDate()).isNotNull(); + assertThat(retrievedLifecycle.getInstallationDate()).isNotNull(); + assertThat(retrievedLifecycle.getRemovalDate()).isNotNull(); + assertThat(retrievedLifecycle.getRetirementDate()).isNotNull(); + } + + @Test + @DisplayName("Should persist AcceptanceTest with all fields") + void shouldPersistAcceptanceTestWithAllFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("POSTGRES-ACCEPTANCE-TEST"); + + Asset.AcceptanceTest acceptanceTest = new Asset.AcceptanceTest(); + acceptanceTest.setSuccess(true); + acceptanceTest.setDateTime(OffsetDateTime.now().minusWeeks(2)); + acceptanceTest.setType("Quality Assurance Test"); + endDevice.setAcceptanceTest(acceptanceTest); + + // Act + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Asset.AcceptanceTest retrievedTest = retrieved.get().getAcceptanceTest(); + assertThat(retrievedTest).isNotNull(); + assertThat(retrievedTest.getSuccess()).isTrue(); + assertThat(retrievedTest.getDateTime()).isNotNull(); + assertThat(retrievedTest.getType()).isEqualTo("Quality Assurance Test"); + } + + @Test + @DisplayName("Should persist Status with all fields") + void shouldPersistStatusWithAllFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("POSTGRES-STATUS-TEST"); + + Status status = new Status(); + status.setValue("retired"); + OffsetDateTime testDateTime = OffsetDateTime.now(); + status.setDateTime(testDateTime); + status.setRemark("PostgreSQL device retirement"); + status.setReason("End of service life"); + endDevice.setStatus(status); + + // Act + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Status retrievedStatus = retrieved.get().getStatus(); + assertThat(retrievedStatus).isNotNull(); + assertThat(retrievedStatus.getValue()).isEqualTo("retired"); + assertThat(retrievedStatus.getDateTime()).isNotNull(); + assertThat(retrievedStatus.getRemark()).isEqualTo("PostgreSQL device retirement"); + assertThat(retrievedStatus.getReason()).isEqualTo("End of service life"); + } + + @Test + @DisplayName("Should persist ElectronicAddress with all fields") + void shouldPersistElectronicAddressWithAllFields() { + // Arrange + EndDeviceEntity endDevice = TestDataBuilders.createValidEndDevice(); + endDevice.setSerialNumber("POSTGRES-ELECTRONIC-TEST"); + + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setLan("10.10.10.100"); + electronicAddress.setMac("FF:EE:DD:CC:BB:AA"); + electronicAddress.setEmail1("device1@postgres.test"); + electronicAddress.setEmail2("device2@postgres.test"); + electronicAddress.setWeb("https://device.postgres.test"); + electronicAddress.setRadio("RADIO-PG-DEVICE-123"); + electronicAddress.setUserID("postgres-device-user"); + electronicAddress.setPassword("postgres-device-pass"); + endDevice.setElectronicAddress(electronicAddress); + + // Act + EndDeviceEntity savedDevice = endDeviceRepository.save(endDevice); + flushAndClear(); + Optional retrieved = endDeviceRepository.findById(savedDevice.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Organisation.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"); + assertThat(retrievedAddress.getEmail1()).isEqualTo("device1@postgres.test"); + assertThat(retrievedAddress.getEmail2()).isEqualTo("device2@postgres.test"); + assertThat(retrievedAddress.getRadio()).isEqualTo("RADIO-PG-DEVICE-123"); + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..d3ba6e26 --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ServiceLocationMySQLIntegrationTest.java @@ -0,0 +1,406 @@ +/* + * + * 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.Organisation; +import org.greenbuttonalliance.espi.common.domain.customer.entity.ServiceLocationEntity; +import org.greenbuttonalliance.espi.common.domain.customer.entity.Status; +import org.greenbuttonalliance.espi.common.repositories.customer.ServiceLocationRepository; +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.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * ServiceLocation entity integration tests using MySQL TestContainer. + * + * Tests full CRUD operations and relationship persistence with a real MySQL database. + */ +@DisplayName("ServiceLocation Integration Tests - MySQL") +@ActiveProfiles({"test", "test-mysql"}) +class ServiceLocationMySQLIntegrationTest 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 ServiceLocationRepository serviceLocationRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve service location with all fields") + void shouldSaveAndRetrieveServiceLocationWithAllFields() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("Commercial Building"); + location.setAccessMethod("Security code 1234 at gate"); + location.setSiteAccessProblem("Guard dog on premises"); + location.setNeedsInspection(true); + location.setGeoInfoReference("GEO-REF-MYSQL-001"); + location.setDirection("North side of Main Street"); + location.setOutageBlock("MYSQL-BLOCK-789"); + + // Main address + Organisation.StreetAddress mainAddress = new Organisation.StreetAddress(); + mainAddress.setStreetDetail("100 MySQL Main Street"); + mainAddress.setTownDetail("MySQL City"); + mainAddress.setStateOrProvince("CA"); + mainAddress.setPostalCode("95000"); + mainAddress.setCountry("USA"); + location.setMainAddress(mainAddress); + + // Secondary address + Organisation.StreetAddress secondaryAddress = new Organisation.StreetAddress(); + secondaryAddress.setStreetDetail("PO Box 12345"); + secondaryAddress.setTownDetail("MySQL City"); + secondaryAddress.setStateOrProvince("CA"); + secondaryAddress.setPostalCode("95001"); + secondaryAddress.setCountry("USA"); + location.setSecondaryAddress(secondaryAddress); + + // Phone numbers + Organisation.TelephoneNumber phone1 = new Organisation.TelephoneNumber(); + phone1.setCountryCode("1"); + phone1.setAreaCode("555"); + phone1.setLocalNumber("1234567"); + location.setPhone1(phone1); + + Organisation.TelephoneNumber phone2 = new Organisation.TelephoneNumber(); + phone2.setCountryCode("1"); + phone2.setAreaCode("555"); + phone2.setLocalNumber("7654321"); + phone2.setExt("100"); + location.setPhone2(phone2); + + // Electronic address + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setEmail1("location@mysql.test"); + electronicAddress.setWeb("https://location.mysql.test"); + location.setElectronicAddress(electronicAddress); + + // Status + Status status = new Status(); + status.setValue("operational"); + status.setDateTime(OffsetDateTime.now()); + status.setReason("MySQL location test"); + location.setStatus(status); + + // Usage point hrefs + List usagePointHrefs = new ArrayList<>(); + usagePointHrefs.add("https://api.example.com/espi/1_1/resource/UsagePoint/100001"); + usagePointHrefs.add("https://api.example.com/espi/1_1/resource/UsagePoint/100002"); + location.setUsagePointHrefs(usagePointHrefs); + + // Act + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + ServiceLocationEntity result = retrieved.get(); + + assertThat(result.getType()).isEqualTo("Commercial Building"); + assertThat(result.getAccessMethod()).isEqualTo("Security code 1234 at gate"); + assertThat(result.getSiteAccessProblem()).isEqualTo("Guard dog on premises"); + assertThat(result.getNeedsInspection()).isTrue(); + assertThat(result.getGeoInfoReference()).isEqualTo("GEO-REF-MYSQL-001"); + assertThat(result.getDirection()).isEqualTo("North side of Main Street"); + assertThat(result.getOutageBlock()).isEqualTo("MYSQL-BLOCK-789"); + + assertThat(result.getMainAddress()).isNotNull(); + assertThat(result.getMainAddress().getStreetDetail()).isEqualTo("100 MySQL Main Street"); + + assertThat(result.getSecondaryAddress()).isNotNull(); + assertThat(result.getSecondaryAddress().getStreetDetail()).isEqualTo("PO Box 12345"); + + assertThat(result.getPhone1()).isNotNull(); + assertThat(result.getPhone1().getAreaCode()).isEqualTo("555"); + assertThat(result.getPhone1().getLocalNumber()).isEqualTo("1234567"); + + assertThat(result.getPhone2()).isNotNull(); + assertThat(result.getPhone2().getExt()).isEqualTo("100"); + + assertThat(result.getElectronicAddress()).isNotNull(); + assertThat(result.getElectronicAddress().getEmail1()).isEqualTo("location@mysql.test"); + + assertThat(result.getStatus()).isNotNull(); + assertThat(result.getStatus().getValue()).isEqualTo("operational"); + + assertThat(result.getUsagePointHrefs()).isNotNull(); + assertThat(result.getUsagePointHrefs()).hasSize(2); + assertThat(result.getUsagePointHrefs().get(0)).contains("UsagePoint/100001"); + } + + @Test + @DisplayName("Should update service location fields") + void shouldUpdateServiceLocationFields() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("Original Type"); + location.setNeedsInspection(false); + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + + // Act + savedLocation.setType("Updated Type"); + savedLocation.setNeedsInspection(true); + savedLocation.setAccessMethod("Updated access method"); + ServiceLocationEntity updatedLocation = serviceLocationRepository.save(savedLocation); + flushAndClear(); + + Optional retrieved = serviceLocationRepository.findById(updatedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + assertThat(retrieved.get().getType()).isEqualTo("Updated Type"); + assertThat(retrieved.get().getNeedsInspection()).isTrue(); + assertThat(retrieved.get().getAccessMethod()).isEqualTo("Updated access method"); + } + + @Test + @DisplayName("Should delete service location") + void shouldDeleteServiceLocation() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("Temporary Location"); + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + + // Act + serviceLocationRepository.deleteById(savedLocation.getId()); + flushAndClear(); + + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isEmpty(); + } + } + + @Nested + @DisplayName("Bulk Operations") + class BulkOperationsTest { + + @Test + @DisplayName("Should handle bulk save operations") + void shouldHandleBulkSaveOperations() { + // Arrange + List locations = TestDataBuilders.createValidEntities(5, + TestDataBuilders::createValidServiceLocation); + + for (int i = 0; i < locations.size(); i++) { + locations.get(i).setType("MySQL Bulk Location " + i); + } + + // Act + List savedLocations = serviceLocationRepository.saveAll(locations); + flushAndClear(); + + // Assert + assertThat(savedLocations).hasSize(5); + assertThat(savedLocations).allMatch(location -> location.getId() != null); + + long count = serviceLocationRepository.count(); + assertThat(count).isGreaterThanOrEqualTo(5); + } + + @Test + @DisplayName("Should handle bulk delete operations") + void shouldHandleBulkDeleteOperations() { + // Arrange + List locations = TestDataBuilders.createValidEntities(3, + TestDataBuilders::createValidServiceLocation); + + List savedLocations = serviceLocationRepository.saveAll(locations); + long initialCount = serviceLocationRepository.count(); + flushAndClear(); + + // Act + serviceLocationRepository.deleteAll(savedLocations); + flushAndClear(); + + // Assert + long finalCount = serviceLocationRepository.count(); + assertThat(finalCount).isEqualTo(initialCount - 3); + } + } + + @Nested + @DisplayName("Embedded Objects Persistence") + class EmbeddedObjectsTest { + + @Test + @DisplayName("Should persist and retrieve addresses with all fields") + void shouldPersistAddressesWithAllFields() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("MySQL Address Test"); + + Organisation.StreetAddress mainAddress = new Organisation.StreetAddress(); + mainAddress.setStreetDetail("500 MySQL Complete Street"); + mainAddress.setTownDetail("Complete City"); + mainAddress.setStateOrProvince("TX"); + mainAddress.setPostalCode("75000"); + mainAddress.setCountry("USA"); + location.setMainAddress(mainAddress); + + Organisation.StreetAddress secondaryAddress = new Organisation.StreetAddress(); + secondaryAddress.setStreetDetail("PO Box 999"); + secondaryAddress.setTownDetail("Complete City"); + secondaryAddress.setStateOrProvince("TX"); + secondaryAddress.setPostalCode("75001"); + secondaryAddress.setCountry("USA"); + location.setSecondaryAddress(secondaryAddress); + + // Act + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + assertThat(retrieved.get().getMainAddress()).isNotNull(); + assertThat(retrieved.get().getMainAddress().getStreetDetail()).isEqualTo("500 MySQL Complete Street"); + assertThat(retrieved.get().getMainAddress().getPostalCode()).isEqualTo("75000"); + assertThat(retrieved.get().getSecondaryAddress()).isNotNull(); + assertThat(retrieved.get().getSecondaryAddress().getStreetDetail()).isEqualTo("PO Box 999"); + } + + @Test + @DisplayName("Should persist Status with all fields") + void shouldPersistStatusWithAllFields() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("MySQL Status Test"); + + Status status = new Status(); + status.setValue("maintenance"); + OffsetDateTime testDateTime = OffsetDateTime.now(); + status.setDateTime(testDateTime); + status.setRemark("MySQL maintenance mode"); + status.setReason("Scheduled upgrade"); + location.setStatus(status); + + // Act + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Status retrievedStatus = retrieved.get().getStatus(); + assertThat(retrievedStatus).isNotNull(); + assertThat(retrievedStatus.getValue()).isEqualTo("maintenance"); + assertThat(retrievedStatus.getDateTime()).isNotNull(); + assertThat(retrievedStatus.getRemark()).isEqualTo("MySQL maintenance mode"); + assertThat(retrievedStatus.getReason()).isEqualTo("Scheduled upgrade"); + } + + @Test + @DisplayName("Should persist ElectronicAddress with all fields") + void shouldPersistElectronicAddressWithAllFields() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("MySQL Electronic Address Test"); + + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setLan("192.168.1.100"); + electronicAddress.setMac("00:1A:2B:3C:4D:5E"); + electronicAddress.setEmail1("primary@mysql-loc.test"); + electronicAddress.setEmail2("secondary@mysql-loc.test"); + electronicAddress.setWeb("https://mysql-location.test"); + electronicAddress.setRadio("RADIO-LOC-123"); + electronicAddress.setUserID("mysql-user"); + electronicAddress.setPassword("mysql-pass"); + location.setElectronicAddress(electronicAddress); + + // Act + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Organisation.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"); + assertThat(retrievedAddress.getEmail1()).isEqualTo("primary@mysql-loc.test"); + assertThat(retrievedAddress.getEmail2()).isEqualTo("secondary@mysql-loc.test"); + assertThat(retrievedAddress.getRadio()).isEqualTo("RADIO-LOC-123"); + } + + @Test + @DisplayName("Should persist UsagePoint href collection") + void shouldPersistUsagePointHrefCollection() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("MySQL UsagePoint Href Test"); + + List usagePointHrefs = new ArrayList<>(); + usagePointHrefs.add("https://api.example.com/espi/1_1/resource/UsagePoint/200001"); + usagePointHrefs.add("https://api.example.com/espi/1_1/resource/UsagePoint/200002"); + usagePointHrefs.add("https://api.example.com/espi/1_1/resource/UsagePoint/200003"); + location.setUsagePointHrefs(usagePointHrefs); + + // Act + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + List retrievedHrefs = retrieved.get().getUsagePointHrefs(); + assertThat(retrievedHrefs).isNotNull(); + assertThat(retrievedHrefs).hasSize(3); + assertThat(retrievedHrefs).contains("https://api.example.com/espi/1_1/resource/UsagePoint/200001"); + assertThat(retrievedHrefs).contains("https://api.example.com/espi/1_1/resource/UsagePoint/200002"); + assertThat(retrievedHrefs).contains("https://api.example.com/espi/1_1/resource/UsagePoint/200003"); + } + } +} 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 new file mode 100644 index 00000000..4c38e72f --- /dev/null +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/integration/ServiceLocationPostgreSQLIntegrationTest.java @@ -0,0 +1,406 @@ +/* + * + * 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.Organisation; +import org.greenbuttonalliance.espi.common.domain.customer.entity.ServiceLocationEntity; +import org.greenbuttonalliance.espi.common.domain.customer.entity.Status; +import org.greenbuttonalliance.espi.common.repositories.customer.ServiceLocationRepository; +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.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * ServiceLocation entity integration tests using PostgreSQL TestContainer. + * + * Tests full CRUD operations and relationship persistence with a real PostgreSQL database. + */ +@DisplayName("ServiceLocation Integration Tests - PostgreSQL") +@ActiveProfiles({"test", "test-postgresql"}) +class ServiceLocationPostgreSQLIntegrationTest 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 ServiceLocationRepository serviceLocationRepository; + + @Nested + @DisplayName("CRUD Operations") + class CrudOperationsTest { + + @Test + @DisplayName("Should save and retrieve service location with all fields") + void shouldSaveAndRetrieveServiceLocationWithAllFields() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("Residential Property"); + location.setAccessMethod("Key under mat"); + location.setSiteAccessProblem("Narrow driveway"); + location.setNeedsInspection(false); + location.setGeoInfoReference("GEO-REF-PG-002"); + location.setDirection("South side of Elm Street"); + location.setOutageBlock("POSTGRES-BLOCK-456"); + + // Main address + Organisation.StreetAddress mainAddress = new Organisation.StreetAddress(); + mainAddress.setStreetDetail("200 PostgreSQL Avenue"); + mainAddress.setTownDetail("Postgres Town"); + mainAddress.setStateOrProvince("WA"); + mainAddress.setPostalCode("98000"); + mainAddress.setCountry("USA"); + location.setMainAddress(mainAddress); + + // Secondary address + Organisation.StreetAddress secondaryAddress = new Organisation.StreetAddress(); + secondaryAddress.setStreetDetail("PO Box 54321"); + secondaryAddress.setTownDetail("Postgres Town"); + secondaryAddress.setStateOrProvince("WA"); + secondaryAddress.setPostalCode("98001"); + secondaryAddress.setCountry("USA"); + location.setSecondaryAddress(secondaryAddress); + + // Phone numbers + Organisation.TelephoneNumber phone1 = new Organisation.TelephoneNumber(); + phone1.setCountryCode("1"); + phone1.setAreaCode("206"); + phone1.setLocalNumber("9876543"); + location.setPhone1(phone1); + + Organisation.TelephoneNumber phone2 = new Organisation.TelephoneNumber(); + phone2.setCountryCode("1"); + phone2.setAreaCode("206"); + phone2.setLocalNumber("3456789"); + phone2.setExt("200"); + location.setPhone2(phone2); + + // Electronic address + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setEmail1("location@postgres.test"); + electronicAddress.setWeb("https://location.postgres.test"); + location.setElectronicAddress(electronicAddress); + + // Status + Status status = new Status(); + status.setValue("active"); + status.setDateTime(OffsetDateTime.now()); + status.setReason("PostgreSQL location test"); + location.setStatus(status); + + // Usage point hrefs + List usagePointHrefs = new ArrayList<>(); + usagePointHrefs.add("https://api.example.com/espi/1_1/resource/UsagePoint/300001"); + usagePointHrefs.add("https://api.example.com/espi/1_1/resource/UsagePoint/300002"); + location.setUsagePointHrefs(usagePointHrefs); + + // Act + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + ServiceLocationEntity result = retrieved.get(); + + assertThat(result.getType()).isEqualTo("Residential Property"); + assertThat(result.getAccessMethod()).isEqualTo("Key under mat"); + assertThat(result.getSiteAccessProblem()).isEqualTo("Narrow driveway"); + assertThat(result.getNeedsInspection()).isFalse(); + assertThat(result.getGeoInfoReference()).isEqualTo("GEO-REF-PG-002"); + assertThat(result.getDirection()).isEqualTo("South side of Elm Street"); + assertThat(result.getOutageBlock()).isEqualTo("POSTGRES-BLOCK-456"); + + assertThat(result.getMainAddress()).isNotNull(); + assertThat(result.getMainAddress().getStreetDetail()).isEqualTo("200 PostgreSQL Avenue"); + + assertThat(result.getSecondaryAddress()).isNotNull(); + assertThat(result.getSecondaryAddress().getStreetDetail()).isEqualTo("PO Box 54321"); + + assertThat(result.getPhone1()).isNotNull(); + assertThat(result.getPhone1().getAreaCode()).isEqualTo("206"); + assertThat(result.getPhone1().getLocalNumber()).isEqualTo("9876543"); + + assertThat(result.getPhone2()).isNotNull(); + assertThat(result.getPhone2().getExt()).isEqualTo("200"); + + assertThat(result.getElectronicAddress()).isNotNull(); + assertThat(result.getElectronicAddress().getEmail1()).isEqualTo("location@postgres.test"); + + assertThat(result.getStatus()).isNotNull(); + assertThat(result.getStatus().getValue()).isEqualTo("active"); + + assertThat(result.getUsagePointHrefs()).isNotNull(); + assertThat(result.getUsagePointHrefs()).hasSize(2); + assertThat(result.getUsagePointHrefs().get(0)).contains("UsagePoint/300001"); + } + + @Test + @DisplayName("Should update service location fields") + void shouldUpdateServiceLocationFields() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("Original PostgreSQL Type"); + location.setNeedsInspection(true); + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + + // Act + savedLocation.setType("Updated PostgreSQL Type"); + savedLocation.setNeedsInspection(false); + savedLocation.setAccessMethod("Updated PostgreSQL access method"); + ServiceLocationEntity updatedLocation = serviceLocationRepository.save(savedLocation); + flushAndClear(); + + Optional retrieved = serviceLocationRepository.findById(updatedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + assertThat(retrieved.get().getType()).isEqualTo("Updated PostgreSQL Type"); + assertThat(retrieved.get().getNeedsInspection()).isFalse(); + assertThat(retrieved.get().getAccessMethod()).isEqualTo("Updated PostgreSQL access method"); + } + + @Test + @DisplayName("Should delete service location") + void shouldDeleteServiceLocation() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("Temporary PostgreSQL Location"); + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + + // Act + serviceLocationRepository.deleteById(savedLocation.getId()); + flushAndClear(); + + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isEmpty(); + } + } + + @Nested + @DisplayName("Bulk Operations") + class BulkOperationsTest { + + @Test + @DisplayName("Should handle bulk save operations") + void shouldHandleBulkSaveOperations() { + // Arrange + List locations = TestDataBuilders.createValidEntities(5, + TestDataBuilders::createValidServiceLocation); + + for (int i = 0; i < locations.size(); i++) { + locations.get(i).setType("PostgreSQL Bulk Location " + i); + } + + // Act + List savedLocations = serviceLocationRepository.saveAll(locations); + flushAndClear(); + + // Assert + assertThat(savedLocations).hasSize(5); + assertThat(savedLocations).allMatch(location -> location.getId() != null); + + long count = serviceLocationRepository.count(); + assertThat(count).isGreaterThanOrEqualTo(5); + } + + @Test + @DisplayName("Should handle bulk delete operations") + void shouldHandleBulkDeleteOperations() { + // Arrange + List locations = TestDataBuilders.createValidEntities(3, + TestDataBuilders::createValidServiceLocation); + + List savedLocations = serviceLocationRepository.saveAll(locations); + long initialCount = serviceLocationRepository.count(); + flushAndClear(); + + // Act + serviceLocationRepository.deleteAll(savedLocations); + flushAndClear(); + + // Assert + long finalCount = serviceLocationRepository.count(); + assertThat(finalCount).isEqualTo(initialCount - 3); + } + } + + @Nested + @DisplayName("Embedded Objects Persistence") + class EmbeddedObjectsTest { + + @Test + @DisplayName("Should persist and retrieve addresses with all fields") + void shouldPersistAddressesWithAllFields() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("PostgreSQL Address Test"); + + Organisation.StreetAddress mainAddress = new Organisation.StreetAddress(); + mainAddress.setStreetDetail("600 PostgreSQL Complete Boulevard"); + mainAddress.setTownDetail("Complete Town"); + mainAddress.setStateOrProvince("OR"); + mainAddress.setPostalCode("97000"); + mainAddress.setCountry("USA"); + location.setMainAddress(mainAddress); + + Organisation.StreetAddress secondaryAddress = new Organisation.StreetAddress(); + secondaryAddress.setStreetDetail("PO Box 111"); + secondaryAddress.setTownDetail("Complete Town"); + secondaryAddress.setStateOrProvince("OR"); + secondaryAddress.setPostalCode("97001"); + secondaryAddress.setCountry("USA"); + location.setSecondaryAddress(secondaryAddress); + + // Act + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + assertThat(retrieved.get().getMainAddress()).isNotNull(); + assertThat(retrieved.get().getMainAddress().getStreetDetail()).isEqualTo("600 PostgreSQL Complete Boulevard"); + assertThat(retrieved.get().getMainAddress().getPostalCode()).isEqualTo("97000"); + assertThat(retrieved.get().getSecondaryAddress()).isNotNull(); + assertThat(retrieved.get().getSecondaryAddress().getStreetDetail()).isEqualTo("PO Box 111"); + } + + @Test + @DisplayName("Should persist Status with all fields") + void shouldPersistStatusWithAllFields() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("PostgreSQL Status Test"); + + Status status = new Status(); + status.setValue("inactive"); + OffsetDateTime testDateTime = OffsetDateTime.now(); + status.setDateTime(testDateTime); + status.setRemark("PostgreSQL inactive mode"); + status.setReason("Seasonal closure"); + location.setStatus(status); + + // Act + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Status retrievedStatus = retrieved.get().getStatus(); + assertThat(retrievedStatus).isNotNull(); + assertThat(retrievedStatus.getValue()).isEqualTo("inactive"); + assertThat(retrievedStatus.getDateTime()).isNotNull(); + assertThat(retrievedStatus.getRemark()).isEqualTo("PostgreSQL inactive mode"); + assertThat(retrievedStatus.getReason()).isEqualTo("Seasonal closure"); + } + + @Test + @DisplayName("Should persist ElectronicAddress with all fields") + void shouldPersistElectronicAddressWithAllFields() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("PostgreSQL Electronic Address Test"); + + Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress(); + electronicAddress.setLan("10.0.0.100"); + electronicAddress.setMac("AA:BB:CC:DD:EE:FF"); + electronicAddress.setEmail1("primary@postgres-loc.test"); + electronicAddress.setEmail2("secondary@postgres-loc.test"); + electronicAddress.setWeb("https://postgres-location.test"); + electronicAddress.setRadio("RADIO-PG-LOC-456"); + electronicAddress.setUserID("postgres-user"); + electronicAddress.setPassword("postgres-pass"); + location.setElectronicAddress(electronicAddress); + + // Act + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + Organisation.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"); + assertThat(retrievedAddress.getEmail1()).isEqualTo("primary@postgres-loc.test"); + assertThat(retrievedAddress.getEmail2()).isEqualTo("secondary@postgres-loc.test"); + assertThat(retrievedAddress.getRadio()).isEqualTo("RADIO-PG-LOC-456"); + } + + @Test + @DisplayName("Should persist UsagePoint href collection") + void shouldPersistUsagePointHrefCollection() { + // Arrange + ServiceLocationEntity location = TestDataBuilders.createValidServiceLocation(); + location.setType("PostgreSQL UsagePoint Href Test"); + + List usagePointHrefs = new ArrayList<>(); + usagePointHrefs.add("https://api.example.com/espi/1_1/resource/UsagePoint/400001"); + usagePointHrefs.add("https://api.example.com/espi/1_1/resource/UsagePoint/400002"); + usagePointHrefs.add("https://api.example.com/espi/1_1/resource/UsagePoint/400003"); + location.setUsagePointHrefs(usagePointHrefs); + + // Act + ServiceLocationEntity savedLocation = serviceLocationRepository.save(location); + flushAndClear(); + Optional retrieved = serviceLocationRepository.findById(savedLocation.getId()); + + // Assert + assertThat(retrieved).isPresent(); + List retrievedHrefs = retrieved.get().getUsagePointHrefs(); + assertThat(retrievedHrefs).isNotNull(); + assertThat(retrievedHrefs).hasSize(3); + assertThat(retrievedHrefs).contains("https://api.example.com/espi/1_1/resource/UsagePoint/400001"); + assertThat(retrievedHrefs).contains("https://api.example.com/espi/1_1/resource/UsagePoint/400002"); + assertThat(retrievedHrefs).contains("https://api.example.com/espi/1_1/resource/UsagePoint/400003"); + } + } +} \ No newline at end of file diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/CustomerExportServiceTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/CustomerExportServiceTest.java index fb9a2ae9..9e68db20 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/CustomerExportServiceTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/CustomerExportServiceTest.java @@ -54,7 +54,6 @@ void setUp() { void shouldDeclareCustomerNamespaceOnly() { // Arrange CustomerDto customer = new CustomerDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440001", // uuid null, // organisation null, // kind "Wheelchair access required", // specialNeed @@ -108,7 +107,6 @@ void shouldDeclareCustomerNamespaceOnly() { void shouldUseAtomPrefixForAtomElements() { // Arrange CustomerDto customer = new CustomerDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440003", // uuid null, // organisation null, // kind null, // specialNeed @@ -159,7 +157,6 @@ void shouldUseAtomPrefixForAtomElements() { void shouldUseCustPrefixForCustomer() { // Arrange CustomerDto customer = new CustomerDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440005", // uuid null, // organisation null, // kind null, // specialNeed @@ -191,7 +188,6 @@ void shouldUseCustPrefixForCustomer() { void shouldProduceValidEspiXmlStructure() { // Arrange CustomerDto customer = new CustomerDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440007", // uuid null, // organisation null, // kind "Hearing impaired", // specialNeed diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/UsageExportServiceTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/UsageExportServiceTest.java index 79a98797..87250fab 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/UsageExportServiceTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/UsageExportServiceTest.java @@ -54,7 +54,6 @@ void setUp() { void shouldDeclareEspiNamespaceOnly() { // Arrange UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440010", new byte[]{0x01}, null, (short) 1, null, null, null, null, null, @@ -106,7 +105,6 @@ void shouldDeclareEspiNamespaceOnly() { void shouldUseAtomPrefixForAtomElements() { // Arrange UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440012", new byte[]{0x02}, null, (short) 1, null, null, null, null, null, @@ -155,7 +153,6 @@ void shouldUseAtomPrefixForAtomElements() { void shouldUseEspiPrefixForUsagePoint() { // Arrange UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:550e8400-e29b-51d4-a716-446655440014", new byte[]{0x03}, null, (short) 1, null, null, null, null, null, @@ -187,7 +184,6 @@ void shouldUseEspiPrefixForUsagePoint() { void shouldProduceValidEspiXmlStructure() { // Arrange UsagePointDto usagePoint = new UsagePointDto( - "urn:uuid:debug-usage", new byte[]{0x01, 0x02}, // roleFlags null, // serviceCategory (short) 1, // status 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 a2e1c3df..9ec91170 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 @@ -21,11 +21,11 @@ import net.datafaker.Faker; import org.greenbuttonalliance.espi.common.domain.common.DateTimeInterval; import org.greenbuttonalliance.espi.common.domain.common.ServiceCategory; -import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerEntity; -import org.greenbuttonalliance.espi.common.domain.customer.entity.StatementEntity; +import org.greenbuttonalliance.espi.common.domain.customer.entity.*; import org.greenbuttonalliance.espi.common.domain.customer.enums.CustomerKind; import org.greenbuttonalliance.espi.common.domain.usage.*; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; @@ -323,4 +323,60 @@ public static LocalDateTime randomLocalDateTime() { return faker.timeAndDate().past(365, java.util.concurrent.TimeUnit.DAYS) .atZone(java.time.ZoneId.systemDefault()).toLocalDateTime(); } + + /** + * Creates a valid CustomerAccountEntity for testing. + */ + public static CustomerAccountEntity createValidCustomerAccount() { + CustomerAccountEntity account = new CustomerAccountEntity(); + account.setDescription(faker.lorem().sentence(4, 8)); + account.setType("Residential Account"); + account.setBillingCycle(String.valueOf(faker.number().numberBetween(1, 31))); + account.setBudgetBill("Standard"); + account.setAccountId("ACCT-" + faker.number().digits(10)); + account.setIsPrePay(false); + return account; + } + + /** + * Creates a valid ServiceLocationEntity for testing. + */ + public static ServiceLocationEntity createValidServiceLocation() { + ServiceLocationEntity location = new ServiceLocationEntity(); + location.setDescription(faker.lorem().sentence(4, 8)); + location.setType("Residential"); + location.setAccessMethod("Front door key in lockbox"); + location.setNeedsInspection(false); + location.setOutageBlock("BLOCK-" + faker.number().digits(6)); + return location; + } + + /** + * Creates a valid CustomerAgreementEntity for testing. + */ + public static CustomerAgreementEntity createValidCustomerAgreement() { + CustomerAgreementEntity agreement = new CustomerAgreementEntity(); + agreement.setDescription(faker.lorem().sentence(4, 8)); + agreement.setType("Service Agreement"); + agreement.setSignDate(OffsetDateTime.now().minusDays(faker.number().numberBetween(1, 365))); + agreement.setIsPrePay(false); + agreement.setCurrency("USD"); + agreement.setAgreementId("AGR-" + faker.number().digits(10)); + return agreement; + } + + /** + * Creates a valid EndDeviceEntity for testing. + */ + public static EndDeviceEntity createValidEndDevice() { + EndDeviceEntity endDevice = new EndDeviceEntity(); + endDevice.setDescription(faker.lorem().sentence(4, 8)); + endDevice.setType("Electric Meter"); + endDevice.setSerialNumber("SN-" + faker.number().digits(12)); + endDevice.setUtcNumber("UTC-" + faker.number().digits(8)); + endDevice.setIsVirtual(false); + endDevice.setIsPan(false); + endDevice.setAmrSystem("AMR-" + faker.lorem().word()); + return endDevice; + } } \ No newline at end of file