futureStatus;
/**
- * Generates the default self href for a customer agreement.
- *
- * @return default self href
+ * Customer agreement identifier.
*/
- public String generateSelfHref() {
- if (uuid != null && customerAccount != null && customerAccount.getUuid() != null) {
- return "/espi/1_1/resource/CustomerAccount/" + customerAccount.getUuid() + "/CustomerAgreement/" + uuid;
- }
- return uuid != null ? "/espi/1_1/resource/CustomerAgreement/" + uuid : null;
- }
+ @XmlElement(name = "agreementId", namespace = "http://naesb.org/espi/customer")
+ private String agreementId;
+
+ // ==================== Nested DTOs ====================
/**
- * Generates the default up href for a customer agreement.
- *
- * @return default up href
+ * Status DTO for document status and future status information.
+ * Matches customer.xsd Status type (lines 1254-1284).
+ * XML type name is "AgreementStatus" to avoid conflict with CustomerDto.StatusDto.
*/
- public String generateUpHref() {
- if (customerAccount != null && customerAccount.getUuid() != null) {
- return "/espi/1_1/resource/CustomerAccount/" + customerAccount.getUuid() + "/CustomerAgreement";
- }
- return "/espi/1_1/resource/CustomerAgreement";
+ @XmlAccessorType(XmlAccessType.FIELD)
+ @XmlType(name = "AgreementStatus", namespace = "http://naesb.org/espi/customer", propOrder = {
+ "value", "dateTime", "remark", "reason"
+ })
+ @Getter
+ @Setter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class StatusDto implements Serializable {
+
+ /**
+ * Status value.
+ */
+ @XmlElement(name = "value", namespace = "http://naesb.org/espi/customer")
+ private String value;
+
+ /**
+ * Date and time for which status value applies.
+ */
+ @XmlElement(name = "dateTime", namespace = "http://naesb.org/espi/customer")
+ private OffsetDateTime dateTime;
+
+ /**
+ * Pertinent information regarding the current value, as free form text.
+ */
+ @XmlElement(name = "remark", namespace = "http://naesb.org/espi/customer")
+ private String remark;
+
+ /**
+ * Reason code or explanation for why an object went to the current status value.
+ */
+ @XmlElement(name = "reason", namespace = "http://naesb.org/espi/customer")
+ private String reason;
}
-}
\ No newline at end of file
+}
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 ae52d795..e3e8ed73 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
@@ -199,14 +199,23 @@ public static class PhoneNumberDto {
/**
* Embeddable DTO for ElectronicAddress.
+ * Per customer.xsd ElectronicAddress type (lines 886-936).
*/
@XmlAccessorType(XmlAccessType.FIELD)
- @XmlType(name = "ElectronicAddress", namespace = "http://naesb.org/espi/customer")
+ @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;
@@ -218,5 +227,11 @@ public static class ElectronicAddressDto {
@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/usage/AuthorizationDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/AuthorizationDto.java
index 6e22cde2..8cef323e 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
@@ -19,6 +19,8 @@
package org.greenbuttonalliance.espi.common.dto.usage;
+import org.greenbuttonalliance.espi.common.dto.common.DateTimeIntervalDto;
+
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.xml.bind.annotation.*;
import lombok.AllArgsConstructor;
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 bed77d2f..8ab63b1c 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
@@ -24,7 +24,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
-import org.greenbuttonalliance.espi.common.dto.usage.DateTimeIntervalDto;
+import org.greenbuttonalliance.espi.common.dto.common.DateTimeIntervalDto;
/**
* ElectricPowerQualitySummary DTO class for JAXB XML marshalling/unmarshalling.
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 490e7161..271beec9 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
@@ -19,6 +19,8 @@
package org.greenbuttonalliance.espi.common.dto.usage;
+import org.greenbuttonalliance.espi.common.dto.common.DateTimeIntervalDto;
+
import jakarta.xml.bind.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/IntervalReadingDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/IntervalReadingDto.java
index d35baaa6..b05604c1 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/IntervalReadingDto.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/IntervalReadingDto.java
@@ -19,6 +19,8 @@
package org.greenbuttonalliance.espi.common.dto.usage;
+import org.greenbuttonalliance.espi.common.dto.common.DateTimeIntervalDto;
+
import jakarta.xml.bind.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/LineItemDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/LineItemDto.java
index cd26536d..32c8a60d 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/LineItemDto.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/LineItemDto.java
@@ -19,6 +19,8 @@
package org.greenbuttonalliance.espi.common.dto.usage;
+import org.greenbuttonalliance.espi.common.dto.common.DateTimeIntervalDto;
+
import jakarta.xml.bind.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
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 5423bd4e..0a1a3a23 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
@@ -19,6 +19,8 @@
package org.greenbuttonalliance.espi.common.dto.usage;
+import org.greenbuttonalliance.espi.common.dto.common.DateTimeIntervalDto;
+
import org.greenbuttonalliance.espi.common.dto.BillingChargeSourceDto;
import org.greenbuttonalliance.espi.common.dto.SummaryMeasurementDto;
import org.greenbuttonalliance.espi.common.dto.atom.LinkDto;
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 e873ec44..409a2a43 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
@@ -23,9 +23,9 @@
import org.greenbuttonalliance.espi.common.dto.customer.CustomerAgreementDto;
import org.greenbuttonalliance.espi.common.mapper.BaseMapperUtils;
import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper;
+import org.greenbuttonalliance.espi.common.mapper.usage.DateTimeIntervalMapper;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
-import org.mapstruct.MappingTarget;
/**
* MapStruct mapper for converting between CustomerAgreementEntity and CustomerAgreementDto.
@@ -33,46 +33,89 @@
* Handles the conversion between the JPA entity used for persistence and the DTO
* used for JAXB XML marshalling in the Green Button API.
*
- * Maps only customer.xsd CustomerAgreement fields. IdentifiedObject fields are NOT part of
- * the customer.xsd CustomerAgreement definition and are handled by AtomFeedDto/AtomEntryDto.
+ * Maps customer.xsd CustomerAgreement fields including Document and Agreement base fields.
+ * Per customer.xsd lines 159-209 (CustomerAgreement), 622-642 (Agreement), 819-885 (Document).
*/
@Mapper(componentModel = "spring", uses = {
DateTimeMapper.class,
- BaseMapperUtils.class
+ BaseMapperUtils.class,
+ ElectronicAddressMapper.class,
+ StatusMapper.class,
+ DateTimeIntervalMapper.class
})
public interface CustomerAgreementMapper {
/**
* Converts a CustomerAgreementEntity to a CustomerAgreementDto.
- * Maps only customer.xsd CustomerAgreement fields.
+ * Maps all Document, Agreement, and CustomerAgreement fields per customer.xsd.
*
* @param entity the customer agreement entity
* @return the customer agreement DTO
*/
- @Mapping(target = "id", ignore = true) // IdentifiedObject field handled by Atom layer
+ @Mapping(target = "uuid", source = "id")
+ // Document fields (11)
+ @Mapping(target = "type", source = "type")
+ @Mapping(target = "authorName", source = "authorName")
+ @Mapping(target = "createdDateTime", source = "createdDateTime")
+ @Mapping(target = "lastModifiedDateTime", source = "lastModifiedDateTime")
+ @Mapping(target = "revisionNumber", source = "revisionNumber")
+ @Mapping(target = "electronicAddress", source = "electronicAddress")
+ @Mapping(target = "subject", source = "subject")
+ @Mapping(target = "title", source = "title")
+ @Mapping(target = "docStatus", source = "docStatus")
+ @Mapping(target = "status", source = "status")
+ @Mapping(target = "comment", source = "comment")
+ // Agreement fields (2)
@Mapping(target = "signDate", source = "signDate")
- @Mapping(target = "validityInterval", ignore = true) // Complex mapping
- @Mapping(target = "customerAccount", ignore = true) // Relationship handled separately
- @Mapping(target = "serviceLocations", ignore = true) // Relationship handled separately
- @Mapping(target = "statements", ignore = true) // Relationship handled separately
+ @Mapping(target = "validityInterval", source = "validityInterval")
+ // CustomerAgreement fields (6)
+ @Mapping(target = "loadMgmt", source = "loadMgmt")
+ @Mapping(target = "isPrePay", source = "isPrePay")
+ @Mapping(target = "shutOffDateTime", source = "shutOffDateTime")
+ @Mapping(target = "currency", source = "currency")
+ @Mapping(target = "futureStatus", source = "futureStatus")
+ @Mapping(target = "agreementId", source = "agreementId")
CustomerAgreementDto toDto(CustomerAgreementEntity entity);
/**
* Converts a CustomerAgreementDto to a CustomerAgreementEntity.
- * Maps only customer.xsd CustomerAgreement fields.
+ * Maps all Document, Agreement, and CustomerAgreement fields per customer.xsd.
*
* @param dto the customer agreement DTO
* @return the customer agreement entity
*/
- @Mapping(target = "id", ignore = true) // IdentifiedObject field handled by Atom layer
+ @Mapping(target = "id", source = "uuid")
+ // Document fields (11)
+ @Mapping(target = "type", source = "type")
+ @Mapping(target = "authorName", source = "authorName")
+ @Mapping(target = "createdDateTime", source = "createdDateTime")
+ @Mapping(target = "lastModifiedDateTime", source = "lastModifiedDateTime")
+ @Mapping(target = "revisionNumber", source = "revisionNumber")
+ @Mapping(target = "electronicAddress", source = "electronicAddress")
+ @Mapping(target = "subject", source = "subject")
+ @Mapping(target = "title", source = "title")
+ @Mapping(target = "docStatus", source = "docStatus")
+ @Mapping(target = "status", source = "status")
+ @Mapping(target = "comment", source = "comment")
+ // Agreement fields (2)
@Mapping(target = "signDate", source = "signDate")
- @Mapping(target = "validityInterval", ignore = true) // Complex mapping
- @Mapping(target = "createdDateTime", ignore = true) // From Document
- @Mapping(target = "lastModifiedDateTime", ignore = true) // From Document
- @Mapping(target = "revisionNumber", ignore = true) // From Document
- @Mapping(target = "subject", ignore = true) // From Document
- @Mapping(target = "title", ignore = true) // From Document
- @Mapping(target = "type", ignore = true) // From Document
+ @Mapping(target = "validityInterval", source = "validityInterval")
+ // CustomerAgreement fields (6)
+ @Mapping(target = "loadMgmt", source = "loadMgmt")
+ @Mapping(target = "isPrePay", source = "isPrePay")
+ @Mapping(target = "shutOffDateTime", source = "shutOffDateTime")
+ @Mapping(target = "currency", source = "currency")
+ @Mapping(target = "futureStatus", source = "futureStatus")
+ @Mapping(target = "agreementId", source = "agreementId")
+ // Entity-only fields not in DTO (relationships commented as TODO)
+ // @Mapping(target = "demandResponsePrograms", ignore = true)
+ // @Mapping(target = "pricingStructures", ignore = true)
+ // IdentifiedObject fields (inherited) - handled by Atom layer
+ @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)
CustomerAgreementEntity toEntity(CustomerAgreementDto dto);
-
}
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 4639a77b..3608c7c4 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
@@ -162,20 +162,28 @@ default Organisation.StreetAddress mapStreetAddressFromDto(CustomerDto.StreetAdd
default CustomerDto.ElectronicAddressDto mapElectronicAddress(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.getRadio(),
+ address.getUserID(),
+ address.getPassword()
);
}
default Organisation.ElectronicAddress mapElectronicAddressFromDto(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;
}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/DateTimeIntervalMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/DateTimeIntervalMapper.java
index adac80bc..8d9b910d 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/DateTimeIntervalMapper.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/DateTimeIntervalMapper.java
@@ -20,7 +20,7 @@
package org.greenbuttonalliance.espi.common.mapper.usage;
import org.greenbuttonalliance.espi.common.domain.common.DateTimeInterval;
-import org.greenbuttonalliance.espi.common.dto.usage.DateTimeIntervalDto;
+import org.greenbuttonalliance.espi.common.dto.common.DateTimeIntervalDto;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepository.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepository.java
new file mode 100644
index 00000000..ce6ae218
--- /dev/null
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepository.java
@@ -0,0 +1,39 @@
+/*
+ *
+ * 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.CustomerAgreementEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.UUID;
+
+/**
+ * Spring Data JPA repository for CustomerAgreementEntity.
+ * Phase 24: CustomerAgreement schema compliance.
+ *
+ * Only uses inherited JpaRepository methods to avoid H2 keyword conflicts.
+ * Per Phase 18 pattern: findById, findAll, save, delete, count, existsById
+ */
+@Repository
+public interface CustomerAgreementRepository extends JpaRepository {
+ // Use only inherited methods to avoid H2 keyword conflicts
+ // No custom query methods required
+}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/EspiIdGeneratorService.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/EspiIdGeneratorService.java
index e587cbfa..5ac1929a 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/EspiIdGeneratorService.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/EspiIdGeneratorService.java
@@ -143,6 +143,36 @@ private UUID bytesToUUID(byte[] bytes) {
return new UUID(msb, lsb);
}
+ /**
+ * Generates a deterministic NAESB ESPI compliant UUID5 for any entity type.
+ *
+ * This method creates a deterministic UUID5 based on the entity type and natural key.
+ * The same entityType and naturalKey will always produce the same UUID, ensuring
+ * idempotency for entity creation and updates.
+ *
+ * @param entityType the type of entity (e.g., "CustomerAccount", "CustomerAgreement")
+ * @param naturalKey the natural key or business identifier (e.g., accountId, agreementId)
+ * @return a deterministic UUID5 identifier for the entity
+ * @throws IllegalArgumentException if entityType or naturalKey is null or empty
+ */
+ public UUID generateEntityId(String entityType, String naturalKey) {
+ if (entityType == null || entityType.trim().isEmpty()) {
+ throw new IllegalArgumentException("entityType cannot be null or empty");
+ }
+ if (naturalKey == null || naturalKey.trim().isEmpty()) {
+ throw new IllegalArgumentException("naturalKey cannot be null or empty");
+ }
+
+ // Build the deterministic name string (no timestamps - purely deterministic)
+ String name = entityType.trim() + ":" + naturalKey.trim();
+
+ try {
+ return generateUUID5(ESPI_NAMESPACE, name);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("SHA-1 algorithm not available", e);
+ }
+ }
+
/**
* Generates a NAESB ESPI compliant UUID5 for a Subscription entity.
*
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerAgreementService.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerAgreementService.java
new file mode 100644
index 00000000..d3e0ba38
--- /dev/null
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerAgreementService.java
@@ -0,0 +1,66 @@
+/*
+ *
+ * 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.CustomerAgreementEntity;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Service interface for CustomerAgreement management.
+ *
+ * Handles business logic for customer agreement operations including contract terms,
+ * service agreements, and agreement lifecycle management.
+ * Per Phase 24 guidelines, only ID-based operations on indexed fields are supported.
+ */
+public interface CustomerAgreementService {
+
+ /**
+ * Find all customer agreements.
+ */
+ List findAll();
+
+ /**
+ * Find customer agreement by UUID.
+ */
+ Optional findById(UUID id);
+
+ /**
+ * Save customer agreement.
+ */
+ CustomerAgreementEntity save(CustomerAgreementEntity customerAgreement);
+
+ /**
+ * Delete customer agreement by UUID.
+ */
+ void deleteById(UUID id);
+
+ /**
+ * Check if customer agreement exists by UUID.
+ */
+ boolean existsById(UUID id);
+
+ /**
+ * Count total customer agreements.
+ */
+ long count();
+}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/CustomerAgreementServiceImpl.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/CustomerAgreementServiceImpl.java
new file mode 100644
index 00000000..bd8be141
--- /dev/null
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/CustomerAgreementServiceImpl.java
@@ -0,0 +1,94 @@
+/*
+ *
+ * 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.CustomerAgreementEntity;
+import org.greenbuttonalliance.espi.common.repositories.customer.CustomerAgreementRepository;
+import org.greenbuttonalliance.espi.common.service.EspiIdGeneratorService;
+import org.greenbuttonalliance.espi.common.service.customer.CustomerAgreementService;
+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 CustomerAgreement management.
+ *
+ * Provides business logic for customer agreement operations including contract terms,
+ * service agreements, and agreement lifecycle management.
+ * Per Phase 24 guidelines, only ID-based operations on indexed fields are supported.
+ */
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class CustomerAgreementServiceImpl implements CustomerAgreementService {
+
+ private final CustomerAgreementRepository customerAgreementRepository;
+ private final EspiIdGeneratorService espiIdGeneratorService;
+
+ @Override
+ @Transactional(readOnly = true)
+ public List findAll() {
+ return customerAgreementRepository.findAll();
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public Optional findById(UUID id) {
+ return customerAgreementRepository.findById(id);
+ }
+
+ @Override
+ public CustomerAgreementEntity save(CustomerAgreementEntity customerAgreement) {
+ // Generate deterministic UUID v5 if not present
+ if (customerAgreement.getId() == null) {
+ // Require agreementId to be set for deterministic UUID v5 generation
+ // Per Green Button Alliance guidelines, UUID v5 must use repeatable, unchangeable attributes
+ if (customerAgreement.getAgreementId() == null || customerAgreement.getAgreementId().trim().isEmpty()) {
+ throw new IllegalArgumentException(
+ "agreementId must be set before saving a new CustomerAgreement to generate deterministic UUID v5"
+ );
+ }
+ UUID uuid5 = espiIdGeneratorService.generateEntityId("CustomerAgreement", customerAgreement.getAgreementId());
+ customerAgreement.setId(uuid5);
+ }
+ return customerAgreementRepository.save(customerAgreement);
+ }
+
+ @Override
+ public void deleteById(UUID id) {
+ customerAgreementRepository.deleteById(id);
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public boolean existsById(UUID id) {
+ return customerAgreementRepository.existsById(id);
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public long count() {
+ return customerAgreementRepository.count();
+ }
+}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java
index c62af189..15358684 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java
@@ -246,7 +246,7 @@ private Marshaller createMarshaller(Class> dtoClass, Set requiredNames
org.greenbuttonalliance.espi.common.dto.usage.ServiceDeliveryPointDto.class,
org.greenbuttonalliance.espi.common.dto.usage.ReadingQualityDto.class,
org.greenbuttonalliance.espi.common.dto.usage.IntervalReadingDto.class,
- org.greenbuttonalliance.espi.common.dto.usage.DateTimeIntervalDto.class,
+ org.greenbuttonalliance.espi.common.dto.common.DateTimeIntervalDto.class,
org.greenbuttonalliance.espi.common.dto.usage.TariffRiderRefDto.class,
org.greenbuttonalliance.espi.common.dto.usage.TariffRiderRefsDto.class,
org.greenbuttonalliance.espi.common.dto.usage.PnodeRefDto.class,
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/UsageExportService.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/UsageExportService.java
index e84d267f..256789e8 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/UsageExportService.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/UsageExportService.java
@@ -96,7 +96,7 @@ protected JAXBContext createJAXBContext() throws JAXBException {
org.greenbuttonalliance.espi.common.dto.usage.ServiceDeliveryPointDto.class,
org.greenbuttonalliance.espi.common.dto.usage.ReadingQualityDto.class,
org.greenbuttonalliance.espi.common.dto.usage.IntervalReadingDto.class,
- org.greenbuttonalliance.espi.common.dto.usage.DateTimeIntervalDto.class,
+ org.greenbuttonalliance.espi.common.dto.common.DateTimeIntervalDto.class,
org.greenbuttonalliance.espi.common.dto.usage.TariffRiderRefDto.class,
org.greenbuttonalliance.espi.common.dto.usage.TariffRiderRefsDto.class,
org.greenbuttonalliance.espi.common.dto.usage.PnodeRefDto.class,
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 5c194cbd..ca61474a 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
@@ -132,10 +132,15 @@ CREATE TABLE customers
customer_postal_state_or_province VARCHAR(255),
customer_postal_postal_code VARCHAR(255),
customer_postal_country VARCHAR(255),
+ -- Organisation.electronicAddress (customer.xsd lines 886-936)
+ customer_lan VARCHAR(255),
+ customer_mac VARCHAR(255),
customer_email1 VARCHAR(255),
customer_email2 VARCHAR(255),
customer_web VARCHAR(255),
customer_radio VARCHAR(255),
+ customer_user_id VARCHAR(255),
+ customer_password VARCHAR(255),
-- Status embedded object columns
status_value VARCHAR(256),
@@ -187,27 +192,48 @@ CREATE TABLE customer_agreements
created TIMESTAMP NOT NULL,
updated TIMESTAMP NOT NULL,
published TIMESTAMP,
- up_link_rel VARCHAR(255),
- up_link_href VARCHAR(1024),
- up_link_type VARCHAR(255),
- self_link_rel VARCHAR(255),
- self_link_href VARCHAR(1024),
- self_link_type VARCHAR(255),
-
- -- Document fields
+ customer_agreement_up_link_rel VARCHAR(255),
+ customer_agreement_up_link_href VARCHAR(1024),
+ customer_agreement_up_link_type VARCHAR(255),
+ customer_agreement_self_link_rel VARCHAR(255),
+ customer_agreement_self_link_href VARCHAR(1024),
+ customer_agreement_self_link_type VARCHAR(255),
+
+ -- Document fields (customer.xsd lines 819-885) - field order matches XSD
+ document_type VARCHAR(256),
+ author_name VARCHAR(256),
created_date_time TIMESTAMP,
last_modified_date_time TIMESTAMP,
revision_number VARCHAR(256),
+ -- Document.electronicAddress embedded object (customer.xsd lines 886-936)
+ doc_lan VARCHAR(256),
+ doc_mac VARCHAR(256),
+ doc_email1 VARCHAR(256),
+ doc_email2 VARCHAR(256),
+ doc_web VARCHAR(256),
+ doc_radio VARCHAR(256),
+ doc_user_id VARCHAR(256),
+ doc_password VARCHAR(256),
subject VARCHAR(256),
title VARCHAR(256),
- type VARCHAR(256),
+ -- Document.docStatus embedded object (customer.xsd lines 1254-1284)
+ doc_status_value VARCHAR(256),
+ doc_status_date_time TIMESTAMP,
+ doc_status_remark VARCHAR(256),
+ doc_status_reason VARCHAR(256),
+ -- Document.status embedded object (customer.xsd lines 1254-1284)
+ status_value VARCHAR(256),
+ status_date_time TIMESTAMP,
+ status_remark VARCHAR(256),
+ status_reason VARCHAR(256),
+ comment VARCHAR(256),
- -- Agreement fields
+ -- Agreement fields (customer.xsd lines 622-642)
sign_date TIMESTAMP,
- start BIGINT,
- duration BIGINT,
+ validity_start BIGINT,
+ validity_duration BIGINT,
- -- Customer agreement specific fields
+ -- Customer agreement specific fields (customer.xsd lines 159-209)
load_mgmt VARCHAR(256),
is_pre_pay BOOLEAN,
shut_off_date_time TIMESTAMP,
@@ -237,6 +263,7 @@ CREATE TABLE customer_agreement_future_status
customer_agreement_id CHAR(36) NOT NULL,
status_value VARCHAR(256),
status_date_time TIMESTAMP,
+ status_remark VARCHAR(256),
status_reason VARCHAR(256),
FOREIGN KEY (customer_agreement_id) REFERENCES customer_agreements (id) ON DELETE CASCADE
);
@@ -267,17 +294,22 @@ CREATE TABLE customer_accounts
created_date_time TIMESTAMP,
last_modified_date_time TIMESTAMP,
revision_number VARCHAR(256),
- -- Document.electronicAddress embedded object
- email1 VARCHAR(256),
- email2 VARCHAR(256),
- web VARCHAR(256),
- radio VARCHAR(256),
+ -- Document.electronicAddress embedded object (customer.xsd lines 886-936)
+ doc_lan VARCHAR(256),
+ doc_mac VARCHAR(256),
+ doc_email1 VARCHAR(256),
+ doc_email2 VARCHAR(256),
+ doc_web VARCHAR(256),
+ doc_radio VARCHAR(256),
+ doc_user_id VARCHAR(256),
+ doc_password VARCHAR(256),
subject VARCHAR(256),
title VARCHAR(256),
- -- Document.docStatus embedded object
- status_value VARCHAR(256),
- status_date_time TIMESTAMP,
- status_reason VARCHAR(512),
+ -- Document.docStatus embedded object (customer.xsd lines 1254-1284)
+ doc_status_value VARCHAR(256),
+ doc_status_date_time TIMESTAMP,
+ doc_status_remark VARCHAR(512),
+ doc_status_reason VARCHAR(512),
-- CustomerAccount specific fields (customer.xsd lines 118-158)
billing_cycle VARCHAR(50),
@@ -297,11 +329,15 @@ CREATE TABLE customer_accounts
postal_state_or_province VARCHAR(256),
postal_postal_code VARCHAR(256),
postal_country VARCHAR(256),
- -- contactInfo.electronicAddress
+ -- contactInfo.electronicAddress (customer.xsd lines 886-936)
+ contact_lan VARCHAR(256),
+ contact_mac VARCHAR(256),
contact_email1 VARCHAR(256),
contact_email2 VARCHAR(256),
contact_web VARCHAR(256),
contact_radio VARCHAR(256),
+ contact_user_id VARCHAR(256),
+ contact_password VARCHAR(256),
-- contactInfo.organisationName
organisation_name VARCHAR(256),
account_id VARCHAR(256),
@@ -422,10 +458,15 @@ CREATE TABLE end_devices
lot_number VARCHAR(100),
purchase_price BIGINT,
critical BOOLEAN DEFAULT FALSE,
+ -- ElectronicAddress (customer.xsd lines 886-936)
+ end_device_lan VARCHAR(255),
+ end_device_mac VARCHAR(255),
end_device_email1 VARCHAR(255),
end_device_email2 VARCHAR(255),
end_device_web VARCHAR(255),
end_device_radio VARCHAR(255),
+ end_device_user_id VARCHAR(255),
+ end_device_password VARCHAR(255),
installation_date TIMESTAMP,
manufactured_date TIMESTAMP,
purchase_date TIMESTAMP,
@@ -594,11 +635,15 @@ CREATE TABLE service_locations
secondary_postal_code VARCHAR(255),
secondary_country VARCHAR(255),
- -- Electronic address embedded object columns
+ -- Electronic address embedded object columns (customer.xsd lines 886-936)
+ electronic_lan VARCHAR(255),
+ electronic_mac VARCHAR(255),
electronic_email1 VARCHAR(255),
electronic_email2 VARCHAR(255),
electronic_web VARCHAR(255),
electronic_radio VARCHAR(255),
+ electronic_user_id VARCHAR(255),
+ electronic_password VARCHAR(255),
-- Status embedded object columns
status_value VARCHAR(256),
@@ -663,10 +708,14 @@ CREATE TABLE service_suppliers
supplier_postal_state_or_province VARCHAR(255),
supplier_postal_postal_code VARCHAR(255),
supplier_postal_country VARCHAR(255),
+ supplier_lan VARCHAR(256),
+ supplier_mac VARCHAR(256),
supplier_email1 VARCHAR(255),
supplier_email2 VARCHAR(255),
supplier_web VARCHAR(255),
- supplier_radio VARCHAR(255)
+ supplier_radio VARCHAR(255),
+ supplier_user_id VARCHAR(256),
+ supplier_password VARCHAR(256)
);
CREATE INDEX idx_service_supplier_kind ON service_suppliers (kind);
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 bc3f1d53..c72d9ac6 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
@@ -75,7 +75,7 @@ void setUp() throws JAXBException {
org.greenbuttonalliance.espi.common.dto.usage.ServiceDeliveryPointDto.class,
org.greenbuttonalliance.espi.common.dto.usage.ReadingQualityDto.class,
org.greenbuttonalliance.espi.common.dto.usage.IntervalReadingDto.class,
- org.greenbuttonalliance.espi.common.dto.usage.DateTimeIntervalDto.class
+ org.greenbuttonalliance.espi.common.dto.common.DateTimeIntervalDto.class
);
}
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 3e811640..0ea8d2ff 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
@@ -206,19 +206,20 @@ void shouldUseCorrectCustomerNamespace() {
private CustomerAccountDto createFullCustomerAccountDto() {
CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto(
- "billing@example.com", "support@example.com", "https://www.example.com", null
+ null, null, "billing@example.com", "support@example.com", "https://www.example.com", null, null, null
);
CustomerAccountDto.StatusDto docStatus = new CustomerAccountDto.StatusDto(
"ACTIVE",
OffsetDateTime.of(2025, 1, 15, 10, 0, 0, 0, ZoneOffset.UTC),
+ null,
"Account in good standing"
);
CustomerDto.OrganisationDto contactInfo = new CustomerDto.OrganisationDto(
new CustomerDto.StreetAddressDto("123 Main St", "Springfield", "IL", "62701", "USA"),
null, null, null,
- new CustomerDto.ElectronicAddressDto("contact@acme.com", null, "https://acme.com", null),
+ new CustomerDto.ElectronicAddressDto(null, null, "contact@acme.com", null, "https://acme.com", null, null, null),
"ACME Corporation"
);
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
new file mode 100644
index 00000000..2d62a2ce
--- /dev/null
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDtoTest.java
@@ -0,0 +1,353 @@
+/*
+ *
+ * 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.AtomFeedDto;
+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.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * XML marshalling/unmarshalling tests for CustomerAgreementDto.
+ * Verifies Jakarta JAXB processes JAXB annotations correctly for ESPI 4.0 customer.xsd compliance.
+ * Phase 24: CustomerAgreement schema compliance testing.
+ */
+@DisplayName("CustomerAgreementDto XML Marshalling Tests")
+class CustomerAgreementDtoTest {
+
+ private DtoExportServiceImpl dtoExportService;
+
+ @BeforeEach
+ void setUp() {
+ org.greenbuttonalliance.espi.common.service.EspiIdGeneratorService espiIdGeneratorService =
+ new org.greenbuttonalliance.espi.common.service.EspiIdGeneratorService();
+ dtoExportService = new DtoExportServiceImpl(null, null, espiIdGeneratorService);
+ }
+
+ @Test
+ @DisplayName("Should export CustomerAgreement with complete Document and Agreement fields")
+ void shouldExportCustomerAgreementWithCompleteDocumentAndAgreementFields() {
+ // Arrange
+ OffsetDateTime now = OffsetDateTime.of(2025, 1, 26, 10, 30, 0, 0, ZoneOffset.UTC);
+
+ CustomerAgreementDto customerAgreement = createFullCustomerAgreementDto();
+ CustomerAtomEntryDto entry = new CustomerAtomEntryDto(
+ "urn:uuid:650e8400-e29b-51d4-a716-446655440000",
+ "Test Customer Agreement",
+ now, now, null, customerAgreement
+ );
+
+ AtomFeedDto feed = new AtomFeedDto(
+ "urn:uuid:feed-id", "Customer Agreement 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("========== CustomerAgreement XML Output ==========");
+ System.out.println(xml);
+ System.out.println("==================================================");
+
+ // Assert - Basic structure
+ assertThat(xml)
+ .startsWith("")
+ .contains("");
+
+ // Assert - Document fields present
+ assertThat(xml)
+ .contains("SERVICE_AGREEMENT")
+ .contains("Utility Service Dept")
+ .contains("2.0")
+ .contains("service@utility.com")
+ .contains("Residential Service Agreement")
+ .contains("Customer Service Agreement AGR-789");
+
+ // Assert - Document.docStatus embedded object
+ assertThat(xml)
+ .contains("FINALIZED");
+
+ // Assert - Document.status embedded object
+ assertThat(xml)
+ .contains("ACTIVE");
+
+ // Assert - Agreement fields present
+ assertThat(xml)
+ .contains("STANDARD")
+ .contains("false")
+ .contains("USD")
+ .contains("AGR-789");
+ }
+
+ @Test
+ @DisplayName("Should verify CustomerAgreement field order matches customer.xsd")
+ void shouldVerifyCustomerAgreementFieldOrder() {
+ // Arrange
+ OffsetDateTime now = OffsetDateTime.of(2025, 1, 26, 10, 30, 0, 0, ZoneOffset.UTC);
+
+ CustomerAgreementDto customerAgreement = createFullCustomerAgreementDto();
+ CustomerAtomEntryDto entry = new CustomerAtomEntryDto(
+ "urn:uuid:650e8400-e29b-51d4-a716-446655440001",
+ "Test Customer Agreement",
+ now, now, null, customerAgreement
+ );
+
+ 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 Document field order per customer.xsd (lines 819-885)
+ int typePos = xml.indexOf("192.168.1.1")
+ .contains("00:11:22:33:44:55")
+ .contains("primary@utility.com")
+ .contains("secondary@utility.com")
+ .contains("https://www.utility.com")
+ .contains("radio-freq-123")
+ .contains("admin_user")
+ .contains("secure_pass");
+ }
+
+ // Helper methods
+
+ private CustomerAgreementDto createFullCustomerAgreementDto() {
+ CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto(
+ "10.0.0.1",
+ "AA:BB:CC:DD:EE:FF",
+ "service@utility.com",
+ "support@utility.com",
+ "https://www.utility.com",
+ null,
+ "service_user",
+ null
+ );
+
+ CustomerAgreementDto.StatusDto docStatus = new CustomerAgreementDto.StatusDto(
+ "FINALIZED",
+ OffsetDateTime.of(2025, 1, 20, 9, 0, 0, 0, ZoneOffset.UTC),
+ "Document finalized",
+ "Complete"
+ );
+
+ CustomerAgreementDto.StatusDto status = new CustomerAgreementDto.StatusDto(
+ "ACTIVE",
+ OffsetDateTime.of(2025, 1, 15, 10, 0, 0, 0, ZoneOffset.UTC),
+ "Service agreement active",
+ "Customer in good standing"
+ );
+
+ OffsetDateTime validityStart = OffsetDateTime.of(2025, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
+ DateTimeIntervalDto validityInterval = new DateTimeIntervalDto(
+ validityStart.toEpochSecond(),
+ 31536000L // 1 year in seconds
+ );
+
+ return new CustomerAgreementDto(
+ "650e8400-e29b-51d4-a716-446655440000",
+ "SERVICE_AGREEMENT",
+ "Utility Service Dept",
+ OffsetDateTime.of(2024, 12, 15, 0, 0, 0, 0, ZoneOffset.UTC),
+ OffsetDateTime.of(2025, 1, 26, 10, 30, 0, 0, ZoneOffset.UTC),
+ "2.0",
+ electronicAddress,
+ "Residential Service Agreement",
+ "Customer Service Agreement AGR-789",
+ docStatus,
+ status,
+ "Standard terms and conditions apply",
+ OffsetDateTime.of(2025, 1, 10, 14, 0, 0, 0, ZoneOffset.UTC),
+ validityInterval,
+ "STANDARD",
+ false,
+ null,
+ "USD",
+ null,
+ "AGR-789"
+ );
+ }
+
+ 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 9d85f394..cf56871a 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
@@ -82,10 +82,14 @@ void shouldMarshalCustomerWithAllFields() {
);
CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto(
+ null, // lan
+ null, // mac
"customer@example.com", // email1
"billing@example.com", // email2
"https://customer.example.com", // web
- null // radio
+ null, // radio
+ null, // userID
+ null // password
);
CustomerDto.StreetAddressDto streetAddress = new CustomerDto.StreetAddressDto(
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 2a41d7c0..f3e46ea1 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
@@ -282,7 +282,7 @@ private CustomerDto createFullCustomerDto() {
);
CustomerDto.ElectronicAddressDto electronicAddress = new CustomerDto.ElectronicAddressDto(
- "customer@example.com", "support@example.com", "https://www.example.com", null
+ null, null, "customer@example.com", "support@example.com", "https://www.example.com", null, null, null
);
CustomerDto.OrganisationDto organisation = new CustomerDto.OrganisationDto(
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepositoryTest.java
new file mode 100644
index 00000000..964429d4
--- /dev/null
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepositoryTest.java
@@ -0,0 +1,540 @@
+/*
+ *
+ * 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.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.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.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 CustomerAgreementRepository.
+ * Phase 24: CustomerAgreement schema compliance testing.
+ *
+ * Tests CRUD operations, Document field persistence, Agreement field persistence,
+ * CustomerAgreement field persistence, Status embedded objects, and IdentifiedObject base functionality.
+ */
+@DisplayName("CustomerAgreement Repository Tests")
+class CustomerAgreementRepositoryTest extends BaseRepositoryTest {
+
+ @Autowired
+ private CustomerAgreementRepository customerAgreementRepository;
+
+ /**
+ * Creates a valid CustomerAgreementEntity with all Document, Agreement, and CustomerAgreement fields for testing.
+ * Phase 24: Includes all fields per customer.xsd (Document + Agreement + CustomerAgreement).
+ */
+ private CustomerAgreementEntity createValidCustomerAgreement() {
+ CustomerAgreementEntity agreement = new CustomerAgreementEntity();
+
+ // IdentifiedObject fields
+ agreement.setDescription("Test Customer Agreement - " + faker.lorem().sentence(3));
+
+ // Document fields (customer.xsd lines 819-885)
+ agreement.setType("SERVICE_AGREEMENT");
+ agreement.setAuthorName(faker.name().fullName());
+ agreement.setCreatedDateTime(randomOffsetDateTime());
+ agreement.setLastModifiedDateTime(randomOffsetDateTime());
+ agreement.setRevisionNumber("1.0");
+
+ // Document.electronicAddress (customer.xsd lines 886-936)
+ Organisation.ElectronicAddress electronicAddress = new Organisation.ElectronicAddress();
+ electronicAddress.setLan("192.168." + faker.number().numberBetween(1, 255) + "." + faker.number().numberBetween(1, 255));
+ electronicAddress.setMac(faker.internet().macAddress());
+ electronicAddress.setEmail1(faker.internet().emailAddress());
+ electronicAddress.setEmail2(faker.internet().emailAddress());
+ electronicAddress.setWeb(faker.internet().url());
+ electronicAddress.setRadio("RADIO-" + faker.number().digits(6));
+ electronicAddress.setUserID(faker.name().username());
+ electronicAddress.setPassword(faker.internet().password());
+ agreement.setElectronicAddress(electronicAddress);
+
+ agreement.setSubject("Service Agreement");
+ agreement.setTitle("Customer Agreement AGR-" + faker.number().digits(6));
+
+ // Document.docStatus (customer.xsd lines 1254-1284)
+ Status docStatus = new Status();
+ docStatus.setValue("FINALIZED");
+ docStatus.setDateTime(randomOffsetDateTime());
+ docStatus.setRemark("Document approved");
+ docStatus.setReason("Signature completed");
+ agreement.setDocStatus(docStatus);
+
+ // Document.status (customer.xsd lines 1254-1284)
+ Status status = new Status();
+ status.setValue("ACTIVE");
+ status.setDateTime(randomOffsetDateTime());
+ status.setRemark("Service active");
+ status.setReason("Agreement in good standing");
+ agreement.setStatus(status);
+
+ agreement.setComment("Standard terms and conditions apply");
+
+ // Agreement fields (customer.xsd lines 622-642)
+ agreement.setSignDate(randomOffsetDateTime());
+ DateTimeInterval validityInterval = new DateTimeInterval();
+ validityInterval.setStart(randomOffsetDateTime().toEpochSecond());
+ validityInterval.setDuration(31536000L); // 1 year in seconds
+ agreement.setValidityInterval(validityInterval);
+
+ // CustomerAgreement specific fields (customer.xsd lines 159-209)
+ agreement.setLoadMgmt("STANDARD");
+ agreement.setIsPrePay(false);
+ agreement.setShutOffDateTime(null);
+ agreement.setCurrency("USD");
+ agreement.setAgreementId("AGR-" + faker.number().digits(8));
+
+ return agreement;
+ }
+
+ @Nested
+ @DisplayName("CRUD Operations")
+ class CrudOperationsTest {
+
+ @Test
+ @DisplayName("Should save and retrieve customer agreement successfully")
+ void shouldSaveAndRetrieveCustomerAgreementSuccessfully() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getId()).isEqualTo(saved.getId());
+ assertThat(retrieved.get().getAgreementId()).isEqualTo(saved.getAgreementId());
+ assertThat(retrieved.get().getLoadMgmt()).isEqualTo(saved.getLoadMgmt());
+ }
+
+ @Test
+ @DisplayName("Should update customer agreement successfully")
+ void shouldUpdateCustomerAgreementSuccessfully() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ UUID savedId = saved.getId();
+
+ // Act
+ saved.setLoadMgmt("PREMIUM");
+ saved.setCurrency("EUR");
+ saved.setComment("Updated terms");
+ customerAgreementRepository.save(saved);
+ flushAndClear();
+
+ // Assert
+ Optional retrieved = customerAgreementRepository.findById(savedId);
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getLoadMgmt()).isEqualTo("PREMIUM");
+ assertThat(retrieved.get().getCurrency()).isEqualTo("EUR");
+ assertThat(retrieved.get().getComment()).isEqualTo("Updated terms");
+ }
+
+ @Test
+ @DisplayName("Should delete customer agreement successfully")
+ void shouldDeleteCustomerAgreementSuccessfully() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ UUID savedId = saved.getId();
+
+ // Act
+ customerAgreementRepository.deleteById(savedId);
+ flushAndClear();
+
+ // Assert
+ Optional retrieved = customerAgreementRepository.findById(savedId);
+ assertThat(retrieved).isEmpty();
+ }
+
+ @Test
+ @DisplayName("Should count customer agreements")
+ void shouldCountCustomerAgreements() {
+ // Arrange
+ CustomerAgreementEntity agreement1 = createValidCustomerAgreement();
+ CustomerAgreementEntity agreement2 = createValidCustomerAgreement();
+
+ // Act
+ long countBefore = customerAgreementRepository.count();
+ persistAndFlush(agreement1);
+ persistAndFlush(agreement2);
+ long countAfter = customerAgreementRepository.count();
+
+ // Assert
+ assertThat(countAfter).isEqualTo(countBefore + 2);
+ }
+
+ @Test
+ @DisplayName("Should find all customer agreements")
+ void shouldFindAllCustomerAgreements() {
+ // Arrange
+ CustomerAgreementEntity agreement1 = createValidCustomerAgreement();
+ CustomerAgreementEntity agreement2 = createValidCustomerAgreement();
+ persistAndFlush(agreement1);
+ persistAndFlush(agreement2);
+
+ // Act
+ List allAgreements = customerAgreementRepository.findAll();
+
+ // Assert
+ assertThat(allAgreements).hasSizeGreaterThanOrEqualTo(2);
+ }
+
+ @Test
+ @DisplayName("Should verify customer agreement exists by ID")
+ void shouldVerifyCustomerAgreementExistsByID() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+
+ // Act
+ boolean exists = customerAgreementRepository.existsById(saved.getId());
+
+ // Assert
+ assertThat(exists).isTrue();
+ }
+ }
+
+ @Nested
+ @DisplayName("Document Field Persistence")
+ class DocumentFieldPersistenceTest {
+
+ @Test
+ @DisplayName("Should persist all Document fields correctly")
+ void shouldPersistAllDocumentFieldsCorrectly() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert - Document fields
+ assertThat(retrieved).isPresent();
+ CustomerAgreementEntity result = retrieved.get();
+
+ assertThat(result.getType()).isEqualTo(agreement.getType());
+ assertThat(result.getAuthorName()).isEqualTo(agreement.getAuthorName());
+ assertThat(result.getCreatedDateTime()).isEqualTo(agreement.getCreatedDateTime());
+ assertThat(result.getLastModifiedDateTime()).isEqualTo(agreement.getLastModifiedDateTime());
+ assertThat(result.getRevisionNumber()).isEqualTo(agreement.getRevisionNumber());
+ assertThat(result.getSubject()).isEqualTo(agreement.getSubject());
+ assertThat(result.getTitle()).isEqualTo(agreement.getTitle());
+ assertThat(result.getComment()).isEqualTo(agreement.getComment());
+ }
+
+ @Test
+ @DisplayName("Should persist ElectronicAddress with all 8 fields")
+ void shouldPersistElectronicAddressWithAllFields() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert - ElectronicAddress fields (customer.xsd lines 886-936)
+ assertThat(retrieved).isPresent();
+ Organisation.ElectronicAddress result = retrieved.get().getElectronicAddress();
+
+ assertThat(result).isNotNull();
+ assertThat(result.getLan()).isEqualTo(agreement.getElectronicAddress().getLan());
+ assertThat(result.getMac()).isEqualTo(agreement.getElectronicAddress().getMac());
+ assertThat(result.getEmail1()).isEqualTo(agreement.getElectronicAddress().getEmail1());
+ assertThat(result.getEmail2()).isEqualTo(agreement.getElectronicAddress().getEmail2());
+ assertThat(result.getWeb()).isEqualTo(agreement.getElectronicAddress().getWeb());
+ assertThat(result.getRadio()).isEqualTo(agreement.getElectronicAddress().getRadio());
+ assertThat(result.getUserID()).isEqualTo(agreement.getElectronicAddress().getUserID());
+ assertThat(result.getPassword()).isEqualTo(agreement.getElectronicAddress().getPassword());
+ }
+
+ @Test
+ @DisplayName("Should persist docStatus with all 4 fields including remark")
+ void shouldPersistDocStatusWithAllFields() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert - docStatus fields (customer.xsd lines 1254-1284)
+ assertThat(retrieved).isPresent();
+ Status result = retrieved.get().getDocStatus();
+
+ assertThat(result).isNotNull();
+ assertThat(result.getValue()).isEqualTo(agreement.getDocStatus().getValue());
+ assertThat(result.getDateTime()).isEqualTo(agreement.getDocStatus().getDateTime());
+ assertThat(result.getRemark()).isEqualTo(agreement.getDocStatus().getRemark());
+ assertThat(result.getReason()).isEqualTo(agreement.getDocStatus().getReason());
+ }
+
+ @Test
+ @DisplayName("Should persist status with all 4 fields including remark")
+ void shouldPersistStatusWithAllFields() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert - status fields (customer.xsd lines 1254-1284)
+ assertThat(retrieved).isPresent();
+ Status result = retrieved.get().getStatus();
+
+ assertThat(result).isNotNull();
+ assertThat(result.getValue()).isEqualTo(agreement.getStatus().getValue());
+ assertThat(result.getDateTime()).isEqualTo(agreement.getStatus().getDateTime());
+ assertThat(result.getRemark()).isEqualTo(agreement.getStatus().getRemark());
+ assertThat(result.getReason()).isEqualTo(agreement.getStatus().getReason());
+ }
+ }
+
+ @Nested
+ @DisplayName("Agreement Field Persistence")
+ class AgreementFieldPersistenceTest {
+
+ @Test
+ @DisplayName("Should persist signDate correctly")
+ void shouldPersistSignDateCorrectly() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ OffsetDateTime signDate = randomOffsetDateTime();
+ agreement.setSignDate(signDate);
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getSignDate()).isEqualTo(signDate);
+ }
+
+ @Test
+ @DisplayName("Should persist validityInterval correctly")
+ void shouldPersistValidityIntervalCorrectly() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ DateTimeInterval validityInterval = new DateTimeInterval();
+ OffsetDateTime start = randomOffsetDateTime();
+ validityInterval.setStart(start.toEpochSecond());
+ validityInterval.setDuration(86400L); // 1 day
+ agreement.setValidityInterval(validityInterval);
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getValidityInterval()).isNotNull();
+ assertThat(retrieved.get().getValidityInterval().getStart()).isEqualTo(validityInterval.getStart());
+ assertThat(retrieved.get().getValidityInterval().getDuration()).isEqualTo(validityInterval.getDuration());
+ }
+ }
+
+ @Nested
+ @DisplayName("CustomerAgreement Field Persistence")
+ class CustomerAgreementFieldPersistenceTest {
+
+ @Test
+ @DisplayName("Should persist loadMgmt correctly")
+ void shouldPersistLoadMgmtCorrectly() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ agreement.setLoadMgmt("PREMIUM");
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getLoadMgmt()).isEqualTo("PREMIUM");
+ }
+
+ @Test
+ @DisplayName("Should persist isPrePay correctly")
+ void shouldPersistIsPrePayCorrectly() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ agreement.setIsPrePay(true);
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getIsPrePay()).isTrue();
+ }
+
+ @Test
+ @DisplayName("Should persist shutOffDateTime correctly")
+ void shouldPersistShutOffDateTimeCorrectly() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ OffsetDateTime shutOffDateTime = randomOffsetDateTime();
+ agreement.setShutOffDateTime(shutOffDateTime);
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getShutOffDateTime()).isEqualTo(shutOffDateTime);
+ }
+
+ @Test
+ @DisplayName("Should persist currency correctly")
+ void shouldPersistCurrencyCorrectly() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ agreement.setCurrency("EUR");
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getCurrency()).isEqualTo("EUR");
+ }
+
+ @Test
+ @DisplayName("Should persist agreementId correctly")
+ void shouldPersistAgreementIdCorrectly() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ String agreementId = "AGR-TEST-" + faker.number().digits(10);
+ agreement.setAgreementId(agreementId);
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getAgreementId()).isEqualTo(agreementId);
+ }
+ }
+
+ @Nested
+ @DisplayName("IdentifiedObject Base Class Tests")
+ class BaseClassTest {
+
+ @Test
+ @DisplayName("Should persist IdentifiedObject description field")
+ void shouldPersistIdentifiedObjectDescriptionField() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ String description = "Custom Description - " + faker.lorem().sentence(5);
+ agreement.setDescription(description);
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getDescription()).isEqualTo(description);
+ }
+
+ @Test
+ @DisplayName("Should auto-generate UUID on persist")
+ void shouldAutoGenerateUuidOnPersist() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+
+ // Assert
+ assertThat(saved.getId()).isNotNull();
+ assertThat(saved.getId()).isInstanceOf(UUID.class);
+ }
+
+ @Test
+ @DisplayName("Should persist created and updated timestamps")
+ void shouldPersistCreatedAndUpdatedTimestamps() {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+
+ // Act
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getCreated()).isNotNull();
+ assertThat(retrieved.get().getUpdated()).isNotNull();
+ }
+
+ @Test
+ @DisplayName("Should update timestamp on modification")
+ void shouldUpdateTimestampOnModification() throws InterruptedException {
+ // Arrange
+ CustomerAgreementEntity agreement = createValidCustomerAgreement();
+ CustomerAgreementEntity saved = persistAndFlush(agreement);
+ java.time.LocalDateTime initialUpdated = saved.getUpdated();
+
+ // Wait to ensure timestamp difference
+ Thread.sleep(100);
+
+ // Act
+ saved.setComment("Modified comment");
+ customerAgreementRepository.save(saved);
+ flushAndClear();
+ Optional retrieved = customerAgreementRepository.findById(saved.getId());
+
+ // Assert
+ assertThat(retrieved).isPresent();
+ assertThat(retrieved.get().getUpdated()).isAfter(initialUpdated);
+ }
+ }
+}