Implement complete CustomerAgreement ESPI 4.0 schema compliance following the successful Phase 18 pattern. CustomerAgreement extends Agreement which extends Document, similar to how CustomerAccount extends Document.
Branch: feature/schema-compliance-phase-24-customer-agreement
Dependencies: CustomerAccount, ServiceLocation, ServiceSupplier, ProgramDateIdMappings (via Atom rel='related' links)
Referenced By: ServiceSupplier, ServiceLocation, ProgramDateIdMappings (via bidirectional Atom rel='related' links)
Atom Wrapper (AtomFeedDto & CustomerAtomEntryDto):
- Contains metadata: id, title, published, updated
- Contains navigation: links (self, up, related)
- Handles relationships via
<link rel="related" href="..."/> - Wraps ESPI resource in
<atom:entry><content>element
ESPI Resource DTO (CustomerAgreementDto):
- Contains ONLY customer.xsd schema-defined fields
- NO Atom fields (published, updated, id, links)
- NO IdentifiedObject fields (description is mapped to AtomEntryDto.title)
- NO embedded relationship DTOs (use Atom links instead)
Correct XML Structure:
<atom:feed xmlns:atom="http://www.w3.org/2005/Atom" xmlns:cust="http://naesb.org/espi/customer">
<atom:entry>
<atom:id>urn:uuid:xxx</atom:id> <!-- CustomerAtomEntryDto.id -->
<atom:title>Agreement Title</atom:title> <!-- CustomerAtomEntryDto.title -->
<atom:published>2025-01-25T...</atom:published> <!-- CustomerAtomEntryDto.published -->
<atom:updated>2025-01-25T...</atom:updated> <!-- CustomerAtomEntryDto.updated -->
<atom:link rel="self" href=".../CustomerAgreement/123"/> <!-- CustomerAtomEntryDto.links -->
<atom:link rel="up" href=".../CustomerAgreement"/>
<atom:link rel="related" href=".../CustomerAccount/456"/> <!-- NOT embedded DTO! -->
<cust:CustomerAgreement> <!-- CustomerAgreementDto content -->
<!-- ONLY customer.xsd fields here, NO Atom fields -->
<cust:type>CONTRACT</cust:type>
<cust:authorName>John Doe</cust:authorName>
<cust:createdDateTime>2025-01-01T...</cust:createdDateTime>
...
<cust:agreementId>AGR-123</cust:agreementId>
</cust:CustomerAgreement>
</atom:entry>
</atom:feed>Reference Implementation: Phase 18 CustomerAccountDto correctly implements this pattern.
CustomerAgreement extends Agreement extends Document extends IdentifiedObject
Document fields (lines 819-872):
- type (String256)
- authorName (String256)
- createdDateTime (TimeType)
- lastModifiedDateTime (TimeType)
- revisionNumber (String256)
- electronicAddress (ElectronicAddress)
- subject (String256)
- title (String256)
- docStatus (Status)
Agreement fields (lines 622-660): 10. signDate (TimeType) 11. validityInterval (DateTimeInterval)
CustomerAgreement fields (lines 159-260): 12. loadMgmt (String256) - optional 13. isPrePay (boolean) - optional 14. shutOffDateTime (TimeType) - optional 15. DemandResponseProgram (collection) - optional - TODO: Future implementation 16. PricingStructures (collection) - optional - TODO: Future implementation 17. currency (Currency/String3) - optional 18. futureStatus (Status collection) - optional, [extension] 19. agreementId (String256) - optional, [extension]
- CustomerAgreementEntity.java: Exists, needs Document field additions and reordering
- CustomerAgreementDto.java: Exists, needs complete rewrite with correct field ordering
- CustomerAgreementMapper.java: Exists, needs updates for new fields
- CustomerAgreementRepository.java: Does not exist, needs creation
- CustomerAgreementService.java: Does not exist, needs creation
- CustomerAgreementServiceImpl.java: Does not exist, needs creation
- CustomerAgreementDtoTest.java: Does not exist, needs creation
- CustomerAgreementRepositoryTest.java: Does not exist, needs creation
Current order in CustomerAgreementEntity.java:
// Document fields (lines 50-86)
createdDateTime, lastModifiedDateTime, revisionNumber, subject, title, type
// Agreement fields (lines 88-100)
signDate, validityInterval
// CustomerAgreement fields (lines 102-158)
loadMgmt, isPrePay, shutOffDateTime, currency, futureStatus, agreementIdProblems:
- ❌ Missing Document fields: type first, authorName, electronicAddress, docStatus
- ❌ Wrong field order: type should be FIRST, not last in Document section
- ❌ Missing embedded Status for docStatus
- ❌ Missing Organisation.ElectronicAddress for electronicAddress
- ❌ futureStatus uses CustomerEntity.Status instead of Status embeddable
⚠️ @ElementCollection with @AttributeOverrides wrapper (should apply directly per java:S1710)
Correct order per XSD:
// Document fields (1-9)
type, authorName, createdDateTime, lastModifiedDateTime, revisionNumber,
electronicAddress, subject, title, docStatus
// Agreement fields (10-11)
signDate, validityInterval
// CustomerAgreement fields (12-19)
loadMgmt, isPrePay, shutOffDateTime, currency, futureStatus, agreementIdIMPORTANT: Atom protocol fields (published, updated, id, title, links) are handled by AtomFeedDto, AtomEntryDto (specifically CustomerAtomEntryDto), and LinkDto. These fields MUST NOT appear in CustomerAgreementDto.
Current propOrder in CustomerAgreementDto.java (line 41-45):
"published", "updated", "selfLink", "upLink", "relatedLinks",
"description", "signDate", "validityInterval", "customerAccount",
"serviceLocations", "statements"CRITICAL Problems:
- ❌ Atom Fields Present (WRONG!): published, updated, selfLink, upLink, relatedLinks
- These are handled by CustomerAtomEntryDto and AtomFeedDto, NOT CustomerAgreementDto
- ❌ IdentifiedObject Fields Present (WRONG!): description (id handled by AtomEntryDto.id, description by AtomEntryDto.title)
- ❌ Relationship DTOs Present (WRONG!): customerAccount, serviceLocations, statements
- Relationships handled via Atom
<link rel="related">in AtomEntryDto.links, NOT embedded DTOs
- Relationships handled via Atom
- ❌ Missing ALL Document Fields: type, authorName, createdDateTime, lastModifiedDateTime, revisionNumber, electronicAddress, subject, title, docStatus
- ❌ Missing ALL CustomerAgreement Fields: loadMgmt, isPrePay, shutOffDateTime, currency, futureStatus, agreementId
- ❌ Wrong Type: validityInterval is String instead of DateTimeIntervalDto
Correct propOrder per XSD (ONLY customer.xsd fields, NO Atom fields):
"type", "authorName", "createdDateTime", "lastModifiedDateTime", "revisionNumber",
"electronicAddress", "subject", "title", "docStatus",
"signDate", "validityInterval",
"loadMgmt", "isPrePay", "shutOffDateTime", "currency", "futureStatus", "agreementId"Reference: Phase 18 CustomerAccountDto (correctly excludes Atom fields):
- ✅ Contains ONLY 15 XSD-defined fields (9 Document + 6 CustomerAccount)
- ✅ NO Atom fields (published, updated, id, links)
- ✅ NO IdentifiedObject fields (description)
- ✅ NO embedded relationship DTOs
CustomerAgreementMapper.java needs updates for:
- All 9 Document field mappings
- All 2 Agreement field mappings
- All 6 CustomerAgreement field mappings (excluding TODO collections)
- Status embeddable mapping (reuse StatusMapper from Phase 18)
- ElectronicAddress embeddable mapping (reuse ElectronicAddressMapper from Phase 18)
- DateTimeInterval mapping
- No Repository interface/implementation
- No Service interface/implementation
- No DTO tests for XML marshalling
- No Repository tests for CRUD operations
1.1 Add Missing Document Fields
// Add these fields at the top in correct Document field order
@Column(name = "document_type", length = 256)
private String type; // Move from line 85 to after class declaration
@Column(name = "author_name", length = 256)
private String authorName; // NEW FIELD
// createdDateTime (already exists)
// lastModifiedDateTime (already exists)
// revisionNumber (already exists)
@Embedded
private Organisation.ElectronicAddress electronicAddress; // NEW FIELD
// subject (already exists)
// title (already exists)
@Embedded
private Status docStatus; // NEW FIELD (replace futureStatus CustomerEntity.Status usage)1.2 Reorder All Fields to Match XSD Reorder sections:
- Document fields (9 fields total)
- Agreement fields (2 fields: signDate, validityInterval)
- CustomerAgreement fields (6 fields, excluding TODO collections)
1.3 Fix futureStatus Field
// BEFORE:
@ElementCollection
@CollectionTable(name = "customer_agreement_future_status", joinColumns = @JoinColumn(name = "customer_agreement_id"))
@AttributeOverrides({...})
private List<CustomerEntity.Status> futureStatus;
// AFTER:
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "customer_agreement_future_status", joinColumns = @JoinColumn(name = "customer_agreement_id"))
@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 List<Status> futureStatus;1.4 Apply @AttributeOverride Directly Without Wrapper Per java:S1710, apply annotations directly on class level for upLink/selfLink:
@Entity
@Table(name = "customer_agreements")
@AttributeOverride(name = "upLink.rel", column = @Column(name = "customer_agreement_up_link_rel"))
@AttributeOverride(name = "upLink.href", column = @Column(name = "customer_agreement_up_link_href"))
@AttributeOverride(name = "upLink.type", column = @Column(name = "customer_agreement_up_link_type"))
@AttributeOverride(name = "selfLink.rel", column = @Column(name = "customer_agreement_self_link_rel"))
@AttributeOverride(name = "selfLink.href", column = @Column(name = "customer_agreement_self_link_href"))
@AttributeOverride(name = "selfLink.type", column = @Column(name = "customer_agreement_self_link_type"))1.5 Update equals/hashCode for Pattern Matching
Class<?> oEffectiveClass = o instanceof HibernateProxy hibernateProxy ?
hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : o.getClass();2.1 Remove ALL Atom Fields and Relationship DTOs Delete these fields (handled by AtomEntryDto/LinkDto):
- ❌ id (Long) - replaced by uuid (String)
- ❌ published (OffsetDateTime) - handled by CustomerAtomEntryDto
- ❌ updated (OffsetDateTime) - handled by CustomerAtomEntryDto
- ❌ relatedLinks (List) - handled by CustomerAtomEntryDto.links
- ❌ selfLink (LinkDto) - handled by CustomerAtomEntryDto.links
- ❌ upLink (LinkDto) - handled by CustomerAtomEntryDto.links
- ❌ description (String) - handled by CustomerAtomEntryDto.title
- ❌ customerAccount (CustomerAccountDto) - handled by Atom
<link rel="related"> - ❌ serviceLocations (List) - handled by Atom
<link rel="related"> - ❌ statements (List) - handled by Atom
<link rel="related"> - ❌ Helper methods (getSelfHref, getUpHref, generateSelfHref, generateUpHref)
2.2 Complete Rewrite with ONLY XSD-Defined Fields
@XmlRootElement(name = "CustomerAgreement", namespace = "http://naesb.org/espi/customer")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "CustomerAgreement", namespace = "http://naesb.org/espi/customer", propOrder = {
"type", "authorName", "createdDateTime", "lastModifiedDateTime", "revisionNumber",
"electronicAddress", "subject", "title", "docStatus",
"signDate", "validityInterval",
"loadMgmt", "isPrePay", "shutOffDateTime", "currency", "futureStatus", "agreementId"
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class CustomerAgreementDto {
// UUID for internal mapping (NOT marshalled to XML - handled by AtomEntryDto.id)
@XmlTransient
private String uuid;
// Document fields (9)
@XmlElement(name = "type", namespace = "http://naesb.org/espi/customer")
private String type;
@XmlElement(name = "authorName", namespace = "http://naesb.org/espi/customer")
private String authorName;
@XmlElement(name = "createdDateTime", namespace = "http://naesb.org/espi/customer")
private OffsetDateTime createdDateTime;
@XmlElement(name = "lastModifiedDateTime", namespace = "http://naesb.org/espi/customer")
private OffsetDateTime lastModifiedDateTime;
@XmlElement(name = "revisionNumber", namespace = "http://naesb.org/espi/customer")
private String revisionNumber;
@XmlElement(name = "electronicAddress", namespace = "http://naesb.org/espi/customer")
private CustomerDto.ElectronicAddressDto electronicAddress;
@XmlElement(name = "subject", namespace = "http://naesb.org/espi/customer")
private String subject;
@XmlElement(name = "title", namespace = "http://naesb.org/espi/customer")
private String title;
@XmlElement(name = "docStatus", namespace = "http://naesb.org/espi/customer")
private StatusDto docStatus;
// Agreement fields (2)
@XmlElement(name = "signDate", namespace = "http://naesb.org/espi/customer")
private OffsetDateTime signDate;
@XmlElement(name = "validityInterval", namespace = "http://naesb.org/espi/customer")
private DateTimeIntervalDto validityInterval;
// CustomerAgreement fields (6)
@XmlElement(name = "loadMgmt", namespace = "http://naesb.org/espi/customer")
private String loadMgmt;
@XmlElement(name = "isPrePay", namespace = "http://naesb.org/espi/customer")
private Boolean isPrePay;
@XmlElement(name = "shutOffDateTime", namespace = "http://naesb.org/espi/customer")
private OffsetDateTime shutOffDateTime;
@XmlElement(name = "currency", namespace = "http://naesb.org/espi/customer")
private String currency;
@XmlElement(name = "futureStatus", namespace = "http://naesb.org/espi/customer")
@XmlElementWrapper(name = "futureStatus", namespace = "http://naesb.org/espi/customer")
private List<StatusDto> futureStatus;
@XmlElement(name = "agreementId", namespace = "http://naesb.org/espi/customer")
private String agreementId;
}NOTE: Reuse existing nested DTOs from Phase 18:
CustomerAccountDto.StatusDtofor docStatus and futureStatus fieldsCustomerDto.ElectronicAddressDtofor electronicAddress field- Create
DateTimeIntervalDtoif not already available
3.1 Add All Field Mappings
@Mapper(componentModel = "spring", uses = {
StatusMapper.class,
ElectronicAddressMapper.class,
DateTimeIntervalMapper.class
})
public interface CustomerAgreementMapper {
@Mapping(target = "uuid", source = "id")
// Document fields (9)
@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")
// Agreement fields (2)
@Mapping(target = "signDate", source = "signDate")
@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);
@InheritInverseConfiguration
CustomerAgreementEntity toEntity(CustomerAgreementDto dto);
}3.2 Create DateTimeIntervalMapper (if missing)
@Mapper(componentModel = "spring")
public interface DateTimeIntervalMapper {
DateTimeIntervalDto toDto(DateTimeInterval entity);
DateTimeInterval toEntity(DateTimeIntervalDto dto);
}4.1 Create Repository Interface
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.
*/
@Repository
public interface CustomerAgreementRepository extends JpaRepository<CustomerAgreementEntity, UUID> {
// Use only inherited methods: findById, findAll, save, delete, count, existsById
// No custom query methods to avoid H2 keyword conflicts
}5.1 Create Service Interface
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 entity.
* Phase 24: CustomerAgreement schema compliance.
*/
public interface CustomerAgreementService {
/**
* Save a customer agreement.
*/
CustomerAgreementEntity save(CustomerAgreementEntity agreement);
/**
* Find customer agreement by ID.
*/
Optional<CustomerAgreementEntity> findById(UUID id);
/**
* Find all customer agreements.
*/
List<CustomerAgreementEntity> findAll();
/**
* Delete customer agreement by ID.
*/
void deleteById(UUID id);
/**
* Check if customer agreement exists.
*/
boolean existsById(UUID id);
/**
* Count all customer agreements.
*/
long count();
}5.2 Create Service Implementation
package org.greenbuttonalliance.espi.common.service.customer.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 entity.
* Phase 24: CustomerAgreement schema compliance.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomerAgreementServiceImpl implements CustomerAgreementService {
private static final String NAMESPACE = "ESPI-CUSTOMER-AGREEMENT";
private final CustomerAgreementRepository repository;
private final EspiIdGeneratorService idGenerator;
@Override
@Transactional
public CustomerAgreementEntity save(CustomerAgreementEntity agreement) {
if (agreement.getId() == null) {
String seed = agreement.getAgreementId() != null ?
agreement.getAgreementId() : UUID.randomUUID().toString();
UUID deterministicId = idGenerator.generateV5UUID(NAMESPACE, seed);
agreement.setId(deterministicId);
log.debug("Generated UUID v5 for CustomerAgreement: {}", deterministicId);
}
return repository.save(agreement);
}
@Override
@Transactional(readOnly = true)
public Optional<CustomerAgreementEntity> findById(UUID id) {
return repository.findById(id);
}
@Override
@Transactional(readOnly = true)
public List<CustomerAgreementEntity> findAll() {
return repository.findAll();
}
@Override
@Transactional
public void deleteById(UUID id) {
repository.deleteById(id);
}
@Override
@Transactional(readOnly = true)
public boolean existsById(UUID id) {
return repository.existsById(id);
}
@Override
@Transactional(readOnly = true)
public long count() {
return repository.count();
}
}6.1 Update V3__Create_additiional_Base_Tables.sql
Add missing Document fields to customer_agreements table:
-- Add missing Document fields
ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS document_type VARCHAR(256);
ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS author_name VARCHAR(256);
ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS status_value VARCHAR(256);
ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS status_date_time TIMESTAMP WITH TIME ZONE;
ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS status_reason VARCHAR(512);
-- Add ElectronicAddress embedded fields
ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS email1 VARCHAR(256);
ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS email2 VARCHAR(256);
ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS web VARCHAR(256);
ALTER TABLE customer_agreements ADD COLUMN IF NOT EXISTS radio VARCHAR(256);
-- Reorder columns to match XSD (PostgreSQL/MySQL - comment out for H2)
-- Document fields: type, author_name, created_date_time, last_modified_date_time,
-- revision_number, email1, email2, web, radio, subject, title,
-- status_value, status_date_time, status_reason
-- Agreement fields: sign_date, validity_interval_start, validity_interval_duration
-- CustomerAgreement fields: load_mgmt, is_pre_pay, shut_off_date_time, currency, agreement_id7.1 Create CustomerAgreementDtoTest.java
Follow Phase 18 CustomerAccountDtoTest pattern:
@DisplayName("CustomerAgreementDto XML Marshalling Tests")
class CustomerAgreementDtoTest {
private DtoExportServiceImpl dtoExportService;
@BeforeEach
void setUp() {
EspiIdGeneratorService espiIdGeneratorService = new EspiIdGeneratorService();
dtoExportService = new DtoExportServiceImpl(null, null, espiIdGeneratorService);
}
@Test
@DisplayName("Should export CustomerAgreement with complete Document/Agreement fields")
void shouldExportCustomerAgreementWithCompleteFields() {
// Test all 17 fields marshal correctly
// Verify field order matches customer.xsd
}
@Test
@DisplayName("Should verify CustomerAgreement field order matches customer.xsd")
void shouldVerifyCustomerAgreementFieldOrder() {
// Assert Document fields order (lines 819-872)
// Assert Agreement fields order
// Assert CustomerAgreement fields order (lines 159-260)
}
@Test
@DisplayName("Should use correct customer namespace")
void shouldUseCorrectCustomerNamespace() {
// Verify cust: namespace prefix usage
// Verify http://naesb.org/espi/customer namespace
}
}7.2 Create CustomerAgreementRepositoryTest.java
Follow Phase 18 CustomerAccountRepositoryTest pattern with 21+ tests:
@DisplayName("CustomerAgreement Repository Tests")
class CustomerAgreementRepositoryTest extends BaseRepositoryTest {
@Autowired
private CustomerAgreementRepository customerAgreementRepository;
@Nested
@DisplayName("CRUD Operations")
class CrudOperationsTest {
// 7 tests: save, retrieve, update, delete, findAll, exists, count
}
@Nested
@DisplayName("Document Field Persistence")
class DocumentFieldPersistenceTest {
// 3 tests: All Document fields, electronicAddress, docStatus
}
@Nested
@DisplayName("Agreement Field Persistence")
class AgreementFieldPersistenceTest {
// 2 tests: signDate, validityInterval
}
@Nested
@DisplayName("CustomerAgreement Field Persistence")
class CustomerAgreementFieldPersistenceTest {
// 3 tests: All CustomerAgreement fields, futureStatus collection, null optional fields
}
@Nested
@DisplayName("Base Class Functionality")
class BaseClassTest {
// 5 tests: IdentifiedObject inheritance, timestamps, unique IDs, equals/hashCode, toString
}
}7.3 Service and Mapper Tests
- Create CustomerAgreementServiceTest
- Create CustomerAgreementMapperTest (if needed)
Ensure zero violations by following Phase 18 patterns:
8.1 Avoid Common Violations
- ✅ No IOException throws on test methods
- ✅ Chain multiple assertions using fluent API
- ✅ Apply @AttributeOverride directly without wrapper
- ✅ Make all embeddable objects Serializable
- ✅ Use instanceof with pattern variables
- ✅ Remove unused variables
- ✅ No Thread.sleep() in tests
- ✅ No empty catch blocks
- ✅ Use hasSameHashCodeAs() for hashCode assertions
- ✅ Remove all commented-out code
9.1 Git Workflow
# Create feature branch
git checkout -b feature/schema-compliance-phase-24-customer-agreement
# Stage all changes
git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java
git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDto.java
git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/CustomerAgreementMapper.java
git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/DateTimeIntervalMapper.java # if new
git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepository.java
git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/CustomerAgreementService.java
git add openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/CustomerAgreementServiceImpl.java
git add openespi-common/src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql
git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/customer/CustomerAgreementDtoTest.java
git add openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/CustomerAgreementRepositoryTest.java
# Commit with detailed message
git commit -m "feat: ESPI 4.0 Schema Compliance - Phase 24: CustomerAgreement Implementation"
# Push and create PR
git push -u origin feature/schema-compliance-phase-24-customer-agreement
gh pr create --base main --title "feat: Phase 24 - CustomerAgreement ESPI 4.0 Compliance" --body "..."9.2 Update Issue #28
gh issue comment 28 --body "Phase 24: CustomerAgreement implementation completed. PR #XX ready for review."| File | Change Type | Description |
|---|---|---|
| CustomerAgreementEntity.java | MODIFY | Add 3 Document fields, reorder all fields, fix futureStatus type, add @AttributeOverride |
| CustomerAgreementDto.java | REWRITE | Remove ALL Atom fields, remove relationship DTOs, add 17 XSD fields, correct propOrder |
| CustomerAgreementMapper.java | MODIFY | Add 17 field mappings, use StatusMapper/ElectronicAddressMapper |
| DateTimeIntervalMapper.java | CREATE | New mapper for DateTimeInterval ↔ DateTimeIntervalDto |
| CustomerAgreementRepository.java | CREATE | New JpaRepository interface |
| CustomerAgreementService.java | CREATE | New service interface |
| CustomerAgreementServiceImpl.java | CREATE | New service implementation with UUID v5 generation |
| V3__Create_additiional_Base_Tables.sql | MODIFY | Add 8 new columns for Document fields |
| CustomerAgreementDtoTest.java | CREATE | 3+ XML marshalling tests |
| CustomerAgreementRepositoryTest.java | CREATE | 21+ repository tests |
Total: 10 files (3 modified, 7 created)
✅ All 609+ existing tests pass ✅ All new CustomerAgreement tests pass (24+ new tests) ✅ Zero SonarQube violations ✅ Integration tests pass (H2, MySQL, PostgreSQL) ✅ XML marshalling validates against customer.xsd ✅ Field ordering matches customer.xsd exactly ✅ All Document base class fields implemented ✅ All Agreement base class fields implemented ✅ All CustomerAgreement specific fields implemented ✅ Repository, service, and mapper layers complete ✅ CI/CD pipeline passes all checks
- Phase 18 Implementation: CustomerAccount (successful pattern to follow)
- customer.xsd: Lines 159-260 (CustomerAgreement), 622-660 (Agreement), 819-872 (Document)
- Issue #28: Phase 24 task list
- CLAUDE.md: Project conventions and patterns
-
🚨 CRITICAL: NO ATOM FIELDS IN DTO 🚨
- CustomerAgreementDto must NOT contain: published, updated, id, title, links, description
- These are handled by CustomerAtomEntryDto and AtomFeedDto
- Current DTO incorrectly has these fields - they MUST be removed
- CustomerAccountDto from Phase 18 shows correct pattern (ONLY XSD fields)
-
Reuse Existing Mappers: StatusMapper and ElectronicAddressMapper from Phase 18
-
Serializable Requirement: All embedded classes must implement Serializable
-
UUID v5 Generation: Use namespace "ESPI-CUSTOMER-AGREEMENT" with agreementId seed
-
TODO Collections: DemandResponseProgram and PricingStructures commented out - future implementation
-
Atom Links: Relationships to CustomerAccount/ServiceLocation/ServiceSupplier via Atom
<link rel="related">links (NOT embedded DTOs) -
futureStatus: Element collection of Status objects (not CustomerEntity.Status)
-
Follow Phase 18 Pattern: Use CustomerAccount implementation as template for consistency
Plan Created: 2026-01-25 Target Completion: Phase 24 implementation Next Phase: Phase 25 - EndDevice