From e4a3734ddb65e9fc18ede9d5b1882e251921532c Mon Sep 17 00:00:00 2001 From: "Donald F. Coffin" Date: Thu, 15 Jan 2026 17:13:55 -0500 Subject: [PATCH] feat: ESPI 4.0 Schema Compliance - Phase 16a: UsagePoint Add Missing Fields This commit adds 10 missing extension fields to UsagePoint entity per ESPI 4.0 XSD specification. This is the first sub-phase of Phase 16, focusing only on additive changes (Boolean and String fields) to minimize risk. Key Changes: - Added 10 new fields to UsagePointEntity (5 Boolean, 5 String types): - checkBilling, grounded, isSdp, isVirtual, minimalUsageExpected (Boolean) - outageRegion, readCycle, readRoute, serviceDeliveryRemark, servicePriority (String) - Updated vendor-specific V2 migrations with proper XSD sequence order: - H2: Added new columns with BOOLEAN and VARCHAR types - MySQL: Added new columns with BOOLEAN and VARCHAR types - PostgreSQL: Added new columns with BOOLEAN and VARCHAR types - Added comprehensive repository test for Phase 16a fields - All fields properly documented with XSD references Technical Details: - Fields added in exact ESPI 4.0 XSD element sequence order - Added inline comments marking XSD sequence positions (1-21) - Identified 2 legacy fields (kind, uri) not in XSD - flagged for Phase 16b review - Enum fields (amiBillingReady, connectionState, phaseCode) deferred to Phase 16b - All 583 tests passing (added 1 new test for Phase 16a fields) Migration Structure: - Fields inserted in XSD-compliant order to avoid future reordering - Comments mark which fields are Phase 16a vs Phase 16b - Maintains consistency across H2, MySQL, and PostgreSQL migrations This is an additive-only change with zero breaking changes. Existing code continues to work, and new fields are all nullable optional fields. Related: Issue #28 - Phase 16: UsagePoint (Sub-phase 16a) Co-Authored-By: Claude Sonnet 4.5 --- .../common/domain/usage/UsagePointEntity.java | 83 +++++++++++++++++++ .../db/vendor/h2/V2__H2_Specific_Tables.sql | 44 +++++++--- .../mysql/V2__MySQL_Specific_Tables.sql | 44 +++++++--- .../V2__PostgreSQL_Specific_Tables.sql | 44 +++++++--- .../usage/UsagePointRepositoryTest.java | 45 ++++++++++ 5 files changed, 224 insertions(+), 36 deletions(-) diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/UsagePointEntity.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/UsagePointEntity.java index 55bb6beb..055c0080 100644 --- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/UsagePointEntity.java +++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/UsagePointEntity.java @@ -129,6 +129,89 @@ public class UsagePointEntity extends IdentifiedObject { }) private SummaryMeasurement ratedPower; + /** + * True if as a result of an inspection or otherwise, there is a reason to suspect + * that a previous billing may have been performed with erroneous data. + * Value should be reset once this potential discrepancy has been resolved. + * Per ESPI 4.0 XSD: [extension] boolean field. + */ + @Column(name = "check_billing") + private Boolean checkBilling; + + /** + * True if grounded. + * Per ESPI 4.0 XSD: [extension] boolean field. + */ + @Column(name = "grounded") + private Boolean grounded; + + /** + * If true, this usage point is a service delivery point, i.e., a usage point + * where the ownership of the service changes hands. + * Per ESPI 4.0 XSD: [extension] boolean field. + */ + @Column(name = "is_sdp") + private Boolean isSdp; + + /** + * If true, this usage point is virtual, i.e., no physical location exists in the + * network where a meter could be located to collect the meter readings. + * For example, one may define a virtual usage point to serve as an aggregation of + * usage for all of a company's premises distributed widely across the distribution territory. + * Otherwise, the usage point is physical, i.e., there is a logical point in the network + * where a meter could be located to collect meter readings. + * Per ESPI 4.0 XSD: [extension] boolean field. + */ + @Column(name = "is_virtual") + private Boolean isVirtual; + + /** + * If true, minimal or zero usage is expected at this usage point for situations such as + * premises vacancy, logical or physical disconnect. + * It is used for readings validation and estimation. + * Per ESPI 4.0 XSD: [extension] boolean field. + */ + @Column(name = "minimal_usage_expected") + private Boolean minimalUsageExpected; + + /** + * Outage region in which this usage point is located. + * Per ESPI 4.0 XSD: [extension] String256 field (max length 256). + */ + @Column(name = "outage_region", length = 256) + private String outageRegion; + + /** + * Cycle day on which the meter for this usage point will normally be read. + * Usually correlated with the billing cycle. + * Per ESPI 4.0 XSD: [extension] String256 field (max length 256). + */ + @Column(name = "read_cycle", length = 256) + private String readCycle; + + /** + * Identifier of the route to which this usage point is assigned for purposes of meter reading. + * Typically used to configure hand held meter reading systems prior to collection of reads. + * Per ESPI 4.0 XSD: [extension] String256 field (max length 256). + */ + @Column(name = "read_route", length = 256) + private String readRoute; + + /** + * Remarks about this usage point, for example the reason for it being rated with a non-nominal priority. + * Per ESPI 4.0 XSD: [extension] String256 field (max length 256). + */ + @Column(name = "service_delivery_remark", length = 256) + private String serviceDeliveryRemark; + + /** + * Priority of service for this usage point. + * Note that usage points at the same service location can have different priorities. + * Per ESPI 4.0 XSD: [extension] String32 field (max length 32). + */ + @Column(name = "service_priority", length = 32) + private String servicePriority; + /** * Service delivery point associated with this usage point. * ServiceDeliveryPoint is now a standalone ESPI resource. 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 af4047b0..3b391f1c 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 @@ -98,42 +98,62 @@ CREATE TABLE usage_points self_link_href VARCHAR(1024), self_link_type VARCHAR(255), - -- Usage point specific fields - kind VARCHAR(50), - status smallint, - uri VARCHAR(1024), - service_category VARCHAR(50), - service_delivery_remark VARCHAR(255), - role_flags VARBINARY(255), - - -- Embedded SummaryMeasurement: estimatedLoad + -- Usage point specific fields (ordered per ESPI 4.0 XSD sequence) + -- XSD sequence: roleFlags, ServiceCategory, status, serviceDeliveryPoint, amiBillingReady, checkBilling, connectionState, estimatedLoad, grounded, isSdp, isVirtual, minimalUsageExpected, nominalServiceVoltage, outageRegion, phaseCode, ratedCurrent, ratedPower, readCycle, readRoute, serviceDeliveryRemark, servicePriority + + role_flags VARBINARY(255), -- 1. roleFlags + service_category VARCHAR(50), -- 2. ServiceCategory + status SMALLINT, -- 3. status + -- 4. serviceDeliveryPoint (FK handled below) + -- 5. amiBillingReady (enum - Phase 16b) + check_billing BOOLEAN, -- 6. checkBilling (Phase 16a) + -- 7. connectionState (enum - Phase 16b) + + -- 8. estimatedLoad (embedded SummaryMeasurement) estimated_load_multiplier VARCHAR(255), estimated_load_timestamp BIGINT, estimated_load_uom VARCHAR(50), estimated_load_value BIGINT, estimated_load_reading_type_ref VARCHAR(512), - -- Embedded SummaryMeasurement: nominalServiceVoltage + grounded BOOLEAN, -- 9. grounded (Phase 16a) + is_sdp BOOLEAN, -- 10. isSdp (Phase 16a) + is_virtual BOOLEAN, -- 11. isVirtual (Phase 16a) + minimal_usage_expected BOOLEAN, -- 12. minimalUsageExpected (Phase 16a) + + -- 13. nominalServiceVoltage (embedded SummaryMeasurement) nominal_voltage_multiplier VARCHAR(255), nominal_voltage_timestamp BIGINT, nominal_voltage_uom VARCHAR(50), nominal_voltage_value BIGINT, nominal_voltage_reading_type_ref VARCHAR(512), - -- Embedded SummaryMeasurement: ratedCurrent + outage_region VARCHAR(256), -- 14. outageRegion (Phase 16a) + -- 15. phaseCode (enum - Phase 16b) + + -- 16. ratedCurrent (embedded SummaryMeasurement) rated_current_multiplier VARCHAR(255), rated_current_timestamp BIGINT, rated_current_uom VARCHAR(50), rated_current_value BIGINT, rated_current_reading_type_ref VARCHAR(512), - -- Embedded SummaryMeasurement: ratedPower + -- 17. ratedPower (embedded SummaryMeasurement) rated_power_multiplier VARCHAR(255), rated_power_timestamp BIGINT, rated_power_uom VARCHAR(50), rated_power_value BIGINT, rated_power_reading_type_ref VARCHAR(512), + read_cycle VARCHAR(256), -- 18. readCycle (Phase 16a) + read_route VARCHAR(256), -- 19. readRoute (Phase 16a) + service_delivery_remark VARCHAR(256), -- 20. serviceDeliveryRemark (Phase 16a) + service_priority VARCHAR(32), -- 21. servicePriority (Phase 16a) + + -- EXTRA FIELDS (not in ESPI 4.0 XSD - to be reviewed in Phase 16b) + kind VARCHAR(50), -- NOT IN XSD (legacy field?) + uri VARCHAR(1024), -- NOT IN XSD (legacy field?) + -- Foreign key relationships retail_customer_id BIGINT, service_delivery_point_id BIGINT, 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 de9eedb9..d2f1d3a2 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 @@ -95,42 +95,62 @@ CREATE TABLE usage_points self_link_href VARCHAR(1024), self_link_type VARCHAR(255), - -- Usage point specific fields - kind VARCHAR(50), - status SMALLINT, - uri VARCHAR(1024), - service_category VARCHAR(50), - service_delivery_remark VARCHAR(255), - role_flags BLOB, - - -- Embedded SummaryMeasurement: estimatedLoad + -- Usage point specific fields (ordered per ESPI 4.0 XSD sequence) + -- XSD sequence: roleFlags, ServiceCategory, status, serviceDeliveryPoint, amiBillingReady, checkBilling, connectionState, estimatedLoad, grounded, isSdp, isVirtual, minimalUsageExpected, nominalServiceVoltage, outageRegion, phaseCode, ratedCurrent, ratedPower, readCycle, readRoute, serviceDeliveryRemark, servicePriority + + role_flags BLOB, -- 1. roleFlags + service_category VARCHAR(50), -- 2. ServiceCategory + status SMALLINT, -- 3. status + -- 4. serviceDeliveryPoint (FK handled below) + -- 5. amiBillingReady (enum - Phase 16b) + check_billing BOOLEAN, -- 6. checkBilling (Phase 16a) + -- 7. connectionState (enum - Phase 16b) + + -- 8. estimatedLoad (embedded SummaryMeasurement) estimated_load_multiplier VARCHAR(255), estimated_load_timestamp BIGINT, estimated_load_uom VARCHAR(50), estimated_load_value BIGINT, estimated_load_reading_type_ref VARCHAR(512), - -- Embedded SummaryMeasurement: nominalServiceVoltage + grounded BOOLEAN, -- 9. grounded (Phase 16a) + is_sdp BOOLEAN, -- 10. isSdp (Phase 16a) + is_virtual BOOLEAN, -- 11. isVirtual (Phase 16a) + minimal_usage_expected BOOLEAN, -- 12. minimalUsageExpected (Phase 16a) + + -- 13. nominalServiceVoltage (embedded SummaryMeasurement) nominal_voltage_multiplier VARCHAR(255), nominal_voltage_timestamp BIGINT, nominal_voltage_uom VARCHAR(50), nominal_voltage_value BIGINT, nominal_voltage_reading_type_ref VARCHAR(512), - -- Embedded SummaryMeasurement: ratedCurrent + outage_region VARCHAR(256), -- 14. outageRegion (Phase 16a) + -- 15. phaseCode (enum - Phase 16b) + + -- 16. ratedCurrent (embedded SummaryMeasurement) rated_current_multiplier VARCHAR(255), rated_current_timestamp BIGINT, rated_current_uom VARCHAR(50), rated_current_value BIGINT, rated_current_reading_type_ref VARCHAR(512), - -- Embedded SummaryMeasurement: ratedPower + -- 17. ratedPower (embedded SummaryMeasurement) rated_power_multiplier VARCHAR(255), rated_power_timestamp BIGINT, rated_power_uom VARCHAR(50), rated_power_value BIGINT, rated_power_reading_type_ref VARCHAR(512), + read_cycle VARCHAR(256), -- 18. readCycle (Phase 16a) + read_route VARCHAR(256), -- 19. readRoute (Phase 16a) + service_delivery_remark VARCHAR(256), -- 20. serviceDeliveryRemark (Phase 16a) + service_priority VARCHAR(32), -- 21. servicePriority (Phase 16a) + + -- EXTRA FIELDS (not in ESPI 4.0 XSD - to be reviewed in Phase 16b) + kind VARCHAR(50), -- NOT IN XSD (legacy field?) + uri VARCHAR(1024), -- NOT IN XSD (legacy field?) + -- Foreign key relationships retail_customer_id BIGINT, service_delivery_point_id BIGINT, 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 2044aac9..12a84a69 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 @@ -96,42 +96,62 @@ CREATE TABLE usage_points self_link_href VARCHAR(1024), self_link_type VARCHAR(255), - -- Usage point specific fields - kind VARCHAR(50), - status SMALLINT, - uri VARCHAR(1024), - service_category VARCHAR(50), - service_delivery_remark VARCHAR(255), - role_flags BYTEA, - - -- Embedded SummaryMeasurement: estimatedLoad + -- Usage point specific fields (ordered per ESPI 4.0 XSD sequence) + -- XSD sequence: roleFlags, ServiceCategory, status, serviceDeliveryPoint, amiBillingReady, checkBilling, connectionState, estimatedLoad, grounded, isSdp, isVirtual, minimalUsageExpected, nominalServiceVoltage, outageRegion, phaseCode, ratedCurrent, ratedPower, readCycle, readRoute, serviceDeliveryRemark, servicePriority + + role_flags BYTEA, -- 1. roleFlags + service_category VARCHAR(50), -- 2. ServiceCategory + status SMALLINT, -- 3. status + -- 4. serviceDeliveryPoint (FK handled below) + -- 5. amiBillingReady (enum - Phase 16b) + check_billing BOOLEAN, -- 6. checkBilling (Phase 16a) + -- 7. connectionState (enum - Phase 16b) + + -- 8. estimatedLoad (embedded SummaryMeasurement) estimated_load_multiplier VARCHAR(255), estimated_load_timestamp BIGINT, estimated_load_uom VARCHAR(50), estimated_load_value BIGINT, estimated_load_reading_type_ref VARCHAR(512), - -- Embedded SummaryMeasurement: nominalServiceVoltage + grounded BOOLEAN, -- 9. grounded (Phase 16a) + is_sdp BOOLEAN, -- 10. isSdp (Phase 16a) + is_virtual BOOLEAN, -- 11. isVirtual (Phase 16a) + minimal_usage_expected BOOLEAN, -- 12. minimalUsageExpected (Phase 16a) + + -- 13. nominalServiceVoltage (embedded SummaryMeasurement) nominal_voltage_multiplier VARCHAR(255), nominal_voltage_timestamp BIGINT, nominal_voltage_uom VARCHAR(50), nominal_voltage_value BIGINT, nominal_voltage_reading_type_ref VARCHAR(512), - -- Embedded SummaryMeasurement: ratedCurrent + outage_region VARCHAR(256), -- 14. outageRegion (Phase 16a) + -- 15. phaseCode (enum - Phase 16b) + + -- 16. ratedCurrent (embedded SummaryMeasurement) rated_current_multiplier VARCHAR(255), rated_current_timestamp BIGINT, rated_current_uom VARCHAR(50), rated_current_value BIGINT, rated_current_reading_type_ref VARCHAR(512), - -- Embedded SummaryMeasurement: ratedPower + -- 17. ratedPower (embedded SummaryMeasurement) rated_power_multiplier VARCHAR(255), rated_power_timestamp BIGINT, rated_power_uom VARCHAR(50), rated_power_value BIGINT, rated_power_reading_type_ref VARCHAR(512), + read_cycle VARCHAR(256), -- 18. readCycle (Phase 16a) + read_route VARCHAR(256), -- 19. readRoute (Phase 16a) + service_delivery_remark VARCHAR(256), -- 20. serviceDeliveryRemark (Phase 16a) + service_priority VARCHAR(32), -- 21. servicePriority (Phase 16a) + + -- EXTRA FIELDS (not in ESPI 4.0 XSD - to be reviewed in Phase 16b) + kind VARCHAR(50), -- NOT IN XSD (legacy field?) + uri VARCHAR(1024), -- NOT IN XSD (legacy field?) + -- Foreign key relationships retail_customer_id BIGINT, service_delivery_point_id BIGINT, diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/UsagePointRepositoryTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/UsagePointRepositoryTest.java index 524869b2..416315aa 100644 --- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/UsagePointRepositoryTest.java +++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/UsagePointRepositoryTest.java @@ -165,6 +165,51 @@ void shouldCountUsagePoints() { // Assert assertThat(finalCount).isEqualTo(initialCount + 5); } + + @Test + @DisplayName("Should persist and retrieve Phase 16a extension fields") + void shouldPersistAndRetrievePhase16aExtensionFields() { + // Arrange - Create usage point with all Phase 16a fields set + UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint(); + usagePoint.setDescription("Usage Point with Phase 16a fields"); + + // Set Phase 16a boolean fields + usagePoint.setCheckBilling(true); + usagePoint.setGrounded(false); + usagePoint.setIsSdp(true); + usagePoint.setIsVirtual(false); + usagePoint.setMinimalUsageExpected(true); + + // Set Phase 16a string fields + usagePoint.setOutageRegion("North Region"); + usagePoint.setReadCycle("Monthly"); + usagePoint.setReadRoute("Route-42"); + usagePoint.setServiceDeliveryRemark("High priority customer"); + usagePoint.setServicePriority("P1"); + + // Act - Save and retrieve + UsagePointEntity saved = usagePointRepository.save(usagePoint); + flushAndClear(); + Optional retrieved = usagePointRepository.findById(saved.getId()); + + // Assert - Verify all Phase 16a fields persisted correctly + assertThat(retrieved).isPresent(); + UsagePointEntity entity = retrieved.get(); + + // Verify boolean fields + assertThat(entity.getCheckBilling()).isTrue(); + assertThat(entity.getGrounded()).isFalse(); + assertThat(entity.getIsSdp()).isTrue(); + assertThat(entity.getIsVirtual()).isFalse(); + assertThat(entity.getMinimalUsageExpected()).isTrue(); + + // Verify string fields + assertThat(entity.getOutageRegion()).isEqualTo("North Region"); + assertThat(entity.getReadCycle()).isEqualTo("Monthly"); + assertThat(entity.getReadRoute()).isEqualTo("Route-42"); + assertThat(entity.getServiceDeliveryRemark()).isEqualTo("High priority customer"); + assertThat(entity.getServicePriority()).isEqualTo("P1"); + } } @Nested