diff --git a/openespi-common/PHASE_A_ANALYSIS_FINDINGS.md b/openespi-common/PHASE_A_ANALYSIS_FINDINGS.md new file mode 100644 index 00000000..9eb7aa24 --- /dev/null +++ b/openespi-common/PHASE_A_ANALYSIS_FINDINGS.md @@ -0,0 +1,380 @@ +# Phase A: Schema Compliance Analysis Findings + +**Date**: 2026-01-06 +**Branch**: `fix/schema-compliance-analysis` +**Related Issue**: #28 +**Related Plan**: SCHEMA_COMPLIANCE_REMEDIATION_PLAN.md + +--- + +## Executive Summary + +Analysis of 11 entities reveals **ALL correctly do NOT extend IdentifiedObject**. This is good news - the Java entity layer already reflects proper XSD compliance. However, **11 incorrect `related_links` database tables** still exist and must be removed. + +**Key Finding**: The entity Java files have already been corrected (likely in previous work), but the database schema (Flyway migrations) has not been updated to match. + +--- + +## Entity Analysis Results + +### Category 1: XSD Object-Based Entities (7 entities) + +These entities extend `Object` in XSD, not `IdentifiedObject`. They are element collections within parent resources. + +| # | Entity | Current Java Inheritance | Database Table | Related Links Table | XSD Reference | Status | +|---|--------|--------------------------|----------------|---------------------|---------------|--------| +| 1 | **IntervalReadingEntity** | Does NOT extend IdentifiedObject ✅ | `interval_readings` | `interval_reading_related_links` ❌ | espi.xsd:1016 extends Object | Entity ✅ DB ❌ | +| 2 | **ReadingQualityEntity** | Does NOT extend IdentifiedObject ✅ | `reading_qualities` | `reading_quality_related_links` ❌ | espi.xsd:1062 extends Object | Entity ✅ DB ❌ | +| 3 | **PnodeRefEntity** | **EXTENDS IdentifiedObject** ❌ | `pnode_refs` | `pnode_ref_related_links` ❌ | espi.xsd:1539 extends Object | Entity ❌ DB ❌ | +| 4 | **AggregatedNodeRefEntity** | Does NOT extend IdentifiedObject ✅ | `aggregated_node_refs` | `aggregated_node_ref_related_links` ❌ | espi.xsd:1570 extends Object | Entity ✅ DB ❌ | +| 5 | **LineItemEntity** | Does NOT extend IdentifiedObject ✅ | `line_items` | `line_item_related_links` ❌ | espi.xsd:1444 extends Object | Entity ✅ DB ❌ | +| 6 | **ServiceDeliveryPointEntity** | **EXTENDS IdentifiedObject** ❌ | `service_delivery_points` | `service_delivery_point_related_links` ❌ | espi.xsd:1161 extends Object | Entity ❌ DB ❌ | +| 7 | **StatementRefEntity** | Does NOT extend IdentifiedObject ✅ | `statement_refs` | `statement_ref_related_links` ❌ | customer.xsd:285 extends Object | Entity ✅ DB ❌ | + +**Critical Issue Discovered**: +- ✅ **5 entities correctly do NOT extend IdentifiedObject**: IntervalReading, ReadingQuality, AggregatedNodeRef, LineItem, StatementRef +- ❌ **2 entities INCORRECTLY extend IdentifiedObject**: PnodeRef, ServiceDeliveryPoint + +--- + +### Category 2: Custom Entities Using Direct FK References (2 entities) + +| # | Entity | Current Java Inheritance | Database Table | Related Links Table | Purpose | Status | +|---|--------|--------------------------|----------------|---------------------|---------|--------| +| 8 | **RetailCustomerEntity** | **EXTENDS IdentifiedObject** ✅ | `retail_customers` | `retail_customer_related_links` ❌ | Authentication/authorization bridge | Entity ✅ DB ❌ | +| 9 | **SubscriptionEntity** | **EXTENDS IdentifiedObject** ✅ | `subscriptions` | `subscription_related_links` ❌ | OAuth2 access token authorization | Entity ✅ DB ❌ | + +**Note**: RetailCustomer and Subscription correctly extend IdentifiedObject (they are API entities with identity), but they use **direct FK relationships** instead of Atom `rel="related"` links. The `related_links` tables are unnecessary. + +--- + +### Category 3: Element Wrappers (2 entities) + +| # | Entity | Current Java Inheritance | Database Table | Related Links Table | XSD Type | Status | +|---|--------|--------------------------|----------------|---------------------|----------|--------| +| 10 | **PhoneNumberEntity** | Does NOT extend IdentifiedObject ✅ | `phone_numbers` | `phone_number_related_links` ❌ | NOT in XSD (custom) | Entity ✅ DB ❌ | +| 11 | **BatchListEntity** | Does NOT extend IdentifiedObject ✅ | `batch_lists` | `batch_list_related_links` ❌ | BatchListType (sequence) | Entity ✅ DB ❌ | + +--- + +## Detailed Entity Findings + +### 1. IntervalReadingEntity ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/usage/IntervalReadingEntity.java` + +**Current State**: +- ✅ Does NOT extend IdentifiedObject +- ✅ Has UUID primary key (`@GeneratedValue(strategy = GenerationType.UUID)`) +- ✅ Has `@ManyToOne` relationship to IntervalBlock (line 112: `private IntervalBlockEntity intervalBlock;`) +- ✅ Has `@OneToMany` relationship to ReadingQuality collection (line 125: `List readingQualities`) +- ✅ Javadoc correctly states: "Note: IntervalReading does NOT extend IdentifiedObject per ESPI 4.0 specification" (line 45) + +**Issues**: +- ❌ Database has `interval_reading_related_links` table (should not exist) + +**Required Changes**: +- Remove `interval_reading_related_links` table from Flyway migration +- Verify DTO does not have Atom link elements +- Verify no services attempt to access IntervalReading as standalone resource + +--- + +### 2. ReadingQualityEntity ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ReadingQualityEntity.java` + +**Current State**: +- ✅ Does NOT extend IdentifiedObject +- ✅ Has UUID primary key +- ✅ Has `@ManyToOne` relationship to IntervalReading (line 98: `private IntervalReadingEntity intervalReading;`) +- ✅ Javadoc correctly states: "Note: ReadingQuality does NOT extend IdentifiedObject per ESPI 4.0 specification" (line 44) +- ✅ Has quality constants and validation methods + +**Issues**: +- ❌ Database has `reading_quality_related_links` table (should not exist) + +**Required Changes**: +- Remove `reading_quality_related_links` table from Flyway migration +- Verify DTO does not have Atom link elements + +--- + +### 3. PnodeRefEntity ❌ CRITICAL + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/usage/PnodeRefEntity.java` + +**Current State**: +- ❌ **EXTENDS IdentifiedObject** (line 40: `public class PnodeRefEntity extends IdentifiedObject`) +- Has UUID primary key (inherited from IdentifiedObject) +- Has `@ManyToOne` relationship to UsagePoint (line 74: `private UsagePointEntity usagePoint;`) +- Has fields: apnodeType, ref, startEffectiveDate, endEffectiveDate + +**Issues**: +- ❌ **INCORRECTLY extends IdentifiedObject** - should extend Object per espi.xsd:1539 +- ❌ Database has `pnode_ref_related_links` table (should not exist) +- ❌ Inherits selfLink, upLink, relatedLinks from IdentifiedObject + +**Required Changes**: +- **Remove `extends IdentifiedObject`** from entity class +- Add explicit UUID primary key field (no longer inherited) +- Remove IdentifiedObject-specific methods (getSelfHref, getUpHref, etc.) +- Remove `pnode_ref_related_links` table from Flyway migration +- Update DTO to remove Atom link elements +- Update mapper to handle non-IdentifiedObject mapping + +--- + +### 4. AggregatedNodeRefEntity ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/usage/AggregatedNodeRefEntity.java` + +**Current State**: +- ✅ Does NOT extend IdentifiedObject +- ✅ Has UUID primary key explicitly defined (line 56: `private UUID id;`) +- ✅ Has `@ManyToOne` relationship to PnodeRef (line 91: `private PnodeRefEntity pnodeRef;`) +- ✅ Has `@ManyToOne` relationship to UsagePoint (line 99: `private UsagePointEntity usagePoint;`) +- ✅ Javadoc correctly states: "Note: AggregatedNodeRef does NOT extend IdentifiedObject per ESPI 4.0 specification" (line 39) + +**Issues**: +- ❌ Database has `aggregated_node_ref_related_links` table (should not exist) + +**Required Changes**: +- Remove `aggregated_node_ref_related_links` table from Flyway migration +- Verify DTO does not have Atom link elements + +--- + +### 5. LineItemEntity ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/usage/LineItemEntity.java` + +**Current State**: +- ✅ Does NOT extend IdentifiedObject +- ✅ Has UUID primary key +- ✅ Has `@ManyToOne` relationship to UsageSummary (line 109: `private UsageSummaryEntity usageSummary;`) +- ✅ Javadoc correctly states: "Note: LineItem does NOT extend IdentifiedObject per ESPI 4.0 specification" (line 48) +- ✅ Has extensive utility methods for currency conversions + +**Issues**: +- ❌ Database has `line_item_related_links` table (should not exist) + +**Required Changes**: +- Remove `line_item_related_links` table from Flyway migration +- Verify DTO does not have Atom link elements + +--- + +### 6. ServiceDeliveryPointEntity ❌ CRITICAL + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ServiceDeliveryPointEntity.java` + +**Current State**: +- ❌ **EXTENDS IdentifiedObject** (line 48: `public class ServiceDeliveryPointEntity extends IdentifiedObject`) +- Has UUID primary key (inherited from IdentifiedObject) +- Has fields: mrid, name, tariffProfile, customerAgreement +- Javadoc says "ServiceDeliveryPoint is now a standalone ESPI resource that extends IdentifiedObject" (line 41) + +**Issues**: +- ❌ **INCORRECTLY extends IdentifiedObject** - should extend Object per espi.xsd:1161 +- ❌ Database has `service_delivery_point_related_links` table (should not exist) +- ❌ Inherits selfLink, upLink, relatedLinks from IdentifiedObject +- ❌ Javadoc is incorrect - claims it's a "standalone ESPI resource" + +**Required Changes**: +- **Remove `extends IdentifiedObject`** from entity class +- Add explicit UUID primary key field +- Remove IdentifiedObject-specific methods +- Update javadoc to reflect it's NOT a standalone resource +- Remove `service_delivery_point_related_links` table from Flyway migration +- Update DTO to remove Atom link elements +- Update mapper to handle non-IdentifiedObject mapping +- **Consider**: Could be `@Embedded` in UsagePoint instead of separate entity + +--- + +### 7. StatementRefEntity ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/StatementRefEntity.java` + +**Current State**: +- ✅ Does NOT extend IdentifiedObject +- ✅ Has UUID primary key +- ✅ Has `@ManyToOne` relationship to Statement (line 79: `private StatementEntity statement;`) +- ✅ Javadoc correctly states: "Note: StatementRef does NOT extend IdentifiedObject per ESPI 4.0 specification" (line 38) +- ✅ Has fields: fileName, mediaType, statementURL + +**Issues**: +- ❌ Database has `statement_ref_related_links` table (should not exist) + +**Required Changes**: +- Remove `statement_ref_related_links` table from Flyway migration +- Verify DTO does not have Atom link elements + +--- + +### 8. PhoneNumberEntity ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/customer/entity/PhoneNumberEntity.java` + +**Current State**: +- ✅ Does NOT extend IdentifiedObject +- ✅ Has UUID primary key +- ✅ Has polymorphic parent tracking (line 93: `parentEntityUuid`, line 99: `parentEntityType`) +- ✅ Javadoc correctly states: "Note: PhoneNumber does NOT extend IdentifiedObject per ESPI 4.0 specification" (line 39) +- ✅ Has PhoneType enum (PRIMARY, SECONDARY, LOCATION_PRIMARY, LOCATION_SECONDARY) + +**Issues**: +- ❌ Database has `phone_number_related_links` table (should not exist) + +**Required Changes**: +- Remove `phone_number_related_links` table from Flyway migration +- Verify DTO does not have Atom link elements + +--- + +### 9. RetailCustomerEntity ⚠️ SPECIAL CASE + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/usage/RetailCustomerEntity.java` + +**Current State**: +- ✅ **EXTENDS IdentifiedObject** (line 52: `public class RetailCustomerEntity extends IdentifiedObject`) +- Has UUID primary key (inherited) +- Has `@OneToMany` to UsagePoint collection (line 171: `List usagePoints`) +- Has `@OneToMany` to Authorization collection (line 180: `List authorizations`) +- **Uses direct FK relationships, NOT Atom rel="related" links** +- Overrides `getSelfHref()` to return `/espi/1_1/resource/RetailCustomer/{id}` (line 296) + +**Analysis**: +- ✅ Correctly extends IdentifiedObject (it's a top-level API entity with identity) +- ❌ However, it uses **direct FK references** (`usage_point.retail_customer_id`) instead of `related_links` table +- ❌ The `retail_customer_related_links` table is unused and should be removed + +**Required Changes**: +- Remove `retail_customer_related_links` table from Flyway migration +- Verify services use direct FK queries, not related_links +- Keep IdentifiedObject inheritance (this is correct) +- Document in javadoc: "Uses direct FK relationships for marshalling, not Atom rel='related' links" + +--- + +### 10. SubscriptionEntity ⚠️ SPECIAL CASE + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/usage/SubscriptionEntity.java` + +**Current State**: +- ✅ **EXTENDS IdentifiedObject** (line 54: `public class SubscriptionEntity extends IdentifiedObject`) +- Has UUID primary key (inherited) +- Has `@ManyToOne` to RetailCustomer (line 76: `private RetailCustomerEntity retailCustomer;`) +- Has `@OneToOne` to Authorization (line 85: `private AuthorizationEntity authorization;`) +- Has `@ManyToOne` to ApplicationInformation (line 93: `private ApplicationInformationEntity applicationInformation;`) +- Has `@ManyToMany` to UsagePoint collection (line 102: `List usagePoints`) +- **Uses direct FK relationships, NOT Atom rel="related" links** + +**Analysis**: +- ✅ Correctly extends IdentifiedObject (it's a top-level API entity with identity) +- ❌ However, it uses **direct FK references** instead of `related_links` table +- ❌ The `subscription_related_links` table is unused and should be removed + +**Required Changes**: +- Remove `subscription_related_links` table from Flyway migration +- Verify services use direct FK queries, not related_links +- Keep IdentifiedObject inheritance (this is correct) +- Document in javadoc: "Uses direct FK relationships for marshalling, not Atom rel='related' links" + +--- + +### 11. BatchListEntity ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/domain/usage/BatchListEntity.java` + +**Current State**: +- ✅ Does NOT extend IdentifiedObject +- ✅ Has UUID primary key +- ✅ Has `@ElementCollection` for resources (line 72: `List resources`) +- ✅ Javadoc correctly states: "Note: BatchList does NOT extend IdentifiedObject per ESPI 4.0 specification" (line 45) +- ✅ Has extensive utility methods for managing resource URIs + +**Issues**: +- ❌ Database has `batch_list_related_links` table (should not exist) +- ⚠️ **Question**: Should BatchList be persisted at all? XSD shows it's just a wrapper for URI collections + +**Required Changes**: +- Remove `batch_list_related_links` table from Flyway migration +- Verify DTO does not have Atom link elements +- **Evaluate**: Consider making BatchList transient/in-memory only if not actively used + +--- + +## Critical Issues Summary + +### High Priority: Remove IdentifiedObject Inheritance + +**2 entities incorrectly extend IdentifiedObject:** + +1. **PnodeRefEntity** (line 40) + - Must remove `extends IdentifiedObject` + - Add explicit UUID primary key + - Remove inherited Atom link methods + +2. **ServiceDeliveryPointEntity** (line 48) + - Must remove `extends IdentifiedObject` + - Add explicit UUID primary key + - Remove inherited Atom link methods + - Update incorrect javadoc + +### Medium Priority: Remove Unused Related Links Tables + +**11 related_links tables to remove from database:** + +| Table Name | Category | Flyway File | +|-----------|----------|-------------| +| `interval_reading_related_links` | Collection | V3__Create_additiional_Base_Tables.sql | +| `reading_quality_related_links` | Collection | V3__Create_additiional_Base_Tables.sql | +| `pnode_ref_related_links` | Collection | V3__Create_additiional_Base_Tables.sql | +| `aggregated_node_ref_related_links` | Collection | V3__Create_additiional_Base_Tables.sql | +| `line_item_related_links` | Collection | V3__Create_additiional_Base_Tables.sql | +| `service_delivery_point_related_links` | Collection | V1__Create_Base_Tables.sql | +| `statement_ref_related_links` | Collection | V3__Create_additiional_Base_Tables.sql | +| `phone_number_related_links` | Collection | V3__Create_additiional_Base_Tables.sql | +| `retail_customer_related_links` | API Entity (FK-based) | V1__Create_Base_Tables.sql | +| `subscription_related_links` | API Entity (FK-based) | V1__Create_Base_Tables.sql | +| `batch_list_related_links` | Wrapper | V1__Create_Base_Tables.sql | + +--- + +## Next Steps + +1. ✅ **Phase A Complete**: Entity analysis documented +2. ⏭️ **Read DTOs**: Analyze 11 corresponding DTO files for Atom link elements +3. ⏭️ **Identify Flyway migrations**: Find exact line numbers for table removals +4. ⏭️ **Create inventory**: Detailed change list for each entity/DTO/migration +5. ⏭️ **Update FLYWAY_SCHEMA_SUMMARY.md**: Document findings +6. ⏭️ **Create migration templates**: V4-V6 scripts for table removals + +--- + +## Recommendations + +### Before Phase B Implementation: + +1. **Address PnodeRef and ServiceDeliveryPoint FIRST** - these have incorrect inheritance that blocks proper schema compliance +2. **Verify DTO layer** - ensure DTOs don't have Atom link elements for these 11 entities +3. **Check services** - ensure no REST endpoints treat these as standalone resources +4. **Run all tests** - baseline test suite before making changes + +### Implementation Order: + +**Phase 0** (NEW - PREREQUISITE): +- Fix PnodeRefEntity inheritance +- Fix ServiceDeliveryPointEntity inheritance +- Update corresponding DTOs and mappers +- Run tests to ensure no breakage + +**Then proceed with original Phases B-E** as planned. + +--- + +**Analysis Status**: ✅ Complete +**Critical Issues Found**: 2 (PnodeRef, ServiceDeliveryPoint extend IdentifiedObject incorrectly) +**Next Action**: Read corresponding DTO files diff --git a/openespi-common/PHASE_A_COMPLETE_SUMMARY.md b/openespi-common/PHASE_A_COMPLETE_SUMMARY.md new file mode 100644 index 00000000..e69de29b diff --git a/openespi-common/Phase_A-DTO_Analysis.md b/openespi-common/Phase_A-DTO_Analysis.md new file mode 100644 index 00000000..bcdc2025 --- /dev/null +++ b/openespi-common/Phase_A-DTO_Analysis.md @@ -0,0 +1,348 @@ +# Phase A: DTO Analysis - Atom Link Serialization + +**Date**: 2026-01-06 +**Branch**: `fix/schema-compliance-analysis` +**Related**: PHASE_A_ANALYSIS_FINDINGS.md + +--- + +## Executive Summary + +Analysis of DTO files for 11 entities reveals: +- ✅ **7 DTOs found** - ALL correctly have NO Atom link elements +- ℹ️ **4 DTOs not found** - Likely embedded inline in parent DTOs +- ⚠️ **1 DTO has entity/field mismatch** - StatementRefDto + +**Key Finding**: All existing DTOs are correctly implemented without Atom links. No DTO changes needed for Phase B-E. + +--- + +## DTOs WITH Separate Files (7 found) + +All 7 DTOs are **correctly implemented** - NONE have Atom link elements: + +| # | DTO File | Has Atom Links? | Has mRID? | Access Type | Status | Notes | +|---|----------|-----------------|-----------|-------------|--------|-------| +| 1 | **IntervalReadingDto.java** | ❌ NO | ❌ NO | FIELD | ✅ Correct | Clean implementation | +| 2 | **ReadingQualityDto.java** | ❌ NO | ❌ NO | FIELD | ✅ Correct | Clean implementation | +| 3 | **PnodeRefDto.java** | ❌ NO | ❌ NO | PROPERTY | ✅ Correct | Uses getter methods | +| 4 | **AggregatedNodeRefDto.java** | ❌ NO | ❌ NO | PROPERTY | ✅ Correct | Uses getter methods | +| 5 | **ServiceDeliveryPointDto.java** | ❌ NO | ✅ YES (@XmlAttribute) | PROPERTY | ✅ Correct | mRID is NOT Atom link | +| 6 | **StatementRefDto.java** | ❌ NO | ❌ NO | FIELD | ⚠️ WARNING | Entity/DTO field mismatch | +| 7 | **RetailCustomerDto.java** | ❌ NO | ❌ NO | PROPERTY | ✅ Correct | Has collection refs | + +**Key Finding**: All DTOs correctly avoid Atom link elements (no selfLink, upLink, or relatedLinks). + +**StatementRefDto Warning**: +- **Entity has**: fileName, mediaType, statementURL +- **DTO has**: referenceId, referenceType, referenceDate, referenceUrl +- **Issue**: Mapper will fail due to field name mismatch +- **Action**: Needs field alignment in future PR (separate from schema compliance) + +--- + +## DTOs WITHOUT Separate Files (4 not found) + +These entities don't have standalone DTO files: + +| # | Entity | DTO File | Likely Serialization Pattern | +|---|--------|----------|------------------------------| +| 8 | **LineItemEntity** | ❌ NOT FOUND | Embedded in UsageSummaryDto | +| 9 | **PhoneNumberEntity** | ❌ NOT FOUND | Embedded in parent DTOs (Customer, ServiceSupplier, etc.) | +| 10 | **SubscriptionEntity** | ❌ NOT FOUND | API-only entity, not ESPI resource | +| 11 | **BatchListEntity** | ❌ NOT FOUND | Transient wrapper, not serialized | + +**Analysis**: +- **LineItem**: Likely serialized inline within UsageSummaryDto.lineItems collection +- **PhoneNumber**: Likely embedded in Organisation DTOs (Customer, ServiceSupplier) +- **Subscription**: OAuth2 API entity, not part of ESPI XML resources +- **BatchList**: Just a URI collection wrapper, may not need DTO at all + +--- + +## Detailed DTO Findings + +### 1. IntervalReadingDto ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/usage/IntervalReadingDto.java` + +**Structure**: +```java +@XmlRootElement(name = "IntervalReading", namespace = "http://naesb.org/espi") +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(propOrder = { + "cost", "currency", "value", "timePeriod", "readingQualities", + "consumptionTier", "tou", "cpp" +}) +public record IntervalReadingDto(...) +``` + +**Fields**: +- ✅ Only ESPI business data elements +- ✅ NO Atom link elements +- ✅ Javadoc correctly states: "Note: IntervalReading does NOT extend IdentifiedObject per ESPI 4.0 specification" (line 32) + +**Conclusion**: Perfect implementation - no changes needed. + +--- + +### 2. ReadingQualityDto ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ReadingQualityDto.java` + +**Structure**: +```java +@XmlRootElement(name = "ReadingQuality", namespace = "http://naesb.org/espi") +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(propOrder = { "quality" }) +public record ReadingQualityDto(...) +``` + +**Fields**: +- ✅ Single field: quality +- ✅ NO Atom link elements +- ✅ Javadoc correctly states: "Note: ReadingQuality does NOT extend IdentifiedObject per ESPI 4.0 specification" (line 30) + +**Conclusion**: Perfect implementation - no changes needed. + +--- + +### 3. PnodeRefDto ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/usage/PnodeRefDto.java` + +**Structure**: +```java +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(name = "PnodeRef", namespace = "http://naesb.org/espi", propOrder = { + "apnodeType", "ref", "startEffectiveDate", "endEffectiveDate" +}) +public record PnodeRefDto(...) +``` + +**Fields**: +- ✅ Only pricing node business data +- ✅ Uses PROPERTY access with getter methods (lines 48-77) +- ✅ NO Atom link elements +- ✅ NO mRID attribute + +**Conclusion**: Correct implementation - no changes needed. + +--- + +### 4. AggregatedNodeRefDto ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/usage/AggregatedNodeRefDto.java` + +**Structure**: +```java +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(name = "AggregatedNodeRef", namespace = "http://naesb.org/espi", propOrder = { + "anodeType", "ref", "startEffectiveDate", "endEffectiveDate", "pnodeRef" +}) +public record AggregatedNodeRefDto(...) +``` + +**Fields**: +- ✅ Only aggregated node business data +- ✅ Includes nested PnodeRefDto (line 84) +- ✅ Uses PROPERTY access with getter methods +- ✅ NO Atom link elements + +**Conclusion**: Correct implementation - no changes needed. + +--- + +### 5. ServiceDeliveryPointDto ✅ (with mRID attribute) + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ServiceDeliveryPointDto.java` + +**Structure**: +```java +@XmlRootElement(name = "ServiceDeliveryPoint", namespace = "http://naesb.org/espi") +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = { + "description", "name", "tariffProfile", "customerAgreement", "tariffRiderRefs" +}) +public record ServiceDeliveryPointDto(...) +``` + +**Special Fields**: +```java +@XmlTransient +public Long getId() { + return id; +} + +@XmlAttribute(name = "mRID") +public String getUuid() { + return uuid; +} +``` + +**Analysis**: +- ✅ Uses `@XmlAttribute(name = "mRID")` - this is **NOT an Atom link** +- ✅ mRID is a business identifier defined in ESPI XSD, completely separate from Atom protocol +- ✅ Javadoc correctly states: "ServiceDeliveryPoint is an embedded element within UsagePoint and contains only ESPI business data - no Atom metadata (links, timestamps) as it's not a standalone resource" (line 31-32) +- ✅ NO selfLink, upLink, or relatedLinks elements + +**Conclusion**: mRID is an ESPI business identifier, NOT an Atom link. Implementation is correct. + +--- + +### 6. StatementRefDto ⚠️ WARNING + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/customer/StatementRefDto.java` + +**Structure**: +```java +@XmlRootElement(name = "StatementRef", namespace = "http://naesb.org/espi/customer") +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(propOrder = { + "referenceId", "referenceType", "referenceDate", "referenceUrl", "statement" +}) +public record StatementRefDto(...) +``` + +**Javadoc Warning** (Line 34-37): +> WARNING: DTO fields do not currently match entity fields. +> Entity has: fileName, mediaType, statementURL +> DTO has: referenceId, referenceType, referenceDate, referenceUrl +> This mismatch needs to be resolved. + +**Critical Issue**: Entity/DTO field mismatch will cause mapper failures. + +**Entity Fields** (StatementRefEntity.java): +```java +private String fileName; +private String mediaType; +private String statementURL; +``` + +**DTO Fields** (StatementRefDto.java): +```java +String referenceId; +String referenceType; +OffsetDateTime referenceDate; +String referenceUrl; +``` + +**Impact**: +- ❌ MapStruct mapper cannot map between mismatched fields +- ❌ Serialization/deserialization will fail +- ❌ Integration tests likely failing for StatementRef + +**Recommendation**: +- Create **separate issue** to fix StatementRefDto field alignment +- NOT part of schema compliance remediation (different problem) +- Should align DTO fields to match entity: fileName, mediaType, statementURL + +**Atom Link Status**: +- ✅ NO Atom link elements (correctly implemented in this regard) + +--- + +### 7. RetailCustomerDto ✅ + +**File**: `src/main/java/org/greenbuttonalliance/espi/common/dto/usage/RetailCustomerDto.java` + +**Structure**: +```java +@XmlRootElement(name = "RetailCustomer", namespace = "http://naesb.org/espi") +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlType(propOrder = { + "username", "firstName", "lastName", "email", "phone", "role", + "enabled", "accountCreated", "lastLogin", "accountLocked", + "failedLoginAttempts", "usagePoints", "authorizations" +}) +public record RetailCustomerDto(...) +``` + +**Collection Fields**: +```java +List usagePoints, +List authorizations +``` + +**Analysis**: +- ✅ Uses **direct collection references**, not Atom rel="related" links +- ✅ This is correct for API DTOs that use FK-based relationships +- ✅ NO Atom link elements (no selfLink, upLink, relatedLinks) +- ✅ Represents API entity with direct relationships, not ESPI Atom resource + +**Conclusion**: RetailCustomerDto correctly implements direct FK pattern, not Atom linking pattern. + +--- + +## Atom Link Element Verification + +**None of the 7 DTOs contain these Atom link patterns:** + +❌ NO `selfLink` element +❌ NO `upLink` element +❌ NO `relatedLinks` collection +❌ NO `@XmlElement(name = "link")` with rel="self", rel="up", or rel="related" + +**Conclusion**: All DTOs are clean and compliant. No DTO changes needed for schema compliance remediation. + +--- + +## Impact on Schema Compliance Remediation + +### Phase B-D: No DTO Changes Required + +**Good News**: All 7 existing DTOs are already compliant. + +**Tasks for Phase B-D**: +1. ✅ DTOs already have NO Atom links - **no changes needed** +2. ❌ Still need to remove `related_links` tables from database +3. ❌ Still need to fix PnodeRefEntity and ServiceDeliveryPointEntity inheritance +4. ❌ Still need to verify mappers work with non-IdentifiedObject entities + +**Phase B Impact**: NO DTO edits required for collection entities (IntervalReading, ReadingQuality, etc.) + +**Phase C Impact**: NO DTO edits required for API entities (RetailCustomer, Subscription) + +**Phase D Impact**: NO DTO edits required for special cases (ServiceDeliveryPoint, BatchList) + +--- + +## Recommendations + +### 1. Fix StatementRefDto Field Mismatch (Separate Issue) + +**NOT part of schema compliance remediation**: +- Create new issue for StatementRefDto entity/DTO field alignment +- Update DTO fields to match entity: fileName, mediaType, statementURL +- Fix mapper after field alignment + +### 2. Update Mappers for Non-IdentifiedObject Entities + +**Phase 0 prerequisite**: +- After removing IdentifiedObject inheritance from PnodeRef and ServiceDeliveryPoint entities +- Update mappers to NOT expect IdentifiedObject fields +- Verify mapper tests pass + +--- + +## Summary + +**DTO Analysis Complete**: ✅ + +**Findings**: +- ✅ All 7 existing DTOs correctly have NO Atom link elements +- ✅ NO DTO changes needed for schema compliance remediation (Phases B-E) +- ⚠️ StatementRefDto has entity/field mismatch (separate issue needed) +- ℹ️ 4 entities have no standalone DTOs (inline serialization pattern) + +**Next Phase A Tasks**: +1. Identify Flyway migration files requiring updates +2. Create detailed inventory of changes required +3. Update FLYWAY_SCHEMA_SUMMARY.md with findings +4. Create migration script templates for Phases B-E + +--- + +**Analysis Status**: ✅ Complete +**DTOs Reviewed**: 7 of 11 (4 confirmed as inline/not needed) +**Critical Issues**: 1 (StatementRefDto mismatch - separate from this remediation) diff --git a/openespi-common/Phase_A-Flyway_Migration_Inventory.md b/openespi-common/Phase_A-Flyway_Migration_Inventory.md new file mode 100644 index 00000000..a593bd6a --- /dev/null +++ b/openespi-common/Phase_A-Flyway_Migration_Inventory.md @@ -0,0 +1,531 @@ +# Phase A: Flyway Migration Inventory + +**Date**: 2026-01-06 +**Branch**: `fix/schema-compliance-analysis` +**Related**: PHASE_A_ANALYSIS_FINDINGS.md, Phase_A-DTO_Analysis.md + +--- + +## Executive Summary + +Identified **11 incorrect `related_links` tables** across 2 Flyway migration files that must be removed: +- **V1__Create_Base_Tables.sql**: 4 tables (retail_customer, service_delivery_point, subscription, batch_list) +- **V3__Create_additiional_Base_Tables.sql**: 7 tables (interval_reading, reading_quality, pnode_ref, aggregated_node_ref, line_item, phone_number, statement_ref) + +Each table removal includes: +- DROP TABLE statement +- DROP INDEX statement + +**Total SQL statements to remove**: 22 (11 CREATE TABLE + 11 CREATE INDEX) + +--- + +## Migration Files Overview + +### V1__Create_Base_Tables.sql + +**Location**: `src/main/resources/db/migration/V1__Create_Base_Tables.sql` + +**Tables to Remove**: 4 +- retail_customer_related_links +- service_delivery_point_related_links +- subscription_related_links +- batch_list_related_links + +--- + +### V3__Create_additiional_Base_Tables.sql + +**Location**: `src/main/resources/db/migration/V3__Create_additiional_Base_Tables.sql` + +**Tables to Remove**: 7 +- interval_reading_related_links +- reading_quality_related_links +- pnode_ref_related_links +- aggregated_node_ref_related_links +- line_item_related_links +- phone_number_related_links +- statement_ref_related_links + +--- + +## Detailed Removal Inventory + +### V1 Migration - Table 1: retail_customer_related_links + +**Lines**: 175-182 + +**SQL to Remove**: +```sql +CREATE TABLE retail_customer_related_links +( + retail_customer_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (retail_customer_id) REFERENCES retail_customers (id) ON DELETE CASCADE +); + +CREATE INDEX idx_retail_customer_related_links ON retail_customer_related_links (retail_customer_id); +``` + +**Rationale**: RetailCustomer uses direct FK relationships, not Atom rel="related" links. + +--- + +### V1 Migration - Table 2: service_delivery_point_related_links + +**Lines**: 213-220 + +**SQL to Remove**: +```sql +CREATE TABLE service_delivery_point_related_links +( + service_delivery_point_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (service_delivery_point_id) REFERENCES service_delivery_points (id) ON DELETE CASCADE +); + +CREATE INDEX idx_sdp_related_links ON service_delivery_point_related_links (service_delivery_point_id); +``` + +**Rationale**: ServiceDeliveryPoint extends Object in espi.xsd:1161, not IdentifiedObject. It's an embedded element, not a standalone resource. + +--- + +### V1 Migration - Table 3: subscription_related_links + +**Lines**: 378-385 + +**SQL to Remove**: +```sql +CREATE TABLE subscription_related_links +( + subscription_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (subscription_id) REFERENCES subscriptions (id) ON DELETE CASCADE +); + +CREATE INDEX idx_subscription_related_links ON subscription_related_links (subscription_id); +``` + +**Rationale**: Subscription uses direct FK relationships for OAuth2 authorization, not Atom rel="related" links. + +--- + +### V1 Migration - Table 4: batch_list_related_links + +**Lines**: 411-418 + +**SQL to Remove**: +```sql +CREATE TABLE batch_list_related_links +( + batch_list_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (batch_list_id) REFERENCES batch_lists (id) ON DELETE CASCADE +); + +CREATE INDEX idx_batch_list_related_links ON batch_list_related_links (batch_list_id); +``` + +**Rationale**: BatchListType extends Object in espi.xsd:1432 - it's a sequence wrapper, not an IdentifiedObject. + +--- + +### V3 Migration - Table 1: interval_reading_related_links + +**Lines**: 122-130 + +**SQL to Remove**: +```sql +CREATE TABLE interval_reading_related_links +( + interval_reading_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (interval_reading_id) REFERENCES interval_readings (id) ON DELETE CASCADE +); + +-- Indexes for interval_reading_related_links table +CREATE INDEX idx_interval_reading_related_links ON interval_reading_related_links (interval_reading_id); +``` + +**Rationale**: IntervalReading extends Object in espi.xsd:1016, not IdentifiedObject. It's a child element collection of IntervalBlock. + +--- + +### V3 Migration - Table 2: reading_quality_related_links + +**Lines**: 163-171 + +**SQL to Remove**: +```sql +CREATE TABLE reading_quality_related_links +( + reading_quality_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (reading_quality_id) REFERENCES reading_qualities (id) ON DELETE CASCADE +); + +-- Indexes for reading_quality_related_links table +CREATE INDEX idx_reading_quality_related_links ON reading_quality_related_links (reading_quality_id); +``` + +**Rationale**: ReadingQuality extends Object in espi.xsd:1062, not IdentifiedObject. It's a child element collection of IntervalReading. + +--- + +### V3 Migration - Table 3: pnode_ref_related_links + +**Lines**: 355-363 + +**SQL to Remove**: +```sql +CREATE TABLE pnode_ref_related_links +( + pnode_ref_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (pnode_ref_id) REFERENCES pnode_refs (id) ON DELETE CASCADE +); + +-- Indexes for pnode_ref_related_links table +CREATE INDEX idx_pnode_ref_related_links ON pnode_ref_related_links (pnode_ref_id); +``` + +**Rationale**: PnodeRef extends Object in espi.xsd:1539, not IdentifiedObject. It's a reference element collection within UsagePoint. + +--- + +### V3 Migration - Table 4: aggregated_node_ref_related_links + +**Lines**: 403-411 + +**SQL to Remove**: +```sql +CREATE TABLE aggregated_node_ref_related_links +( + aggregated_node_ref_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (aggregated_node_ref_id) REFERENCES aggregated_node_refs (id) ON DELETE CASCADE +); + +-- Indexes for aggregated_node_ref_related_links table +CREATE INDEX idx_aggregated_node_ref_related_links ON aggregated_node_ref_related_links (aggregated_node_ref_id); +``` + +**Rationale**: AggregatedNodeRef extends Object in espi.xsd:1570, not IdentifiedObject. It's a reference element collection within UsagePoint. + +--- + +### V3 Migration - Table 5: line_item_related_links + +**Lines**: 775-782 + +**SQL to Remove**: +```sql +CREATE TABLE line_item_related_links +( + line_item_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (line_item_id) REFERENCES line_items (id) ON DELETE CASCADE +); + +CREATE INDEX idx_line_item_related_links ON line_item_related_links (line_item_id); +``` + +**Rationale**: LineItem extends Object in espi.xsd:1444, not IdentifiedObject. It's a child element collection of UsageSummary (costAdditionalDetailLastPeriod). + +--- + +### V3 Migration - Table 6: phone_number_related_links + +**Lines**: 843-850 + +**SQL to Remove**: +```sql +CREATE TABLE phone_number_related_links +( + phone_number_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (phone_number_id) REFERENCES phone_numbers (id) ON DELETE CASCADE +); + +CREATE INDEX idx_phone_number_related_links ON phone_number_related_links (phone_number_id); +``` + +**Rationale**: PhoneNumber is not in ESPI XSD - it's a custom addition. Polymorphic ownership suggests it's a value object collection. + +--- + +### V3 Migration - Table 7: statement_ref_related_links + +**Lines**: 1081-1088 + +**SQL to Remove**: +```sql +CREATE TABLE statement_ref_related_links +( + statement_ref_id CHAR(36) NOT NULL, + related_links VARCHAR(1024), + FOREIGN KEY (statement_ref_id) REFERENCES statement_refs (id) ON DELETE CASCADE +); + +CREATE INDEX idx_statement_ref_related_links ON statement_ref_related_links (statement_ref_id); +``` + +**Rationale**: StatementRef extends Object in customer.xsd:285, not IdentifiedObject. It's a document reference collection within Statement. + +--- + +## Implementation Strategy + +### Option 1: Update Existing Migrations (Development Only) + +**When to Use**: Project is in development, database has not been deployed to production. + +**Approach**: +1. Directly remove the CREATE TABLE and CREATE INDEX statements from V1 and V3 migration files +2. Add comments explaining why tables were removed (XSD compliance) +3. Reset Flyway baseline if needed for local development + +**Pros**: +- Cleaner migration history +- Fewer migration files +- Easier to understand final schema + +**Cons**: +- Cannot be used if any environment has already run V1/V3 migrations +- Breaks Flyway checksum validation + +--- + +### Option 2: Create New DROP TABLE Migrations (Production Safe) + +**When to Use**: Database migrations have been deployed to any environment (dev, test, production). + +**Approach**: +1. Keep V1 and V3 migrations unchanged +2. Create new migration files: + - `V4__Remove_Collection_Related_Links.sql` (7 tables from V3) + - `V5__Remove_API_Entity_Related_Links.sql` (2 tables from V1: retail_customer, subscription) + - `V6__Remove_Special_Case_Related_Links.sql` (2 tables from V1: service_delivery_point, batch_list) +3. Use `DROP TABLE IF EXISTS` for safety + +**Pros**: +- Production safe +- Maintains Flyway checksum integrity +- Clear migration history +- Reversible (can create undo migrations) + +**Cons**: +- More migration files +- Migration files remain with incorrect schema definitions + +--- + +## Recommended Approach + +**Recommendation**: Use **Option 2 (New DROP TABLE Migrations)** for the following reasons: + +1. **Safety**: Ensures compatibility with any environments that have already run V1/V3 +2. **Audit Trail**: Clear documentation of schema evolution +3. **Reversibility**: Can create undo migrations if needed +4. **Best Practice**: Follows Flyway recommended patterns + +--- + +## New Migration Scripts + +### V4__Remove_Collection_Related_Links.sql + +**Tables**: 7 (collection-based entities) + +```sql +-- ============================================================================ +-- V4__Remove_Collection_Related_Links.sql +-- +-- Removes related_links tables for entities extending Object, not IdentifiedObject +-- per ESPI 4.0 XSD schema compliance (Issue #28) +-- +-- Rationale: Entities extending Object in ESPI XSD are element collections +-- within parent resources, not standalone IdentifiedObject resources. +-- Atom related links (rel="related") only apply to IdentifiedObject resources. +-- +-- Category 1: XSD Object-Based Entities (7 tables) +-- ============================================================================ + +-- Remove IntervalReading related links (espi.xsd:1016 extends Object) +DROP TABLE IF EXISTS interval_reading_related_links; + +-- Remove ReadingQuality related links (espi.xsd:1062 extends Object) +DROP TABLE IF EXISTS reading_quality_related_links; + +-- Remove PnodeRef related links (espi.xsd:1539 extends Object) +DROP TABLE IF EXISTS pnode_ref_related_links; + +-- Remove AggregatedNodeRef related links (espi.xsd:1570 extends Object) +DROP TABLE IF EXISTS aggregated_node_ref_related_links; + +-- Remove LineItem related links (espi.xsd:1444 extends Object) +DROP TABLE IF EXISTS line_item_related_links; + +-- Remove PhoneNumber related links (custom addition, not in XSD) +DROP TABLE IF EXISTS phone_number_related_links; + +-- Remove StatementRef related links (customer.xsd:285 extends Object) +DROP TABLE IF EXISTS statement_ref_related_links; +``` + +--- + +### V5__Remove_API_Entity_Related_Links.sql + +**Tables**: 2 (API entities with direct FK relationships) + +```sql +-- ============================================================================ +-- V5__Remove_API_Entity_Related_Links.sql +-- +-- Removes related_links tables for API entities that use direct FK relationships +-- instead of Atom rel="related" links (Issue #28) +-- +-- Rationale: RetailCustomer and Subscription extend IdentifiedObject +-- (they are top-level entities with identity), but they use direct foreign key +-- references for relationships instead of Atom related links tables. +-- +-- Category 2: Custom Entities Using Direct FK References (2 tables) +-- ============================================================================ + +-- Remove RetailCustomer related links +-- Uses direct FK: usage_points.retail_customer_id, authorizations.retail_customer_id +DROP TABLE IF EXISTS retail_customer_related_links; + +-- Remove Subscription related links +-- Uses direct FK: subscription.retail_customer_id, subscription.authorization_id, etc. +DROP TABLE IF EXISTS subscription_related_links; +``` + +--- + +### V6__Remove_Special_Case_Related_Links.sql + +**Tables**: 2 (special cases - embedded/wrapper types) + +```sql +-- ============================================================================ +-- V6__Remove_Special_Case_Related_Links.sql +-- +-- Removes related_links tables for special case entities (Issue #28) +-- +-- Rationale: +-- - ServiceDeliveryPoint: Extends Object in espi.xsd:1161, not IdentifiedObject. +-- It's an embedded element within UsagePoint, not a standalone resource. +-- - BatchList: BatchListType in espi.xsd:1432 is a sequence wrapper, not IdentifiedObject. +-- It's a transient collection for batch operations. +-- +-- Category 3: Element Wrappers (2 tables) +-- ============================================================================ + +-- Remove ServiceDeliveryPoint related links (espi.xsd:1161 extends Object) +DROP TABLE IF EXISTS service_delivery_point_related_links; + +-- Remove BatchList related links (espi.xsd:1432 BatchListType - sequence wrapper) +DROP TABLE IF EXISTS batch_list_related_links; +``` + +--- + +## Verification Queries + +After running migrations, verify tables are removed: + +```sql +-- Check for any remaining incorrect related_links tables +SELECT table_name +FROM information_schema.tables +WHERE table_schema = DATABASE() + AND table_name IN ( + 'interval_reading_related_links', + 'reading_quality_related_links', + 'pnode_ref_related_links', + 'aggregated_node_ref_related_links', + 'line_item_related_links', + 'service_delivery_point_related_links', + 'statement_ref_related_links', + 'phone_number_related_links', + 'retail_customer_related_links', + 'subscription_related_links', + 'batch_list_related_links' + ); + +-- Should return 0 rows after successful migration +``` + +--- + +## Testing Strategy + +### Unit Tests +- Verify entity persistence works without related_links tables +- Verify repository methods function correctly +- Verify cascade deletes work properly + +### Integration Tests (TestContainers) +1. **MySQL Tests**: Run migrations V1-V6, verify schema correctness +2. **PostgreSQL Tests**: Run migrations V1-V6, verify schema correctness +3. **H2 Tests**: Run migrations V1-V6, verify schema correctness + +### Flyway Migration Tests +- Verify migrations V4-V6 are idempotent (can run multiple times) +- Verify Flyway checksum validation passes +- Verify migration order is correct +- Verify rollback scenarios (if undo migrations created) + +--- + +## Rollback Plan + +If migrations V4-V6 cause issues: + +### Immediate Rollback (Before Production Deployment) +1. Create undo migrations: + - `U4__Restore_Collection_Related_Links.sql` + - `U5__Restore_API_Entity_Related_Links.sql` + - `U6__Restore_Special_Case_Related_Links.sql` +2. Run undo migrations to restore tables +3. Investigate root cause + +### Production Rollback (After Deployment) +1. **DO NOT** run undo migrations in production without testing +2. Deploy hotfix with undo migrations to staging first +3. Verify application functions correctly with restored tables +4. Schedule maintenance window for production rollback + +--- + +## Summary + +**Migration Files Identified**: 2 +- V1__Create_Base_Tables.sql (4 tables to remove) +- V3__Create_additiional_Base_Tables.sql (7 tables to remove) + +**New Migration Files Required**: 3 +- V4__Remove_Collection_Related_Links.sql (7 tables) +- V5__Remove_API_Entity_Related_Links.sql (2 tables) +- V6__Remove_Special_Case_Related_Links.sql (2 tables) + +**Total SQL Statements to Remove**: 22 +- 11 CREATE TABLE statements +- 11 CREATE INDEX statements + +**Recommended Approach**: Create new DROP TABLE migrations (V4-V6) instead of modifying existing V1/V3 migrations. + +**Next Steps**: +1. Create V4-V6 migration script files +2. Test migrations with TestContainers (MySQL, PostgreSQL, H2) +3. Update FLYWAY_SCHEMA_SUMMARY.md with changes +4. Document in MULTI_PHASE_SCHEMA_COMPLIANCE_PLAN.md + +--- + +**Analysis Status**: ✅ Complete +**Migration Files Created**: Ready for Phase B implementation +**Testing**: Pending Phase B diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/PnodeRefEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/PnodeRefEntity.java index f7cfe4f4..80375766 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/PnodeRefEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/PnodeRefEntity.java @@ -21,7 +21,6 @@ import jakarta.persistence.*; import lombok.*; -import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject; import org.hibernate.proxy.HibernateProxy; import java.util.Objects; @@ -31,13 +30,24 @@ *

* Represents a reference to a pricing node in the electrical grid used within UsagePoint. * Each UsagePoint can have multiple pricing node references for different time periods. + *

+ * PnodeRef extends Object (not IdentifiedObject) in ESPI 4.0 XSD (espi.xsd:1539), + * so it does NOT have Atom links, timestamps, or related_links table. */ @Entity @Table(name = "pnode_refs") @Getter @Setter @NoArgsConstructor -public class PnodeRefEntity extends IdentifiedObject { +public class PnodeRefEntity { + + /** + * Primary key identifier (48+ bits as per ESPI requirement). + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; /** * Type of the aggregated pricing node. @@ -139,10 +149,6 @@ public String toString() { "apnodeType = " + getApnodeType() + ", " + "ref = " + getRef() + ", " + "startEffectiveDate = " + getStartEffectiveDate() + ", " + - "endEffectiveDate = " + getEndEffectiveDate() + ", " + - "description = " + getDescription() + ", " + - "created = " + getCreated() + ", " + - "updated = " + getUpdated() + ", " + - "published = " + getPublished() + ")"; + "endEffectiveDate = " + getEndEffectiveDate() + ")"; } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ServiceDeliveryPointEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ServiceDeliveryPointEntity.java index e5eefd98..7f16db90 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ServiceDeliveryPointEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/ServiceDeliveryPointEntity.java @@ -19,14 +19,11 @@ package org.greenbuttonalliance.espi.common.domain.usage; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; +import jakarta.persistence.*; import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject; import org.hibernate.proxy.HibernateProxy; import java.util.Objects; @@ -38,27 +35,27 @@ * This is typically associated with a physical address and represents the endpoint * of the utility's distribution system where energy is consumed. *

- * ServiceDeliveryPoint is now a standalone ESPI resource that extends IdentifiedObject. + * ServiceDeliveryPoint extends Object (not IdentifiedObject) in ESPI 4.0 XSD (espi.xsd:1161), + * so it does NOT have Atom links, timestamps, or related_links table. */ @Entity @Table(name = "service_delivery_points") @Getter @Setter @NoArgsConstructor -public class ServiceDeliveryPointEntity extends IdentifiedObject { - +public class ServiceDeliveryPointEntity { + /** - * ServiceDeliveryPoint mRID - identifier for the service delivery point. - * This is embedded within the UsagePoint but has its own identifier. + * Primary key identifier (48+ bits as per ESPI requirement). */ - @Column(name = "sdp_mrid", length = 64) - @Size(max = 64, message = "ServiceDeliveryPoint mRID cannot exceed 64 characters") - private String mrid; - + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; /** * Human-readable name for this service delivery point. - * Often corresponds to a physical address or location identifier. + * The name is any free human readable and possibly non unique text naming the object. */ @Column(name = "sdp_name", length = 256) @Size(max = 256, message = "Service delivery point name cannot exceed 256 characters") @@ -81,29 +78,22 @@ public class ServiceDeliveryPointEntity extends IdentifiedObject { private String customerAgreement; /** - * Constructor with mRID and basic information. - * - * @param mrid the mRID identifier for the service delivery point + * Constructor with basic information. + * * @param name the name of the service delivery point */ - public ServiceDeliveryPointEntity(String mrid, String name) { - this.mrid = mrid; + public ServiceDeliveryPointEntity(String name) { this.name = name; } /** - * Constructor with mRID and full service delivery point information. - * - * @param mrid the mRID identifier - * @param description human-readable description + * Constructor with full service delivery point information. + * * @param name the name of the service delivery point * @param tariffProfile the tariff profile identifier * @param customerAgreement the customer agreement identifier */ - public ServiceDeliveryPointEntity(String mrid, String description, String name, - String tariffProfile, String customerAgreement) { - this.mrid = mrid; - setDescription(description); + public ServiceDeliveryPointEntity(String name, String tariffProfile, String customerAgreement) { this.name = name; this.tariffProfile = tariffProfile; this.customerAgreement = customerAgreement; @@ -112,14 +102,14 @@ public ServiceDeliveryPointEntity(String mrid, String description, String name, /** * Gets a display name for this service delivery point. * Uses the name if available, otherwise creates a default display name. - * + * * @return display name string */ public String getDisplayName() { if (name != null && !name.trim().isEmpty()) { return name.trim(); } - return "Service Delivery Point " + (mrid != null ? mrid : "Unknown"); + return "Service Delivery Point " + (id != null ? id : "Unknown"); } /** @@ -142,13 +132,12 @@ public boolean hasCustomerAgreement() { /** * Validates the service delivery point configuration. - * + * * @return true if valid, false otherwise */ public boolean isValid() { - // A service delivery point is considered valid if it has at least a name or mRID - return (name != null && !name.trim().isEmpty()) || - (mrid != null && !mrid.trim().isEmpty()); + // A service delivery point is considered valid if it has a name + return name != null && !name.trim().isEmpty(); } @Override @@ -171,13 +160,8 @@ public final int hashCode() { public String toString() { return getClass().getSimpleName() + "(" + "id = " + getId() + ", " + - "mrid = " + getMrid() + ", " + "name = " + getName() + ", " + "tariffProfile = " + getTariffProfile() + ", " + - "customerAgreement = " + getCustomerAgreement() + ", " + - "description = " + getDescription() + ", " + - "created = " + getCreated() + ", " + - "updated = " + getUpdated() + ", " + - "published = " + getPublished() + ")"; + "customerAgreement = " + getCustomerAgreement() + ")"; } } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/PnodeRefMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/PnodeRefMapper.java index b985660c..7aff365e 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/PnodeRefMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/PnodeRefMapper.java @@ -21,20 +21,22 @@ import org.greenbuttonalliance.espi.common.domain.usage.PnodeRefEntity; import org.greenbuttonalliance.espi.common.dto.usage.PnodeRefDto; -import org.greenbuttonalliance.espi.common.mapper.BaseIdentifiedObjectMapper; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; /** * MapStruct mapper for converting between PnodeRefEntity and PnodeRefDto. + *

+ * PnodeRef extends Object (not IdentifiedObject) in ESPI 4.0, so it does not + * have Atom links or timestamps - only business data fields. */ @Mapper(componentModel = "spring") -public interface PnodeRefMapper extends BaseIdentifiedObjectMapper { +public interface PnodeRefMapper { /** * Converts a PnodeRefEntity to a PnodeRefDto. - * + * * @param entity the pricing node reference entity * @return the pricing node reference DTO */ diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ServiceDeliveryPointMapper.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ServiceDeliveryPointMapper.java index a8c05fec..9d2e8e3b 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ServiceDeliveryPointMapper.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/ServiceDeliveryPointMapper.java @@ -21,51 +21,52 @@ import org.greenbuttonalliance.espi.common.domain.usage.ServiceDeliveryPointEntity; import org.greenbuttonalliance.espi.common.dto.usage.ServiceDeliveryPointDto; -import org.greenbuttonalliance.espi.common.mapper.BaseIdentifiedObjectMapper; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; /** * MapStruct mapper for converting between ServiceDeliveryPointEntity and ServiceDeliveryPointDto. - * - * Handles the conversion between the JPA entity used for persistence and the DTO + *

+ * ServiceDeliveryPoint extends Object (not IdentifiedObject) in ESPI 4.0 XSD (espi.xsd:1161), + * so it does not have Atom links or timestamps - only business data fields. + *

+ * 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") -public interface ServiceDeliveryPointMapper extends BaseIdentifiedObjectMapper { +public interface ServiceDeliveryPointMapper { /** * Converts a ServiceDeliveryPointEntity to a ServiceDeliveryPointDto. - * Maps service delivery point attributes and connection information. - * + * Maps service delivery point attributes per ESPI 4.0 XSD. + * * @param entity the service delivery point entity * @return the service delivery point DTO */ - @Mapping(target = "id", ignore = true) // Handled by BaseIdentifiedObjectMapper - @Mapping(target = "uuid", source = "mrid") // Map mrid to uuid field in DTO + @Mapping(target = "id", ignore = true) + @Mapping(target = "uuid", ignore = true) // No mRID in entity or XSD + @Mapping(target = "description", ignore = true) // Not in XSD for ServiceDeliveryPoint @Mapping(target = "tariffRiderRefs", ignore = true) // Relationship handled separately ServiceDeliveryPointDto toDto(ServiceDeliveryPointEntity entity); /** * Converts a ServiceDeliveryPointDto to a ServiceDeliveryPointEntity. - * Maps service delivery point attributes and connection information. - * + * Maps service delivery point attributes per ESPI 4.0 XSD. + * * @param dto the service delivery point DTO * @return the service delivery point entity */ - @Mapping(target = "id", ignore = true) // Handled by BaseIdentifiedObjectMapper - @Mapping(target = "mrid", source = "uuid") // Map uuid field in DTO to mrid + @Mapping(target = "id", ignore = true) ServiceDeliveryPointEntity toEntity(ServiceDeliveryPointDto dto); /** * Updates an existing ServiceDeliveryPointEntity with data from a ServiceDeliveryPointDto. * Useful for merge operations where entity values need to be updated. - * + * * @param dto the source DTO * @param entity the target entity to update */ - @Mapping(target = "id", ignore = true) // Handled by BaseIdentifiedObjectMapper - @Mapping(target = "mrid", source = "uuid") // Map uuid field in DTO to mrid + @Mapping(target = "id", ignore = true) void updateEntity(ServiceDeliveryPointDto dto, @MappingTarget ServiceDeliveryPointEntity entity); } \ No newline at end of file diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/PnodeRefRepository.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/PnodeRefRepository.java index 2a33f604..f9829f60 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/PnodeRefRepository.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/PnodeRefRepository.java @@ -31,15 +31,18 @@ /** * Spring Data JPA repository for PnodeRefEntity. - * + *

+ * PnodeRef extends Object (not IdentifiedObject) in ESPI 4.0 XSD, + * so it uses Long ID (not UUID). + *

* Provides CRUD operations and custom queries for pricing node references. */ @Repository -public interface PnodeRefRepository extends JpaRepository { +public interface PnodeRefRepository extends JpaRepository { /** * Find all pricing node references for a specific usage point. - * + * * @param usagePoint the usage point * @return list of pricing node references */ @@ -47,8 +50,8 @@ public interface PnodeRefRepository extends JpaRepository /** * Find pricing node references by usage point ID. - * - * @param usagePointId the usage point ID + * + * @param usagePointId the usage point ID (UUID for UsagePoint which extends IdentifiedObject) * @return list of pricing node references */ List findByUsagePointId(UUID usagePointId); diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/ServiceDeliveryPointRepository.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/ServiceDeliveryPointRepository.java index 9e76dd78..65c66091 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/ServiceDeliveryPointRepository.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/ServiceDeliveryPointRepository.java @@ -28,20 +28,19 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.UUID; @Repository -public interface ServiceDeliveryPointRepository extends JpaRepository { +public interface ServiceDeliveryPointRepository extends JpaRepository { // JpaRepository provides: save(), findById(), findAll(), deleteById(), etc. @Modifying @Transactional @Query("DELETE FROM ServiceDeliveryPointEntity s WHERE s.id = :id") - void deleteById(@Param("id") UUID id); + void deleteById(@Param("id") Long id); @Query("SELECT s.id FROM ServiceDeliveryPointEntity s") - List findAllIds(); + List findAllIds(); @Query("SELECT s FROM ServiceDeliveryPointEntity s WHERE s.name = :name") List findByName(@Param("name") String name); 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 727b0983..cbfadb9a 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 @@ -171,53 +171,9 @@ CREATE INDEX idx_retail_customer_username ON retail_customers (username); CREATE INDEX idx_retail_customer_created ON retail_customers (created); CREATE INDEX idx_retail_customer_updated ON retail_customers (updated); --- Related Links Table for Retail Customers -CREATE TABLE retail_customer_related_links -( - retail_customer_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (retail_customer_id) REFERENCES retail_customers (id) ON DELETE CASCADE -); - -CREATE INDEX idx_retail_customer_related_links ON retail_customer_related_links (retail_customer_id); - --- Service Delivery Point Table -CREATE TABLE service_delivery_points -( - 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), - - -- Service delivery point specific fields - sdp_mrid VARCHAR(64), - sdp_name VARCHAR(256), - sdp_tariff_profile VARCHAR(256), - sdp_customer_agreement VARCHAR(256) -); - -CREATE INDEX idx_sdp_name ON service_delivery_points (sdp_name); -CREATE INDEX idx_sdp_tariff_profile ON service_delivery_points (sdp_tariff_profile); -CREATE INDEX idx_sdp_customer_agreement ON service_delivery_points (sdp_customer_agreement); -CREATE INDEX idx_sdp_created ON service_delivery_points (created); -CREATE INDEX idx_sdp_updated ON service_delivery_points (updated); - --- Related Links Table for Service Delivery Points -CREATE TABLE service_delivery_point_related_links -( - service_delivery_point_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (service_delivery_point_id) REFERENCES service_delivery_points (id) ON DELETE CASCADE -); - -CREATE INDEX idx_sdp_related_links ON service_delivery_point_related_links (service_delivery_point_id); +-- Service Delivery Point Table - Moved to vendor-specific V2 migration files +-- ServiceDeliveryPoint extends Object (not IdentifiedObject) per ESPI 4.0 XSD (espi.xsd:1161) +-- Table creation moved to V2 vendor files due to auto-increment syntax differences -- Authorization Table CREATE TABLE authorizations @@ -374,16 +330,6 @@ CREATE INDEX idx_subscription_last_update ON subscriptions (last_update); CREATE INDEX idx_subscription_created ON subscriptions (created); CREATE INDEX idx_subscription_updated ON subscriptions (updated); --- Related Links Table for Subscriptions -CREATE TABLE subscription_related_links -( - subscription_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (subscription_id) REFERENCES subscriptions (id) ON DELETE CASCADE -); - -CREATE INDEX idx_subscription_related_links ON subscription_related_links (subscription_id); - -- Batch List Table (Independent - no foreign key dependencies) CREATE TABLE batch_lists ( @@ -407,16 +353,6 @@ CREATE INDEX idx_batch_list_created ON batch_lists (created); CREATE INDEX idx_batch_list_resource_count ON batch_lists (resource_count); CREATE INDEX idx_batch_list_updated ON batch_lists (updated); --- Related Links Table for Batch Lists -CREATE TABLE batch_list_related_links -( - batch_list_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (batch_list_id) REFERENCES batch_lists (id) ON DELETE CASCADE -); - -CREATE INDEX idx_batch_list_related_links ON batch_list_related_links (batch_list_id); - -- Batch List Resources Collection Table CREATE TABLE batch_list_resources ( 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 8e1a3f10..05c8cac3 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 @@ -117,18 +117,6 @@ CREATE INDEX idx_interval_reading_value ON interval_readings (reading_value); CREATE INDEX idx_interval_reading_created ON interval_readings (created); CREATE INDEX idx_interval_reading_updated ON interval_readings (updated); - --- Related Links Table for Interval Readings -CREATE TABLE interval_reading_related_links -( - interval_reading_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (interval_reading_id) REFERENCES interval_readings (id) ON DELETE CASCADE -); - --- Indexes for interval_reading_related_links table -CREATE INDEX idx_interval_reading_related_links ON interval_reading_related_links (interval_reading_id); - -- Reading Quality Table CREATE TABLE reading_qualities ( @@ -159,17 +147,6 @@ CREATE INDEX idx_reading_quality_quality ON reading_qualities (quality); CREATE INDEX idx_reading_quality_created ON reading_qualities (created); CREATE INDEX idx_reading_quality_updated ON reading_qualities (updated); --- Related Links Table for Reading Qualities -CREATE TABLE reading_quality_related_links -( - reading_quality_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (reading_quality_id) REFERENCES reading_qualities (id) ON DELETE CASCADE -); - --- Indexes for reading_quality_related_links table -CREATE INDEX idx_reading_quality_related_links ON reading_quality_related_links (reading_quality_id); - -- Usage Summary Table CREATE TABLE usage_summaries ( @@ -317,50 +294,9 @@ ALTER TABLE authorizations ADD CONSTRAINT fk_authorization_subscription ALTER TABLE usage_points ADD CONSTRAINT fk_usage_point_subscription FOREIGN KEY (subscription_id) REFERENCES subscriptions (id) ON DELETE SET NULL; --- PnodeRef Table -CREATE TABLE pnode_refs -( - 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), - - -- PnodeRef specific fields - apnode_type VARCHAR(64), - ref VARCHAR(256) NOT NULL, - start_effective_date BIGINT, - end_effective_date BIGINT, - - -- Foreign key relationships - usage_point_id CHAR(36) NOT NULL, - - FOREIGN KEY (usage_point_id) REFERENCES usage_points (id) ON DELETE CASCADE -); - --- Indexes for pnode_refs table -CREATE INDEX idx_pnode_ref_apnode_type ON pnode_refs (apnode_type); -CREATE INDEX idx_pnode_ref_ref ON pnode_refs (ref); -CREATE INDEX idx_pnode_ref_usage_point_id ON pnode_refs (usage_point_id); -CREATE INDEX idx_pnode_ref_created ON pnode_refs (created); -CREATE INDEX idx_pnode_ref_updated ON pnode_refs (updated); - --- Related Links Table for PnodeRefs -CREATE TABLE pnode_ref_related_links -( - pnode_ref_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (pnode_ref_id) REFERENCES pnode_refs (id) ON DELETE CASCADE -); - --- Indexes for pnode_ref_related_links table -CREATE INDEX idx_pnode_ref_related_links ON pnode_ref_related_links (pnode_ref_id); +-- PnodeRef Table - Moved to vendor-specific V2 migration files +-- PnodeRef extends Object (not IdentifiedObject) per ESPI 4.0 XSD (espi.xsd:1539) +-- Table creation moved to V2 vendor files due to auto-increment syntax differences -- AggregatedNodeRef Table (from V1_9 migration) CREATE TABLE aggregated_node_refs @@ -384,7 +320,7 @@ CREATE TABLE aggregated_node_refs end_effective_date BIGINT, -- Foreign key relationships - pnode_ref_id CHAR(36) , + pnode_ref_id BIGINT, usage_point_id CHAR(36) NOT NULL, FOREIGN KEY (pnode_ref_id) REFERENCES pnode_refs (id) ON DELETE SET NULL, @@ -399,17 +335,6 @@ CREATE INDEX idx_aggregated_node_ref_usage_point_id ON aggregated_node_refs (usa CREATE INDEX idx_aggregated_node_ref_created ON aggregated_node_refs (created); CREATE INDEX idx_aggregated_node_ref_updated ON aggregated_node_refs (updated); --- Related Links Table for AggregatedNodeRefs -CREATE TABLE aggregated_node_ref_related_links -( - aggregated_node_ref_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (aggregated_node_ref_id) REFERENCES aggregated_node_refs (id) ON DELETE CASCADE -); - --- Indexes for aggregated_node_ref_related_links table -CREATE INDEX idx_aggregated_node_ref_related_links ON aggregated_node_ref_related_links (aggregated_node_ref_id); - -- Customer Table CREATE TABLE customers ( @@ -771,16 +696,6 @@ CREATE INDEX idx_line_item_amount ON line_items (amount); CREATE INDEX idx_line_item_created ON line_items (created); CREATE INDEX idx_line_item_updated ON line_items (updated); --- Related Links Table for Line Items -CREATE TABLE line_item_related_links -( - line_item_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (line_item_id) REFERENCES line_items (id) ON DELETE CASCADE -); - -CREATE INDEX idx_line_item_related_links ON line_item_related_links (line_item_id); - -- Meter Entity Table (Joined inheritance from EndDevice) CREATE TABLE meters ( @@ -837,18 +752,6 @@ CREATE INDEX idx_phone_number_itu_phone ON phone_numbers (itu_phone); CREATE INDEX idx_phone_number_created ON phone_numbers (created); CREATE INDEX idx_phone_number_updated ON phone_numbers (updated); - - --- Related Links Table for Phone Numbers -CREATE TABLE phone_number_related_links -( - phone_number_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (phone_number_id) REFERENCES phone_numbers (id) ON DELETE CASCADE -); - -CREATE INDEX idx_phone_number_related_links ON phone_number_related_links (phone_number_id); - -- Program Date ID Mappings Table CREATE TABLE program_date_id_mappings ( @@ -1075,14 +978,4 @@ CREATE TABLE statement_refs CREATE INDEX idx_statement_ref_statement_id ON statement_refs (statement_id); CREATE INDEX idx_statement_ref_created ON statement_refs (created); -CREATE INDEX idx_statement_ref_updated ON statement_refs (updated); - --- Related Links Table for Statement Refs -CREATE TABLE statement_ref_related_links -( - statement_ref_id CHAR(36) NOT NULL, - related_links VARCHAR(1024), - FOREIGN KEY (statement_ref_id) REFERENCES statement_refs (id) ON DELETE CASCADE -); - -CREATE INDEX idx_statement_ref_related_links ON statement_ref_related_links (statement_ref_id); \ No newline at end of file +CREATE INDEX idx_statement_ref_updated ON statement_refs (updated); \ No newline at end of file diff --git a/openespi-common/src/main/resources/db/vendor/h2/V2__H2_Specific_Tables.sql b/openespi-common/src/main/resources/db/vendor/h2/V2__H2_Specific_Tables.sql index 7ac4ca68..11f1c906 100644 --- a/openespi-common/src/main/resources/db/vendor/h2/V2__H2_Specific_Tables.sql +++ b/openespi-common/src/main/resources/db/vendor/h2/V2__H2_Specific_Tables.sql @@ -17,10 +17,8 @@ * - meter_reading_related_links (FK dependency) * - interval_blocks (FK dependency on meter_readings) * - interval_block_related_links (FK dependency) - * - interval_readings (FK dependency on interval_blocks) - * - interval_reading_related_links (FK dependency) - * - reading_qualities (FK dependency on interval_readings) - * - reading_quality_related_links (FK dependency) + * - interval_readings (FK dependency on interval_blocks - no related_links, extends Object) + * - reading_qualities (FK dependency on interval_readings - no related_links, extends Object) * - usage_summaries (FK dependency on usage_points) * - usage_summary_related_links (FK dependency) * - subscription_usage_points (join table) @@ -30,6 +28,21 @@ * Compatible with: H2 Database */ +-- Service Delivery Point Table (Object-based entity, no IdentifiedObject) +-- Must be created before usage_points which references it +-- ServiceDeliveryPoint extends Object per ESPI 4.0 XSD (espi.xsd:1161) +CREATE TABLE service_delivery_points +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + sdp_name VARCHAR(256), + sdp_tariff_profile VARCHAR(256), + sdp_customer_agreement VARCHAR(256) +); + +CREATE INDEX idx_sdp_name ON service_delivery_points (sdp_name); +CREATE INDEX idx_sdp_tariff_profile ON service_delivery_points (sdp_tariff_profile); +CREATE INDEX idx_sdp_customer_agreement ON service_delivery_points (sdp_customer_agreement); + -- Time Configuration Table (H2 with BINARY columns) CREATE TABLE time_configurations ( @@ -120,7 +133,7 @@ CREATE TABLE usage_points -- Foreign key relationships retail_customer_id CHAR(36), - service_delivery_point_id CHAR(36), + service_delivery_point_id BIGINT, local_time_parameters_id CHAR(36), subscription_id CHAR(36), @@ -148,3 +161,21 @@ CREATE TABLE usage_point_related_links -- Create index for usage_point_related_links table CREATE INDEX idx_usage_point_related_links ON usage_point_related_links (usage_point_id); + +-- PnodeRef Table (Object-based entity, no IdentifiedObject) +-- Must be created after usage_points which it references +-- PnodeRef extends Object per ESPI 4.0 XSD (espi.xsd:1539) +CREATE TABLE pnode_refs +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + apnode_type VARCHAR(64), + ref VARCHAR(256) NOT NULL, + start_effective_date BIGINT, + end_effective_date BIGINT, + usage_point_id CHAR(36) NOT NULL, + FOREIGN KEY (usage_point_id) REFERENCES usage_points (id) ON DELETE CASCADE +); + +CREATE INDEX idx_pnode_ref_apnode_type ON pnode_refs (apnode_type); +CREATE INDEX idx_pnode_ref_ref ON pnode_refs (ref); +CREATE INDEX idx_pnode_ref_usage_point_id ON pnode_refs (usage_point_id); diff --git a/openespi-common/src/main/resources/db/vendor/mysql/V2__MySQL_Specific_Tables.sql b/openespi-common/src/main/resources/db/vendor/mysql/V2__MySQL_Specific_Tables.sql index 10441e03..87c0e0ad 100644 --- a/openespi-common/src/main/resources/db/vendor/mysql/V2__MySQL_Specific_Tables.sql +++ b/openespi-common/src/main/resources/db/vendor/mysql/V2__MySQL_Specific_Tables.sql @@ -17,10 +17,8 @@ * - meter_reading_related_links (FK dependency) * - interval_blocks (FK dependency on meter_readings) * - interval_block_related_links (FK dependency) - * - interval_readings (FK dependency on interval_blocks) - * - interval_reading_related_links (FK dependency) - * - reading_qualities (FK dependency on interval_readings) - * - reading_quality_related_links (FK dependency) + * - interval_readings (FK dependency on interval_blocks - no related_links, extends Object) + * - reading_qualities (FK dependency on interval_readings - no related_links, extends Object) * - usage_summaries (FK dependency on usage_points) * - usage_summary_related_links (FK dependency) * - subscription_usage_points (join table) @@ -30,6 +28,21 @@ * Compatible with: MySQL 8.0+ */ +-- Service Delivery Point Table (Object-based entity, no IdentifiedObject) +-- Must be created before usage_points which references it +-- ServiceDeliveryPoint extends Object per ESPI 4.0 XSD (espi.xsd:1161) +CREATE TABLE service_delivery_points +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + sdp_name VARCHAR(256), + sdp_tariff_profile VARCHAR(256), + sdp_customer_agreement VARCHAR(256) +); + +CREATE INDEX idx_sdp_name ON service_delivery_points (sdp_name); +CREATE INDEX idx_sdp_tariff_profile ON service_delivery_points (sdp_tariff_profile); +CREATE INDEX idx_sdp_customer_agreement ON service_delivery_points (sdp_customer_agreement); + -- Time Configuration Table (MySQL with BLOB columns) CREATE TABLE time_configurations ( @@ -117,7 +130,7 @@ CREATE TABLE usage_points -- Foreign key relationships retail_customer_id CHAR(36), - service_delivery_point_id CHAR(36), + service_delivery_point_id BIGINT, local_time_parameters_id CHAR(36), subscription_id CHAR(36), @@ -159,3 +172,21 @@ CREATE TABLE usage_point_related_links + +-- PnodeRef Table (Object-based entity, no IdentifiedObject) +-- Must be created after usage_points which it references +-- PnodeRef extends Object per ESPI 4.0 XSD (espi.xsd:1539) +CREATE TABLE pnode_refs +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + apnode_type VARCHAR(64), + ref VARCHAR(256) NOT NULL, + start_effective_date BIGINT, + end_effective_date BIGINT, + usage_point_id CHAR(36) NOT NULL, + FOREIGN KEY (usage_point_id) REFERENCES usage_points (id) ON DELETE CASCADE +); + +CREATE INDEX idx_pnode_ref_apnode_type ON pnode_refs (apnode_type); +CREATE INDEX idx_pnode_ref_ref ON pnode_refs (ref); +CREATE INDEX idx_pnode_ref_usage_point_id ON pnode_refs (usage_point_id); diff --git a/openespi-common/src/main/resources/db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql b/openespi-common/src/main/resources/db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql index 9670192b..63659f92 100644 --- a/openespi-common/src/main/resources/db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql +++ b/openespi-common/src/main/resources/db/vendor/postgres/V2__PostgreSQL_Specific_Tables.sql @@ -17,10 +17,8 @@ * - meter_reading_related_links (FK dependency) * - interval_blocks (FK dependency on meter_readings) * - interval_block_related_links (FK dependency) - * - interval_readings (FK dependency on interval_blocks) - * - interval_reading_related_links (FK dependency) - * - reading_qualities (FK dependency on interval_readings) - * - reading_quality_related_links (FK dependency) + * - interval_readings (FK dependency on interval_blocks - no related_links, extends Object) + * - reading_qualities (FK dependency on interval_readings - no related_links, extends Object) * - usage_summaries (FK dependency on usage_points) * - usage_summary_related_links (FK dependency) * - subscription_usage_points (join table) @@ -30,6 +28,21 @@ * Compatible with: PostgreSQL 12+ */ +-- Service Delivery Point Table (Object-based entity, no IdentifiedObject) +-- Must be created before usage_points which references it +-- ServiceDeliveryPoint extends Object per ESPI 4.0 XSD (espi.xsd:1161) +CREATE TABLE service_delivery_points +( + id BIGSERIAL PRIMARY KEY, + sdp_name VARCHAR(256), + sdp_tariff_profile VARCHAR(256), + sdp_customer_agreement VARCHAR(256) +); + +CREATE INDEX idx_sdp_name ON service_delivery_points (sdp_name); +CREATE INDEX idx_sdp_tariff_profile ON service_delivery_points (sdp_tariff_profile); +CREATE INDEX idx_sdp_customer_agreement ON service_delivery_points (sdp_customer_agreement); + -- Time Configuration Table (PostgreSQL with BYTEA columns) CREATE TABLE time_configurations ( @@ -118,7 +131,7 @@ CREATE TABLE usage_points -- Foreign key relationships retail_customer_id CHAR(36), - service_delivery_point_id CHAR(36), + service_delivery_point_id BIGINT, local_time_parameters_id CHAR(36), subscription_id CHAR(36), @@ -145,3 +158,21 @@ CREATE TABLE usage_point_related_links CREATE INDEX idx_usage_point_related_links ON usage_point_related_links (usage_point_id); + +-- PnodeRef Table (Object-based entity, no IdentifiedObject) +-- Must be created after usage_points which it references +-- PnodeRef extends Object per ESPI 4.0 XSD (espi.xsd:1539) +CREATE TABLE pnode_refs +( + id BIGSERIAL PRIMARY KEY, + apnode_type VARCHAR(64), + ref VARCHAR(256) NOT NULL, + start_effective_date BIGINT, + end_effective_date BIGINT, + usage_point_id CHAR(36) NOT NULL, + FOREIGN KEY (usage_point_id) REFERENCES usage_points (id) ON DELETE CASCADE +); + +CREATE INDEX idx_pnode_ref_apnode_type ON pnode_refs (apnode_type); +CREATE INDEX idx_pnode_ref_ref ON pnode_refs (ref); +CREATE INDEX idx_pnode_ref_usage_point_id ON pnode_refs (usage_point_id); diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/PnodeRefRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/PnodeRefRepositoryTest.java index 0cdf763c..ee9f8e4a 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/PnodeRefRepositoryTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/PnodeRefRepositoryTest.java @@ -410,26 +410,29 @@ void shouldGenerateDisplayNameCorrectly() { } @Nested - @DisplayName("Base Class Functionality") - class BaseClassTest { + @DisplayName("Entity Persistence") + class EntityPersistenceTest { @Test - @DisplayName("Should inherit IdentifiedObject functionality") - void shouldInheritIdentifiedObjectFunctionality() { + @DisplayName("Should persist and retrieve pricing node reference") + void shouldPersistAndRetrievePnodeRef() { // Arrange UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint(); UsagePointEntity savedUsagePoint = usagePointRepository.save(usagePoint); - - PnodeRefEntity pnodeRef = new PnodeRefEntity("HUB", "BASE_CLASS_TEST", savedUsagePoint); + + PnodeRefEntity pnodeRef = new PnodeRefEntity("HUB", "PERSISTENCE_TEST", savedUsagePoint); // Act PnodeRefEntity saved = pnodeRefRepository.save(pnodeRef); flushAndClear(); // Assert + // PnodeRef extends Object (not IdentifiedObject) in ESPI 4.0 XSD, + // so it has Long ID but no Atom links or timestamps assertThat(saved.getId()).isNotNull(); - assertThat(saved.getCreated()).isNotNull(); - assertThat(saved.getUpdated()).isNotNull(); + assertThat(saved.getApnodeType()).isEqualTo("HUB"); + assertThat(saved.getRef()).isEqualTo("PERSISTENCE_TEST"); + assertThat(saved.getUsagePoint().getId()).isEqualTo(savedUsagePoint.getId()); } } } \ No newline at end of file diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/ServiceDeliveryPointRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/ServiceDeliveryPointRepositoryTest.java index 43e28019..a9ae5af2 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/ServiceDeliveryPointRepositoryTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/ServiceDeliveryPointRepositoryTest.java @@ -29,15 +29,17 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.UUID; import static org.assertj.core.api.Assertions.*; /** * Comprehensive test suite for ServiceDeliveryPointRepository. - * - * Tests all CRUD operations, 5 custom query methods, service delivery location testing, - * validation constraints, business logic methods, and IdentifiedObject base functionality. + *

+ * ServiceDeliveryPoint extends Object (not IdentifiedObject) in ESPI 4.0 XSD, + * so tests focus on business data fields only (no Atom links or timestamps). + * + * Tests all CRUD operations, custom query methods, validation constraints, + * and business logic methods. */ @DisplayName("ServiceDeliveryPoint Repository Tests") class ServiceDeliveryPointRepositoryTest extends BaseRepositoryTest { @@ -50,8 +52,6 @@ class ServiceDeliveryPointRepositoryTest extends BaseRepositoryTest { */ private ServiceDeliveryPointEntity createValidServiceDeliveryPoint() { ServiceDeliveryPointEntity sdp = new ServiceDeliveryPointEntity(); - sdp.setDescription("Test Service Delivery Point - " + faker.lorem().sentence(3)); - sdp.setMrid("SDP-" + faker.number().digits(8)); sdp.setName(faker.address().fullAddress()); sdp.setTariffProfile("TARIFF-" + faker.number().digits(6)); sdp.setCustomerAgreement("AGREEMENT-" + faker.number().digits(8)); @@ -63,7 +63,6 @@ private ServiceDeliveryPointEntity createValidServiceDeliveryPoint() { */ private ServiceDeliveryPointEntity createMinimalServiceDeliveryPoint() { ServiceDeliveryPointEntity sdp = new ServiceDeliveryPointEntity(); - sdp.setDescription("Minimal Service Delivery Point"); sdp.setName("Basic Location"); return sdp; } @@ -77,7 +76,6 @@ class CrudOperationsTest { void shouldSaveAndRetrieveServiceDeliveryPointSuccessfully() { // Arrange ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); - sdp.setDescription("Test Service Delivery Point for CRUD"); sdp.setName("123 Main Street, Anytown, USA"); // Act @@ -89,9 +87,7 @@ void shouldSaveAndRetrieveServiceDeliveryPointSuccessfully() { assertThat(saved).isNotNull(); assertThat(saved.getId()).isNotNull(); assertThat(retrieved).isPresent(); - assertThat(retrieved.get().getDescription()).isEqualTo("Test Service Delivery Point for CRUD"); assertThat(retrieved.get().getName()).isEqualTo("123 Main Street, Anytown, USA"); - assertThat(retrieved.get().getMrid()).isNotNull(); } @Test @@ -102,7 +98,6 @@ void shouldUpdateServiceDeliveryPointSuccessfully() { ServiceDeliveryPointEntity saved = persistAndFlush(sdp); // Act - saved.setDescription("Updated Service Delivery Point Description"); saved.setName("456 Updated Street, New City, USA"); saved.setTariffProfile("UPDATED-TARIFF-123456"); ServiceDeliveryPointEntity updated = serviceDeliveryPointRepository.save(saved); @@ -111,7 +106,6 @@ void shouldUpdateServiceDeliveryPointSuccessfully() { // Assert Optional retrieved = serviceDeliveryPointRepository.findById(updated.getId()); assertThat(retrieved).isPresent(); - assertThat(retrieved.get().getDescription()).isEqualTo("Updated Service Delivery Point Description"); assertThat(retrieved.get().getName()).isEqualTo("456 Updated Street, New City, USA"); assertThat(retrieved.get().getTariffProfile()).isEqualTo("UPDATED-TARIFF-123456"); } @@ -122,7 +116,7 @@ void shouldDeleteServiceDeliveryPointSuccessfully() { // Arrange ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); ServiceDeliveryPointEntity saved = persistAndFlush(sdp); - UUID savedId = saved.getId(); + Long savedId = saved.getId(); // Act serviceDeliveryPointRepository.deleteById(savedId); @@ -141,7 +135,7 @@ void shouldFindAllServiceDeliveryPoints() { sdp1.setName("First Location"); ServiceDeliveryPointEntity sdp2 = createValidServiceDeliveryPoint(); sdp2.setName("Second Location"); - + persistAndFlush(sdp1); persistAndFlush(sdp2); @@ -162,7 +156,7 @@ void shouldCountServiceDeliveryPointsCorrectly() { long initialCount = serviceDeliveryPointRepository.count(); ServiceDeliveryPointEntity sdp1 = createValidServiceDeliveryPoint(); ServiceDeliveryPointEntity sdp2 = createValidServiceDeliveryPoint(); - + persistAndFlush(sdp1); persistAndFlush(sdp2); @@ -188,7 +182,7 @@ void shouldFindServiceDeliveryPointsByName() { sdp2.setName("Unique Location Name"); // Same name ServiceDeliveryPointEntity sdp3 = createValidServiceDeliveryPoint(); sdp3.setName("Different Location Name"); - + persistAndFlush(sdp1); persistAndFlush(sdp2); persistAndFlush(sdp3); @@ -212,7 +206,7 @@ void shouldFindServiceDeliveryPointsByTariffProfile() { sdp2.setTariffProfile("RESIDENTIAL-STANDARD"); // Same tariff ServiceDeliveryPointEntity sdp3 = createValidServiceDeliveryPoint(); sdp3.setTariffProfile("COMMERCIAL-BASIC"); - + persistAndFlush(sdp1); persistAndFlush(sdp2); persistAndFlush(sdp3); @@ -236,7 +230,7 @@ void shouldFindServiceDeliveryPointsByCustomerAgreement() { sdp2.setCustomerAgreement("AGREEMENT-12345"); // Same agreement ServiceDeliveryPointEntity sdp3 = createValidServiceDeliveryPoint(); sdp3.setCustomerAgreement("AGREEMENT-67890"); - + persistAndFlush(sdp1); persistAndFlush(sdp2); persistAndFlush(sdp3); @@ -260,7 +254,7 @@ void shouldFindServiceDeliveryPointsByNameContaining() { sdp2.setName("Oak Street Residential"); ServiceDeliveryPointEntity sdp3 = createValidServiceDeliveryPoint(); sdp3.setName("Pine Avenue Commercial"); - + persistAndFlush(sdp1); persistAndFlush(sdp2); persistAndFlush(sdp3); @@ -286,7 +280,7 @@ void shouldCountServiceDeliveryPointsByTariffProfile() { sdp3.setTariffProfile("COMMERCIAL-PREMIUM"); ServiceDeliveryPointEntity sdp4 = createValidServiceDeliveryPoint(); sdp4.setTariffProfile("RESIDENTIAL-BASIC"); - + persistAndFlush(sdp1); persistAndFlush(sdp2); persistAndFlush(sdp3); @@ -305,12 +299,12 @@ void shouldFindAllIds() { // Arrange ServiceDeliveryPointEntity sdp1 = createValidServiceDeliveryPoint(); ServiceDeliveryPointEntity sdp2 = createValidServiceDeliveryPoint(); - + ServiceDeliveryPointEntity saved1 = persistAndFlush(sdp1); ServiceDeliveryPointEntity saved2 = persistAndFlush(sdp2); // Act - List allIds = serviceDeliveryPointRepository.findAllIds(); + List allIds = serviceDeliveryPointRepository.findAllIds(); // Assert assertThat(allIds).contains(saved1.getId(), saved2.getId()); @@ -321,23 +315,6 @@ void shouldFindAllIds() { @DisplayName("Validation Testing") class ValidationTest { - @Test - @DisplayName("Should validate mRID length constraint") - void shouldValidateMridLengthConstraint() { - // Arrange - ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); - sdp.setMrid("x".repeat(65)); // Exceeds 64 character limit - - // Act - Set> violations = validator.validate(sdp); - - // Assert - assertThat(violations).isNotEmpty(); - assertThat(violations) - .extracting(ConstraintViolation::getMessage) - .contains("ServiceDeliveryPoint mRID cannot exceed 64 characters"); - } - @Test @DisplayName("Should validate name length constraint") void shouldValidateNameLengthConstraint() { @@ -394,7 +371,6 @@ void shouldValidateCustomerAgreementLengthConstraint() { void shouldAcceptValidFieldLengths() { // Arrange ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); - sdp.setMrid("x".repeat(64)); // Exactly 64 characters sdp.setName("x".repeat(256)); // Exactly 256 characters sdp.setTariffProfile("x".repeat(256)); // Exactly 256 characters sdp.setCustomerAgreement("x".repeat(256)); // Exactly 256 characters @@ -411,8 +387,6 @@ void shouldAcceptValidFieldLengths() { void shouldAcceptNullOptionalFields() { // Arrange ServiceDeliveryPointEntity sdp = new ServiceDeliveryPointEntity(); - sdp.setDescription("Test SDP with null fields"); - sdp.setMrid(null); sdp.setName("Valid Name"); sdp.setTariffProfile(null); sdp.setCustomerAgreement(null); @@ -435,7 +409,6 @@ void shouldGenerateCorrectDisplayNameWithName() { // Arrange ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); sdp.setName(" 123 Main Street "); - sdp.setMrid("SDP-12345"); // Act String displayName = sdp.getDisplayName(); @@ -445,48 +418,33 @@ void shouldGenerateCorrectDisplayNameWithName() { } @Test - @DisplayName("Should generate display name with mRID when name is null") - void shouldGenerateDisplayNameWithMridWhenNameIsNull() { + @DisplayName("Should generate display name with ID when name is null") + void shouldGenerateDisplayNameWithIdWhenNameIsNull() { // Arrange ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); sdp.setName(null); - sdp.setMrid("SDP-67890"); + ServiceDeliveryPointEntity saved = persistAndFlush(sdp); // Act - String displayName = sdp.getDisplayName(); + String displayName = saved.getDisplayName(); // Assert - assertThat(displayName).isEqualTo("Service Delivery Point SDP-67890"); + assertThat(displayName).isEqualTo("Service Delivery Point " + saved.getId()); } @Test - @DisplayName("Should generate display name with mRID when name is empty") - void shouldGenerateDisplayNameWithMridWhenNameIsEmpty() { + @DisplayName("Should generate display name with ID when name is empty") + void shouldGenerateDisplayNameWithIdWhenNameIsEmpty() { // Arrange ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); sdp.setName(" "); - sdp.setMrid("SDP-11111"); + ServiceDeliveryPointEntity saved = persistAndFlush(sdp); // Act - String displayName = sdp.getDisplayName(); + String displayName = saved.getDisplayName(); // Assert - assertThat(displayName).isEqualTo("Service Delivery Point SDP-11111"); - } - - @Test - @DisplayName("Should generate default display name when both name and mRID are null") - void shouldGenerateDefaultDisplayNameWhenBothNameAndMridAreNull() { - // Arrange - ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); - sdp.setName(null); - sdp.setMrid(null); - - // Act - String displayName = sdp.getDisplayName(); - - // Assert - assertThat(displayName).isEqualTo("Service Delivery Point Unknown"); + assertThat(displayName).contains("Service Delivery Point"); } @Test @@ -495,10 +453,10 @@ void shouldCorrectlyIdentifyWhenTariffProfileIsPresent() { // Arrange ServiceDeliveryPointEntity sdp1 = createValidServiceDeliveryPoint(); sdp1.setTariffProfile("RESIDENTIAL-STANDARD"); - + ServiceDeliveryPointEntity sdp2 = createValidServiceDeliveryPoint(); sdp2.setTariffProfile(null); - + ServiceDeliveryPointEntity sdp3 = createValidServiceDeliveryPoint(); sdp3.setTariffProfile(" "); @@ -514,10 +472,10 @@ void shouldCorrectlyIdentifyWhenCustomerAgreementIsPresent() { // Arrange ServiceDeliveryPointEntity sdp1 = createValidServiceDeliveryPoint(); sdp1.setCustomerAgreement("AGREEMENT-12345"); - + ServiceDeliveryPointEntity sdp2 = createValidServiceDeliveryPoint(); sdp2.setCustomerAgreement(null); - + ServiceDeliveryPointEntity sdp3 = createValidServiceDeliveryPoint(); sdp3.setCustomerAgreement(" "); @@ -533,22 +491,6 @@ void shouldValidateServiceDeliveryPointWithName() { // Arrange ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); sdp.setName("Valid Location"); - sdp.setMrid(null); - - // Act - boolean isValid = sdp.isValid(); - - // Assert - assertThat(isValid).isTrue(); - } - - @Test - @DisplayName("Should validate service delivery point with mRID") - void shouldValidateServiceDeliveryPointWithMrid() { - // Arrange - ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); - sdp.setName(null); - sdp.setMrid("SDP-12345"); // Act boolean isValid = sdp.isValid(); @@ -558,12 +500,11 @@ void shouldValidateServiceDeliveryPointWithMrid() { } @Test - @DisplayName("Should invalidate service delivery point without name or mRID") - void shouldInvalidateServiceDeliveryPointWithoutNameOrMrid() { + @DisplayName("Should invalidate service delivery point without name") + void shouldInvalidateServiceDeliveryPointWithoutName() { // Arrange ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); sdp.setName(null); - sdp.setMrid(null); // Act boolean isValid = sdp.isValid(); @@ -573,12 +514,11 @@ void shouldInvalidateServiceDeliveryPointWithoutNameOrMrid() { } @Test - @DisplayName("Should invalidate service delivery point with empty name and mRID") - void shouldInvalidateServiceDeliveryPointWithEmptyNameAndMrid() { + @DisplayName("Should invalidate service delivery point with empty name") + void shouldInvalidateServiceDeliveryPointWithEmptyName() { // Arrange ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); sdp.setName(" "); - sdp.setMrid(" "); // Act boolean isValid = sdp.isValid(); @@ -589,12 +529,12 @@ void shouldInvalidateServiceDeliveryPointWithEmptyNameAndMrid() { } @Nested - @DisplayName("Base Class Functionality") - class BaseClassTest { + @DisplayName("Entity Persistence") + class EntityPersistenceTest { @Test - @DisplayName("Should inherit IdentifiedObject functionality") - void shouldInheritIdentifiedObjectFunctionality() { + @DisplayName("Should persist and retrieve by ID") + void shouldPersistAndRetrieveById() { // Arrange ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); @@ -605,37 +545,10 @@ void shouldInheritIdentifiedObjectFunctionality() { // Assert Optional retrieved = serviceDeliveryPointRepository.findById(saved.getId()); assertThat(retrieved).isPresent(); - + ServiceDeliveryPointEntity entity = retrieved.get(); assertThat(entity.getId()).isNotNull(); - assertThat(entity.getCreated()).isNotNull(); - assertThat(entity.getUpdated()).isNotNull(); - assertThat(entity.getDescription()).isNotNull(); - } - - @Test - @DisplayName("Should update timestamps on modification") - void shouldUpdateTimestampsOnModification() { - // Arrange - ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); - ServiceDeliveryPointEntity saved = persistAndFlush(sdp); - - // Wait a moment to ensure timestamp difference - try { - Thread.sleep(10); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - - // Act - saved.setDescription("Updated Description"); - ServiceDeliveryPointEntity updated = serviceDeliveryPointRepository.save(saved); - flushAndClear(); - - // Assert - Optional retrieved = serviceDeliveryPointRepository.findById(updated.getId()); - assertThat(retrieved).isPresent(); - assertThat(retrieved.get().getUpdated()).isAfter(retrieved.get().getCreated()); + assertThat(entity.getName()).isNotNull(); } @Test @@ -662,19 +575,17 @@ void shouldHandleEqualsAndHashCodeCorrectly() { // Arrange ServiceDeliveryPointEntity sdp1 = createValidServiceDeliveryPoint(); ServiceDeliveryPointEntity sdp2 = createValidServiceDeliveryPoint(); - + ServiceDeliveryPointEntity saved1 = persistAndFlush(sdp1); ServiceDeliveryPointEntity saved2 = persistAndFlush(sdp2); // Act & Assert assertThat(saved1).isNotEqualTo(saved2); - // Note: Hibernate proxy-aware hashCode implementation returns class hashCode for different entities - // This is expected behavior for entities with different IDs - + // Same entity should be equal to itself assertThat(saved1).isEqualTo(saved1); assertThat(saved1.hashCode()).isEqualTo(saved1.hashCode()); - + // Different entities with different IDs should not be equal assertThat(saved1.getId()).isNotEqualTo(saved2.getId()); } @@ -684,7 +595,6 @@ void shouldHandleEqualsAndHashCodeCorrectly() { void shouldGenerateMeaningfulToStringRepresentation() { // Arrange ServiceDeliveryPointEntity sdp = createValidServiceDeliveryPoint(); - sdp.setMrid("SDP-12345"); sdp.setName("Test Location"); ServiceDeliveryPointEntity saved = persistAndFlush(sdp); @@ -694,8 +604,7 @@ void shouldGenerateMeaningfulToStringRepresentation() { // Assert assertThat(toString).contains("ServiceDeliveryPointEntity"); assertThat(toString).contains("id = " + saved.getId()); - assertThat(toString).contains("mrid = SDP-12345"); assertThat(toString).contains("name = Test Location"); } } -} \ No newline at end of file +} diff --git a/openespi-common/src/test/resources/application-test.yml b/openespi-common/src/test/resources/application-test.yml index 0211d017..f995df55 100644 --- a/openespi-common/src/test/resources/application-test.yml +++ b/openespi-common/src/test/resources/application-test.yml @@ -36,6 +36,8 @@ spring: flyway: enabled: true baseline-on-migrate: true + clean-disabled: false + clean-on-validation-error: true locations: - classpath:db/migration - classpath:db/vendor/h2