From d3b82c200e2e8b1d42079a63c2e145616f94a352 Mon Sep 17 00:00:00 2001 From: "Donald F. Coffin" Date: Sat, 31 Jan 2026 15:47:56 -0500 Subject: [PATCH 1/3] docs: Update Phase 17 plan to include EndDevice and Meter in Phase A0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extended Phase A0 (PREREQUISITE) to include bidirectional Atom links for EndDevice and Meter entities in addition to the original 4 entities: Previous (4 entities): - CustomerAgreement - ProgramDateIdMappings - ServiceLocation - ServiceSupplier Added (2 additional entities): - EndDevice - Meter (inherits from EndDevice) Investigation confirmed: - All 6 related_links database tables exist in V3 migration - All 6 entities lack @ElementCollection mappings for relatedLinks - Bidirectional relationships required: - ServiceLocation ↔ EndDevice - ServiceLocation ↔ Meter - CustomerAgreement ↔ ProgramDateIdMappings - CustomerAgreement ↔ ServiceLocation - CustomerAgreement ↔ ServiceSupplier Per NAESB ESPI 4.0 standard, these relationships are implemented via Atom elements, not via customer.xsd definitions. Related to #28 (Phase 17: ProgramDateIdMappings) --- ...AM_DATE_ID_MAPPINGS_IMPLEMENTATION_PLAN.md | 1194 +++++++++++++++++ 1 file changed, 1194 insertions(+) create mode 100644 openespi-common/PHASE_17_PROGRAM_DATE_ID_MAPPINGS_IMPLEMENTATION_PLAN.md diff --git a/openespi-common/PHASE_17_PROGRAM_DATE_ID_MAPPINGS_IMPLEMENTATION_PLAN.md b/openespi-common/PHASE_17_PROGRAM_DATE_ID_MAPPINGS_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..b6180008 --- /dev/null +++ b/openespi-common/PHASE_17_PROGRAM_DATE_ID_MAPPINGS_IMPLEMENTATION_PLAN.md @@ -0,0 +1,1194 @@ +# Phase 17: ProgramDateIdMappings - ESPI 4.0 Schema Compliance + +## Overview +Implement full ESPI 4.0 schema compliance for ProgramDateIdMappings, including creating the nested ProgramDateIdMapping embeddable class and ProgramDateKind enum. This phase includes cleanup of the existing DTO which incorrectly includes IdentifiedObject fields directly in the DTO instead of relying on AtomEntryDto. + +**CRITICAL PREREQUISITE**: Enable bidirectional Atom links infrastructure for CustomerAgreement, ProgramDateIdMappings, ServiceLocation, ServiceSupplier, EndDevice, and Meter entities to support ESPI 4.0 `` requirements. + +**Related Issue**: #28 - ESPI 4.0 Schema Compliance (Phase 17: ProgramDateIdMappings) + +**IMPORTANT**: Issue #28 tracks the multi-phase ESPI 4.0 schema compliance effort. This plan implements Phase 17 only. **DO NOT close Issue #28** when Phase 17 is complete - additional phases (Phase 18+) remain to be implemented. + +**Current State Issues**: +- ProgramDateIdMappingsEntity exists but has NO fields (only extends IdentifiedObject) +- ProgramDateIdMappingsDto has IdentifiedObject fields (id, uuid, published, updated, links) mixed into the DTO +- ProgramDateIdMapping embeddable class does NOT exist +- ProgramDateKind enum does NOT exist +- No mapper, repository, or service implementations +- Database table exists but doesn't match XSD structure +- **CRITICAL**: Related links tables exist in database but JPA @ElementCollection mappings are MISSING from all 6 entities (CustomerAgreement, ProgramDateIdMappings, ServiceLocation, ServiceSupplier, EndDevice, Meter) + +**XSD Compliance Requirements**: +- ProgramDateIdMappings extends IdentifiedObject with ONE field: programDateIdMapping +- ProgramDateIdMapping extends Object (NOT IdentifiedObject) with 4 fields +- ProgramDateKind enum has 4 values defined in customer.xsd +- DTO must follow Phase 21 pattern: NO IdentifiedObject fields (handled by AtomEntryDto) +- Support Atom `` for bidirectional relationships with CustomerAgreement +- Service layer queries ONLY by ID (no timestamp or other field queries) + +## XSD Structure + +**ProgramDateIdMappings extends IdentifiedObject** (customer.xsd lines 269-283): + +### ProgramDateIdMappings Fields (1 field) +1. **programDateIdMapping** (ProgramDateIdMapping, optional) - Single customer energy efficiency program date mapping + +### ProgramDateIdMapping Structure (customer.xsd lines 1223-1251) +ProgramDateIdMapping extends **Object** (NOT IdentifiedObject) - 4 fields: +- **programDateType** (ProgramDateKind enum) - Type of customer energy efficiency program date +- **code** (String64) - Code value (may be alphanumeric) +- **name** (String256) - Name associated with code +- **note** (String256, optional) - Optional description of code + +### ProgramDateKind Enum (customer.xsd lines 1997-2030) +Per XSD, this enum has 4 values: +```xml + + + + +``` + +**Note**: The XSD uses `xs:union memberTypes="String64"` which means the enum can also accept custom string values not in the enumeration list. + +### Architecture Decision +- **ProgramDateIdMappingsEntity**: Extends IdentifiedObject, has ONE field (programDateIdMapping) + relatedLinks collection +- **ProgramDateIdMapping**: @Embeddable class (extends Object conceptually, not IdentifiedObject) +- **ProgramDateIdMappingsDto**: JAXB DTO with NO IdentifiedObject fields (uses AtomEntryDto pattern from Phase 21) +- **ProgramDateIdMappingDto**: Nested DTO for the embedded object +- **ProgramDateKind**: Java enum with 4 values +- **Related Links**: Support Atom `` bidirectional references (NAESB ESPI 4.0 standard) +- **Service Queries**: ID-based only (findById, findAll, deleteById) + +## Tasks + +### Phase A0: Enable Bidirectional Atom Links (PREREQUISITE) + +**Context**: The NAESB ESPI 4.0 standard defines bidirectional relationships between entities using Atom `` elements. While the database tables exist, the JPA entity mappings are missing. This includes relationships such as ServiceLocation ↔ EndDevice, ServiceLocation ↔ Meter, CustomerAgreement ↔ ProgramDateIdMappings, and CustomerAgreement ↔ ServiceLocation/ServiceSupplier. + +**Database Infrastructure** (already exists in V3 migration): +- `customer_agreement_related_links` table ✓ +- `program_date_id_mapping_related_links` table ✓ +- `service_location_related_links` table ✓ +- `service_supplier_related_links` table ✓ +- `end_device_related_links` table ✓ +- `meter_related_links` table ✓ + +**Missing JPA Mappings** (must be added): +All six entities lack the `@ElementCollection` field for relatedLinks. + +#### Task A0.1: Add relatedLinks to CustomerAgreementEntity +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java` + +**Add Field** (after existing fields, before equals/hashCode methods): +```java +/** + * Atom related links for bidirectional references. + * Per NAESB ESPI 4.0 standard, CustomerAgreement can have related links to: + * - ProgramDateIdMappings (Demand Response program enrollment information) + * - ServiceLocation (where service is delivered) + * - ServiceSupplier (who supplies the service) + * + * Stored as href strings that will be wrapped in elements + * during XML serialization by the service layer. + */ +@ElementCollection +@CollectionTable(name = "customer_agreement_related_links", + joinColumns = @JoinColumn(name = "customer_agreement_id")) +@Column(name = "related_links", length = 1024) +private List relatedLinks; +``` + +**Update toString() method**: Add `relatedLinks` to the toString output. + +#### Task A0.2: Add relatedLinks to ProgramDateIdMappingsEntity +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ProgramDateIdMappingsEntity.java` + +**Add Field** (after programDateIdMapping field): +```java +/** + * Atom related links for bidirectional references. + * Per NAESB ESPI 4.0 standard, ProgramDateIdMappings can have related links to: + * - CustomerAgreement (the agreement this program enrollment relates to) + * + * Stored as href strings that will be wrapped in elements + * during XML serialization by the service layer. + */ +@ElementCollection +@CollectionTable(name = "program_date_id_mapping_related_links", + joinColumns = @JoinColumn(name = "program_date_id_mapping_id")) +@Column(name = "related_links", length = 1024) +private List relatedLinks; +``` + +**Update toString() method**: Add `relatedLinks` to the toString output. + +#### Task A0.3: Add relatedLinks to ServiceLocationEntity +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java` + +**Add Field** (after existing @ElementCollection for usagePointHrefs): +```java +/** + * Atom related links for bidirectional references. + * Per NAESB ESPI 4.0 standard, ServiceLocation can have related links to: + * - CustomerAgreement (agreements for service at this location) + * - UsagePoint (meters at this location - via usagePointHrefs already implemented) + * + * Stored as href strings that will be wrapped in elements + * during XML serialization by the service layer. + */ +@ElementCollection +@CollectionTable(name = "service_location_related_links", + joinColumns = @JoinColumn(name = "service_location_id")) +@Column(name = "related_links", length = 1024) +private List relatedLinks; +``` + +**Update toString() method**: Add `relatedLinks` to the toString output. + +#### Task A0.4: Add relatedLinks to ServiceSupplierEntity +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceSupplierEntity.java` + +**Add Field** (after organisation field): +```java +/** + * Atom related links for bidirectional references. + * Per NAESB ESPI 4.0 standard, ServiceSupplier can have related links to: + * - CustomerAgreement (agreements where this supplier provides service) + * - Customer (customers this supplier serves) + * + * Stored as href strings that will be wrapped in elements + * during XML serialization by the service layer. + */ +@ElementCollection +@CollectionTable(name = "service_supplier_related_links", + joinColumns = @JoinColumn(name = "service_supplier_id")) +@Column(name = "related_links", length = 1024) +private List relatedLinks; +``` + +**Update toString() method**: Add `relatedLinks` to the toString output. + +#### Task A0.5: Add relatedLinks to EndDeviceEntity +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceEntity.java` + +**Add Field** (after existing fields, before equals/hashCode methods): +```java +/** + * Atom related links for bidirectional references. + * Per NAESB ESPI 4.0 standard, EndDevice can have related links to: + * - ServiceLocation (where this end device is located) + * - UsagePoint (usage data associated with this end device) + * + * Stored as href strings that will be wrapped in elements + * during XML serialization by the service layer. + */ +@ElementCollection +@CollectionTable(name = "end_device_related_links", + joinColumns = @JoinColumn(name = "end_device_id")) +@Column(name = "related_links", length = 1024) +private List relatedLinks; +``` + +**Update toString() method**: Add `relatedLinks` to the toString output (in EndDeviceEntity, not in subclasses). + +**Note**: MeterEntity extends EndDeviceEntity, so it will inherit this relatedLinks field automatically. + +#### Task A0.6: Verify Meter inherits relatedLinks from EndDevice +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/MeterEntity.java` + +**Context**: MeterEntity extends EndDeviceEntity using `@Inheritance(strategy = InheritanceType.JOINED)`. The relatedLinks field will be inherited from EndDeviceEntity. + +**Verification**: +- [ ] Confirm MeterEntity extends EndDeviceEntity +- [ ] Confirm EndDeviceEntity has @Inheritance(strategy = InheritanceType.JOINED) +- [ ] Confirm meter_related_links table exists in V3 migration (line 575) +- [ ] NO changes needed to MeterEntity (inherits relatedLinks from parent) + +**Benefits**: +- ✅ Meter automatically gets relatedLinks via inheritance +- ✅ Consistent with JPA inheritance pattern +- ✅ Database table meter_related_links already exists + +#### Task A0.7: Verification +**Verification Steps**: +- [ ] All 6 entities have @ElementCollection for relatedLinks (5 explicit + 1 inherited): + - [ ] CustomerAgreementEntity (explicit) + - [ ] ProgramDateIdMappingsEntity (explicit) + - [ ] ServiceLocationEntity (explicit) + - [ ] ServiceSupplierEntity (explicit) + - [ ] EndDeviceEntity (explicit) + - [ ] MeterEntity (inherited from EndDeviceEntity) +- [ ] All entities use correct table names (matching V3 migration) +- [ ] All entities have updated toString() methods +- [ ] Build succeeds: `mvn clean compile` +- [ ] No JPA mapping errors in logs + +**Benefits**: +- ✅ Enables ESPI 4.0 compliant bidirectional Atom links across all customer entities +- ✅ Supports ServiceLocation ↔ EndDevice bidirectional relationships +- ✅ Supports ServiceLocation ↔ Meter bidirectional relationships +- ✅ Supports CustomerAgreement ↔ ProgramDateIdMappings/ServiceLocation/ServiceSupplier bidirectional relationships +- ✅ Service layer can populate `` elements in XML +- ✅ Consistent pattern across all customer domain entities +- ✅ Uses existing database infrastructure (no migration changes needed) + +--- + +### Phase A: Enum and Embeddable Creation + +#### Task A1: Create ProgramDateKind Enum +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/enums/ProgramDateKind.java` + +**Requirements**: +- Create new enum with 4 values from XSD +- Add JavaDoc for each value +- Use descriptive names matching XSD + +**Implementation**: +```java +package org.greenbuttonalliance.espi.common.domain.customer.enums; + +/** + * Type of Demand Response program date based on ESPI 4.0 customer.xsd specification. + * + * Per customer.xsd lines 1997-2030. + * Note: XSD uses union type, allowing both enumerated values and custom String64 values. + * + * Ordinal mapping: + * 0 = CUST_DR_PROGRAM_ENROLLMENT_DATE + * 1 = CUST_DR_PROGRAM_DE_ENROLLMENT_DATE + * 2 = CUST_DR_PROGRAM_TERM_DATE_REGARDLESS_FINANCIAL + * 3 = CUST_DR_PROGRAM_TERM_DATE_WITHOUT_FINANCIAL + */ +public enum ProgramDateKind { + /** + * Date customer enrolled in Demand Response program. + * Ordinal: 0 + */ + CUST_DR_PROGRAM_ENROLLMENT_DATE, + + /** + * Date customer terminated enrollment in Demand Response program. + * Ordinal: 1 + */ + CUST_DR_PROGRAM_DE_ENROLLMENT_DATE, + + /** + * Earliest date customer can terminate Demand Response enrollment, regardless of financial impact. + * Ordinal: 2 + */ + CUST_DR_PROGRAM_TERM_DATE_REGARDLESS_FINANCIAL, + + /** + * Earliest date customer can terminate Demand Response enrollment, without financial impact. + * Ordinal: 3 + */ + CUST_DR_PROGRAM_TERM_DATE_WITHOUT_FINANCIAL +} +``` + +**Verification Checklist**: +- ✅ Location: `customer/enums` directory +- ✅ Sequence: Matches XSD exactly (ordinals 0-3) +- ✅ Values: Four enum constants matching XSD enumeration values +- ✅ JavaDoc: Comprehensive documentation for each value + +#### Task A2: Create ProgramDateIdMapping Embeddable Class +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/common/ProgramDateIdMapping.java` + +**Requirements**: +- @Embeddable annotation (not @Entity) +- Implements Serializable +- 4 fields matching XSD +- Column definitions with appropriate lengths +- Proper JavaDoc + +**Implementation**: +```java +package org.greenbuttonalliance.espi.common.domain.customer.common; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.greenbuttonalliance.espi.common.domain.customer.enums.ProgramDateKind; + +import java.io.Serializable; + +/** + * Embeddable class for single customer energy efficiency program date mapping. + * + * Per customer.xsd lines 1223-1251, ProgramDateIdMapping extends Object (NOT IdentifiedObject). + * This is an embedded component, not a standalone entity. + */ +@Embeddable +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ProgramDateIdMapping implements Serializable { + + /** + * Type of customer energy efficiency program date. + */ + @Enumerated(EnumType.STRING) + @Column(name = "program_date_type", length = 64) + private ProgramDateKind programDateType; + + /** + * Code value (may be alphanumeric). + */ + @Column(name = "code", length = 64) + private String code; + + /** + * Name associated with code. + */ + @Column(name = "name", length = 256) + private String name; + + /** + * Optional description of code. + */ + @Column(name = "note", length = 256) + private String note; + + @Override + public String toString() { + return "ProgramDateIdMapping{" + + "programDateType=" + programDateType + + ", code='" + code + '\'' + + ", name='" + name + '\'' + + ", note='" + note + '\'' + + '}'; + } +} +``` + +### Phase B: Entity Updates + +#### Task B1: Update ProgramDateIdMappingsEntity +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ProgramDateIdMappingsEntity.java` + +**Current State**: +- Extends IdentifiedObject +- Has NO fields (completely empty) + +**Changes Required**: +1. Add programDateIdMapping embedded field +2. Add relatedLinks collection (from Phase A0) +3. Update JavaDoc to reflect XSD structure +4. Add toString() method + +**Updated Implementation**: +```java +package org.greenbuttonalliance.espi.common.domain.customer.entity; + +import lombok.*; +import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject; +import org.greenbuttonalliance.espi.common.domain.customer.common.ProgramDateIdMapping; + +import jakarta.persistence.*; +import java.util.List; + +/** + * Pure JPA/Hibernate entity for ProgramDateIdMappings without JAXB concerns. + * + * [extension] Collection of all customer Energy Efficiency programs. + * Per customer.xsd lines 269-283, extends IdentifiedObject with one optional field. + */ +@Entity +@Table(name = "program_date_id_mappings") +@Getter +@Setter +@NoArgsConstructor +public class ProgramDateIdMappingsEntity extends IdentifiedObject { + + /** + * [extension] Program date description. + * Optional single customer energy efficiency program date mapping. + */ + @Embedded + private ProgramDateIdMapping programDateIdMapping; + + /** + * Atom related links for bidirectional references. + * Per NAESB ESPI 4.0 standard, ProgramDateIdMappings can have related links to: + * - CustomerAgreement (the agreement this program enrollment relates to) + * + * Stored as href strings that will be wrapped in elements + * during XML serialization by the service layer. + */ + @ElementCollection + @CollectionTable(name = "program_date_id_mapping_related_links", + joinColumns = @JoinColumn(name = "program_date_id_mapping_id")) + @Column(name = "related_links", length = 1024) + private List relatedLinks; + + @Override + public String toString() { + return "ProgramDateIdMappingsEntity{" + + "id=" + getId() + + ", programDateIdMapping=" + programDateIdMapping + + ", relatedLinks=" + relatedLinks + + '}'; + } +} +``` + +### Phase C: DTO Implementation + +#### Task C1: Create ProgramDateIdMappingDto +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingDto.java` + +**Requirements**: +- Use JAXB annotations for XML marshalling +- Include ONLY 4 fields from XSD +- NO IdentifiedObject fields +- Namespace: `http://naesb.org/espi/customer` + +**Implementation**: +```java +package org.greenbuttonalliance.espi.common.dto.customer; + +import jakarta.xml.bind.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.greenbuttonalliance.espi.common.domain.customer.enums.ProgramDateKind; + +/** + * ProgramDateIdMapping DTO class for JAXB XML marshalling/unmarshalling. + * + * Represents a single customer energy efficiency program date mapping. + * Per customer.xsd lines 1223-1251, extends Object (NOT IdentifiedObject). + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ProgramDateIdMapping", namespace = "http://naesb.org/espi/customer", propOrder = { + "programDateType", "code", "name", "note" +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ProgramDateIdMappingDto { + + /** + * Type of customer energy efficiency program date. + */ + @XmlElement(name = "programDateType", namespace = "http://naesb.org/espi/customer") + private ProgramDateKind programDateType; + + /** + * Code value (may be alphanumeric). + */ + @XmlElement(name = "code", namespace = "http://naesb.org/espi/customer", required = true) + private String code; + + /** + * Name associated with code. + */ + @XmlElement(name = "name", namespace = "http://naesb.org/espi/customer", required = true) + private String name; + + /** + * Optional description of code. + */ + @XmlElement(name = "note", namespace = "http://naesb.org/espi/customer") + private String note; +} +``` + +#### Task C2: Update ProgramDateIdMappingsDto +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingsDto.java` + +**Current State**: +- Has IdentifiedObject fields (id, uuid, published, updated, selfLink, upLink, relatedLinks) +- Has extra fields (programId, programDate, mappingId, mappingType, isActive, customer) +- NOT XSD-compliant + +**Changes Required**: +1. **REMOVE** all IdentifiedObject fields (id, uuid, published, updated, selfLink, upLink, relatedLinks) +2. **REMOVE** all non-XSD fields (programId, programDate, mappingId, mappingType, isActive, customer) +3. **ADD** ONLY the programDateIdMapping field from XSD +4. Update propOrder to match XSD element sequence +5. Update JavaDoc + +**Corrected Implementation**: +```java +package org.greenbuttonalliance.espi.common.dto.customer; + +import jakarta.xml.bind.annotation.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * ProgramDateIdMappings DTO class for JAXB XML marshalling/unmarshalling. + * + * [extension] Collection of all customer Energy Efficiency programs. + * Per customer.xsd lines 269-283, extends IdentifiedObject with one optional field. + * + * IMPORTANT: IdentifiedObject fields (id, published, updated, links) are handled by + * AtomEntryDto wrapper, NOT included in this resource DTO. This follows the ESPI 4.0 + * pattern where Atom protocol metadata is separate from resource data. + */ +@XmlRootElement(name = "ProgramDateIdMappings", namespace = "http://naesb.org/espi/customer") +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ProgramDateIdMappings", namespace = "http://naesb.org/espi/customer", propOrder = { + "programDateIdMapping" +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ProgramDateIdMappingsDto { + + /** + * [extension] Program date description. + * Optional single customer energy efficiency program date mapping. + */ + @XmlElement(name = "programDateIdMapping", namespace = "http://naesb.org/espi/customer") + private ProgramDateIdMappingDto programDateIdMapping; +} +``` + +### Phase D: Mapper Implementation + +#### Task D1: Create ProgramDateIdMappingMapper +**File**: `src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ProgramDateIdMappingMapper.java` + +**Requirements**: +- MapStruct interface +- Maps 4 fields between entity and DTO +- No IdentifiedObject fields (ProgramDateIdMapping extends Object, not IdentifiedObject) + +**Implementation**: +```java +package org.greenbuttonalliance.espi.common.mapper.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.common.ProgramDateIdMapping; +import org.greenbuttonalliance.espi.common.dto.customer.ProgramDateIdMappingDto; +import org.mapstruct.Mapper; + +/** + * MapStruct mapper for converting between ProgramDateIdMapping and ProgramDateIdMappingDto. + * + * Maps the 4 fields of ProgramDateIdMapping embeddable component. + * This is NOT an IdentifiedObject, so there are no id/link fields to ignore. + */ +@Mapper(componentModel = "spring") +public interface ProgramDateIdMappingMapper { + + /** + * Converts a ProgramDateIdMapping embeddable to a ProgramDateIdMappingDto. + * + * @param mapping the program date ID mapping embeddable + * @return the program date ID mapping DTO + */ + ProgramDateIdMappingDto toDto(ProgramDateIdMapping mapping); + + /** + * Converts a ProgramDateIdMappingDto to a ProgramDateIdMapping embeddable. + * + * @param dto the program date ID mapping DTO + * @return the program date ID mapping embeddable + */ + ProgramDateIdMapping toEmbeddable(ProgramDateIdMappingDto dto); +} +``` + +#### Task D2: Create ProgramDateIdMappingsMapper +**File**: `src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/ProgramDateIdMappingsMapper.java` + +**Requirements**: +- MapStruct interface +- Maps ONLY 1 XSD field (programDateIdMapping) +- NO Atom field mappings (id, links, timestamps handled by service layer via AtomEntryDto) +- Uses ProgramDateIdMappingMapper for the nested object +- Does NOT map relatedLinks (managed by service layer) + +**Implementation**: +```java +package org.greenbuttonalliance.espi.common.mapper.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.ProgramDateIdMappingsEntity; +import org.greenbuttonalliance.espi.common.dto.customer.ProgramDateIdMappingsDto; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +/** + * MapStruct mapper for converting between ProgramDateIdMappingsEntity and ProgramDateIdMappingsDto. + * + * Maps only the programDateIdMapping field from customer.xsd. + * IdentifiedObject fields (id, links, timestamps) and relatedLinks are NOT part of the DTO + * and are handled separately by the service layer via AtomEntryDto. + * + * Handles the conversion between the JPA entity used for persistence and the DTO + * used for JAXB XML marshalling in the Green Button API. + */ +@Mapper(componentModel = "spring", uses = { + ProgramDateIdMappingMapper.class +}) +public interface ProgramDateIdMappingsMapper { + + /** + * Converts a ProgramDateIdMappingsEntity to a ProgramDateIdMappingsDto. + * Maps only the programDateIdMapping field. + * + * @param entity the program date ID mappings entity + * @return the program date ID mappings DTO + */ + ProgramDateIdMappingsDto toDto(ProgramDateIdMappingsEntity entity); + + /** + * Converts a ProgramDateIdMappingsDto to a ProgramDateIdMappingsEntity. + * Maps only the programDateIdMapping field. + * + * @param dto the program date ID mappings DTO + * @return the program date ID mappings entity (IdentifiedObject fields will be null) + */ + ProgramDateIdMappingsEntity toEntity(ProgramDateIdMappingsDto dto); + + /** + * Updates an existing ProgramDateIdMappingsEntity with data from a ProgramDateIdMappingsDto. + * Updates only the programDateIdMapping field. + * + * @param dto the program date ID mappings DTO with updated data + * @param entity the existing program date ID mappings entity to update + */ + void updateEntityFromDto(ProgramDateIdMappingsDto dto, @MappingTarget ProgramDateIdMappingsEntity entity); +} +``` + +**Note**: The mapper does NOT need explicit `@Mapping(target = "...", ignore = true)` annotations because MapStruct automatically only maps fields that exist in both the source and target. Since ProgramDateIdMappingsDto has NO IdentifiedObject fields or relatedLinks, MapStruct won't try to map them. + +### Phase E: Repository Implementation + +#### Task E1: Create ProgramDateIdMappingsRepository +**File**: `src/main/java/org/greenbuttonalliance/espi/common/repositories/customer/ProgramDateIdMappingsRepository.java` + +**Requirements**: +- Extends JpaRepository +- NO custom query methods (only JpaRepository defaults: findById, findAll, save, deleteById, count) +- Following Phase 17 guidance: Service layer queries ONLY by ID + +**Implementation**: +```java +package org.greenbuttonalliance.espi.common.repositories.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.ProgramDateIdMappingsEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +/** + * Repository interface for ProgramDateIdMappingsEntity. + * + * Provides standard CRUD operations for program date ID mappings. + * Following Phase 17 guidance: Service layer queries ONLY by ID. + * All query methods are provided by JpaRepository. + */ +@Repository +public interface ProgramDateIdMappingsRepository extends JpaRepository { + // No custom methods - only JpaRepository defaults: + // - Optional findById(ID id) + // - List findAll() + // - S save(S entity) + // - void deleteById(ID id) + // - long count() +} +``` + +### Phase F: Service Implementation + +#### Task F1: Create ProgramDateIdMappingsService Interface +**File**: `src/main/java/org/greenbuttonalliance/espi/common/service/customer/ProgramDateIdMappingsService.java` + +**Requirements**: +- Standard CRUD operations (ID-based only) +- No timestamp-based or field-based queries + +**Implementation**: +```java +package org.greenbuttonalliance.espi.common.service.customer; + +import org.greenbuttonalliance.espi.common.domain.customer.entity.ProgramDateIdMappingsEntity; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Service interface for ProgramDateIdMappings operations. + * + * Provides business logic for managing program date ID mappings. + * Following Phase 17 guidance: Queries ONLY by ID. + */ +public interface ProgramDateIdMappingsService { + + /** + * Create a new program date ID mappings. + * + * @param entity the program date ID mappings entity to create + * @return the created entity with generated ID + */ + ProgramDateIdMappingsEntity create(ProgramDateIdMappingsEntity entity); + + /** + * Find a program date ID mappings by ID. + * + * @param id the program date ID mappings ID + * @return optional containing the entity if found + */ + Optional findById(UUID id); + + /** + * Find all program date ID mappings. + * + * @return list of all program date ID mappings entities + */ + List findAll(); + + /** + * Update an existing program date ID mappings. + * + * @param entity the program date ID mappings entity with updated data + * @return the updated entity + */ + ProgramDateIdMappingsEntity update(ProgramDateIdMappingsEntity entity); + + /** + * Delete a program date ID mappings by ID. + * + * @param id the program date ID mappings ID to delete + */ + void deleteById(UUID id); + + /** + * Count all program date ID mappings. + * + * @return total count of program date ID mappings + */ + long count(); +} +``` + +#### Task F2: Create ProgramDateIdMappingsServiceImpl +**File**: `src/main/java/org/greenbuttonalliance/espi/common/service/customer/impl/ProgramDateIdMappingsServiceImpl.java` + +**Requirements**: +- Implements ProgramDateIdMappingsService +- Delegates to repository +- ID-based operations only + +**Implementation**: +```java +package org.greenbuttonalliance.espi.common.service.customer.impl; + +import lombok.RequiredArgsConstructor; +import org.greenbuttonalliance.espi.common.domain.customer.entity.ProgramDateIdMappingsEntity; +import org.greenbuttonalliance.espi.common.repositories.customer.ProgramDateIdMappingsRepository; +import org.greenbuttonalliance.espi.common.service.customer.ProgramDateIdMappingsService; +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 ProgramDateIdMappings operations. + */ +@Service +@RequiredArgsConstructor +@Transactional +public class ProgramDateIdMappingsServiceImpl implements ProgramDateIdMappingsService { + + private final ProgramDateIdMappingsRepository repository; + + @Override + public ProgramDateIdMappingsEntity create(ProgramDateIdMappingsEntity entity) { + return repository.save(entity); + } + + @Override + @Transactional(readOnly = true) + public Optional findById(UUID id) { + return repository.findById(id); + } + + @Override + @Transactional(readOnly = true) + public List findAll() { + return repository.findAll(); + } + + @Override + public ProgramDateIdMappingsEntity update(ProgramDateIdMappingsEntity entity) { + return repository.save(entity); + } + + @Override + public void deleteById(UUID id) { + repository.deleteById(id); + } + + @Override + @Transactional(readOnly = true) + public long count() { + return repository.count(); + } +} +``` + +### Phase G: Database Migration + +#### Task G1: Update V3 Flyway Migration Script +**File**: `src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql` + +**Current State**: +```sql +CREATE TABLE program_date_id_mappings +( + id CHAR(36) PRIMARY KEY, + description VARCHAR(255), + 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), + + -- Program date ID mapping specific fields + program_date BIGINT, + program_id VARCHAR(100) +); +``` + +**Issues**: +- Has fields `program_date` and `program_id` that don't exist in XSD +- Missing embedded ProgramDateIdMapping fields (program_date_type, code, name, note) + +**Changes Required**: +1. **REMOVE** non-XSD fields: program_date, program_id +2. **ADD** embedded ProgramDateIdMapping fields with proper column names +3. **KEEP** IdentifiedObject fields (id, description, created, updated, published, link fields) +4. **KEEP** related_links table (already exists, just needs reference in comment) + +**Updated CREATE TABLE Statement**: +```sql +-- ProgramDateIdMappings table +-- Per customer.xsd lines 269-283, extends IdentifiedObject +-- Contains one embedded ProgramDateIdMapping (lines 1223-1251) +CREATE TABLE program_date_id_mappings +( + -- IdentifiedObject fields + id CHAR(36) PRIMARY KEY, + description VARCHAR(255), + 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), + + -- Embedded ProgramDateIdMapping fields (optional, minOccurs="0") + program_date_type VARCHAR(64), -- ProgramDateKind enum + code VARCHAR(64), -- String64 + name VARCHAR(256), -- String256 + note VARCHAR(256) -- String256, optional +); + +-- Indexes for IdentifiedObject timestamp fields +CREATE INDEX idx_program_date_id_mappings_created ON program_date_id_mappings (created); +CREATE INDEX idx_program_date_id_mappings_updated ON program_date_id_mappings (updated); + +-- Related Links Table for Program Date ID Mappings (for Atom bidirectional links) +-- Note: Table already exists, managed by @ElementCollection +CREATE TABLE program_date_id_mapping_related_links +( + program_date_id_mapping_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (program_date_id_mapping_id) REFERENCES program_date_id_mappings (id) ON DELETE CASCADE +); + +CREATE INDEX idx_program_date_id_mapping_related_links ON program_date_id_mapping_related_links (program_date_id_mapping_id); +``` + +### Phase H: DtoExportService Integration + +#### Task H1: Update DtoExportServiceImpl +**File**: `src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java` + +**Changes**: +1. Add ProgramDateIdMappingsMapper injection +2. Register ProgramDateIdMappingsDto.class in CustomerExportService JAXBContext +3. Ensure CustomerAtomEntryDto has @XmlElement for ProgramDateIdMappings + +**CustomerAtomEntryDto.java Update**: +Verify/add ProgramDateIdMappings to the @XmlElements annotation: +```java +@XmlElements({ + // ... other customer domain elements ... + @XmlElement(name = "ProgramDateIdMappings", type = ProgramDateIdMappingsDto.class, namespace = "http://naesb.org/espi/customer") +}) +``` + +### Phase I: Testing + +#### Task I1: Create ProgramDateIdMappingDtoTest +**File**: `src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingDtoTest.java` + +**Test Coverage** (8 tests): +1. shouldMarshalProgramDateIdMappingWithAllFields +2. shouldMarshalProgramDateIdMappingWithMinimalFields +3. shouldUnmarshalProgramDateIdMappingXml +4. shouldHandleNullFields +5. shouldVerifyXmlNamespaceAndElements +6. shouldVerifyFieldOrder +7. shouldHandleAllEnumValues +8. shouldSerializeEnumAsString + +#### Task I2: Create ProgramDateIdMappingsDtoTest +**File**: `src/test/java/org/greenbuttonalliance/espi/common/dto/customer/ProgramDateIdMappingsDtoTest.java` + +**Test Coverage** (8 tests): +1. shouldMarshalProgramDateIdMappingsWithEmbedded +2. shouldMarshalProgramDateIdMappingsWithoutEmbedded +3. shouldUnmarshalProgramDateIdMappingsXml +4. shouldHandleNullProgramDateIdMapping +5. shouldVerifyXmlNamespaceAndElements +6. shouldVerifyFieldOrder +7. shouldWrapInAtomEntry +8. shouldNotIncludeIdentifiedObjectFields + +#### Task I3: Create ProgramDateIdMappingMapperTest +**File**: `src/test/java/org/greenbuttonalliance/espi/common/mapper/customer/ProgramDateIdMappingMapperTest.java` + +**Test Coverage** (6 tests): +1. shouldMapToDtoWithAllFields +2. shouldMapToDtoWithNullFields +3. shouldMapToEmbeddableWithAllFields +4. shouldMapToEmbeddableWithNullFields +5. shouldHandleNullInput +6. shouldMapAllEnumValues + +#### Task I4: Create ProgramDateIdMappingsMapperTest +**File**: `src/test/java/org/greenbuttonalliance/espi/common/mapper/customer/ProgramDateIdMappingsMapperTest.java` + +**Test Coverage** (8 tests): +1. shouldMapToDtoWithEmbedded +2. shouldMapToDtoWithoutEmbedded +3. shouldMapToEntityWithEmbedded +4. shouldMapToEntityWithoutEmbedded +5. shouldUpdateEntityFromDto +6. shouldNotMapIdentifiedObjectFields +7. shouldHandleNullInput +8. shouldMapNestedObject + +#### Task I5: Create ProgramDateIdMappingsRepositoryTest +**File**: `src/test/java/org/greenbuttonalliance/espi/common/repositories/customer/ProgramDateIdMappingsRepositoryTest.java` + +**Test Coverage** (10 tests): +1. shouldSaveAndFindById +2. shouldFindAll +3. shouldUpdate +4. shouldDelete +5. shouldCount +6. shouldReturnEmptyOptionalWhenNotFound +7. shouldPersistEmbeddedFields +8. shouldPersistAllEnumValues +9. shouldHandleNullEmbeddedObject +10. shouldPersistRelatedLinks + +#### Task I6: Create ProgramDateIdMappingsServiceTest +**File**: `src/test/java/org/greenbuttonalliance/espi/common/service/customer/ProgramDateIdMappingsServiceTest.java` + +**Test Coverage** (8 tests): +1. shouldCreateProgramDateIdMappings +2. shouldFindById +3. shouldFindAll +4. shouldUpdate +5. shouldDeleteById +6. shouldCount +7. shouldHandleNonExistentId +8. shouldDelegateToRepository + +## Testing Strategy + +### Test Breakdown +- **DTO Tests (ProgramDateIdMappingDto)**: 8 tests +- **DTO Tests (ProgramDateIdMappingsDto)**: 8 tests +- **Mapper Tests (ProgramDateIdMappingMapper)**: 6 tests +- **Mapper Tests (ProgramDateIdMappingsMapper)**: 8 tests +- **Repository Tests**: 10 tests +- **Service Tests**: 8 tests + +**Total New Tests**: ~48 tests + +### Current Test Baseline +Based on Phase 21 completion: 760 tests + +### Expected Test Count After Phase 17 +**Target**: ~808 tests (760 + 48) + +### Regression Testing +- All existing tests must pass +- Integration tests verify database schema updates +- XML marshalling validates against customer.xsd +- Related links collections work correctly + +## Execution Checklist + +### Pre-Implementation Review +- [ ] Review customer.xsd for ProgramDateIdMappings (lines 269-283) +- [ ] Review customer.xsd for ProgramDateIdMapping (lines 1223-1251) +- [ ] Review customer.xsd for ProgramDateKind enum (lines 1997-2030) +- [ ] Understand current database schema issues +- [ ] Understand current DTO cleanup requirements +- [ ] Understand NAESB ESPI 4.0 bidirectional link requirements + +### Phase A0: Enable Bidirectional Atom Links (PREREQUISITE) +- [ ] Add relatedLinks @ElementCollection to CustomerAgreementEntity +- [ ] Add relatedLinks @ElementCollection to ProgramDateIdMappingsEntity +- [ ] Add relatedLinks @ElementCollection to ServiceLocationEntity +- [ ] Add relatedLinks @ElementCollection to ServiceSupplierEntity +- [ ] Update toString() methods in all 4 entities +- [ ] Verify build succeeds with no JPA errors + +### Phase A: Enum and Embeddable Creation +- [ ] Create ProgramDateKind enum (4 values) +- [ ] Create ProgramDateIdMapping embeddable class (4 fields) + +### Phase B: Entity Updates +- [ ] Update ProgramDateIdMappingsEntity (add programDateIdMapping field + relatedLinks) + +### Phase C: DTO Implementation +- [ ] Create ProgramDateIdMappingDto (4 fields, NO IdentifiedObject fields) +- [ ] Update ProgramDateIdMappingsDto (remove IdentifiedObject fields, keep only 1 XSD field) + +### Phase D: Mapper Implementation +- [ ] Create ProgramDateIdMappingMapper +- [ ] Create ProgramDateIdMappingsMapper (NO Atom field mappings) + +### Phase E: Repository Implementation +- [ ] Create ProgramDateIdMappingsRepository (NO custom query methods) + +### Phase F: Service Implementation +- [ ] Create ProgramDateIdMappingsService interface (ID-based queries only) +- [ ] Create ProgramDateIdMappingsServiceImpl + +### Phase G: Database Migration +- [ ] Update V3 migration: remove non-XSD fields (program_date, program_id) +- [ ] Update V3 migration: add embedded fields (program_date_type, code, name, note) +- [ ] Verify indexes on created/updated fields +- [ ] Verify related_links table exists (no changes needed) + +### Phase H: DtoExportService Integration +- [ ] Update DtoExportServiceImpl with ProgramDateIdMappingsMapper +- [ ] Update CustomerAtomEntryDto with ProgramDateIdMappings @XmlElement + +### Phase I: Testing +- [ ] Create ProgramDateIdMappingDtoTest (8 tests) +- [ ] Create ProgramDateIdMappingsDtoTest (8 tests) +- [ ] Create ProgramDateIdMappingMapperTest (6 tests) +- [ ] Create ProgramDateIdMappingsMapperTest (8 tests) +- [ ] Create ProgramDateIdMappingsRepositoryTest (10 tests including relatedLinks) +- [ ] Create ProgramDateIdMappingsServiceTest (8 tests) + +### Final Verification +- [ ] Run full test suite: `mvn clean test` +- [ ] Run integration tests: `mvn verify -Pintegration-tests` +- [ ] Verify test count: ~808 tests +- [ ] All tests passing on H2, MySQL, PostgreSQL +- [ ] XML marshalling validates against customer.xsd +- [ ] No IdentifiedObject fields in ProgramDateIdMappingsDto +- [ ] Related links work correctly in all 4 entities + +### Documentation and Issue Tracking +- [ ] Update Issue #28 with Phase 17 completion status +- [ ] **DO NOT close Issue #28** - more phases remain (Phase 18+) +- [ ] Document ProgramDateIdMapping architecture (embeddable pattern) +- [ ] Document ProgramDateKind enum values +- [ ] Document bidirectional Atom links pattern +- [ ] Update CLAUDE.md if needed + +## Success Criteria + +1. ✅ ProgramDateKind enum created (4 values matching XSD) +2. ✅ ProgramDateIdMapping embeddable created (4 fields) +3. ✅ ProgramDateIdMappingsEntity updated (has programDateIdMapping field + relatedLinks) +4. ✅ ProgramDateIdMappingDto created (4 fields, NO IdentifiedObject fields) +5. ✅ ProgramDateIdMappingsDto cleaned up (ONLY programDateIdMapping field, NO IdentifiedObject fields) +6. ✅ ProgramDateIdMappingMapper implemented (simple 4-field mapping) +7. ✅ ProgramDateIdMappingsMapper implemented (simple 1-field mapping, NO Atom field ignores) +8. ✅ ProgramDateIdMappingsRepository created (NO custom query methods) +9. ✅ ProgramDateIdMappingsService and impl created (ID-based queries only) +10. ✅ V3 Flyway migration updated (correct embedded fields) +11. ✅ All 4 entities have relatedLinks @ElementCollection (CustomerAgreement, ProgramDateIdMappings, ServiceLocation, ServiceSupplier) +12. ✅ All 48 new tests passing +13. ✅ Total test count: ~808 tests +14. ✅ No test regressions +15. ✅ XML validates against customer.xsd +16. ✅ Build succeeds +17. ✅ Integration tests pass on all databases +18. ✅ DTO follows AtomEntryDto pattern (NO IdentifiedObject fields in resource DTO) +19. ✅ Bidirectional Atom links infrastructure enabled for ESPI 4.0 compliance +20. ✅ Service layer uses ONLY ID-based queries +21. ✅ Issue #28 updated with Phase 17 status (NOT closed - more phases remain) + +## Benefits + +### XSD Compliance +- ✅ ProgramDateIdMappings matches customer.xsd lines 269-283 +- ✅ ProgramDateIdMapping matches customer.xsd lines 1223-1251 +- ✅ ProgramDateKind matches customer.xsd lines 1997-2030 +- ✅ DTO follows Phase 21 pattern (Atom metadata separated from resource data) + +### ESPI 4.0 Standard Compliance +- ✅ Supports NAESB ESPI 4.0 bidirectional Atom links via `` +- ✅ Consistent pattern across all customer domain entities +- ✅ Service layer can populate related links for CustomerAgreement relationships + +### Type Safety +- ✅ Enum for program date types +- ✅ Compile-time checked relationships +- ✅ Standard JPA embeddable pattern + +### Performance +- ✅ Single table (no JOINs for embedded object) +- ✅ Indexed timestamp fields for efficient queries +- ✅ Simpler queries (ID-based only) + +### Architecture +- ✅ Clean separation: Atom protocol (AtomEntryDto) vs resource data (ProgramDateIdMappingsDto) +- ✅ Standard JPA embedded pattern for nested objects +- ✅ Consistent with Phase 21 ServiceSupplier implementation +- ✅ MapStruct for clean entity ↔ DTO conversion +- ✅ Simple mappers with NO unnecessary field ignores +- ✅ Reusable relatedLinks pattern across customer domain +- ✅ Minimal service layer (ID-based operations only) + +## References + +- **XSD**: `openespi-common/src/main/resources/schema/ESPI_4.0/customer.xsd` + - ProgramDateIdMappings: lines 269-283 + - ProgramDateIdMapping: lines 1223-1251 + - ProgramDateKind: lines 1997-2030 +- **NAESB ESPI 4.0 Standard**: Bidirectional relationships via Atom `` elements +- **Entity**: `ProgramDateIdMappingsEntity.java`, `ProgramDateIdMapping.java` +- **Embeddable**: `ProgramDateIdMapping.java` (extends Object, not IdentifiedObject) +- **Pattern Reference**: Phase 21 ServiceSupplier implementation +- **Issue**: #28 (Phase 17: ProgramDateIdMappings) From 276d08a0f2b05a91b278a4ad547c3b2c9a6c379d Mon Sep 17 00:00:00 2001 From: "Donald F. Coffin" Date: Sun, 1 Feb 2026 00:14:54 -0500 Subject: [PATCH 2/3] feat: Issue #97 - Standardize relatedLinks Infrastructure with Composition Pattern Implements Issue #97 to standardize relatedLinks infrastructure across all ESPI entities using @AssociationOverride and composition pattern with Lombok @Delegate. Key Changes: - Converted Asset from @MappedSuperclass to @Embeddable for composition - Created EndDeviceFields @Embeddable for 4 EndDevice-specific fields - Refactored EndDeviceEntity and MeterEntity to use composition instead of inheritance - Both entities now embed Asset + EndDeviceFields with @Delegate for transparent access - Added @AssociationOverride to 13 entities (4 usage domain + 9 customer domain) Entity Changes: - Usage Domain: ApplicationInformationEntity, AuthorizationEntity, ElectricPowerQualitySummaryEntity, ReadingTypeEntity - Customer Domain: CustomerEntity, CustomerAccountEntity, CustomerAgreementEntity, EndDeviceEntity, MeterEntity, ProgramDateIdMappingsEntity, ServiceLocationEntity, ServiceSupplierEntity, StatementEntity Mapper Updates: - EndDeviceMapper: Updated to map from nested embedded objects (asset.*, endDeviceFields.*) - MeterMapper: Updated to map from nested embedded objects (asset.*, endDeviceFields.*) - Required because MapStruct runs before Lombok @Delegate generates delegation methods Database Migrations: - V1__Create_Base_Tables.sql: Added relatedLinks tables for usage domain entities - V3__Create_additiional_Base_Tables.sql: Added relatedLinks tables for customer domain, expanded meters table with all IdentifiedObject, Asset, and EndDeviceFields columns Technical Details: - Resolved Hibernate inheritance conflict where MeterEntity couldn't override relatedLinks - Used composition (HAS-A) instead of inheritance (IS-A) per NAESB ESPI 4.0 customer.xsd - Lombok @Delegate provides transparent access to embedded fields for service layer - Each entity now has separate relatedLinks join table via @AssociationOverride Test Results: - All 760 unit tests passing - H2 in-memory database integration: 3 tests passing - MySQL TestContainers integration: 2 tests passing - PostgreSQL TestContainers integration: 59 tests passing - Total: 824+ tests passing across all database platforms Co-Authored-By: Claude Sonnet 4.5 --- .../common/domain/customer/entity/Asset.java | 37 ++-- .../entity/CustomerAccountEntity.java | 7 + .../entity/CustomerAgreementEntity.java | 20 +- .../customer/entity/CustomerEntity.java | 7 + .../customer/entity/EndDeviceEntity.java | 187 ++++++------------ .../customer/entity/EndDeviceFields.java | 65 ++++++ .../domain/customer/entity/MeterEntity.java | 61 +++++- .../entity/ProgramDateIdMappingsEntity.java | 9 + .../entity/ServiceLocationEntity.java | 26 ++- .../entity/ServiceSupplierEntity.java | 20 +- .../customer/entity/StatementEntity.java | 7 + .../usage/ApplicationInformationEntity.java | 7 + .../domain/usage/AuthorizationEntity.java | 7 + .../ElectricPowerQualitySummaryEntity.java | 7 + .../domain/usage/ReadingTypeEntity.java | 7 + .../mapper/customer/EndDeviceMapper.java | 72 +++---- .../common/mapper/customer/MeterMapper.java | 72 +++---- .../db/migration/V1__Create_Base_Tables.sql | 12 +- .../V3__Create_additiional_Base_Tables.sql | 109 ++++++++-- 19 files changed, 473 insertions(+), 266 deletions(-) create mode 100644 openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceFields.java diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Asset.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Asset.java index 4341c871..95e1aafa 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Asset.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/Asset.java @@ -20,33 +20,26 @@ package org.greenbuttonalliance.espi.common.domain.customer.entity; import lombok.Data; -import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; -import lombok.ToString; import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress; import jakarta.persistence.*; -import java.io.Serializable; import java.math.BigDecimal; /** - * Abstract base class for Asset without JAXB concerns. - * - * Tangible resource of the utility, including power system equipment, various end devices, - * cabinets, buildings, etc. Asset description places emphasis on the physical characteristics + * Embeddable component for Asset without JAXB concerns. + * + * Tangible resource of the utility, including power system equipment, various end devices, + * cabinets, buildings, etc. Asset description places emphasis on the physical characteristics * of the equipment fulfilling that role. - * - * This is a @MappedSuperclass that provides asset-specific fields but does not extend IdentifiedObject. - * Actual ESPI resource entities that represent assets should extend IdentifiedObject directly. + * + * This is an @Embeddable component that can be embedded in ESPI resource entities. + * Per NAESB ESPI 4.0 customer.xsd, Asset extends IdentifiedObject (lines 643-713). */ -@MappedSuperclass +@Embeddable @Data -@EqualsAndHashCode @NoArgsConstructor -@ToString -public abstract class Asset implements Serializable { - - private static final long serialVersionUID = 1L; +public class Asset { /** * Utility-specific classification of Asset and its subtypes, according to their corporate standards, @@ -87,16 +80,9 @@ public abstract class Asset implements Serializable { /** * Electronic address. + * Note: Column names will be overridden by the entity embedding this Asset. */ @Embedded - @AttributeOverride(name = "lan", column = @Column(name = "asset_lan")) - @AttributeOverride(name = "mac", column = @Column(name = "asset_mac")) - @AttributeOverride(name = "email1", column = @Column(name = "asset_email1")) - @AttributeOverride(name = "email2", column = @Column(name = "asset_email2")) - @AttributeOverride(name = "web", column = @Column(name = "asset_web")) - @AttributeOverride(name = "radio", column = @Column(name = "asset_radio")) - @AttributeOverride(name = "userID", column = @Column(name = "asset_user_id")) - @AttributeOverride(name = "password", column = @Column(name = "asset_password")) private ElectronicAddress electronicAddress; /** @@ -126,9 +112,10 @@ public abstract class Asset implements Serializable { /** * Status of this asset. + * Note: Uses Status embeddable (column names will be overridden by embedding entity). */ @Embedded - private CustomerEntity.Status status; + private Status status; /** * Embeddable class for LifecycleDate diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java index 36276f3e..1a9931fe 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAccountEntity.java @@ -50,6 +50,13 @@ @AttributeOverride(name = "selfLink.rel", column = @Column(name = "customer_account_self_link_rel")) @AttributeOverride(name = "selfLink.href", column = @Column(name = "customer_account_self_link_href")) @AttributeOverride(name = "selfLink.type", column = @Column(name = "customer_account_self_link_type")) +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "customer_account_related_links", + joinColumns = @JoinColumn(name = "customer_account_id") + ) +) @Getter @Setter @NoArgsConstructor diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java index 2883e487..bb15aca8 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerAgreementEntity.java @@ -48,6 +48,13 @@ @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")) +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "customer_agreement_related_links", + joinColumns = @JoinColumn(name = "customer_agreement_id") + ) +) @Getter @Setter @NoArgsConstructor @@ -235,6 +242,12 @@ public final int hashCode() { public String toString() { return getClass().getSimpleName() + "(" + "id = " + getId() + ", " + + "description = " + getDescription() + ", " + + "created = " + getCreated() + ", " + + "updated = " + getUpdated() + ", " + + "published = " + getPublished() + ", " + + "upLink = " + getUpLink() + ", " + + "selfLink = " + getSelfLink() + ", " + "type = " + getType() + ", " + "authorName = " + getAuthorName() + ", " + "createdDateTime = " + getCreatedDateTime() + ", " + @@ -252,11 +265,6 @@ public String toString() { "isPrePay = " + getIsPrePay() + ", " + "shutOffDateTime = " + getShutOffDateTime() + ", " + "currency = " + getCurrency() + ", " + - "futureStatus = " + getFutureStatus() + ", " + - "agreementId = " + getAgreementId() + ", " + - "description = " + getDescription() + ", " + - "created = " + getCreated() + ", " + - "updated = " + getUpdated() + ", " + - "published = " + getPublished() + ")"; + "agreementId = " + getAgreementId() + ")"; } } diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java index 2842e849..d08b38ac 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/CustomerEntity.java @@ -48,6 +48,13 @@ */ @Entity @Table(name = "customers") +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "customer_related_links", + joinColumns = @JoinColumn(name = "customer_id") + ) +) @Getter @Setter @NoArgsConstructor diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceEntity.java index 1c872823..2ad5d4bf 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceEntity.java @@ -20,13 +20,14 @@ package org.greenbuttonalliance.espi.common.domain.customer.entity; import jakarta.persistence.*; +import lombok.Delegate; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject; -import org.greenbuttonalliance.espi.common.domain.customer.common.ElectronicAddress; +import org.hibernate.proxy.HibernateProxy; -import java.math.BigDecimal; +import java.util.Objects; /** * Pure JPA/Hibernate entity for EndDevice without JAXB concerns. @@ -45,149 +46,87 @@ */ @Entity @Table(name = "end_devices") -@Inheritance(strategy = InheritanceType.JOINED) +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "end_device_related_links", + joinColumns = @JoinColumn(name = "end_device_id") + ) +) @Getter @Setter @NoArgsConstructor public class EndDeviceEntity extends IdentifiedObject { - // Asset fields (previously inherited from Asset superclass) - - /** - * Utility-specific classification of Asset and its subtypes, according to their corporate standards, - * practices, and existing IT systems (e.g., for management of assets, maintenance, work, outage, customers, etc.). - */ - @Column(name = "type", length = 256) - private String type; - - /** - * Uniquely tracked commodity (UTC) number. - */ - @Column(name = "utc_number", length = 256) - private String utcNumber; - - /** - * Serial number of this asset. - */ - @Column(name = "serial_number", length = 256) - private String serialNumber; - - /** - * Lot number for this asset. Even for the same model and version number, many assets are manufactured in lots. - */ - @Column(name = "lot_number", length = 256) - private String lotNumber; - - /** - * Purchase price of asset. - */ - @Column(name = "purchase_price") - private Long purchasePrice; - - /** - * True if asset is considered critical for some reason (for example, a pole with critical attachments). - */ - @Column(name = "critical") - private Boolean critical; - - /** - * Electronic address. - */ - @Embedded - @AttributeOverrides({ - @AttributeOverride(name = "lan", column = @Column(name = "end_device_lan")), - @AttributeOverride(name = "mac", column = @Column(name = "end_device_mac")), - @AttributeOverride(name = "email1", column = @Column(name = "end_device_email1")), - @AttributeOverride(name = "email2", column = @Column(name = "end_device_email2")), - @AttributeOverride(name = "web", column = @Column(name = "end_device_web")), - @AttributeOverride(name = "radio", column = @Column(name = "end_device_radio")), - @AttributeOverride(name = "userID", column = @Column(name = "end_device_user_id")), - @AttributeOverride(name = "password", column = @Column(name = "end_device_password")) - }) - private ElectronicAddress electronicAddress; - - /** - * Lifecycle dates for this asset. - */ - @Embedded - private Asset.LifecycleDate lifecycle; - - /** - * Information on acceptance test. - */ + // Asset fields (embedded component per NAESB ESPI 4.0 customer.xsd lines 643-713) @Embedded @AttributeOverrides({ - @AttributeOverride(name = "dateTime", column = @Column(name = "acceptance_test_date_time")), - @AttributeOverride(name = "success", column = @Column(name = "acceptance_test_success")), - @AttributeOverride(name = "type", column = @Column(name = "acceptance_test_type")) + @AttributeOverride(name = "electronicAddress.lan", column = @Column(name = "end_device_lan")), + @AttributeOverride(name = "electronicAddress.mac", column = @Column(name = "end_device_mac")), + @AttributeOverride(name = "electronicAddress.email1", column = @Column(name = "end_device_email1")), + @AttributeOverride(name = "electronicAddress.email2", column = @Column(name = "end_device_email2")), + @AttributeOverride(name = "electronicAddress.web", column = @Column(name = "end_device_web")), + @AttributeOverride(name = "electronicAddress.radio", column = @Column(name = "end_device_radio")), + @AttributeOverride(name = "electronicAddress.userID", column = @Column(name = "end_device_user_id")), + @AttributeOverride(name = "electronicAddress.password", column = @Column(name = "end_device_password")), + @AttributeOverride(name = "status.value", column = @Column(name = "status_value")), + @AttributeOverride(name = "status.dateTime", column = @Column(name = "status_date_time")), + @AttributeOverride(name = "status.remark", column = @Column(name = "status_remark")), + @AttributeOverride(name = "status.reason", column = @Column(name = "status_reason")), + @AttributeOverride(name = "acceptanceTest.dateTime", column = @Column(name = "acceptance_test_date_time")), + @AttributeOverride(name = "acceptanceTest.success", column = @Column(name = "acceptance_test_success")), + @AttributeOverride(name = "acceptanceTest.type", column = @Column(name = "acceptance_test_type")) }) - private Asset.AcceptanceTest acceptanceTest; - - /** - * Condition of asset in inventory or at time of installation. Examples include new, rebuilt, - * overhaul required, other. Refer to inspection data for information on the most current condition of the asset. - */ - @Column(name = "initial_condition", length = 256) - private String initialCondition; - - /** - * Whenever an asset is reconditioned, percentage of expected life for the asset when it was new; zero for new devices. - */ - @Column(name = "initial_loss_of_life") - private BigDecimal initialLossOfLife; + @Delegate(excludes = {Object.class}) + private Asset asset = new Asset(); - /** - * Status of this asset. - */ + // EndDevice-specific fields (per NAESB ESPI 4.0 customer.xsd lines 218-238) @Embedded - @AttributeOverride(name = "value", column = @Column(name = "status_value")) - @AttributeOverride(name = "dateTime", column = @Column(name = "status_date_time")) - @AttributeOverride(name = "remark", column = @Column(name = "status_remark")) - @AttributeOverride(name = "reason", column = @Column(name = "status_reason")) - private Status status; - - // AssetContainer fields (AssetContainer is simply an Asset that can contain other assets - no additional fields) - - // EndDevice specific fields - - /** - * If true, there is no physical device. As an example, a virtual meter can be defined to aggregate - * the consumption for two or more physical meters. Otherwise, this is a physical hardware device. - */ - @Column(name = "is_virtual") - private Boolean isVirtual; + @Delegate(excludes = {Object.class}) + private EndDeviceFields endDeviceFields = new EndDeviceFields(); - /** - * If true, this is a premises area network (PAN) device. - */ - @Column(name = "is_pan") - private Boolean isPan; - - /** - * Installation code. - */ - @Column(name = "install_code", length = 256) - private String installCode; - - /** - * Automated meter reading (AMR) or other communication system responsible for communications to this end device. - */ - @Column(name = "amr_system", length = 256) - private String amrSystem; @Override public final boolean equals(Object o) { if (this == o) return true; if (o == null) return false; - Class oEffectiveClass = o instanceof org.hibernate.proxy.HibernateProxy ? ((org.hibernate.proxy.HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); - Class thisEffectiveClass = this instanceof org.hibernate.proxy.HibernateProxy ? ((org.hibernate.proxy.HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); + Class oEffectiveClass = o instanceof HibernateProxy hibernateProxy ? hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : o.getClass(); + Class thisEffectiveClass = this instanceof HibernateProxy hibernateProxy ? hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : this.getClass(); if (thisEffectiveClass != oEffectiveClass) return false; EndDeviceEntity that = (EndDeviceEntity) o; - return getId() != null && java.util.Objects.equals(getId(), that.getId()); + return getId() != null && Objects.equals(getId(), that.getId()); } @Override public final int hashCode() { - return this instanceof org.hibernate.proxy.HibernateProxy ? ((org.hibernate.proxy.HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); + return this instanceof HibernateProxy hibernateProxy ? hibernateProxy.getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + + "id = " + getId() + ", " + + "description = " + getDescription() + ", " + + "created = " + getCreated() + ", " + + "updated = " + getUpdated() + ", " + + "published = " + getPublished() + ", " + + "upLink = " + getUpLink() + ", " + + "selfLink = " + getSelfLink() + ", " + + "type = " + getType() + ", " + + "utcNumber = " + getUtcNumber() + ", " + + "serialNumber = " + getSerialNumber() + ", " + + "lotNumber = " + getLotNumber() + ", " + + "purchasePrice = " + getPurchasePrice() + ", " + + "critical = " + getCritical() + ", " + + "electronicAddress = " + getElectronicAddress() + ", " + + "lifecycle = " + getLifecycle() + ", " + + "acceptanceTest = " + getAcceptanceTest() + ", " + + "initialCondition = " + getInitialCondition() + ", " + + "initialLossOfLife = " + getInitialLossOfLife() + ", " + + "status = " + getStatus() + ", " + + "isVirtual = " + getIsVirtual() + ", " + + "isPan = " + getIsPan() + ", " + + "installCode = " + getInstallCode() + ", " + + "amrSystem = " + getAmrSystem() + ")"; } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceFields.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceFields.java new file mode 100644 index 00000000..4bd3dc1e --- /dev/null +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/EndDeviceFields.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (c) 2025 Green Button Alliance, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.greenbuttonalliance.espi.common.domain.customer.entity; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.*; + +/** + * Embeddable component for EndDevice-specific fields. + * + * Per NAESB ESPI 4.0 customer.xsd, EndDevice extends AssetContainer and adds these 4 fields (lines 218-238). + * This embeddable component captures those EndDevice-specific fields for reuse in both EndDeviceEntity + * and MeterEntity. + */ +@Embeddable +@Data +@NoArgsConstructor +public class EndDeviceFields { + + /** + * If true, there is no physical device. As an example, a virtual meter can be defined + * to aggregate the consumption for two or more physical meters. Otherwise, this is a + * physical hardware device. + */ + @Column(name = "is_virtual") + private Boolean isVirtual; + + /** + * If true, this is a premises area network (PAN) device. + */ + @Column(name = "is_pan") + private Boolean isPan; + + /** + * Installation code. + */ + @Column(name = "install_code", length = 256) + private String installCode; + + /** + * Automated meter reading (AMR) or other communication system responsible for + * communications to this end device. + */ + @Column(name = "amr_system", length = 256) + private String amrSystem; +} diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/MeterEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/MeterEntity.java index 2edd1495..76c31384 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/MeterEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/MeterEntity.java @@ -20,9 +20,11 @@ package org.greenbuttonalliance.espi.common.domain.customer.entity; import jakarta.persistence.*; +import lombok.Delegate; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject; import org.greenbuttonalliance.espi.common.domain.customer.common.MeterMultiplier; import org.hibernate.proxy.HibernateProxy; @@ -31,16 +33,53 @@ /** * Pure JPA/Hibernate entity for Meter without JAXB concerns. - * - * Physical asset that performs the metering role of the usage point. + * + * Physical asset that performs the metering role of the usage point. * Used for measuring consumption and detection of events. + * + * Per NAESB ESPI 4.0 customer.xsd, Meter extends EndDevice (lines 243-259). + * Implementation uses composition: embeds Asset + EndDeviceFields instead of inheritance. */ @Entity @Table(name = "meters") +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "meter_related_links", + joinColumns = @JoinColumn(name = "meter_id") + ) +) @Getter @Setter @NoArgsConstructor -public class MeterEntity extends EndDeviceEntity { +public class MeterEntity extends IdentifiedObject { + + // Asset fields (embedded component per NAESB ESPI 4.0 customer.xsd lines 643-713) + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "electronicAddress.lan", column = @Column(name = "end_device_lan")), + @AttributeOverride(name = "electronicAddress.mac", column = @Column(name = "end_device_mac")), + @AttributeOverride(name = "electronicAddress.email1", column = @Column(name = "end_device_email1")), + @AttributeOverride(name = "electronicAddress.email2", column = @Column(name = "end_device_email2")), + @AttributeOverride(name = "electronicAddress.web", column = @Column(name = "end_device_web")), + @AttributeOverride(name = "electronicAddress.radio", column = @Column(name = "end_device_radio")), + @AttributeOverride(name = "electronicAddress.userID", column = @Column(name = "end_device_user_id")), + @AttributeOverride(name = "electronicAddress.password", column = @Column(name = "end_device_password")), + @AttributeOverride(name = "status.value", column = @Column(name = "status_value")), + @AttributeOverride(name = "status.dateTime", column = @Column(name = "status_date_time")), + @AttributeOverride(name = "status.remark", column = @Column(name = "status_remark")), + @AttributeOverride(name = "status.reason", column = @Column(name = "status_reason")), + @AttributeOverride(name = "acceptanceTest.dateTime", column = @Column(name = "acceptance_test_date_time")), + @AttributeOverride(name = "acceptanceTest.success", column = @Column(name = "acceptance_test_success")), + @AttributeOverride(name = "acceptanceTest.type", column = @Column(name = "acceptance_test_type")) + }) + @Delegate(excludes = {Object.class}) + private Asset asset = new Asset(); + + // EndDevice-specific fields (per NAESB ESPI 4.0 customer.xsd lines 218-238) + @Embedded + @Delegate(excludes = {Object.class}) + private EndDeviceFields endDeviceFields = new EndDeviceFields(); /** * Meter form designation per ANSI C12.10 or other applicable standard. @@ -66,6 +105,22 @@ public class MeterEntity extends EndDeviceEntity { @Column(name = "interval_length") private Long intervalLength; + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = o instanceof HibernateProxy hibernateProxy ? hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : o.getClass(); + Class thisEffectiveClass = this instanceof HibernateProxy hibernateProxy ? hibernateProxy.getHibernateLazyInitializer().getPersistentClass() : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + MeterEntity that = (MeterEntity) o; + return getId() != null && Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy hibernateProxy ? hibernateProxy.getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); + } + @Override public String toString() { return getClass().getSimpleName() + "(" + diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ProgramDateIdMappingsEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ProgramDateIdMappingsEntity.java index 5ea6a2f5..a31b735c 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ProgramDateIdMappingsEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ProgramDateIdMappingsEntity.java @@ -21,8 +21,10 @@ import lombok.*; import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject; +import org.hibernate.proxy.HibernateProxy; import jakarta.persistence.*; +import java.util.Objects; /** * Pure JPA/Hibernate entity for ProgramDateIdMappings without JAXB concerns. @@ -31,6 +33,13 @@ */ @Entity @Table(name = "program_date_id_mappings") +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "program_date_id_mapping_related_links", + joinColumns = @JoinColumn(name = "program_date_id_mapping_id") + ) +) @Getter @Setter @NoArgsConstructor diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java index dd7d1a27..b6fc5bff 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceLocationEntity.java @@ -41,6 +41,13 @@ */ @Entity @Table(name = "service_locations") +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "service_location_related_links", + joinColumns = @JoinColumn(name = "service_location_id") + ) +) @Getter @Setter @NoArgsConstructor @@ -233,21 +240,24 @@ public final int hashCode() { public String toString() { return getClass().getSimpleName() + "(" + "id = " + getId() + ", " + + "description = " + getDescription() + ", " + + "created = " + getCreated() + ", " + + "updated = " + getUpdated() + ", " + + "published = " + getPublished() + ", " + + "upLink = " + getUpLink() + ", " + + "selfLink = " + getSelfLink() + ", " + "type = " + getType() + ", " + "mainAddress = " + getMainAddress() + ", " + "secondaryAddress = " + getSecondaryAddress() + ", " + "electronicAddress = " + getElectronicAddress() + ", " + - "geoInfoReference = " + getGeoInfoReference() + ", " + - "direction = " + getDirection() + ", " + "status = " + getStatus() + ", " + + "phone1 = " + getPhone1() + ", " + + "phone2 = " + getPhone2() + ", " + "accessMethod = " + getAccessMethod() + ", " + "siteAccessProblem = " + getSiteAccessProblem() + ", " + "needsInspection = " + getNeedsInspection() + ", " + - "usagePointHrefs = " + getUsagePointHrefs() + ", " + - "outageBlock = " + getOutageBlock() + ", " + - "description = " + getDescription() + ", " + - "created = " + getCreated() + ", " + - "updated = " + getUpdated() + ", " + - "published = " + getPublished() + ")"; + "direction = " + getDirection() + ", " + + "geoInfoReference = " + getGeoInfoReference() + ", " + + "outageBlock = " + getOutageBlock() + ")"; } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceSupplierEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceSupplierEntity.java index a8ef8ff4..f7560a70 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceSupplierEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/ServiceSupplierEntity.java @@ -27,6 +27,7 @@ import org.hibernate.proxy.HibernateProxy; import java.time.OffsetDateTime; +import java.util.List; import java.util.Objects; /** @@ -36,6 +37,13 @@ */ @Entity @Table(name = "service_suppliers") +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "service_supplier_related_links", + joinColumns = @JoinColumn(name = "service_supplier_id") + ) +) @Getter @Setter @NoArgsConstructor @@ -125,13 +133,15 @@ public final int hashCode() { public String toString() { return getClass().getSimpleName() + "(" + "id = " + getId() + ", " + - "organisation = " + getOrganisation() + ", " + - "kind = " + getKind() + ", " + - "issuerIdentificationNumber = " + getIssuerIdentificationNumber() + ", " + - "effectiveDate = " + getEffectiveDate() + ", " + "description = " + getDescription() + ", " + "created = " + getCreated() + ", " + "updated = " + getUpdated() + ", " + - "published = " + getPublished() + ")"; + "published = " + getPublished() + ", " + + "upLink = " + getUpLink() + ", " + + "selfLink = " + getSelfLink() + ", " + + "kind = " + getKind() + ", " + + "issuerIdentificationNumber = " + getIssuerIdentificationNumber() + ", " + + "effectiveDate = " + getEffectiveDate() + ", " + + "organisation = " + getOrganisation() + ")"; } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementEntity.java index 0be89e77..afd1e57f 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementEntity.java @@ -36,6 +36,13 @@ */ @Entity @Table(name = "statements") +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "statement_related_links", + joinColumns = @JoinColumn(name = "statement_id") + ) +) @Getter @Setter @NoArgsConstructor diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ApplicationInformationEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ApplicationInformationEntity.java index f17bdaec..908085fa 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ApplicationInformationEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ApplicationInformationEntity.java @@ -44,6 +44,13 @@ */ @Entity @Table(name = "application_information") +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "application_information_related_links", + joinColumns = @JoinColumn(name = "application_information_id") + ) +) @Getter @Setter @NoArgsConstructor diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/AuthorizationEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/AuthorizationEntity.java index 3ef02461..252c89e0 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/AuthorizationEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/AuthorizationEntity.java @@ -44,6 +44,13 @@ @Index(name = "idx_authorization_state", columnList = "state"), @Index(name = "idx_authorization_resource_uri", columnList = "resource_uri") }) +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "authorization_related_links", + joinColumns = @JoinColumn(name = "authorization_id") + ) +) @Getter @Setter @NoArgsConstructor diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ElectricPowerQualitySummaryEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ElectricPowerQualitySummaryEntity.java index ee0f0a1d..451b6780 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ElectricPowerQualitySummaryEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ElectricPowerQualitySummaryEntity.java @@ -39,6 +39,13 @@ */ @Entity @Table(name = "electric_power_quality_summaries") +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "electric_power_quality_summary_related_links", + joinColumns = @JoinColumn(name = "electric_power_quality_summary_id") + ) +) @Getter @Setter @NoArgsConstructor diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ReadingTypeEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ReadingTypeEntity.java index 258871da..aaac3ddb 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ReadingTypeEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ReadingTypeEntity.java @@ -41,6 +41,13 @@ */ @Entity @Table(name = "reading_types") +@AssociationOverride( + name = "relatedLinks", + joinTable = @JoinTable( + name = "reading_type_related_links", + joinColumns = @JoinColumn(name = "reading_type_id") + ) +) @Getter @Setter @NoArgsConstructor diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/EndDeviceMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/EndDeviceMapper.java index 1b2522bd..4a1a8eef 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/EndDeviceMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/EndDeviceMapper.java @@ -53,24 +53,24 @@ public interface EndDeviceMapper { * @param entity the end device entity * @return the end device DTO */ - // Asset fields (12) - @Mapping(target = "type", source = "type") - @Mapping(target = "utcNumber", source = "utcNumber") - @Mapping(target = "serialNumber", source = "serialNumber") - @Mapping(target = "lotNumber", source = "lotNumber") - @Mapping(target = "purchasePrice", source = "purchasePrice") - @Mapping(target = "critical", source = "critical") - @Mapping(target = "electronicAddress", source = "electronicAddress") - @Mapping(target = "lifecycle", source = "lifecycle") - @Mapping(target = "acceptanceTest", source = "acceptanceTest") - @Mapping(target = "initialCondition", source = "initialCondition") - @Mapping(target = "initialLossOfLife", source = "initialLossOfLife") - @Mapping(target = "status", source = "status") - // EndDevice fields (4) - @Mapping(target = "isVirtual", source = "isVirtual") - @Mapping(target = "isPan", source = "isPan") - @Mapping(target = "installCode", source = "installCode") - @Mapping(target = "amrSystem", source = "amrSystem") + // Asset fields (12) - mapped from embedded asset + @Mapping(target = "type", source = "asset.type") + @Mapping(target = "utcNumber", source = "asset.utcNumber") + @Mapping(target = "serialNumber", source = "asset.serialNumber") + @Mapping(target = "lotNumber", source = "asset.lotNumber") + @Mapping(target = "purchasePrice", source = "asset.purchasePrice") + @Mapping(target = "critical", source = "asset.critical") + @Mapping(target = "electronicAddress", source = "asset.electronicAddress") + @Mapping(target = "lifecycle", source = "asset.lifecycle") + @Mapping(target = "acceptanceTest", source = "asset.acceptanceTest") + @Mapping(target = "initialCondition", source = "asset.initialCondition") + @Mapping(target = "initialLossOfLife", source = "asset.initialLossOfLife") + @Mapping(target = "status", source = "asset.status") + // EndDevice fields (4) - mapped from embedded endDeviceFields + @Mapping(target = "isVirtual", source = "endDeviceFields.isVirtual") + @Mapping(target = "isPan", source = "endDeviceFields.isPan") + @Mapping(target = "installCode", source = "endDeviceFields.installCode") + @Mapping(target = "amrSystem", source = "endDeviceFields.amrSystem") EndDeviceDto toDto(EndDeviceEntity entity); /** @@ -80,24 +80,24 @@ public interface EndDeviceMapper { * @param dto the end device DTO * @return the end device entity */ - // Asset fields (12) - @Mapping(target = "type", source = "type") - @Mapping(target = "utcNumber", source = "utcNumber") - @Mapping(target = "serialNumber", source = "serialNumber") - @Mapping(target = "lotNumber", source = "lotNumber") - @Mapping(target = "purchasePrice", source = "purchasePrice") - @Mapping(target = "critical", source = "critical") - @Mapping(target = "electronicAddress", source = "electronicAddress") - @Mapping(target = "lifecycle", source = "lifecycle") - @Mapping(target = "acceptanceTest", source = "acceptanceTest") - @Mapping(target = "initialCondition", source = "initialCondition") - @Mapping(target = "initialLossOfLife", source = "initialLossOfLife") - @Mapping(target = "status", source = "status") - // EndDevice fields (4) - @Mapping(target = "isVirtual", source = "isVirtual") - @Mapping(target = "isPan", source = "isPan") - @Mapping(target = "installCode", source = "installCode") - @Mapping(target = "amrSystem", source = "amrSystem") + // Asset fields (12) - mapped to embedded asset + @Mapping(target = "asset.type", source = "type") + @Mapping(target = "asset.utcNumber", source = "utcNumber") + @Mapping(target = "asset.serialNumber", source = "serialNumber") + @Mapping(target = "asset.lotNumber", source = "lotNumber") + @Mapping(target = "asset.purchasePrice", source = "purchasePrice") + @Mapping(target = "asset.critical", source = "critical") + @Mapping(target = "asset.electronicAddress", source = "electronicAddress") + @Mapping(target = "asset.lifecycle", source = "lifecycle") + @Mapping(target = "asset.acceptanceTest", source = "acceptanceTest") + @Mapping(target = "asset.initialCondition", source = "initialCondition") + @Mapping(target = "asset.initialLossOfLife", source = "initialLossOfLife") + @Mapping(target = "asset.status", source = "status") + // EndDevice fields (4) - mapped to embedded endDeviceFields + @Mapping(target = "endDeviceFields.isVirtual", source = "isVirtual") + @Mapping(target = "endDeviceFields.isPan", source = "isPan") + @Mapping(target = "endDeviceFields.installCode", source = "installCode") + @Mapping(target = "endDeviceFields.amrSystem", source = "amrSystem") // IdentifiedObject fields (inherited) - handled by Atom layer, ignore during mapping @Mapping(target = "description", ignore = true) @Mapping(target = "created", ignore = true) diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/MeterMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/MeterMapper.java index 9623cfb0..c187857f 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/MeterMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/customer/MeterMapper.java @@ -56,24 +56,24 @@ public interface MeterMapper { * @param entity the meter entity * @return the meter DTO */ - // Asset fields (12) - customer.xsd lines 650-709 - @Mapping(target = "type", source = "type") - @Mapping(target = "utcNumber", source = "utcNumber") - @Mapping(target = "serialNumber", source = "serialNumber") - @Mapping(target = "lotNumber", source = "lotNumber") - @Mapping(target = "purchasePrice", source = "purchasePrice") - @Mapping(target = "critical", source = "critical") - @Mapping(target = "electronicAddress", source = "electronicAddress") - @Mapping(target = "lifecycle", source = "lifecycle") - @Mapping(target = "acceptanceTest", source = "acceptanceTest") - @Mapping(target = "initialCondition", source = "initialCondition") - @Mapping(target = "initialLossOfLife", source = "initialLossOfLife") - @Mapping(target = "status", source = "status") - // EndDevice fields (4) - customer.xsd lines 219-238 - @Mapping(target = "isVirtual", source = "isVirtual") - @Mapping(target = "isPan", source = "isPan") - @Mapping(target = "installCode", source = "installCode") - @Mapping(target = "amrSystem", source = "amrSystem") + // Asset fields (12) - mapped from embedded asset + @Mapping(target = "type", source = "asset.type") + @Mapping(target = "utcNumber", source = "asset.utcNumber") + @Mapping(target = "serialNumber", source = "asset.serialNumber") + @Mapping(target = "lotNumber", source = "asset.lotNumber") + @Mapping(target = "purchasePrice", source = "asset.purchasePrice") + @Mapping(target = "critical", source = "asset.critical") + @Mapping(target = "electronicAddress", source = "asset.electronicAddress") + @Mapping(target = "lifecycle", source = "asset.lifecycle") + @Mapping(target = "acceptanceTest", source = "asset.acceptanceTest") + @Mapping(target = "initialCondition", source = "asset.initialCondition") + @Mapping(target = "initialLossOfLife", source = "asset.initialLossOfLife") + @Mapping(target = "status", source = "asset.status") + // EndDevice fields (4) - mapped from embedded endDeviceFields + @Mapping(target = "isVirtual", source = "endDeviceFields.isVirtual") + @Mapping(target = "isPan", source = "endDeviceFields.isPan") + @Mapping(target = "installCode", source = "endDeviceFields.installCode") + @Mapping(target = "amrSystem", source = "endDeviceFields.amrSystem") // Meter fields (3) - customer.xsd lines 250-264 @Mapping(target = "formNumber", source = "formNumber") @Mapping(target = "meterMultipliers", ignore = true) // TODO: Implement when MeterMultiplierEntity exists @@ -87,24 +87,24 @@ public interface MeterMapper { * @param dto the meter DTO * @return the meter entity */ - // Asset fields (12) - customer.xsd lines 650-709 - @Mapping(target = "type", source = "type") - @Mapping(target = "utcNumber", source = "utcNumber") - @Mapping(target = "serialNumber", source = "serialNumber") - @Mapping(target = "lotNumber", source = "lotNumber") - @Mapping(target = "purchasePrice", source = "purchasePrice") - @Mapping(target = "critical", source = "critical") - @Mapping(target = "electronicAddress", source = "electronicAddress") - @Mapping(target = "lifecycle", source = "lifecycle") - @Mapping(target = "acceptanceTest", source = "acceptanceTest") - @Mapping(target = "initialCondition", source = "initialCondition") - @Mapping(target = "initialLossOfLife", source = "initialLossOfLife") - @Mapping(target = "status", source = "status") - // EndDevice fields (4) - customer.xsd lines 219-238 - @Mapping(target = "isVirtual", source = "isVirtual") - @Mapping(target = "isPan", source = "isPan") - @Mapping(target = "installCode", source = "installCode") - @Mapping(target = "amrSystem", source = "amrSystem") + // Asset fields (12) - mapped to embedded asset + @Mapping(target = "asset.type", source = "type") + @Mapping(target = "asset.utcNumber", source = "utcNumber") + @Mapping(target = "asset.serialNumber", source = "serialNumber") + @Mapping(target = "asset.lotNumber", source = "lotNumber") + @Mapping(target = "asset.purchasePrice", source = "purchasePrice") + @Mapping(target = "asset.critical", source = "critical") + @Mapping(target = "asset.electronicAddress", source = "electronicAddress") + @Mapping(target = "asset.lifecycle", source = "lifecycle") + @Mapping(target = "asset.acceptanceTest", source = "acceptanceTest") + @Mapping(target = "asset.initialCondition", source = "initialCondition") + @Mapping(target = "asset.initialLossOfLife", source = "initialLossOfLife") + @Mapping(target = "asset.status", source = "status") + // EndDevice fields (4) - mapped to embedded endDeviceFields + @Mapping(target = "endDeviceFields.isVirtual", source = "isVirtual") + @Mapping(target = "endDeviceFields.isPan", source = "isPan") + @Mapping(target = "endDeviceFields.installCode", source = "installCode") + @Mapping(target = "endDeviceFields.amrSystem", source = "amrSystem") // Meter fields (3) - customer.xsd lines 250-264 @Mapping(target = "formNumber", source = "formNumber") // meterMultipliers not mapped - collection commented out in entity (TODO) diff --git a/openespi-common/src/main/resources/db/migration/V1__Create_Base_Tables.sql b/openespi-common/src/main/resources/db/migration/V1__Create_Base_Tables.sql index 264b38c0..a7679257 100644 --- a/openespi-common/src/main/resources/db/migration/V1__Create_Base_Tables.sql +++ b/openespi-common/src/main/resources/db/migration/V1__Create_Base_Tables.sql @@ -111,7 +111,9 @@ CREATE INDEX idx_application_updated ON application_information (updated); CREATE TABLE application_information_related_links ( application_information_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (application_information_id) REFERENCES application_information (id) ON DELETE CASCADE ); @@ -205,7 +207,9 @@ CREATE INDEX idx_authorization_updated ON authorizations (updated); CREATE TABLE authorization_related_links ( authorization_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (authorization_id) REFERENCES authorizations (id) ON DELETE CASCADE ); @@ -259,7 +263,9 @@ CREATE INDEX idx_reading_type_updated ON reading_types (updated); CREATE TABLE reading_type_related_links ( reading_type_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (reading_type_id) REFERENCES reading_types (id) ON DELETE CASCADE ); 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 29ed995a..27dbac3e 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 @@ -195,8 +195,10 @@ CREATE INDEX idx_customer_updated ON customers (updated); -- Related Links Table for Customers CREATE TABLE customer_related_links ( - customer_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + customer_id CHAR(36) NOT NULL, + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (customer_id) REFERENCES customers (id) ON DELETE CASCADE ); @@ -268,7 +270,9 @@ CREATE INDEX idx_customer_agreement_updated ON customer_agreements (updated); CREATE TABLE customer_agreement_related_links ( customer_agreement_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (customer_agreement_id) REFERENCES customer_agreements (id) ON DELETE CASCADE ); @@ -396,7 +400,9 @@ CREATE INDEX idx_customer_account_updated ON customer_accounts (updated); CREATE TABLE customer_account_related_links ( customer_account_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (customer_account_id) REFERENCES customer_accounts (id) ON DELETE CASCADE ); @@ -466,7 +472,9 @@ CREATE INDEX idx_epqs_updated ON electric_power_quality_summaries (updated); CREATE TABLE electric_power_quality_summary_related_links ( electric_power_quality_summary_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (electric_power_quality_summary_id) REFERENCES electric_power_quality_summaries (id) ON DELETE CASCADE ); @@ -535,7 +543,9 @@ CREATE INDEX idx_end_device_updated ON end_devices (updated); CREATE TABLE end_device_related_links ( end_device_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (end_device_id) REFERENCES end_devices (id) ON DELETE CASCADE ); @@ -548,16 +558,65 @@ CREATE INDEX idx_end_device_related_links ON end_device_related_links (end_devic -- db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql -- db/vendor/h2/V2__H2_Specific_Tables.sql --- Meter Entity Table (Joined inheritance from EndDevice) +-- Meter Entity Table (extends IdentifiedObject, embeds Asset + EndDeviceFields) +-- Per customer.xsd, Meter extends EndDevice which extends Asset +-- Implementation uses composition: Meter embeds Asset + EndDeviceFields as @Embeddable CREATE TABLE meters ( - id CHAR(36) PRIMARY KEY, - form_number VARCHAR(256), - interval_length BIGINT, - FOREIGN KEY (id) REFERENCES end_devices (id) ON DELETE CASCADE -); + id CHAR(36) PRIMARY KEY , + description VARCHAR(255), + 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), + + -- Asset fields (embedded from Asset.java) - Same structure as end_devices + type VARCHAR(50), + utc_number VARCHAR(100), + serial_number VARCHAR(100), + 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, + received_date TIMESTAMP, + retirement_date TIMESTAMP, + removal_date TIMESTAMP, + acceptance_test_date_time TIMESTAMP, + acceptance_test_success BOOLEAN, + acceptance_test_type VARCHAR(255), + initial_condition VARCHAR(255), + initial_loss_of_life DECIMAL(5, 2), + status_value VARCHAR(256), + status_date_time TIMESTAMP, + status_remark VARCHAR(256), + status_reason VARCHAR(256), + + -- EndDevice fields (embedded from EndDeviceFields.java) + is_virtual BOOLEAN DEFAULT FALSE, + is_pan BOOLEAN DEFAULT FALSE, + install_code VARCHAR(255), + amr_system VARCHAR(100), -CREATE INDEX idx_meters_form_number ON meters (form_number); + -- Meter-specific fields (customer.xsd Meter lines 250-264) + form_number VARCHAR(256), + interval_length BIGINT +); -- Meter Multipliers Collection Table (@ElementCollection for MeterEntity.meterMultipliers) -- Per customer.xsd MeterMultiplier type (embedded collection) @@ -574,8 +633,10 @@ CREATE INDEX idx_meter_multipliers_meter_id ON meter_multipliers (meter_id); -- Related Links Table for Meters CREATE TABLE meter_related_links ( - meter_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + meter_id CHAR(36) NOT NULL, + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (meter_id) REFERENCES meters (id) ON DELETE CASCADE ); @@ -645,7 +706,9 @@ CREATE INDEX idx_program_date_id_mapping_updated ON program_date_id_mappings (up CREATE TABLE program_date_id_mapping_related_links ( program_date_id_mapping_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (program_date_id_mapping_id) REFERENCES program_date_id_mappings (id) ON DELETE CASCADE ); @@ -740,7 +803,9 @@ CREATE INDEX idx_service_location_updated ON service_locations (updated); CREATE TABLE service_location_related_links ( service_location_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (service_location_id) REFERENCES service_locations (id) ON DELETE CASCADE ); @@ -828,7 +893,9 @@ CREATE INDEX idx_service_supplier_updated ON service_suppliers (updated); CREATE TABLE service_supplier_related_links ( service_supplier_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (service_supplier_id) REFERENCES service_suppliers (id) ON DELETE CASCADE ); @@ -869,8 +936,10 @@ CREATE INDEX idx_statement_updated ON statements (updated); -- Related Links Table for Statements CREATE TABLE statement_related_links ( - statement_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), + statement_id CHAR(36) NOT NULL, + rel VARCHAR(255), + href VARCHAR(1024), + link_type VARCHAR(255), FOREIGN KEY (statement_id) REFERENCES statements (id) ON DELETE CASCADE ); From f4482416c8baf4b2ab061bf0fa8924e4baf079dc Mon Sep 17 00:00:00 2001 From: "Donald F. Coffin" Date: Sun, 1 Feb 2026 00:16:25 -0500 Subject: [PATCH 3/3] docs: Add toString() method sequencing guideline to CLAUDE.md Added JPA mapping guideline for entity toString() method sequencing to ensure consistency with database schema definitions. Guideline enforces that toString() methods must follow exact database field sequence from Flyway migration scripts: - Standard sequence: id, description, created, updated, published, upLink, selfLink, [type-specific fields in database column order], relatedLinks - Ensures toString() output matches CREATE TABLE statement column order - Improves debugging and log readability by maintaining schema alignment This guideline supports Issue #97 relatedLinks standardization work where consistent field ordering across entities is critical. Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index db7980d0..642ca0ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -288,6 +288,7 @@ This practice prevents issues like missing imports, broken references, and compi - Avoid `@Where` annotation conflicts with `@JoinColumn` on the same field - Be careful with `@ElementCollection` and `@CollectionTable` for embedded collections - Phone numbers and addresses are embedded collections using `@ElementCollection` +- **toString() method sequencing**: Entity toString() methods MUST follow the exact database field sequence from Flyway migration scripts. Standard sequence is: id, description, created, updated, published, upLink, selfLink, [type-specific fields in database column order], relatedLinks (if present as last field). Always verify toString() matches the CREATE TABLE statement column order. #### REST Controller Development - Controllers in `openespi-datacustodian` implement ESPI REST API